/* Studio 303 Wellness Collective
   Design system reverse-engineered from ikebanastudioma.com.au (see ref/DESIGN-SYSTEM.md).
   Signature moves carried over: 0.1em display tracking, zero radius everywhere,
   hairline cards, cream/photo alternation, ghost buttons, fade-and-rise on scroll. */

/* Clash Display is SELF-HOSTED on purpose. frontlinewebdesign.tech sends a site-wide CSP with
   `font-src 'self' data: https://fonts.gstatic.com` and `style-src 'self' ... fonts.googleapis.com`,
   so the Fontshare CDN is blocked outright (observed live as FAILED(csp), headings silently fell
   back to Helvetica). Editing that CSP would touch Tanner's whole live site, so the font comes
   from our own directory instead, which `'self'` already permits. Licence: Fontshare, free for
   commercial use including webfont embedding. */
@font-face {
  font-family: "Clash Display";
  src: url("../fonts/clash-display-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Clash Display";
  src: url("../fonts/clash-display-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

/* ---------- tokens ---------- */
:root {
  --ink: #242323;
  --cream: #eee9dd;
  --paper: #fefaef;
  --white: #ffffff;
  --muted: #5f5a55;          /* 5.7:1 on cream */
  --hairline: #d6cfc0;
  /* Two pairs on purpose. The -text values are for body-size runs and clear 4.5:1. The vivid
     pair is ONLY for the display-size glyph, which renders 44px to 88px and is therefore WCAG
     large text at a 3:1 bar. The first cut used the dark pair everywhere, which passed 4.5:1
     it did not need and made the centrepiece look washed out. Measured: hot 3.86:1, cold 3.96:1. */
  --ember: #d14209;          /* vivid hot, display only */
  --plunge: #0e7c9b;         /* vivid cold, display only */
  --ember-text: #a34524;     /* 5.05:1 on cream, for small text like the chips */
  --plunge-text: #2e5a6b;    /* 6.21:1 on cream */

  /* Clash Display (Fontshare, free for commercial use) replaced Space Grotesk on 2026-08-24.
     Two reasons: impeccable flags Space Grotesk as one of the faces every AI-generated UI
     converges on, and Clash is a much closer stand-in for the reference site's Benzin. */
  --display: "Clash Display", "Helvetica Neue", Arial, sans-serif;
  --body: "Work Sans", "Helvetica Neue", Arial, sans-serif;

  --track: 0.1em;
  --measure: 45ch;
  --gutter: clamp(1.25rem, 5vw, 5rem);
  --band: clamp(5rem, 12vw, 11rem);   /* vertical section rhythm */
}

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------- a11y ---------- */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--ink); color: var(--cream);
  padding: 0.75rem 1.25rem; text-decoration: none;
  font-family: var(--display); text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.75rem;
}
.skip-link:focus { left: 0; }

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
.on-photo :focus-visible, .band-dark :focus-visible { outline-color: var(--cream); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---------- type ---------- */
h1, h2, h3, .display {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track);
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: clamp(1.9rem, 1.1rem + 3.4vw, 3.3rem); line-height: 1.2; }
h2 { font-size: clamp(1.5rem, 1.05rem + 1.9vw, 2.2rem); line-height: 1.3; }
h3 { font-size: clamp(1.15rem, 1rem + 0.7vw, 1.6rem); line-height: 1.6; }
p  { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }

.eyebrow {
  font-family: var(--display);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.3em;
  color: var(--muted);
  margin: 0 0 0.9rem;
}
.on-photo .eyebrow, .band-dark .eyebrow { color: var(--cream); opacity: 0.85; }

.statement {
  font-size: clamp(1.25rem, 0.95rem + 1.5vw, 1.95rem);
  line-height: 1.4;
  max-width: var(--measure);
}
.lede { font-size: clamp(1.05rem, 1rem + 0.4vw, 1.25rem); max-width: 58ch; color: var(--muted); }

/* ---------- layout ---------- */
.wrap { width: min(1280px, 100%); margin-inline: auto; padding-inline: var(--gutter); }
.band { padding-block: var(--band); }
.band-cream { background: var(--cream); }
.band-paper { background: var(--paper); }
.band-dark  { background: var(--ink); color: var(--cream); }
.band-dark .lede, .band-dark .muted { color: #cdc6b8; }

.stack > * + * { margin-top: 1.25rem; }
.stack-lg > * + * { margin-top: clamp(2rem, 4vw, 3.5rem); }

/* ---------- header ---------- */
/* ALWAYS fixed. It used to be `absolute` over the hero and `sticky` after it, and swapping
   between them put the header back into flow, which shifted the page by its own 92px, which
   moved the hero, which re-fired the observer, which flipped it back. That feedback loop is
   the flicker at the bottom of the hero. Only paint changes now, never layout, so there is
   nothing left to oscillate. Every page opens with a full-bleed .hero, so nothing is covered. */
.site-header {
  position: fixed; inset-inline: 0; top: 0; z-index: 100;
  background: var(--cream);
  border-bottom: 1px solid var(--hairline);
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease);
}
.site-header.is-over {           /* transparent over a hero photo */
  background: transparent; border-bottom-color: transparent;
}
.header-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; min-height: 92px;   /* raised with the logo from 52px to 68px */
}
.wordmark { display: inline-flex; align-items: center; text-decoration: none; }
/* The supplied logo is a black square badge. It reads on cream, but on a dark hero photo it
   measured 1.10:1 against the wall behind it, i.e. camouflaged. So there are two marks: the
   badge for the solid cream header, and a knockout of the same artwork (black field dropped,
   marks recoloured cream) for when the header sits over photography. Never an invert filter,
   which would turn the badge into a white tile. */
