/*
Theme Name: Salt of the Earth
Theme URI: https://saltoftheearthcfh.com/
Author: Salt of the Earth Center for Healing
Description: Custom theme for Salt of the Earth Center for Healing, Chestnut Ridge NY. Replaces the Bridge + Visual Composer build. Services are a custom post type; the salt cave page uses its own template.
Version: 1.0.1
Requires at least: 6.4
Requires PHP: 8.0
License: GPL-2.0-or-later
Text Domain: sote
*/

:root{
  --bg:#FBF7F3;
  --surface:#FFFFFF;
  --band:#F6E6DD;
  --ink:#241D1A;
  --ink-2:#4A403A;
  --stone:#6E6259;
  --accent:#B4563A;
  --accent-dark:#8F4029;
  --glow:#E8A98C;
  --line:rgba(36,29,26,0.12);
  --dark:#2A2320;
  --dark-line:rgba(241,233,222,0.14);
  --radius:2px;
  --measure:1180px;
}

*{box-sizing:border-box}
/* clip, not hidden. The crystals sit outside the viewport edges and would
   otherwise add a horizontal scrollbar — but overflow-x: hidden on html or
   body kills position: sticky, and both the header and the service page's
   left column depend on it. overflow: clip trims without that side effect. */
html{scroll-behavior:smooth;overflow-x:clip}
body{
  overflow-x:clip;
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:"Work Sans",system-ui,sans-serif;
  line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
/* height:auto is load-bearing. WordPress prints width/height attributes on
   editor images; without it the height attribute is honoured while the width
   shrinks, and the picture stretches vertically. */
img{max-width:100%;height:auto;display:block}
a{color:var(--accent)}
a:hover{color:var(--accent-dark)}
::selection{background:#F3D3C4;color:var(--ink)}

h1,h2,h3{font-family:"Fraunces",Georgia,serif;font-weight:500;line-height:1.1;margin:0}
/* Display serif, not the mono face — mono stays for the things that are
   actually data (prices, dates), so the page settles into one typographic
   voice (display + body) instead of display+mono+sans on every label. */
.eyebrow{
  font-family:"Fraunces",Georgia,serif;
  font-style:italic;font-variant-caps:small-caps;font-weight:500;
  font-size:1rem;letter-spacing:.01em;color:var(--ink-2);
}
/* A tick of the same halite silhouette the hero crystals are cut from,
   standing in for the color the label used to carry — the accent shows
   once, on the mark, rather than tinting every label on the page. */
.eyebrow::before{
  content:"";display:inline-block;width:7px;height:7px;margin-right:8px;vertical-align:2px;
  clip-path:polygon(50% 0,100% 26%,100% 74%,50% 100%,0 74%,0 26%);
  background:var(--accent);
}
.wrap{max-width:var(--measure);margin:0 auto;padding:0 28px}
/* Everything after the hero paints above the crystals that spill out of it.
   These sections are transparent, so the glow still shows through the gaps —
   it just never lands on top of the words. */
.section,.band,.site-footer{position:relative;z-index:1}
.section{padding:64px 0}
/* Directly after a hero, the hero's own bottom padding is already doing the
   separating — a full section-top on top of it was ~200px of dead air before
   the next heading. 40px keeps a beat without the gap reading as a mistake. */
.hero + .section,.hero + .band{padding-top:40px}
.lede{color:var(--stone);font-size:1.05rem;max-width:56ch;text-wrap:pretty}

/* ---------- buttons ---------- */
/* .wp-block-button__link rides along on every rule here so a button added in
   the block editor — which otherwise falls back to core's own dark, rounded
   default — matches one hand-coded into a template. Its "Outline" style
   variation maps onto .btn-ghost; everything else (including no style chosen)
   reads as .btn-primary. */
.btn,.wp-block-button__link{
  display:inline-flex;align-items:center;gap:10px;
  font-family:"IBM Plex Mono",ui-monospace,monospace;
  font-size:.78rem;letter-spacing:.05em;text-transform:uppercase;
  padding:15px 26px;border-radius:var(--radius);text-decoration:none;
  border:1px solid transparent;
  transition:background .18s ease,border-color .18s ease,color .18s ease,
             transform .15s cubic-bezier(.32,.72,0,1),box-shadow .18s ease;
}
.btn-primary,.wp-block-button__link{background:var(--accent);color:#FFF8F3}
.btn-primary:hover,.wp-block-button__link:hover{background:var(--accent-dark);color:#FFF8F3;transform:translateY(-2px);box-shadow:0 10px 20px rgba(143,64,41,.28)}
.btn-ghost,.wp-block-button.is-style-outline .wp-block-button__link{background:transparent;border-color:rgba(36,29,26,.22);color:var(--ink)}
.btn-ghost:hover,.wp-block-button.is-style-outline .wp-block-button__link:hover{background:transparent;border-color:var(--accent);color:var(--accent);transform:translateY(-2px)}
/* Feedback on the press itself, not just the eventual click — the button
   should feel touched the instant it's down, not only once something happens.
   Sinks back below the resting line, not just to it, so the hover lift has
   somewhere to go. */
.btn:active,.wp-block-button__link:active{transform:translateY(1px) scale(.97);box-shadow:none}
.btn:focus-visible,.wp-block-button__link:focus-visible{outline:2px solid var(--glow);outline-offset:3px}

/* ---------- header ---------- */
.site-header{
  position:sticky;top:0;z-index:50;
  background:rgba(251,247,243,.93);backdrop-filter:blur(8px);
  border-bottom:1px solid rgba(36,29,26,.1);
}
.site-header__inner{
  max-width:var(--measure);margin:0 auto;padding:14px clamp(16px,4vw,28px);
  display:flex;align-items:center;justify-content:space-between;
  flex-wrap:wrap;column-gap:clamp(12px,2vw,24px);row-gap:10px;
}
.site-logo{font-family:"Fraunces",Georgia,serif;font-size:1.3rem;color:var(--ink);text-decoration:none;white-space:nowrap;display:block}
.site-logo span{color:#C0674A}
.site-logo img,.custom-logo-link img{height:48px;width:auto;display:block}
.custom-logo-link{display:block}
.site-nav ul{list-style:none;display:flex;gap:26px;margin:0;padding:0}
.site-nav a{color:var(--ink-2);text-decoration:none;font-size:.92rem;padding:4px 0;border-bottom:1px solid transparent;white-space:nowrap}
.site-nav a:hover,.site-nav .current-menu-item > a{color:var(--ink);border-color:var(--glow)}
.site-nav li{position:relative}
/* One level of submenu. focus-within alongside hover so it opens for keyboard
   users too, and a padded bridge so the pointer can cross the gap. */
.site-nav .sub-menu{
  display:none;position:absolute;top:100%;left:-14px;z-index:60;
  flex-direction:column;gap:0;min-width:212px;padding:8px 0;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  box-shadow:0 12px 28px rgba(36,29,26,.09);
}
.site-nav li:hover > .sub-menu,
.site-nav li:focus-within > .sub-menu{display:flex}
.site-nav .sub-menu a{display:block;padding:9px 18px;border:0;font-size:.9rem;white-space:nowrap}
.site-nav .sub-menu a:hover{background:var(--band);color:var(--ink)}
/* The repeated parent. Ruled off, so it reads as "the page itself" rather than
   as another child sitting alongside FAQ and Research. */
.site-nav .sote-overview{border-bottom:1px solid var(--line);margin-bottom:6px;padding-bottom:6px}
.site-nav .sote-overview > a{color:var(--ink);font-weight:500}
.site-nav .menu-item-has-children > a::after{
  content:"";display:inline-block;width:5px;height:5px;margin-left:7px;vertical-align:2px;
  border-right:1px solid currentColor;border-bottom:1px solid currentColor;transform:rotate(45deg);
}
.header-actions{display:flex;align-items:center;gap:14px;white-space:nowrap}
.nav-toggle{display:none;background:none;border:0;padding:10px 6px;cursor:pointer;line-height:0}
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after{display:block;width:22px;height:2px;background:var(--ink);transition:transform .2s ease,opacity .2s ease}
.nav-toggle__bars{position:relative}
.nav-toggle__bars::before,
.nav-toggle__bars::after{content:"";position:absolute;left:0}
.nav-toggle__bars::before{top:-7px}
.nav-toggle__bars::after{top:7px}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars{background:transparent}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before{transform:translateY(7px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after{transform:translateY(-7px) rotate(-45deg)}

/* ---------- hero ---------- */
/* The hero no longer clips. The crystals bleed past its bottom edge onto the
   page, which works because the gradient ends on the same colour the body is
   painted in — there is no seam for them to cross. Two things make it safe:
   the sections below carry z-index 1 so they paint over the glow rather than
   under it, and html gets overflow-x: clip further down. */
.hero{position:relative;background:linear-gradient(170deg,#FDEFE7 0%,var(--bg) 76%);padding:104px 0 116px}
.hero--page{padding:70px 0 78px}
.hero__inner{position:relative;z-index:2}
.hero__split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.92fr);gap:60px;align-items:center}
.hero h1{font-size:clamp(2.3rem,4.4vw,3.5rem);margin:18px 0 22px;text-wrap:pretty}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero p{font-size:1.06rem;color:#5F544D;max-width:46ch;margin:0 0 34px;text-wrap:pretty}
.hero__media{width:100%;height:460px;object-fit:cover;border-radius:var(--radius)}
/* Salt is halite, which grows as cubes. The glow is the isometric silhouette
   of one, blurred far enough to stay atmospheric — the shape is felt more than
   seen. A smaller crystal drifts behind it so it reads as a cluster.

   The two crystals are tilted off-axis, because the cube's left facet is a
   dead-vertical edge and it snapped to the hero image's edge. They also run on
   13s and 19s: coprime periods, so the pair takes over four minutes to return
   to the same arrangement and never reads as a single pulsing object. */
.glow{
  position:absolute;top:-20%;right:-6%;width:620px;height:620px;
  pointer-events:none;
  /* --px/--py are this crystal's own eased position, written by
     assets/nav.js each frame — the mirror and bookend below read their own
     --px-mirror/--px-bookend pair instead, each easing at its own speed, so
     the three feel like individual objects catching up to the cursor rather
     than one shape and two synced copies. Default to 0 so a page with JS
     disabled, or the crystal's first paint before the script runs, is
     identical to today. */
  transform:translate3d(var(--px,0px),var(--py,0px),0);
  will-change:transform;
}
.glow::before,
.glow::after{
  content:"";position:absolute;
  clip-path:polygon(50% 0,100% 26%,100% 74%,50% 100%,0 74%,0 26%);
  will-change:transform,opacity;
}
/* The blur belongs to each crystal separately, not to both at once, because
   it only softens an edge convincingly in proportion to the shape it is
   softening. 54px across the 620px crystal is 8.7% of it and reads as haze
   on a silhouette. The same 54px across the companion, which used to be 44%
   of that — 273px — was 20%, which is past the point where a hexagon still
   has corners. It was not sitting behind the big crystal, it was dissolved.
   That is why it has never been visible, at any width, since this was built.

   So: the companion is larger (62%), its blur is set from its own size, and
   it carries more of the gradient's opacity. Only then does moving it do
   anything. */
.glow::before{
  inset:0;
  filter:blur(54px);
  background:linear-gradient(155deg,rgba(240,168,138,.62),rgba(236,164,132,.30) 58%,rgba(240,168,138,.16));
  animation:crystaldrift 13s ease-in-out infinite;
}
/* Down and a little right, which is the only direction with room: these
   crystals hang off the left edge, and the hero photo starts at x685 on a
   1280 screen, which is what pushed them left in the first place. The
   companion ends at 345 and stays clear of it. */
.glow::after{
  width:62%;height:62%;left:10%;bottom:-34%;
  filter:blur(33px);
  background:linear-gradient(155deg,rgba(236,152,118,.52),rgba(240,168,138,.20));
  animation:crystaldriftsmall 19s ease-in-out infinite;
  /* The parent still carries the cursor-driven shift (moves both shapes),
     so the companion cancels it out here — the exact inverse of whatever
     the parent is about to add — leaving only its own idle drift. Only the
     crystal (::before) is meant to answer the cursor; the companion holds
     its position. Mirror overrides this below: --px-mirror isn't negated on
     X because the parent's scaleX(-1) will flip it back on the way out. */
  --cancel-x:calc(var(--px,0px) * -1);
  --cancel-y:calc(var(--py,0px) * -1);
}

@keyframes crystaldrift{
  0%,100%{transform:rotate(-11deg) scale(1) translate(0,0);opacity:.74}
  50%    {transform:rotate(-4deg) scale(1.09) translate(-14px,10px);opacity:1}
}
@keyframes crystaldriftsmall{
  0%,100%{transform:translate3d(var(--cancel-x,0px),var(--cancel-y,0px),0) rotate(9deg) scale(1) translate(0,0);opacity:.5}
  45%    {transform:translate3d(var(--cancel-x,0px),var(--cancel-y,0px),0) rotate(17deg) scale(.92) translate(16px,-14px);opacity:.85}
}

@media (prefers-reduced-motion:reduce){
  .glow::before{animation:none;transform:rotate(-11deg)}
  .glow::after{animation:none;transform:rotate(9deg)}
}

/* On the home page the hero carries a photo on the right, which sat exactly
   where the crystals were and hid them. They move to the left instead, over
   the headline's side of the split. One rule; delete it to put them back. */
.home .glow{right:auto;left:-8%;top:-26%}
@media (max-width:960px){
  .home .glow{left:auto;right:-6%;top:-20%}
}

/* Every interior hero — salt cave, services, about, specials, research,
   events, every single service — carries one crystal and nothing beside it:
   there is no photo on that side for it to compose against the way the home
   page's split has one. Left at the base position above, only 14% of it runs
   off the corner and the rest sits fully inside the frame, composed over
   empty air. That is exactly the failure the home-page override exists to
   avoid — "sized to sit wholly on screen at full opacity it stops reading as
   weather and becomes a flat shape." Smaller and pushed further into the
   corner, roughly half of it is ever on screen, in line with the home page's
   own pair. The blur is retuned to match — see the crystaldrift comment
   above for why it has to move with the shape's own size rather than stay a
   fixed number. */
body:not(.home) .glow{right:-18%;top:-26%;width:480px;height:480px}
body:not(.home) .glow::before{filter:blur(42px)}
body:not(.home) .glow::after{filter:blur(26px)}
@media (max-width:960px){
  body:not(.home) .glow{right:-24%;top:-22%}
}

/* Phones only. At 390px the 620px crystal is wider than the screen — 63% of
   it shows — and a hexagon with two of its six edges cropped away is just a
   glowing corner. The shape is the whole point: halite grows as cubes, and
   the silhouette has to close for that to land.

   The blur scales with it. 54px on 620 is 8.7% of the shape; left alone on a
   300px crystal it would be 18% and would smear the silhouette into a round
   blob, losing the reference more thoroughly than the cropping did. 26px on
   300 holds the same ratio, so it reads exactly as it does on a desktop,
   smaller. right:4% rather than -6% is what brings the last edge on screen.

   Tablets keep the full size: at 601px the crystal is already 94% visible,
   and by 960 it is comfortably whole. The mirror and the bookend are hidden
   below 960 already, so this is the only crystal on a phone. */
@media (max-width:600px){
  .glow{width:300px;height:300px}
  .glow::before{filter:blur(28px)}
  /* The companion goes left here, not right. On a phone the crystal hangs off
     the right edge instead of the left, so the direction with room reverses —
     sent right, as it is on a desktop, it would land under the edge of the
     screen. */
  .glow::after{left:-34%;bottom:-24%;filter:blur(15px)}
  /* top:1% puts the top vertex just below the sticky header, which ends at
     128. It is a point, and a hexagon missing it does not close any better
     than one missing its sides.

     right:-6% and opacity .8 are the other half of the correction. Sized to
     fit exactly inside the viewport and at full strength, the crystal stopped
     being weather and became a flat pink shape sitting on the headline. It
     wants to run off one edge and to sit under the words rather than on
     them — felt more than seen, which is the whole idea. */
  .home .glow{right:-6%;top:1%;opacity:.8}
  /* Interior pages get the same vertex-below-the-header placement as the
     home page's phone crystal — it is the same 300px shape, so the same
     correction applies — but keep their own, deeper horizontal bleed rather
     than borrowing -6%. There is no photo to dodge on an interior hero, so
     the crystal can sit further into the corner without an asymmetric flip:
     it is always the top-right corner, never mirrored left like the home
     page's is. */
  body:not(.home) .glow{right:-38%;top:1%;opacity:.8}
  /* The home hero's own bottom padding stacks with the mission section's
     top padding right after it, same as everywhere else — but here it is a
     photo running into the gap rather than body copy, which reads as a lot
     more dead air for the same pixel count. 64px is the value already used
     for a section's own rhythm elsewhere; scoped to .home so the interior
     hero--page pages, already tuned, are untouched. Scoped to phones, where
     the crystal's top:1% anchor (above) does not move when the hero's
     height changes — a tablet-width version of this would need its own
     check, since .home .glow still sits at top:-20% up there. */
  .home .hero{padding-bottom:64px}
}

/* The second cluster, low on the right, so the hero is weighted at both
   corners rather than one. Mirrored, smaller, and on 17s/23s against the first
   pair's 13s/19s — four periods with no common factor, so the four crystals
   never line up into a single beat. */
.home .glow--mirror{
  left:auto;right:-10%;top:auto;bottom:-34%;
  width:470px;height:470px;opacity:.8;
  /* translate3d listed first, so it lands in screen space after the flip —
     the mirror drifts the same direction on screen as the other two rather
     than mirroring the parallax itself. Its own --px-mirror/--py-mirror,
     eased at its own speed in nav.js, is what keeps it from moving in
     lockstep with the main crystal. */
  transform:translate3d(var(--px-mirror,0px),var(--py-mirror,0px),0) scaleX(-1);
}
.home .glow--mirror::before{animation-duration:17s}
.home .glow--mirror::after{
  animation-duration:23s;
  /* X isn't negated here — the parent's scaleX(-1) flips it back on the way
     out, so the un-negated value is what cancels correctly. Y still is. */
  --cancel-x:var(--px-mirror,0px);
  --cancel-y:calc(var(--py-mirror,0px) * -1);
}

/* The bookend. Bottom left, well under half the strength of the hero pair and
   slower again — 29s and 31s. It should register as the page closing, not as a
   third event competing with the top. */
.home .glow--bookend{
  left:-14%;right:auto;top:auto;bottom:-24%;
  width:430px;height:430px;opacity:.5;z-index:-1;
  transform:translate3d(var(--px-bookend,0px),var(--py-bookend,0px),0);
}
.home .glow--bookend::before{animation-duration:29s}
.home .glow--bookend::after{
  animation-duration:31s;
  --cancel-x:calc(var(--px-bookend,0px) * -1);
  --cancel-y:calc(var(--py-bookend,0px) * -1);
}
@media (max-width:960px){
  .home .glow--bookend{display:none}
}

/* Animation costs nothing while it is off screen. */
.glow.is-idle::before,
.glow.is-idle::after{animation-play-state:paused}

@media (max-width:960px){
  .home .glow--mirror{display:none}
}

.breadcrumb{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.76rem;color:#8A7C72;margin-bottom:14px}
.breadcrumb a{color:var(--stone);text-decoration:none}

/* ---------- mission ---------- */
.mission{display:grid;grid-template-columns:1.05fr .95fr;gap:64px;align-items:start}
.mission__quote{
  font-family:"Fraunces",Georgia,serif;font-style:italic;
  font-size:clamp(1.4rem,2.6vw,1.95rem);line-height:1.38;color:#2E2521;
  border-left:2px solid #E0A88F;padding-left:26px;margin:22px 0 0;text-wrap:pretty;
}
.owner-card{background:var(--surface);border:1px solid var(--line);padding:24px 26px;border-radius:var(--radius)}
.owner-card h3{font-family:"Work Sans",sans-serif;font-weight:600;font-size:1.05rem}
.owner-card .role{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.72rem;text-transform:uppercase;
  letter-spacing:.06em;color:var(--accent);display:block;margin:5px 0 10px;
}
.owner-card p{margin:0;color:#5F544D;font-size:.94rem}

/* WordPress renders a group block as an outer div wrapping an inner container:
   <div class="mission"><div class="wp-block-group__inner-container">…</div></div>
   That leaves any grid or flex container here holding exactly one child, so
   the two-column mission collapsed into a single stacked column. display:
   contents makes the wrapper transparent to layout, and the real children take
   their places again. */
.mission > .wp-block-group__inner-container,
.stack > .wp-block-group__inner-container,
.page-cols > .wp-block-group__inner-container,
.page-gallery > .wp-block-group__inner-container{display:contents}
/* Landing on #susan-toron from another page should not tuck the card under
   the sticky header, and a brief tint says "this is the one you clicked". */
.owner-panel{scroll-margin-top:110px}
.owner-panel:target{border-color:var(--glow);box-shadow:0 0 0 3px rgba(232,169,140,.22)}
.stack{display:flex;flex-direction:column;gap:20px}

/* ---------- band ---------- */
.band{background:var(--band);padding:64px 0}
.band__grid{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center}
.band__media{width:100%;height:430px;object-fit:cover;border-radius:var(--radius)}
.tick-list{list-style:none;margin:0 0 30px;padding:0;display:grid;gap:11px}
/* The li is a flex row so the — hangs beside the text rather than indenting
   it. That makes every direct child a flex item, including ones the theme
   never wrote: iOS Safari detects phone numbers in the copy and wraps them in
   a tel: link, which then sat as a third column, 37px wide, breaking
   845-290-0678 over three lines. So the templates wrap each bullet in a span
   and the li has exactly two children — marker and span — with anything
   Safari injects flowing inline inside the span. Do not remove that span; it
   looks redundant and is not. The same applies to the address and date
   formats Safari also detects. */
.tick-list li{display:flex;gap:12px;font-size:.96rem;color:var(--ink-2)}
.tick-list li::before{content:"—";color:var(--accent);flex-shrink:0}

/* ---------- service grid ---------- */
/* A grid, so every tile sits on the same column rhythm — flex let the last
   row grow to different widths from the rows above it.

   The hairlines are drawn as borders on the tiles rather than as a 1px gap
   over a line-coloured background. That is what makes a short last row safe:
   an unfilled cell draws nothing, instead of showing the background through
   as a grey block. That block is what appeared when a service was removed. */
.service-tiles{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(228px,1fr));
  border-top:1px solid var(--line);border-left:1px solid var(--line);
}
.service-tile{
  border-right:1px solid var(--line);border-bottom:1px solid var(--line);
  background:var(--bg);padding:28px 24px;min-height:150px;
  display:flex;flex-direction:column;gap:10px;text-decoration:none;color:var(--ink);
  transition:background .18s ease,transform .15s cubic-bezier(.32,.72,0,1);
}
.service-tile:hover{background:var(--surface)}
/* No lift here — the tiles share hairline borders edge to edge, and a
   translateY would visibly tear one loose from its neighbours. A press-scale
   reads fine since it's brief and symmetric, not a persistent hover state. */
.service-tile:active{transform:scale(.97)}
.service-tile h3{font-family:"Work Sans",sans-serif;font-weight:600;font-size:1rem}
.service-tile p{margin:0;font-size:.86rem;color:var(--stone)}
/* The closing tile covers two columns, which with ten services closes the last
   row exactly at every column count the grid produces — four, three, two and
   one. Change the number of services and it may not; the row then ends short
   rather than grey, which is the point of the borders above. */
.service-tile--cta{grid-column:span 2;background:var(--accent);color:#FFF3EB}
@media (max-width:520px){
  .service-tile--cta{grid-column:span 1}
}
.service-tile--cta h3,.service-tile--cta a{color:#FFF3EB}
.service-tile--cta p{color:rgba(255,243,235,.85)}
/* The tile hover turns the background white, which on this one left pale text
   on pale ground. It darkens instead. */
.service-tile--cta:hover{background:var(--accent-dark)}

/* The "more services" row at the foot of a single service page. It was
   styled inline with no :hover/:active/:focus-visible at all — the one
   clickable thing on the site that gave back nothing when touched. Brought
   in line with .service-tile's own press-scale, since these are the same
   kind of object: a link to another service, not a card with its own
   content to protect from a lift. */
.service-chip{
  font-size:.88rem;color:var(--ink-2);text-decoration:none;
  border:1px solid rgba(36,29,26,.14);border-radius:var(--radius);
  padding:7px 13px;
  transition:background .18s ease,border-color .18s ease,color .18s ease,
             transform .15s cubic-bezier(.32,.72,0,1);
}
.service-chip:hover{background:var(--surface);border-color:var(--accent);color:var(--accent)}
.service-chip:active{transform:scale(.96)}
.service-chip:focus-visible{outline:2px solid var(--glow);outline-offset:2px}

.service-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px}
.service-card{
  background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:28px 26px;text-decoration:none;color:var(--ink);display:flex;flex-direction:column;gap:12px;
  transition:transform .2s cubic-bezier(.32,.72,0,1),box-shadow .2s cubic-bezier(.32,.72,0,1),border-color .18s ease;
}
.service-card:hover{border-color:rgba(36,29,26,.2);box-shadow:0 14px 28px rgba(36,29,26,.1);transform:translateY(-3px)}
.service-card:active{transform:translateY(-1px) scale(.99);box-shadow:0 6px 14px rgba(36,29,26,.08)}
.service-card h3{font-size:1.28rem}
.service-card p{margin:0;color:var(--stone);font-size:.94rem;flex-grow:1}
.service-card__meta{
  display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  border-top:1px solid rgba(36,29,26,.09);padding-top:12px;margin-top:4px;
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.78rem;
}
.service-card__meta .price{color:var(--accent)}
.service-card__meta .who{color:#8A7C72}

/* ---------- specials ----------
   The home page band is a teaser of the same offers the specials page lists
   in full, so the two share a vocabulary: name, the offer in mono, the end
   date in small caps. The band cards are links; the page's are not. */
.special-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:22px}
.special-card{
  background:var(--band);border-radius:var(--radius);padding:26px 24px;
  text-decoration:none;color:var(--ink);display:flex;flex-direction:column;gap:10px;
  transition:background .18s ease,transform .2s cubic-bezier(.32,.72,0,1),box-shadow .2s cubic-bezier(.32,.72,0,1);
}
.special-card:hover{background:#F2DBCF;color:var(--ink);transform:translateY(-3px);box-shadow:0 14px 28px rgba(36,29,26,.1)}
.special-card:active{transform:translateY(-1px) scale(.99);box-shadow:0 6px 14px rgba(36,29,26,.08)}
.special-card h3{font-size:1.2rem}
.special-card__offer{
  margin:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.86rem;color:var(--accent-dark);
}
.special-card__until{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.72rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--stone);margin-top:auto;
}

.special-list{display:flex;flex-direction:column;gap:1px;background:var(--line);border:1px solid var(--line)}
.special{background:var(--surface);padding:30px 28px;display:flex;flex-direction:column;gap:12px}
.special__head{display:flex;justify-content:space-between;align-items:baseline;gap:20px;flex-wrap:wrap}
.special__head h2{font-size:1.45rem}
.special__until{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.72rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--stone);white-space:nowrap;
}
.special__offer{
  margin:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:1rem;color:var(--accent);
}
.special__body{margin:0;color:var(--ink-2);font-size:.96rem;max-width:70ch;text-wrap:pretty}
.special--empty{border:1px solid var(--line);background:var(--band)}
.special-note{margin:20px 0 0;font-size:.88rem;color:var(--stone);max-width:70ch;text-wrap:pretty}
/* The standing copy sits under the offers: it is the part that does not change,
   and the part a search engine has to find something in. */
.special-standing{margin-top:56px;border-top:1px solid var(--line);padding-top:40px;max-width:70ch}
.special-standing h2{font-size:1.4rem;margin:32px 0 12px}
.special-standing p{color:var(--ink-2);text-wrap:pretty}

/* ---------- events ----------
   Replaces The Events Calendar's own styling, which arrived with its own font
   stack and turned the page into a different website inside the frame. The
   date sits in its own column so a run of events reads as a calendar without
   needing a grid nobody scans. */
.event-teasers{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:22px}
.event-teaser{
  background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:26px 24px;text-decoration:none;color:var(--ink);display:flex;flex-direction:column;gap:10px;
  transition:border-color .18s ease,transform .2s cubic-bezier(.32,.72,0,1),box-shadow .2s cubic-bezier(.32,.72,0,1);
}
.event-teaser:hover{border-color:var(--glow);color:var(--ink);transform:translateY(-3px);box-shadow:0 14px 28px rgba(36,29,26,.1)}
.event-teaser:active{transform:translateY(-1px) scale(.99);box-shadow:0 6px 14px rgba(36,29,26,.08)}
.event-teaser__when,
.event-teaser__price{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.74rem;letter-spacing:.06em}
.event-teaser__when{text-transform:uppercase;color:var(--accent)}
.event-teaser__price{color:var(--stone);margin-top:auto}
.event-teaser h3{font-size:1.2rem}

.events-intro{max-width:70ch;margin:0 0 40px}
.events-intro p{color:var(--ink-2);text-wrap:pretty}
.event-month{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.74rem;font-weight:500;
  letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
  margin:38px 0 2px;padding-bottom:10px;border-bottom:1px solid var(--line);
}
.event-month:first-child{margin-top:0}
.event{
  display:grid;grid-template-columns:64px minmax(0,1fr) auto;gap:26px;align-items:start;
  padding:26px 0;border-bottom:1px solid var(--line);
}
.event__when{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  background:var(--band);border-radius:var(--radius);padding:10px 0 12px;
}
.event__day{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.68rem;letter-spacing:.1em;
  text-transform:uppercase;color:#A44C31;
}
.event__date{font-family:"Fraunces",Georgia,serif;font-size:1.6rem;line-height:1.1;color:var(--ink)}
.event__what h3{font-size:1.25rem;margin:0 0 8px}
.event__meta{
  margin:0 0 10px;display:flex;gap:16px;flex-wrap:wrap;
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.76rem;color:var(--stone);
}
.event__price{color:var(--accent)}
.event__body{margin:0;color:var(--ink-2);font-size:.95rem;max-width:64ch;text-wrap:pretty}
.event__cta{align-self:center;white-space:nowrap;padding:12px 20px;font-size:.72rem}
.event--empty{
  display:block;background:var(--band);border-radius:var(--radius);border:0;
  padding:28px 26px;max-width:70ch;
}
.event--empty .btn{margin-top:16px}
@media (max-width:700px){
  /* The date chip goes inline above the title; a 64px column beside a wrapped
     heading leaves the number stranded at the top of a tall empty box. */
  .event{grid-template-columns:1fr;gap:14px}
  .event__when{flex-direction:row;gap:10px;align-items:baseline;justify-content:flex-start;padding:8px 14px;width:max-content}
  .event__date{font-size:1.25rem}
  .event__cta{align-self:start}
}

/* ---------- research ----------
   The finding leads at reading size; the citation is quiet; the abstract is
   folded away. The old page gave a third of its width to author lists and
   keyword strings and squeezed 300 words of clinical prose into the rest. */
.research-intro{max-width:70ch;margin:0 0 48px}
.research-intro p{color:var(--ink-2);text-wrap:pretty}
.study-list{display:flex;flex-direction:column;gap:1px;background:var(--line);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.study{background:var(--bg);padding:30px 0 28px;display:flex;flex-direction:column;gap:12px}
/* Reading measure, not column width. A finding is a sentence, and a sentence
   that runs the full 1180px is a sentence nobody finishes. */
.study__finding{font-size:clamp(1.15rem,2vw,1.45rem);line-height:1.35;max-width:60ch;font-weight:500}
.study__cite{
  margin:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.76rem;line-height:1.6;
  color:var(--stone);max-width:80ch;
}
.study__cite a{color:var(--stone);text-decoration:none;border-bottom:1px solid rgba(36,29,26,.18)}
.study__cite a:hover{color:var(--accent);border-color:var(--accent)}
.study__abstract{max-width:74ch}
.study__abstract summary{
  cursor:pointer;list-style:none;font-family:"IBM Plex Mono",ui-monospace,monospace;
  font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);
}
.study__abstract summary::-webkit-details-marker{display:none}
.study__abstract summary::after{
  content:" +";font-family:inherit;display:inline-block;
  transition:transform .2s cubic-bezier(.32,.72,0,1);
}
.study__abstract[open] summary::after{transform:rotate(45deg)}
.study__abstract p{margin:12px 0 0;font-size:.9rem;color:#5F544D;text-wrap:pretty;animation:reveal .18s cubic-bezier(.32,.72,0,1)}
.research-note{
  margin:40px 0 0;padding:20px 22px;background:var(--band);border-radius:var(--radius);
  font-size:.88rem;color:#4A3F39;max-width:74ch;text-wrap:pretty;
}
.research-next{display:flex;gap:14px;flex-wrap:wrap;margin-top:34px}

/* ---------- detail layout ---------- */
.detail{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,.65fr);gap:64px;align-items:start}
/* Same shape as .detail, one column-width narrower — the about page's photo
   reads better a little wider than a service page's portrait does. This used
   to live as an inline style on the wrap div, which is why it never picked up
   the mobile stack below: an inline style isn't reachable from a media query. */
.about-intro{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);gap:64px;align-items:start}
.detail__body h2{font-size:1.5rem;margin:0 0 16px}
.detail__body p{margin:0 0 16px;color:var(--ink-2);text-wrap:pretty}
.detail__body section{margin-bottom:44px}
.detail__aside{position:sticky;top:96px;display:flex;flex-direction:column;gap:22px}
/* Added by assets/sticky.js, and only when the body column fits on screen and
   the rail is the taller of the two. Applying it blind would strand the bottom
   of a long article above the fold. */
.detail__body.is-stuck{position:sticky;top:96px;align-self:start}
/* Portraits keep their 4:5 shape. A fixed height cropped heads off the top. */
.aside-portrait{width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;object-position:50% 20%;border-radius:var(--radius);border:1px solid var(--line)}
.portrait-figure{margin:0}
.portrait-figure figcaption{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.74rem;color:var(--stone);
  margin-top:8px;letter-spacing:.03em;
}

.price-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:24px 24px 20px}
.price-card .lead{font-family:"Fraunces",Georgia,serif;font-size:2.1rem;margin:10px 0 4px}
.price-card .lead small{font-size:.95rem;font-family:"Work Sans",sans-serif;color:var(--stone)}
.price-list{list-style:none;margin:14px 0 0;padding:0}
.price-list li{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-top:1px solid rgba(36,29,26,.09);font-size:.92rem}
.price-list .amount{font-family:"IBM Plex Mono",ui-monospace,monospace;color:var(--accent);white-space:nowrap}
.booking-card{background:var(--band);border-radius:var(--radius);padding:24px}
.booking-card p{margin:12px 0 16px;font-size:.95rem;color:#4A3F39;text-wrap:pretty}

/* ---------- converted page content ----------
   Used by the pages rebuilt out of Visual Composer: a two-column row and an
   image grid, which is all its 253 shortcodes actually amounted to. */
.page-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:44px;align-items:start;margin:0 0 28px}
/* Three-up rows carry the body text in the first column and short meta in the
   others — the research studies are abstract, authors, keywords. Equal thirds
   squeeze the abstract into a sliver, so the first column takes the room. */
.page-cols--3{grid-template-columns:minmax(0,1.9fr) minmax(0,1fr) minmax(0,1fr);gap:36px}
.page-cols--3 > div:not(:first-child){font-size:.9rem;color:var(--stone)}
.page-cols--3 .btn{margin-top:8px;padding:10px 16px;font-size:.7rem}
@media (max-width:900px){
  .page-cols--3{grid-template-columns:1fr;gap:22px}
  .page-cols--3 > div:not(:first-child){font-size:.94rem}
}
.page-cols > div > :first-child{margin-top:0}
.page-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin:24px 0}
.page-gallery img{width:100%;height:220px;object-fit:cover;border-radius:var(--radius)}
.entry figure{margin:24px 0}
.entry figure img{border-radius:var(--radius)}
@media (max-width:700px){
  .page-cols{gap:28px}
  .page-gallery img{height:180px}
}

/* ---------- faq ---------- */
.faq{max-width:820px}
.faq details{border-top:1px solid var(--line);padding:16px 0}
.faq summary{cursor:pointer;list-style:none;font-weight:500;font-size:1rem}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"+";float:right;color:var(--accent);font-family:"IBM Plex Mono",ui-monospace,monospace;
  display:inline-block;transition:transform .2s cubic-bezier(.32,.72,0,1);
}
/* Rotate the same glyph rather than swap it for "–" — the same 45° turn the
   nav hamburger already does, so the site has one gesture for "this opens". */
.faq details[open] summary::after{transform:rotate(45deg)}
.faq p{margin:12px 0 0;color:#5F544D;font-size:.95rem;max-width:70ch;text-wrap:pretty;animation:reveal .18s cubic-bezier(.32,.72,0,1)}

@keyframes reveal{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){
  .faq p,.study__abstract p{animation:none}
}

/* ---------- gallery ---------- */
.gallery{display:grid;grid-template-columns:1.6fr 1fr 1fr;grid-template-rows:repeat(2,190px);gap:14px}
.gallery img{width:100%;height:100%;object-fit:cover;border-radius:var(--radius)}
.gallery__tall{grid-row:span 2}
.gallery__cta{
  background:var(--band);border-radius:var(--radius);display:flex;flex-direction:column;
  justify-content:flex-end;padding:22px;text-decoration:none;color:var(--ink);
}
.gallery__cta strong{font-family:"Fraunces",Georgia,serif;font-size:1.3rem;font-weight:500;line-height:1.2}
.gallery__cta span{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.76rem;color:#A44C31;margin-top:8px}

/* ---------- reviews ---------- */
/* A row rather than a grid: three cards was a hard ceiling on how many
   reviews could ever show. A scrollable strip has no ceiling, and a card's
   fixed width leaves the next one peeking at the edge as the cue that there
   is more — the same affordance the arrow buttons give explicitly. */
.reviews{display:flex;gap:22px;overflow-x:auto;scroll-snap-type:x proximity;padding-bottom:6px;scrollbar-gutter:stable}
/* margin:0 is not cosmetic. These are blockquotes, and the browser's own
   stylesheet gives a blockquote margin:1em 40px — so every card was handing
   80px back to nothing, at every width. The grid column was 334px on a phone
   and the card drew at 254. */
.review{
  margin:0;background:var(--surface);border:1px solid var(--line);padding:26px;border-radius:var(--radius);
  display:flex;flex-direction:column;gap:14px;flex:0 0 min(340px,85%);scroll-snap-align:start;
}
.review .stars{color:#D08A6C;letter-spacing:2px;font-size:.9rem}
.review p{margin:0;font-size:.95rem;flex-grow:1}
.review cite{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.74rem;color:var(--stone);
  text-transform:uppercase;letter-spacing:.04em;font-style:normal;
}
.reviews-controls{display:flex;gap:10px;margin:22px 0 0}
/* 44px is the floor for anything tappable below 960px elsewhere in the
   theme; these stay at it everywhere rather than shrinking on desktop only
   to grow back at the breakpoint. */
.reviews-nav{
  width:44px;height:44px;border-radius:50%;flex:0 0 auto;
  border:1px solid var(--line);background:var(--surface);color:var(--ink-2);
  font-size:1rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;
}
.reviews-nav:hover{background:var(--band)}
.reviews-nav:disabled{opacity:.35;cursor:default}
.reviews-nav:disabled:hover{background:var(--surface)}

/* ---------- footer ---------- */
.site-footer{background:var(--dark);color:#F1E9DE;padding:70px 0 30px}
.site-footer a{color:rgba(241,233,222,.75);text-decoration:none}
.site-footer a:hover{color:var(--glow)}
.site-footer h3{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.74rem;letter-spacing:.12em;
  text-transform:uppercase;color:var(--glow);margin:0 0 16px;font-weight:500;
}
.footer-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:48px;margin-bottom:44px}
.hours{width:100%;border-collapse:collapse}
.hours td{padding:9px 0;border-bottom:1px solid var(--dark-line);font-size:.94rem}
.hours td:last-child{text-align:right;color:rgba(241,233,222,.7)}
.hours tr.closed td:last-child{color:rgba(241,233,222,.42)}
.footer-list{list-style:none;margin:0 0 24px;padding:0;display:flex;flex-direction:column;gap:10px}
.footer-list a{font-size:.92rem}
.site-footer .btn-ghost{border-color:rgba(241,233,222,.28);color:#F1E9DE}
.site-footer .btn-ghost:hover{border-color:var(--glow);color:var(--glow)}
.footer-bottom{
  border-top:1px solid var(--dark-line);padding-top:24px;display:flex;justify-content:space-between;
  flex-wrap:wrap;gap:12px;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:.76rem;color:rgba(241,233,222,.45);
}

/* ---------- responsive ---------- */
@media (max-width:960px){
  .hero__split,.mission,.band__grid,.detail,.about-intro{grid-template-columns:1fr;gap:40px}
  .detail__aside{position:static}
  .hero__media,.band__media{height:320px}
  .footer-grid{grid-template-columns:1fr;gap:36px}
  .gallery{grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,180px)}
  .gallery__tall{grid-row:span 1}
  /* Four photos and the link make five things in a two-column grid, so the
     last cell was empty and read as a hole in the page rather than as spacing.
     The link takes the whole row instead. The grid is the same height either
     way — this fills the gap, it does not add one. */
  .gallery__cta{grid-column:1 / -1}
}
@media (max-width:1080px){
  /* Both buttons drop to their own row rather than one of them disappearing. */
  .header-actions{flex:1 1 100%;justify-content:flex-start}
  .site-nav ul{gap:20px}
}
/* 960, not 760. Between those widths the nav was still a hover menu, and a
   hover menu on a touch screen is a trap: the first tap follows the parent
   link, so the children are unreachable. Tablets get the toggle menu, where
   the submenus sit open. */
@media (max-width:960px){
  /* Touch targets. Measured at 375px, every control below came in under the
     44px a fingertip needs: the toggle at 34x22, menu items at 22-27, FAQ
     summaries at 25, footer links at 18. The buttons were already right, so
     this is only the text-based controls. Scoped to the toggle breakpoint —
     a mouse does not need the extra room, and desktop keeps its density. */
  .nav-toggle{display:flex;align-items:center;justify-content:center;order:2;width:44px;height:44px;padding:0}
  /* The bars stay 22px wide. Only the button around them grows, so the
     hamburger looks the same and merely stops being hard to hit. */
  .site-logo,.custom-logo-link{order:1;flex:0 1 auto;min-width:0}
  .site-logo img,.custom-logo-link img{height:40px;max-width:100%;object-fit:contain}
  .header-actions{order:3;flex:1 1 100%;justify-content:flex-start;gap:10px}
  .header-actions .btn{flex:1 1 0;min-width:0;justify-content:center;padding:12px 10px;font-size:.7rem}
  /* Opens as a measured reveal instead of a display:none/block snap. Grid
     rows animate cleanly without a JS height read, and grow from directly
     under the toggle, which is where the panel already sits in the flow —
     same spatial relationship the toggle's own bars already promise. The
     visibility swap is stepped, not transitioned, so the collapsed panel
     still leaves the tab order and screen readers exactly as display:none
     did; only the open state is reachable. */
  .site-nav{
    order:4;flex:1 1 100%;background:transparent;border:0;padding:0;
    display:grid;grid-template-rows:0fr;visibility:hidden;
    transition:grid-template-rows .28s cubic-bezier(.32,.72,0,1),visibility 0s .28s;
  }
  /* padding:0 here, not on the collapsed rule below — overflow:hidden clips
     the ul's content but not its own box, so padding would still hold the
     row open at 12px even with nothing visible inside it. */
  .site-nav > ul{overflow:hidden;min-height:0;padding:0}
  .site-nav.is-open{
    grid-template-rows:1fr;visibility:visible;
    transition:grid-template-rows .28s cubic-bezier(.32,.72,0,1),visibility 0s;
  }
  /* The gap moves inside the links: as a gap it was dead space between
     targets, as padding it is part of them. The rhythm is unchanged. */
  .site-nav ul{flex-direction:column;gap:0}
  .site-nav.is-open > ul{padding:8px 0 4px}
  .site-nav a{display:block;padding:11px 0}
  /* No hovering on a phone: submenus sit open and indented. */
  .site-nav .sub-menu{
    display:flex;position:static;border:0;box-shadow:none;background:transparent;
    padding:10px 0 0;margin:0 0 0 14px;gap:12px;min-width:0;
    border-left:1px solid var(--line);
  }
  .site-nav .sub-menu{gap:0}
  .site-nav .sub-menu a{padding:11px 0 11px 14px}
  .site-nav .menu-item-has-children > a::after{display:none}
  /* The repeated parent earns its place on desktop, where the top-level item
     is hover-only and the dropdown is a separate surface. Here the parent is
     already on screen and tappable, one line above its children, so the copy
     is just the same word twice. */
  .site-nav .sote-overview{display:none}
  /* The FAQ summaries are the whole interface on /salt-cave-therapy/ — twelve
     of them, and the only way into the answers. The padding comes off the
     details and goes onto the summary, so the row is the same height on the
     page but all of it is tappable rather than just the text. */
  .faq details{padding:6px 0}
  .faq summary{padding:10px 0}
  /* Same trade in the footer: the gap becomes padding. */
  .footer-list{gap:0}
  .footer-list a{display:inline-block;padding:13px 0}
}
@media (max-width:960px) and (prefers-reduced-motion:reduce){
  .site-nav,.site-nav.is-open{transition:visibility 0s}
}