.wordmark { position: relative; display: inline-flex; }
.wordmark img { height: 68px; width: auto; display: block; }
.wordmark .logo-light { display: none; }
.is-over .wordmark .logo-light { display: block; }
.is-over .wordmark .logo-dark { display: none; }

.nav { display: flex; align-items: center; gap: clamp(1rem, 2vw, 2rem); }
/* :not(.btn) matters. `.nav a` is specificity 0,1,1 and beats `.btn`'s 0,1,0, so without the
   exclusion this padding won and the Book button rendered at `8px 0`: its border sat 1px off
   the "B". The underline rule has the same problem, fighting the button's own 1px border. */
.nav a:not(.btn) {
  font-family: var(--display);
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.16em;
  text-decoration: none; padding: 0.5rem 0;
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s ease;
}
/* The hover underline is drawn by the wiping ::after in the motion block, not by this border. */
.nav .btn { padding: 0.8rem 1.5rem; }
.is-over .nav a, .is-over .nav-toggle { color: var(--cream); }

.nav-toggle {
  display: none; background: none; border: 1px solid currentColor; color: inherit;
  font-family: var(--display); font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.2em; padding: 0.6rem 0.9rem; cursor: pointer; border-radius: 0;
}

@media (max-width: 860px) {
  /* z-index needs a position to take effect. Without these two the fixed .nav overlay below
     paints over its own toggle inside the header's stacking context, elementFromPoint at the
     button's centre returns the nav, and the menu becomes a trap: body scroll is locked and the
     only exits are following a link or pressing Escape, which a phone does not have. */
  .nav-toggle { display: block; position: relative; z-index: 160; }
  .nav {
    position: fixed; inset: 0; z-index: 150;
    flex-direction: column; align-items: flex-start; justify-content: center;
    gap: 1.5rem; padding: var(--gutter);
    background: var(--cream); color: var(--ink);
    transform: translateX(100%); transition: transform 0.3s ease;
  }
  .nav[data-open="true"] { transform: translateX(0); }
  .nav a { font-size: 1.05rem; }
  /* Recolour only the PLAIN links. The old rule was `.is-over .nav a` plus `.nav .btn`, which
     also repainted .btn-solid's text to ink while leaving its ink background alone: the Book
     button rendered as a solid black rectangle at 1.00:1. Measured live before the fix. */
  .is-over .nav a:not(.btn-solid) { color: var(--ink); }
  .nav .btn:not(.btn-solid) { color: var(--ink); border-color: var(--ink); }
  .nav .btn-solid { background: var(--ink); color: var(--cream); border-color: var(--ink); }
}

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 0.6rem;
  font-family: var(--display);
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.18em;
  padding: 0.85rem 1.4rem;
  border: 1px solid var(--ink); border-radius: 0;
  background: transparent; color: var(--ink);
  text-decoration: none; cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
}
.btn::after { content: "\2197"; font-size: 0.95em; line-height: 1; }
.btn:hover { background: var(--ink); color: var(--cream); }
.btn-solid { background: var(--ink); color: var(--cream); }
.btn-solid:hover { background: transparent; color: var(--ink); }
.on-photo .btn, .band-dark .btn { border-color: var(--cream); color: var(--cream); }
.on-photo .btn:hover, .band-dark .btn:hover { background: var(--cream); color: var(--ink); }
.band-dark .btn-solid { background: var(--cream); color: var(--ink); }
.band-dark .btn-solid:hover { background: transparent; color: var(--cream); }
.btn-plain::after { content: none; }

/* ---------- hero ---------- */
.hero { position: relative; min-height: min(92vh, 820px); display: grid; place-items: center; overflow: hidden; }
.hero-media { position: absolute; inset: 0; }
.hero-media img { width: 100%; height: 100%; object-fit: cover; }
.hero-media::after {
  content: ""; position: absolute; inset: 0;
  /* Mid stop is deliberately heavy: the hero photo is a bright orange sauna, and at 0.18 the h1
     measured 4.49:1, i.e. exactly on the AA line. Measured again at 0.38 for real margin. */
  background: linear-gradient(180deg, rgba(20,18,17,0.5) 0%, rgba(20,18,17,0.38) 45%, rgba(20,18,17,0.55) 100%);
}
.hero-inner { position: relative; text-align: center; color: var(--cream); padding: 8rem var(--gutter) 4rem; }
.hero h1 { color: var(--cream); }
.hero-sub {
  font-family: var(--display);
  font-size: clamp(0.85rem, 0.75rem + 0.5vw, 1.1rem);
  letter-spacing: 0.35em; text-transform: uppercase;
  margin: 1.4rem 0 2.4rem; color: var(--cream); opacity: 0.92;
}
/* Interior heroes sit on whatever photo the page owns, and several of those are very bright
   (loft windows, a white carafe, a palm on cream). Measured with a shared gradient they ran
   2.1:1 to 3.5:1. Darkening the image enough to fix that needs ~0.9 alpha, which destroys the
   photograph, so the title gets its own solid panel instead. Flat, square, no radius: same
   language as the logo badge and the hairline cards. */
.hero-page { min-height: min(58vh, 540px); }
.hero-page .hero-inner {
  background: var(--ink);
  padding: clamp(1.75rem, 3.5vw, 3rem) clamp(1.75rem, 4vw, 3.5rem);
  margin: 7rem var(--gutter) 2rem;
  max-width: min(780px, 100%);
}
.hero-page .hero-sub { margin-bottom: 0; }

/* ---------- concept panel (the centrepiece) ---------- */
.concept { display: grid; gap: clamp(2rem, 5vw, 5rem); align-items: start; }
@media (min-width: 880px) { .concept { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); } }
/* Stacked so the gap between the two words IS the idea. Sized off the container, never the
   viewport alone, so it cannot run under the text column beside it. */
/* The gap IS the idea, so it must not be the thing that collapses on a phone. The first cut
   bottomed out at 2.6rem/1.75rem, which put the glyph at ~42px and the gap at 28px at 390px,
   i.e. the centrepiece was weakest exactly where most of the traffic is. Floors raised. */
.concept-glyph {
  font-family: var(--display);
  font-size: clamp(4rem, 2.4rem + 8vw, 7.5rem);
  line-height: 0.88; letter-spacing: 0.01em;
  margin: 0; display: flex; flex-direction: column; align-items: flex-start;
}
.concept-glyph .cold { color: var(--plunge); }
.concept-glyph .hot  { color: var(--ember); }
/* The gap was a 1px hairline in #d6cfc0, which on cream is close to invisible, so the one
   element carrying the whole concept read as empty space. It is now the temperature gradient
   itself: hot at the top, cold at the bottom, with the space still doing the work. */
.concept-glyph .gap {
  align-self: stretch; height: clamp(2.75rem, 6vw, 3.25rem);
  margin-left: 0.08em; position: relative;
}
.concept-glyph .gap::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 7px;
  background: linear-gradient(180deg, var(--ember) 0%, var(--plunge) 100%);
}
.concept-gloss {
  font-family: var(--display); letter-spacing: 0.34em; text-transform: uppercase;
  font-size: 0.92rem; color: var(--ink); margin-top: 1.6rem;
}

/* ---------- cards ---------- */
.grid { display: grid; gap: clamp(1.25rem, 2.5vw, 2rem); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }

.card {
  border: 1px solid var(--hairline);
  border-radius: 0;
  padding: clamp(1.1rem, 2vw, 1.75rem);
  background: transparent;
  display: flex; flex-direction: column;
  transition: transform 0.3s ease, border-color 0.3s ease;
}
.card:hover { transform: translateY(-6px); border-color: var(--ink); }
.card-media { aspect-ratio: 4 / 3; overflow: hidden; margin-bottom: 1.5rem; }
.card-media img { width: 100%; height: 100%; object-fit: cover; }
.card h3 { margin-bottom: 0.9rem; }
.card p { color: var(--muted); }
/* --muted is tuned for cream. On the dark band it lands at 2.3:1, so it must be overridden. */
.band-dark .card p { color: #cdc6b8; }
.card .btn { margin-top: auto; align-self: flex-start; }
/* "From $45" line on a card. Display face so it reads as a price, not as body copy. */
.from {
  font-family: var(--display);
  font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--ink); margin: 0 0 1.3rem; padding-top: 0.9rem;
  border-top: 1px solid var(--hairline);
}
.from .amount { font-size: 1.15rem; }
.from .qual { color: var(--muted); letter-spacing: 0.1em; }
.band-dark .from { color: var(--cream); border-top-color: rgba(238,233,221,0.22); }
.band-dark .from .qual { color: #cdc6b8; }
.card > p + .btn, .card .stack + .btn { margin-top: 1.5rem; }

/* ---------- split (image + text) ---------- */
.split { display: grid; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
@media (min-width: 880px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split-flip .split-media { order: 2; }
}
.split-media img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }
.split-media.wide img { aspect-ratio: 4 / 3; }

/* ---------- photo band with overlaid title ---------- */
.photo-band { position: relative; min-height: min(70vh, 620px); display: grid; align-items: center; overflow: hidden; }
.photo-band > .wrap { position: relative; z-index: 1; }
.photo-band-media { position: absolute; inset: 0; }
.photo-band-media img { width: 100%; height: 100%; object-fit: cover; }
.photo-band-media::after {
  content: ""; position: absolute; inset: 0;
  /* Left side carries the text, so it is heavy. Measured: at 0.66 the longest rule on the sauna
     page fell to 3.88:1 where the bright white tub sits behind it. */
  background: linear-gradient(90deg, rgba(20,18,17,0.82) 0%, rgba(20,18,17,0.58) 45%, rgba(20,18,17,0.2) 100%);
}
/* Colour the whole overlay block, not just h2 and p. Scoping it to those two left <li> text
   inheriting --ink over a dark photograph, which measured as unreadable on the sauna page. */
.photo-band > .wrap { color: var(--cream); }
.photo-band .facts li { border-bottom-color: rgba(238, 233, 221, 0.25); }
.photo-band .facts .k, .photo-band .eyebrow { color: var(--cream); opacity: 0.85; }
.photo-band .measure { max-width: 46ch; }

/* ---------- price list ---------- */
.pricelist { border-top: 1px solid var(--hairline); }
.pricelist li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1rem;
  padding: 1.15rem 0; border-bottom: 1px solid var(--hairline);
}
.pricelist { list-style: none; margin: 0; padding: 0; }
.pricelist .name { font-family: var(--display); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.95rem; }
.pricelist .rule { flex: 1 1 2rem; border-bottom: 1px dotted var(--hairline); translate: 0 -0.25rem; min-width: 1rem; }
.pricelist .price { font-family: var(--display); letter-spacing: 0.06em; white-space: nowrap; }
.pricelist .note { flex-basis: 100%; color: var(--muted); font-size: 0.92rem; }

/* ---------- gallery ----------
   min(50%, 220px) not min(100%, 220px): the 100% floor collapsed this to ONE column at 390px,
   making six square photos 2,350px tall, 22% of the mobile page, with no text in any of it.
   Phone gets two up. Desktop is untouched at six across. */
/* 48%, not 50%. At 390px the 2px gap made two 195px columns need 392px, so auto-fit fell back
   to one and the change silently did nothing. Measured, not assumed. */
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(48%, 220px), 1fr)); gap: 2px; }
.gallery img { width: 100%; aspect-ratio: 1; object-fit: cover; }

/* ---------- stat ----------
   "5.0 from 111 reviews" is the most persuasive fact available and it was set as a 12px muted
   eyebrow. Scale and weight carry hierarchy, so it gets to be the size of the claim it makes. */
.stat { display: flex; align-items: baseline; gap: 0.85rem; flex-wrap: wrap; margin: 0 0 0.6rem; }
.stat .figure {
  font-family: var(--display); font-size: clamp(2.75rem, 1.8rem + 4vw, 4.5rem);
  line-height: 0.9; letter-spacing: 0.02em;
}
.stat .of {
  font-family: var(--display); font-size: 0.8rem; letter-spacing: 0.28em;
  text-transform: uppercase; opacity: 0.85; max-width: 14ch; line-height: 1.5;
}

/* A price row the eye can land on, so an eight-row grid has an anchor instead of eight equals. */
.pricelist li.pick { position: relative; }
/* "Best per minute" is ARITHMETIC from their own published prices, not a popularity claim:
   $45/30min = $1.50, $65/45 = $1.44, $85/60 = $1.42. If the client confirms which session is
   actually most booked, swap this string, it is the better anchor. */
.pricelist li.pick .name::after {
  content: "Best per minute"; display: inline-block; margin-left: 0.75rem;
  font-family: var(--display); font-size: 0.6rem; letter-spacing: 0.22em;
  padding: 0.2rem 0.5rem; border: 1px solid var(--ember-text); color: var(--ember-text);
  vertical-align: middle; text-transform: uppercase;
}
.band-dark .pricelist li.pick .name::after { border-color: #e39c7f; color: #e39c7f; }

/* ---------- facts / contact ---------- */
.facts { list-style: none; margin: 0; padding: 0; }
.facts li { padding: 0.9rem 0; border-bottom: 1px solid var(--hairline); }
.band-dark .facts li { border-bottom-color: rgba(238,233,221,0.22); }
.facts .k { font-family: var(--display); text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.72rem; color: var(--muted); display: block; margin-bottom: 0.35rem; }
.band-dark .facts .k { color: #cdc6b8; }
.facts a { display: inline-block; padding: 0.25rem 0; text-decoration: none; border-bottom: 1px solid currentColor; }

/* 0.72rem = 11.5px. Was 0.6rem = 9.6px, which impeccable correctly flagged as under its 11px
   floor for functional UI text. */
.tbc {
  display: inline-block; margin-left: 0.5rem;
  font-family: var(--display); font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--ember-text); border: 1px solid var(--ember-text); padding: 0.15rem 0.4rem; vertical-align: middle;
}
.band-dark .tbc { color: #e39c7f; border-color: #e39c7f; }

/* ---------- map ----------
   A baked static image, not an embed. Two reasons, both measured: the site-wide CSP has
   `frame-src 'self' app.cal.com cal.com facebook.com ...` with no google.com, so a Maps iframe
   is blocked outright, and Google's Maps JS is heavy enough to undo a 740ms LCP. This is one
   52KB image with zero runtime dependencies, wrapped in a link to the real thing. */
.map-figure { margin: 0; border: 1px solid var(--hairline); }
.map-static { position: relative; }
.map-figure img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.map-activate {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font-family: var(--display); font-size: 0.75rem; letter-spacing: 0.2em;
  text-transform: uppercase; padding: 1rem 1.6rem; cursor: pointer;
  background: var(--ink); color: var(--cream); border: 1px solid var(--ink); border-radius: 0;
  transition: transform 0.3s var(--ease);
}
.map-activate:hover { transform: translate(-50%, -50%) translateY(-3px); }
.map-activate[disabled] { opacity: 0.7; cursor: default; }
/* Leaflet needs a real height; aspect-ratio alone leaves it 0 and the tiles never paint. */
.map-canvas { width: 100%; aspect-ratio: 16 / 9; min-height: 320px; background: var(--hairline); }
.map-canvas:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.map-figure figcaption {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; justify-content: space-between;
  padding: 0.9rem 1.1rem; border-top: 1px solid var(--hairline);
  font-family: var(--display); font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase;
}
.map-figure figcaption a { text-decoration: none; border-bottom: 1px solid currentColor; }
.band-dark .map-figure { border-color: rgba(238,233,221,0.22); }
.band-dark .map-figure figcaption { border-top-color: rgba(238,233,221,0.22); }
.band-dark .map-activate { background: var(--cream); color: var(--ink); border-color: var(--cream); }
/* Every Leaflet override is scoped under .map-canvas on purpose. leaflet.css is injected at
   click time, so it lands AFTER this file and wins any tie on source order. Scoping takes these
   to 0,2,0 against Leaflet's 0,1,0, which wins on specificity no matter when it loads.
   The unscoped version of these rules silently did nothing: the popup stayed default white
   with blue links. */
.map-canvas.leaflet-container { font-family: var(--body); }
.map-canvas .leaflet-container a,
.map-canvas .leaflet-control-attribution a { color: var(--plunge-text); }
.map-canvas .leaflet-bar a,
.map-canvas .leaflet-control-attribution { border-radius: 0; }
.map-canvas .leaflet-bar a { color: var(--ink); }
/* Ember marker, drawn in CSS so it uses the real token and matches the dot on the static
   placeholder. Replaces Leaflet's default blue PNG pin, which was the one off-brand element. */
.map-marker { background: none; border: 0; }
.map-marker-dot {
  display: block; width: 26px; height: 26px; border-radius: 50%;
  background: var(--ember); border: 6px solid var(--cream);
  box-shadow: 0 0 0 1px rgba(36, 35, 35, 0.35);
}
.map-canvas .leaflet-popup-content-wrapper,
.map-canvas .leaflet-popup-tip {
  border-radius: 0; background: var(--ink); color: var(--cream); box-shadow: none;
}
.map-canvas .leaflet-popup-content { font-size: 0.9rem; line-height: 1.55; margin: 1rem 1.2rem; }
.map-canvas .leaflet-popup-content strong {
  font-family: var(--display); font-weight: 400; letter-spacing: 0.06em;
  text-transform: uppercase; display: block; margin-bottom: 0.35rem;
}
.map-canvas a.leaflet-popup-close-button { color: var(--cream); }
.map-canvas a.leaflet-popup-close-button:hover { color: var(--cream); opacity: 0.7; }

/* ---------- footer ---------- */
.site-footer { background: var(--paper); border-top: 1px solid var(--hairline); padding-block: clamp(3rem, 7vw, 5.5rem) 2rem; }
.footer-grid { display: grid; gap: clamp(2rem, 4vw, 3rem); grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }
.site-footer h2 { font-size: 0.75rem; letter-spacing: 0.28em; margin-bottom: 1.1rem; }
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: 0.25rem; }
/* inline-block + padding so every footer link clears a 24px tap target on mobile */
.site-footer a { display: inline-block; padding: 0.3rem 0; text-decoration: none; border-bottom: 1px solid transparent; }
.site-footer a:hover { border-bottom-color: currentColor; }
.footer-base {
  margin-top: clamp(2.5rem, 5vw, 4rem); padding-top: 1.5rem;
  border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
  font-size: 0.82rem; color: var(--muted);
}
.footer-base a { color: inherit; }

/* ---------- scroll reveal ----------
   Content is visible by default. Only once app.js has confirmed it can run and can observe
   does it add .js-reveal to <html> and opt the page into hiding. No JS, failed JS, or a
   ?noreveal query string all leave every section on screen. */
/* One shared easing. Long out, no bounce: motion should whisper. */
:root { --ease: cubic-bezier(0.22, 0.61, 0.36, 1); }

.reveal { transition: opacity 0.8s var(--ease), transform 0.8s var(--ease); }
.js-reveal .reveal { opacity: 0; transform: translateY(18px); }
.js-reveal .reveal.is-in { opacity: 1; transform: none; }

/* Stagger grid and list children instead of firing the whole block at once. A single
   simultaneous fade-up on every section is the pattern the quality bar calls slop. */
.js-reveal .grid.is-in > *,
.js-reveal .pricelist.is-in > li,
.js-reveal .gallery.is-in > * {
  animation: rise 0.7s var(--ease) both;
}
.js-reveal .grid.is-in > :nth-child(1),
.js-reveal .pricelist.is-in > :nth-child(1) { animation-delay: 0.04s; }
.js-reveal .grid.is-in > :nth-child(2),
.js-reveal .pricelist.is-in > :nth-child(2) { animation-delay: 0.12s; }
.js-reveal .grid.is-in > :nth-child(3),
.js-reveal .pricelist.is-in > :nth-child(3) { animation-delay: 0.20s; }
.js-reveal .grid.is-in > :nth-child(4),
.js-reveal .pricelist.is-in > :nth-child(4) { animation-delay: 0.28s; }
.js-reveal .grid.is-in > :nth-child(n+5),
.js-reveal .pricelist.is-in > :nth-child(n+5) { animation-delay: 0.36s; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* THE signature move. The concept is "the work happens in the space between", so on reveal the
   gap between Hot and Cold actually opens. The idea is the animation, not decoration on it. */
.js-reveal .concept-glyph .gap { height: 0; transition: height 1.1s var(--ease) 0.15s; }
.js-reveal .reveal.is-in .concept-glyph .gap { height: clamp(2.75rem, 6vw, 3.25rem); }

/* Hero image settles rather than sitting still. Barely perceptible, which is the point. */
.hero-media img { animation: settle 1.6s var(--ease) both; }
@keyframes settle { from { transform: scale(1.045); } to { transform: none; } }

/* The arrow leans out on hover. One property, transform only, so it cannot cause layout. */
.btn::after { transition: transform 0.3s var(--ease); }
.btn:hover::after { transform: translate(3px, -3px); }

/* Nav underline wipes in from the left instead of just appearing. */
.nav a:not(.btn) { position: relative; border-bottom-color: transparent; }
.nav a:not(.btn)::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform 0.35s var(--ease);
}
.nav a:not(.btn):hover::after,
.nav a:not(.btn)[aria-current="page"]::after { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal { opacity: 1; transform: none; }
  .js-reveal .grid.is-in > *,
  .js-reveal .pricelist.is-in > li,
  .js-reveal .gallery.is-in > * { animation: none; }
  .js-reveal .concept-glyph .gap,
  .js-reveal .reveal.is-in .concept-glyph .gap {
    height: clamp(2.75rem, 6vw, 3.25rem); transition: none;
  }
  .hero-media img { animation: none; }
  .btn:hover::after { transform: none; }
  .nav a:not(.btn)::after { transition: none; }
}
