/* ══════════════════════════════════════════════════════════
   Tyce — hair & barber
   Art direction: her own line, "there is no such thing as bad
   hair", set as a manifesto. Violet pulled from her existing
   logo mark and pushed to saturation so the brand reads
   defiant rather than sweet. Ember red is reserved almost
   entirely for the strike-throughs on the "too much" wall.
   ══════════════════════════════════════════════════════════ */

/* ---------- tokens ---------- */
:root{
  --ink:#17121C;
  --ink-2:#1F1826;
  --ink-3:#2C2136;
  --bone:#F6F1F4;
  --bone-2:#EAE1E7;
  --orchid:#FF3DBE;
  --orchid-dim:#D633A9;
  --plum:#33153F;
  --plum-2:#4A1E5C;
  --ember:#FF5E5B;
  --clay:#E8C8B0;

  --display:'Bricolage Grotesque',system-ui,sans-serif;
  --serif:'Instrument Serif',Georgia,serif;
  --body:'Work Sans',system-ui,sans-serif;

  --pad:clamp(1.25rem,5vw,5.5rem);
  --maxw:1440px;
  --ease:cubic-bezier(.22,.61,.36,1);
}

/* ---------- reset ---------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  background:var(--ink);
  color:var(--bone);
  font-family:var(--body);
  font-size:clamp(1rem,.55vw + .9rem,1.15rem);
  line-height:1.6;
  font-weight:350;
  overflow-x:hidden;
}
img{display:block;max-width:100%;height:auto}
h1,h2,h3{margin:0;font-weight:inherit}
p{margin:0}
ul{margin:0;padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit;border:0;background:none;cursor:pointer}

/* focus — visible everywhere, never removed */
:focus-visible{
  outline:2.5px solid var(--orchid);
  outline-offset:3px;
  border-radius:2px;
}

/* screen-reader-only: readable by AT, invisible on screen. Not display:none —
   that would hide it from assistive tech too. */
.sronly{
  position:absolute;width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);overflow:hidden;white-space:nowrap;
}

.skip{
  position:absolute;left:-9999px;top:0;z-index:200;
  background:var(--orchid);color:var(--ink);
  padding:.8rem 1.2rem;font-weight:600;
}
.skip:focus{left:.5rem;top:.5rem}

/* ══════════ reveal-on-scroll
   Deliberate: content is VISIBLE by default. JS only opts in
   to the pre-animation state, and only when it can guarantee
   it will also take it back out. The old site hid every block
   at opacity:0 in CSS and never fired the reveal — that is
   the bug this structure exists to make impossible. ══════════ */
.js-anim .reveal{opacity:0;transform:translateY(22px);transition:opacity .7s var(--ease),transform .7s var(--ease)}
.js-anim .reveal.in{opacity:1;transform:none}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
  .js-anim .reveal{opacity:1;transform:none}
  /* park the looping gradient on a good frame rather than a random one */
  .thesis{background-position:38% 50%!important}
  .hero__aurora span{animation:none!important}
}


/* ══════════ drifting section meshes ══════════
   The mesh lives on an oversized ::before and moves on transform only, so it
   stays on the compositor. isolation:isolate keeps z-index:-1 above the
   section's own base color but under every child. */
@keyframes meshdrift{
  from{transform:translate3d(-2.2%,-1.8%,0) scale(1)}
  to{transform:translate3d(2.4%,2.2%,0) scale(1.09)}
}
.wall,.work,.vows,.chairs,.school,.menuband{position:relative;isolation:isolate;overflow:hidden}
.wall::before,.work::before,.vows::before,.chairs::before,.school::before,.menuband::before{
  content:"";position:absolute;inset:-26%;z-index:-1;pointer-events:none;
  animation:meshdrift 44s ease-in-out infinite alternate;
  will-change:transform;
}
.wall::before{
  animation-duration:38s;
  background:
    radial-gradient(52rem 30rem at 18% 8%,rgba(255,61,190,.17),transparent 62%),
    radial-gradient(44rem 26rem at 88% 46%,rgba(232,200,176,.26),transparent 58%),
    radial-gradient(30rem 20rem at 55% 96%,rgba(255,94,91,.08),transparent 60%);
}
.work::before{
  animation-duration:48s;animation-delay:-12s;
  background:
    radial-gradient(60rem 34rem at 6% 22%,rgba(74,30,92,.5),transparent 62%),
    radial-gradient(46rem 30rem at 94% 82%,rgba(255,61,190,.13),transparent 60%);
}
.vows::before{
  animation-duration:42s;animation-delay:-24s;
  background:radial-gradient(42rem 26rem at 82% 12%,rgba(255,61,190,.22),transparent 60%);
}
.chairs::before{
  animation-duration:52s;animation-delay:-8s;
  background:
    radial-gradient(52rem 30rem at 50% 4%,rgba(74,30,92,.5),transparent 62%),
    radial-gradient(36rem 24rem at 10% 92%,rgba(255,61,190,.11),transparent 60%);
}
.school::before{
  animation-duration:40s;animation-delay:-30s;
  background:
    radial-gradient(46rem 28rem at 88% 10%,rgba(232,200,176,.32),transparent 58%),
    radial-gradient(50rem 30rem at 10% 92%,rgba(255,61,190,.13),transparent 60%);
}
.menuband::before{animation-duration:46s;animation-delay:-18s}

/* ---------- shared bits ---------- */
.sec-head{max-width:var(--maxw);margin:0 auto 3.5rem}
.sec-head__h{
  font-family:var(--display);
  font-size:clamp(2.4rem,6.5vw,5.5rem);
  font-weight:700;
  line-height:.92;
  letter-spacing:-.035em;
}
.sec-head__note{
  margin-top:1.4rem;max-width:46ch;
  font-size:1.02rem;line-height:1.65;opacity:.68;
}
.sec-head__note strong{color:var(--orchid);font-weight:600}

.btn{
  position:relative;overflow:hidden;isolation:isolate;
  display:inline-flex;align-items:center;gap:.5rem;
  padding:1rem 1.9rem;
  font-family:var(--body);font-size:.9rem;font-weight:600;
  letter-spacing:.09em;text-transform:uppercase;
  text-decoration:none;border-radius:999px;
  /* A pill must never wrap. Squeezed into the header on a 360px phone, "Book a
     chair" broke onto THREE lines and grew the bar to 74px tall. */
  white-space:nowrap;
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),
             border-color .35s var(--ease),color .35s var(--ease);
}
/* a light sweeps across on hover — one gesture, not a light show */
.btn::after{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(105deg,transparent 30%,rgba(255,255,255,.55) 50%,transparent 70%);
  transform:translateX(-130%);
  transition:transform .75s var(--ease);
}
.btn:hover::after{transform:translateX(130%)}

.btn--solid{
  color:#160F1E;
  background:linear-gradient(135deg,#FFA1E1 0%,#FF3DBE 46%,#9A2BF0 100%);
  box-shadow:0 6px 24px -6px rgba(255,61,190,.6),
             inset 0 1px 0 rgba(255,255,255,.45);
}
.btn--solid:hover{
  transform:translateY(-2px);
  box-shadow:0 12px 34px -8px rgba(255,61,190,.8),
             inset 0 1px 0 rgba(255,255,255,.55);
}
.btn--ghost{
  border:1px solid rgba(246,241,244,.22);
  color:var(--bone);
  background:linear-gradient(180deg,rgba(246,241,244,.08),rgba(246,241,244,.02));
  backdrop-filter:blur(10px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.08);
}
.btn--ghost:hover{
  border-color:rgba(255,61,190,.6);color:#fff;transform:translateY(-2px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 8px 26px -10px rgba(255,61,190,.5);
}
.btn--ghost::after{background:linear-gradient(105deg,transparent 30%,rgba(255,61,190,.3) 50%,transparent 70%)}
.btn--sm{padding:.6rem 1.15rem;font-size:.72rem}
.btn--wide{width:100%;justify-content:center}

/* ══════════════════ top bar ══════════════════ */
/* floating glass pill, detached from the top edge — the photo runs under it */
.bar{
  position:fixed;top:clamp(.6rem,1.6vh,1.1rem);left:50%;transform:translateX(-50%);
  z-index:100;
  display:flex;align-items:center;gap:clamp(.9rem,2.2vw,2rem);
  width:min(1440px,calc(100% - 1.5rem));
  padding:.55rem .6rem .55rem 1.4rem;
  border-radius:999px;
  border:1px solid rgba(246,241,244,.15);
  background:rgba(23,18,28,.55);
  backdrop-filter:blur(18px) saturate(1.4);
  box-shadow:0 12px 40px -14px rgba(0,0,0,.65),inset 0 1px 0 rgba(255,255,255,.09);
}
/* Over the white hero the dark pill reads as a gray slab, so the bar inverts
   while it sits on the hero and returns to glass once the curtain covers it.
   JS adds the class; without JS the dark bar stays — legible either way. */
.bar--light{
  background:rgba(255,255,255,.72);
  border-color:rgba(23,18,28,.1);
  box-shadow:0 14px 40px -20px rgba(10,7,14,.3),inset 0 1px 0 rgba(255,255,255,.9);
}
.bar--light .bar__name,.bar--light .bar__nav a{color:var(--ink)}
.bar--light .bar__sub{color:var(--ink);opacity:.72}
.bar--light .bar__now{
  color:rgba(23,18,28,.68);
  border-color:rgba(23,18,28,.13);
  background:linear-gradient(180deg,rgba(23,18,28,.05),rgba(23,18,28,.02));
}
.bar--light .navtoggle span{background:var(--ink)}

/* fixed header: keep anchor jumps from landing underneath it */
section[id],.railgroup[id]{scroll-margin-top:5.5rem}
.bar__mark{display:flex;align-items:baseline;gap:.55rem;text-decoration:none;flex-shrink:0}
.bar__name{
  font-family:var(--display);font-size:1.28rem;font-weight:800;
  letter-spacing:-.04em;line-height:1;
  white-space:nowrap;
}
/* "hair by" is the lead-in, not the mark — dropping it a weight, a size and a
   little opacity puts every bit of emphasis on HER NAME, which is the only word
   in the bar that has to be remembered. Sized in `em` so it tracks .bar__name at
   every breakpoint instead of needing its own phone rule. */
.bar__by{
  font-weight:500;font-size:.74em;
  letter-spacing:-.01em;opacity:.66;
}
/* Tyce, 2026-08-19: "the very top you can't really see where it says hairstylist
   and Barber anymore". It was 9.6px at 50% opacity, and hidden outright below
   1240px — i.e. invisible on every laptop under that and every phone. Measured
   the bar at 12 widths: it actually FITS from 560px up (64px spare even at the
   tightest point, ~600px spare once the nav collapses to the burger), so the
   1240 cut-off was costing visibility for nothing. */
.bar__sub{
  font-size:.68rem;letter-spacing:.18em;text-transform:uppercase;
  opacity:.8;
}
.bar__nav{display:flex;gap:clamp(.65rem,1vw,1.1rem);margin-left:.5rem}
.bar__nav a{
  font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;
  text-decoration:none;opacity:.62;transition:opacity .25s,color .25s;
}
.bar__nav a:hover{opacity:1;color:var(--orchid)}
.bar__now{
  margin-left:auto;display:flex;align-items:center;gap:.5rem;
  padding:.4rem .85rem;border-radius:999px;
  font-size:.7rem;letter-spacing:.05em;
  color:rgba(246,241,244,.7);
  border:1px solid rgba(246,241,244,.13);
  background:linear-gradient(180deg,rgba(246,241,244,.07),rgba(246,241,244,.02));
  white-space:nowrap;
}
.bar__now::before{
  content:"";width:6px;height:6px;border-radius:50%;
  background:var(--orchid);flex-shrink:0;
  box-shadow:0 0 0 0 rgba(255,61,190,.6);
  animation:pip 2.6s ease-out infinite;
}
@keyframes pip{
  0%{box-shadow:0 0 0 0 rgba(255,61,190,.55)}
  70%{box-shadow:0 0 0 9px rgba(255,61,190,0)}
  100%{box-shadow:0 0 0 0 rgba(255,61,190,0)}
}

/* The CTA is the conversion — it must never be the thing that gets squeezed.
   Without this the 9-item nav compressed it from 131px to 97px and clipped the
   label. The nav absorbs the pressure instead (and below 1360px it is replaced
   by the burger entirely). */
.bar .btn--sm{flex-shrink:0}
/* The status pill and the tagline are the two things in the bar that can go
   without losing function, so they yield first as the width tightens. */
/* The "Usually at Mill Valley right now" pill is retired from the bar. With
   "Book an appointment" (196px vs the old 131px) plus 9 nav items it no longer
   fits at ANY width — .bar is width-capped at 1440px, so a bigger screen gives
   it nothing, and above 1580px it forced the nav to wrap. It was already hidden
   below 1580px, so almost nobody ever saw it. Keeping the markup so it can be
   restored elsewhere. */
.bar__now{display:none}
@media (max-width:1220px){
  .bar__nav,.bar__now{display:none}
  .bar{justify-content:space-between}
  .bar .btn--sm{margin-left:auto}
}
/* Phones: the "hair & barber" tagline costs ~90px the bar cannot spare once the
   wordmark, the CTA and the burger all have to share 336px. Dropping it is what
   lets the nowrap button above actually fit instead of overflowing the pill —
   the name alone still reads as the mark, and the tagline is in the hero anyway. */
@media (max-width:560px){
  .bar__sub{display:none}
  .bar{padding-left:1rem}
  .bar .btn--sm{padding:.55rem .95rem;font-size:.68rem;letter-spacing:.07em}
}

/* ══════════════════ hero : the manifesto ══════════════════ */
/* Sky-Estate structure: the photo fills the whole hero, the type sits on it.
   Layers: bg photo (0) → scrim (1) → aurora (2) → grain (3) → body/tag (4). */
/* The hero is PINNED: it stays parked at the top while .curtain (everything
   after it) scrolls up and over it like a sheet. Sticky, not fixed, so it is
   still released once <main> ends and never leaks behind the footer. */
.hero{
  position:sticky;top:0;z-index:0;
  /* EXACTLY one viewport. A pinned hero taller than the screen can never
     scroll — its overflow would be permanently unreachable, which silently
     ate the CTAs the first time this was built. */
  height:100svh;min-height:32rem;
  /* body sits on the FLOOR: centred, the lone CTA floated in empty white
     with nothing to anchor it and read as unplaced. */
  display:flex;align-items:flex-end;
  /* `clip`, not `hidden`: strict clipping with no scroll container, which is
     what a pinned hero wants. Note it does NOT fully remove the ~1px the
     heroBreathe scale adds to documentElement.scrollWidth at 390px — the hero's
     own scrollWidth is exactly 390 and every overflowing element is clipped by
     an ancestor, so that last pixel is the transformed layer's bounds rounding
     out, not layout. `overflow-x:clip` on <html> does not remove it either
     (tried). Body cannot scroll and no scrollbar appears; left as-is. */
  overflow:clip;
  /* Must match the LOOP's DECODED background or a faint rectangle shows around
     the video. Verify after ANY re-encode by drawing a video frame to a canvas
     and reading a corner pixel — the value moves with the encode settings (an
     Historical: while the hero held an OPAQUE video composited with a blend
     mode, this had to match the loop's DECODED white exactly — a single level of
     difference drew a visible rectangle around her, because the video's white
     always loses to a brighter page under darken. That constraint is GONE: the
     hero is now a still with a real alpha channel, so there is no white box to
     match and this is simply the page's white. If an opaque video ever comes
     back, re-measure the decoded corners via canvas and match them here. */
  background:rgb(255,255,255);
}
.curtain{
  position:relative;z-index:1;
  background:var(--ink);   /* opaque floor: nothing may show the hero through a gap */
  /* no leading-edge shadow: it was built to lift the curtain off a DARK hero.
     Over the white one it smears a dirty gray band across her jacket. */
}
/* The curtain effect REQUIRES the hero to fit one viewport exactly. Where the
   copy can't be trusted to fit — narrow phones, very short windows — unpin it
   and let it scroll normally. A pinned hero taller than the screen would hide
   its own bottom (and with it the CTAs) with no way to scroll to them. */
@media (max-width:940px),(max-height:640px){
  .hero{position:static;height:auto;min-height:100svh}
}
/* She is a cutout, not a backdrop: anchored to the bottom-right so she always
   STANDS on the hero's floor at any viewport. object-fit:cover was cropping
   her mid-torso whenever the hero's ratio drifted from the photo's. */
.hero__bg{
  position:absolute;inset:0;z-index:2;margin:0;
  pointer-events:none;
  /* NO blend mode — the media carries a real alpha channel now.
     Why this had to change: darken keeps whichever layer is DARKER, so a
     vibrant wordmark (pink is luma ~134) beat every blonde highlight (~230) and
     the letters showed straight THROUGH her hair. Making the text pale enough
     to always lose means making it nearly white. Vibrant text and opaque hair
     are mutually exclusive under ANY separable blend mode — it needs alpha.
     Do not reintroduce mix-blend-mode here while the assets have alpha. */
}
/* the loop sits exactly on top of the still and cross-fades in once playing */
.hero__vid{
  opacity:0;transition:opacity .9s var(--ease);
  pointer-events:none;
}
.hero__vid.is-live{opacity:1}
@media (prefers-reduced-motion:reduce){.hero__vid{display:none}}

/* ══════════ scattered work polaroids ══════════
   Sits ABOVE the wipe divider (z 6 vs 5) so the split line passes behind them
   and they read as being in front — a full-height line slicing through every
   photo looks like a mistake. Placed only in the empty columns either side of
   her; keep them clear of the CTA block on the floor and the corner labels. */
.hero__scatter{position:absolute;inset:0;z-index:6;pointer-events:none}
.polaroid{
  position:absolute;left:var(--x);top:var(--y);width:var(--w);margin:0;
  transform:rotate(var(--r));
  border-radius:2px;overflow:hidden;
  box-shadow:0 14px 36px -16px rgba(23,18,28,.45),
             0 2px 8px -4px rgba(23,18,28,.18);
  /* no animation here — the intro timeline owns when these arrive. Base state
     is simply "present", so with no JS the collage just renders. */
}
.polaroid img{
  display:block;width:100%;height:auto;
  aspect-ratio:3/4;object-fit:cover;
}
@keyframes polaroidin{
  from{opacity:0;transform:rotate(var(--r)) translateY(14px) scale(.96)}
  to  {opacity:1;transform:rotate(var(--r)) translateY(0) scale(1)}
}
/* Below the wide breakpoint the side columns don't exist — the photos would
   land on top of her and the copy. */
@media (max-width:1080px){ .hero__scatter{display:none} }

/* ══════════ hero runway ══════════
   The spacer that lets the pinned hero hold while the panels rise. Zero by
   default: the height is granted ONLY where the panels actually animate, so
   anywhere they don't (touch, reduced motion, no scroll-timeline support) the
   page is byte-identical to before and nobody scrolls through frozen nothing.
   The min-width matches the .hero__scatter rule directly above — if that
   breakpoint ever moves, this one moves with it. */
.hero-runway{height:0}
@supports (animation-timeline:scroll()){
  @media (prefers-reduced-motion:no-preference) and (min-width:1081px){
    .hero-runway{height:90svh}
  }
}

/* ══════════ Atelier Fifth hero: flush edge panels ══════════
   The reference's insets are hard-edged rectangles DOCKED to the viewport
   edges — not the rotated .polaroid collage above. No rotation, no shadow, no
   gap: they share an edge with the screen and read as a built collage rather
   than photos dropped on top. Sizes are vw/vh so they scale with the hero. */
.hero__panel{
  position:absolute;margin:0;overflow:hidden;
  background:#2A241F;          /* the reference's plates sit on a warm dark card */
}
.hero__panel img{display:block;width:100%;height:100%;object-fit:cover}
/* Left: flush to the left edge. The reference puts this at mid-height, but
   THIS hero keeps her vow and CTA bottom-left, and at 46% the panel sat on top
   of both. Docked to the upper-left instead — same flush-edge relationship,
   clear of the copy. */
/* Sized big deliberately: small panels read as stray thumbnails stuck onto the
   photo, where the reference's are large enough to be a built collage. */
.hero__panel--l { left:0;  top:15%;    width:34vw; height:54vh; }
/* right: a tall strip from the very top, the reference's most distinctive move */
.hero__panel--r { right:0; top:0;      width:24vw; height:80vh; }
/* .hero__panel--br retired 2026-08-20 with the plum-gloss panel — its frame was
   salon clutter and only a sliver ever showed. Rules kept so re-adding a third
   <figure class="hero__panel hero__panel--br"> restores the position. */
.hero__panel--br{ right:26vw; bottom:-8vh; width:20vw; height:46vh; }

/* ★ THE PANELS ARE SCROLL-DRIVEN, NOT AN ENTRANCE.
   Measured on the reference: across the whole hero segment the BACKGROUND has
   exactly 0px of vertical travel while the panel regions move — so the photo
   holds still and the panels RISE over it as you scroll. That is why the hero
   reads as "a pretty background image, and then when you scroll an image moves
   up on the right and left". An on-load entrance (what was here before) is a
   different effect entirely: it fires once and then nothing responds to you.

   .hero is already position:sticky, so the background pins itself for free —
   the panels only need to travel against it.

   Base state (below) is the RESTING position, so browsers without scroll
   timelines, and anyone on reduced motion, simply get the finished collage.
   Nothing is ever stranded off-screen. */
@supports (animation-timeline:scroll()){
  @media (prefers-reduced-motion:no-preference){
    .hero__panel{
      animation:panelRise linear both;
      animation-timeline:scroll(root block);
      /* The range is now pure pacing. It used to be a RACE: with .curtain as
         the hero's immediate sibling the visible hero shrank 1px per 1px of
         scroll, so the panels had to out-run it (2.3x) or never be seen. The
         .hero-runway spacer removes that entirely — nothing closes over the
         hero while they rise. 62svh of a 90svh runway leaves ~28svh where the
         collage sits fully up and still before the page releases. */
      animation-range:0 62svh;
    }
    /* Each --rise-from is set so the panel starts BELOW the fold: at scroll 0
       the hero has to be nothing but the photograph, the monogram and her copy.
       Anything peeking up sits on the CTA. Values are (viewport − the panel's
       own resting top), so: l rests at 15vh, r at 0, br at 77vh.
       The three travel at different rates so the hero gains depth instead of
       one flat sheet sliding. */
    /* recomputed for the larger panels: (viewport − that panel's resting top),
       so each still starts fully below the fold. l rests at 15vh, r at 0,
       br at 100 + 8 − 46 = 62vh. */
    .hero__panel--l { --rise-from: 90vh;  --rise-to:0; }
    .hero__panel--r { --rise-from: 104vh; --rise-to:0; }
    .hero__panel--br{ --rise-from: 42vh;  --rise-to:-9vh; }

    /* NOTE: the hero copy's fade-out is NOT here. initScrollFX() in main.js
       already owns it — a Motion scroll animation on .hero__body tracked
       against .curtain ("hero exit"). A CSS animation on the same element is
       silently inert, because Web Animations beat CSS animations in the
       cascade: three attempts at tuning it here changed nothing at all and the
       measured curve never budged. Retune the offsets in main.js instead. */
  }
}
@keyframes panelRise{
  from{transform:translateY(var(--rise-from,50vh))}
  to  {transform:translateY(var(--rise-to,-10vh))}
}

/* ── the script monogram ── */
.hero__monogram{
  /* CENTRED again. It was pushed to 68% back when the hero was a cutout standing
     on WHITE — a centred monogram put "Tycetyce" on the white third where cream
     type measured 0.97:1 and simply vanished. That hero is gone: this one is a
     full-bleed photograph with the purple grade, so the ground is mid-tone all
     the way across and centre is safe. Contrast re-measured after moving it. */
  position:absolute;left:50%;top:31%;transform:translate(-50%,-50%);
  /* above .hero__scatter (6): at their new size the panels would otherwise
     swallow the wordmark as they rise past it */
  z-index:7;margin:0;pointer-events:none;text-align:center;
  isolation:isolate;
  display:flex;flex-direction:column;align-items:center;gap:1.15rem;
  color:#FBF6F0;
  /* The reference's monogram sits over mid-dark hair; the centre of THIS hero
     is bright rose-blonde, so cream-on-cream vanished. A wide soft shadow plus
     a tight one gives it an edge on light hair without drawing a visible pool
     behind it — a scrim was tried on this hero before and read as a smudge. */
  text-shadow:0 0 2px rgba(46,32,22,.55),
              0 1px 3px rgba(46,32,22,.5),
              0 3px 26px rgba(46,32,22,.55);
}
/* ★ Legibility here was measured, not eyeballed, and it took three goes.
   Her hair is bright rose-blonde standing on a WHITE field; the reference's
   photo is uniformly mid-dark, so its cream script reads anywhere. Scanning the
   whole hero at the monogram's real 600x170 footprint, the best placement on
   this image still only reached p90 luma 138 (1.75:1) and the centred one gave
   p90 255 (0.97:1 — literally invisible).
   A soft radial veil behind the type fixed the numbers (p90 255 -> 129) and
   looked WRONG: on the white third it read as a grey cloud floating in mid-air,
   the same smudge this hero already rejected once.
   The answer is to give the LETTERS an edge instead of giving them a ground —
   a tight dark rim plus a wide soft drop. It costs nothing on the dark hair and
   still separates cream from white, because the rim is doing the work, not a
   patch of tinted background. */
.hero__monogram-mark,.hero__monogram-sub{
  paint-order:stroke fill;
  -webkit-text-stroke:.6px rgba(52,36,24,.42);
}
.hero__monogram-mark{
  font-family:var(--serif);font-style:italic;font-weight:400;
  font-size:clamp(4.4rem,12.5vw,14rem);letter-spacing:-.015em;
  /* ★ line-height and padding-bottom are load-bearing for the GRADIENT, not for
     spacing. `background-clip:text` paints only inside the element's background
     box, so any glyph that reaches past it simply receives no paint. At
     line-height:.9 the descenders of both y's and the tail of the r fell outside
     and rendered as if chopped off with a knife. The box has to be tall enough
     to contain the descenders of an italic serif. */
  line-height:1.0;
  padding-bottom:.16em;
  /* The site's house gradient — the same stops and the same seamless 200%/200%
     travel already used by .thesis and .lbk__title, so the hero wordmark reads
     as the same brand rather than a second purple invented for it. */
  background-image:linear-gradient(100deg,
    #7B2BE8 0%,#A855F7 14%,#C13BFF 28%,#F03DFF 42%,#FF3DBE 50%,
    #F03DFF 58%,#C13BFF 72%,#A855F7 86%,#7B2BE8 100%);
  background-size:200% 100%;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:thesisflow 9s linear infinite;
  /* ★ Both of these MUST go with a transparent glyph fill.
     - text-shadow paints a solid ghost of the letterforms BEHIND the gradient
       (a known trap on .thesis), so the wordmark gets a grey double.
     - -webkit-text-stroke paints on top of the clip and outlines every letter
       in dark, which kills the colour it is there to show.
     The drop-shadow filter replaces both: it acts on the rendered glyphs,
     gradient and all, so the wordmark keeps its lift off the photograph. */
  text-shadow:none;
  -webkit-text-stroke:0;
  filter:drop-shadow(0 2px 10px rgba(28,14,38,.62))
         drop-shadow(0 1px 2px rgba(28,14,38,.5));
}
@media (prefers-reduced-motion:reduce){
  /* park it mid-sweep rather than freezing on an end stop, same as .thesis */
  .hero__monogram-mark{animation:none;background-position:38% 50%}
}
.hero__monogram-sub{
  font-size:clamp(.62rem,.86vw,.82rem);letter-spacing:.42em;text-transform:uppercase;
  opacity:1;padding-left:.42em;   /* optical: tracking adds a phantom right margin */
  text-shadow:0 1px 20px rgba(46,32,22,.85), 0 1px 2px rgba(46,32,22,.55);
}
.hero-intro .hero__monogram{animation:monogramin 1.1s var(--ease) 4s both}
@keyframes monogramin{
  from{opacity:0;transform:translate(-50%,-50%) scale(1.04)}
  to  {opacity:1;transform:translate(-50%,-50%) scale(1)}
}
@media (max-width:1080px){
  .hero__monogram{left:50%;top:34%}
  .hero__monogram-mark{font-size:clamp(3.1rem,12vw,5rem)}
  /* one line, not two: at .42em the city line wrapped to "MILL VALLEY · SAN /
     FRANCISCO", which reads as a mistake rather than a lockup */
  .hero__monogram-sub{letter-spacing:.24em;font-size:.62rem;white-space:nowrap}
}
/* ★ Mobile needs its own ground for the monogram. Originally because the phone
   crop pushed to 74.5%, which sat the wordmark on bright window glass — 1.56:1
   there against 2.6:1 on desktop. The crop is 52% now (see .hero__rev below)
   and the wordmark lands on hair instead, but the wash is KEPT: her roots are
   the lightest part of the frame at that height, and the pink mark needs the
   ground. Re-check contrast here if the crop moves again. */
@media (max-width:940px){
  .hero::before{
    content:"";position:absolute;inset:0 0 auto 0;height:62%;z-index:3;
    pointer-events:none;
    background:linear-gradient(to bottom,
      rgba(24,16,11,.72) 0%, rgba(24,16,11,.5) 42%, rgba(24,16,11,0) 100%);
  }
}


/* the hero creed: bottom-right, deliberately quiet. It is the counterweight to
   the CTA block on the floor-left, and it sits in the only part of the hero the
   centred media never reaches. */
.hero__creed{
  flex:0 1 auto;margin:0;
  max-width:min(34vw,26rem);text-align:right;
  padding-top:clamp(.9rem,2vh,1.3rem);
  border-top:1px solid rgba(23,18,28,.18);
}
.hero__creed p{
  font-family:var(--serif);
  font-size:clamp(1.2rem,1.85vw,1.72rem);
  line-height:1.3;color:rgba(23,18,28,.82);
  text-wrap:balance;
}
.hero__creed em{font-style:italic;color:var(--plum-2)}
.hero__creed cite{
  display:block;margin-top:.75rem;font-style:normal;
  font-size:.64rem;letter-spacing:.24em;text-transform:uppercase;
  color:rgba(23,18,28,.4);
}
/* no room for it beside the media once the columns collapse */
@media (max-width:1080px){ .hero__creed{display:none} }

/* ══════════ THE INTRO ══════════
   The before/after is no longer something the visitor has to find by hovering —
   it is the opening sequence. On load the hero holds on the untreated hair with
   a line over it, swipes left-to-right to the finished hair, pulls focus while
   the wordmark sweeps in behind, then sets the collage and the CTA.

   BASE CSS IS THE FINAL STATE. `.hero--intro` (added by initIntro) layers the
   animations on top, every one with `both` fill so it lands exactly on that
   final state. So with no JS, or under reduced motion, the hero simply renders
   finished — nothing is stranded invisible. This is the same principle as the
   .reveal system: never hide content in CSS you can't guarantee to un-hide.

   TIMELINE (ms)   0 ── quote in ── 1600 out
                1800 ── swipe ── 2700
                2700 ── focus pull ── 3600
                2900 ── wordmark
                3100 ── collage (staggered)
                3300 ── vow + CTA
*/
.hero__layer{position:absolute;inset:0;pointer-events:none}
.hero__layer--after {z-index:1; clip-path:inset(0)}
.hero__layer--before{z-index:2; clip-path:inset(0 0 0 100%)}   /* swiped away */

/* Complementary clips: the finished hair grows from the left exactly as the
   untreated hair retreats from it. Clipping only ONE would let the layer
   underneath show through every transparent pixel of the other — around the
   silhouette and between the strands — as a halo. */
@keyframes introAfter { from{clip-path:inset(0 100% 0 0)} to{clip-path:inset(0)} }
@keyframes introBefore{ from{clip-path:inset(0)} to{clip-path:inset(0 0 0 100%)} }
.hero-intro .hero__layer--after {animation:introAfter  .8s var(--ease) 3.05s both}
.hero-intro .hero__layer--before{animation:introBefore .8s var(--ease) 3.05s both}

/* the blade travels with the seam and dies as it lands */
.hero__wipe{
  position:absolute;top:0;bottom:0;left:0;z-index:5;
  width:2px;margin-left:-1px;pointer-events:none;opacity:0;
  background:linear-gradient(180deg,rgba(255,61,190,0) 0%,#FF3DBE 18%,#9A2BF0 82%,rgba(154,43,240,0) 100%);
}
@keyframes introBlade{
  0%{left:0;opacity:0} 10%{opacity:1} 88%{opacity:1} 100%{left:100%;opacity:0}
}
.hero-intro .hero__wipe{animation:introBlade .8s var(--ease) 3.05s both}

@keyframes introRise{ from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:none} }
.hero-intro .hero__marquee{animation:introRise .8s var(--ease) 4.0s both}
.hero-intro .polaroid{animation:polaroidin .7s var(--ease) calc(4.2s + var(--i) * .1s) both}
.hero-intro .hero__vow{animation:introRise .7s var(--ease) 4.4s both}
.hero-intro .hero__cta{animation:introRise .7s var(--ease) 4.55s both}
.hero-intro .hero__creed{animation:introRise .7s var(--ease) 4.7s both}

/* ── the opening line ──
   Sits over the untreated hair, so it needs its own ground: a soft white bloom
   behind it. A text-shadow can't do that job here — the glyphs are filled by a
   background-clip gradient, and a shadow on transparent text paints a solid
   ghost of the letterforms. */
.hero__quote{
  position:absolute;left:50%;top:46%;z-index:7;
  transform:translate(-50%,-50%);
  margin:0;width:min(94vw,58rem);text-align:center;
  display:grid;place-items:center;
  pointer-events:none;opacity:0;
  font-family:var(--display);font-weight:600;
  font-size:clamp(2.7rem,7.2vw,6.2rem);
  line-height:1.02;letter-spacing:-.035em;
}
.hero__quote span{
  grid-area:1/1;width:100%;opacity:0;
  background-image:linear-gradient(100deg,#7B2BE8 0%,#C13BFF 28%,#FF3DBE 55%,#C13BFF 80%,#7B2BE8 100%);
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
}
/* The pool has to be strong: at .94 falling off at 72% the second line sat on
   the mid-tones of the hair and went muddy. Near-opaque out to 48%, then a long
   fade so there is no visible disc edge. */
/* The two lines land one after the other, then leave together. The IN is per
   line so they can be staggered; the OUT lives on the parent, because two
   separately-delayed animations would otherwise also exit separately. */
/* Each line rises in, holds, and clears BEFORE the next arrives — one complete
   cycle per line, staggered by exactly one duration so they never share the
   screen. The parent no longer animates; it is just the stage. */
@keyframes introLine{
  0%{opacity:0;transform:translateY(20px)}
  26%{opacity:1;transform:none}
  74%{opacity:1;transform:none}
  100%{opacity:0;transform:translateY(-14px)}
}
.hero-intro .hero__quote{opacity:1}
.hero-intro .hero__quote span:first-child{animation:introLine 1.45s var(--ease) .15s both}
.hero-intro .hero__quote span:last-child {animation:introLine 1.45s var(--ease) 1.60s both}

.hero__vow{
  /* text-wrap:balance stops "welcome." being orphaned on its own line — the
     browser evens the two lines out instead of filling the first greedily. */
  margin:0 0 clamp(.75rem,1.8vh,1.15rem);
  max-width:30ch;text-wrap:balance;   /* 21ch was sized for the old shorter vow;
                                         her new line broke to three cramped rows */
  font-family:var(--display);font-weight:600;
  /* Sized up hard (was clamp(1.1rem,1.6vw,1.55rem)) — on a full-bleed photo the
     old size read as a caption rather than the promise it is. */
  font-size:clamp(1.55rem,2.7vw,2.75rem);
  line-height:1.22;letter-spacing:-.015em;
  color:var(--ink);
}

/* Reduced motion: no sequence at all. Base CSS is already the finished state,
   so simply refusing to animate lands in exactly the right place. */
@media (prefers-reduced-motion:reduce){
  .hero-intro .hero__layer--after,
  .hero-intro .hero__layer--before,
  .hero-intro .hero__wipe,
  .hero-intro .hero__bg,
  .hero-intro .hero__marquee,
  .hero-intro .polaroid,
  .hero-intro .hero__vow,
  .hero-intro .hero__cta,
  .hero-intro .hero__creed,
  .hero-intro .hero__quote,
  .hero-intro .hero__quote span{animation:none}
  .hero__quote{display:none}
}

/* ══════════ the scrolling wordmark, and the hair that passes in FRONT of it ══
   The loop is an opaque white RECTANGLE, not a cutout with alpha. Stacked
   normally it would slice the letters along a dead-straight invisible edge.
   mix-blend-mode:multiply fixes that with no re-encode and no keying: against a
   white page, white x anything = anything (the backdrop survives untouched),
   while the dark hair multiplies down and reads as genuinely in front.

   This is WHY the wordmark must stay PALE. Multiply darkens the hair by exactly
   the wordmark's own value, so a saturated colour here would eat the blonde
   ribbons wherever they cross a letter. At ~#EFDCEA the hair only picks up a
   faint rose cast — on-brand for a rose balayage — and the highlights survive.
   If this colour is ever deepened, check the highlights over a letter first. */
.hero{isolation:isolate}   /* keep the blend inside the hero, off the page */

/* ★ Ground for the bottom-left copy. The hero used to be a cutout standing on
   white, so her ink vow and creed had a clean white third to sit on. A
   full-bleed photograph took that away and they became unreadable over her
   client's face. This is a corner wash anchored to two edges — it reads as
   lighting falling off, unlike a free-floating blob (which this hero already
   rejected once behind the monogram). Sits above the photo (z2) and below the
   copy (z4). */
.hero::after{
  content:"";position:absolute;inset:0;z-index:3;pointer-events:none;
  /* Strength was measured, not eyed: sampling the ground under the vow's real
     glyph box, the first attempt left p90 luma 99 (2.4:1 against cream) and a
     mid strength still only reached 4.3:1. These stops land p90 at 40 = 5.6:1,
     clear of the 4.5:1 body-text threshold. Re-measure if the photo changes. */
  background:linear-gradient(to top right,
    rgba(24,16,11,.96) 0%, rgba(24,16,11,.86) 26%,
    rgba(24,16,11,.56) 50%, rgba(24,16,11,0) 74%);
}
/* the copy flips to cream to sit on it */
.hero__vow{color:#FBF6F0}
.hero__creed p{color:rgba(251,246,240,.92)}
.hero__creed em{color:#FFA1E1}
.hero__creed cite{color:rgba(251,246,240,.6)}
.hero__creed{border-top-color:rgba(251,246,240,.22)}
.hero__vow,.hero__creed p,.hero__creed cite{
  text-shadow:0 1px 3px rgba(20,13,9,.55), 0 2px 22px rgba(20,13,9,.45);
}

/* The cost signal under the CTA. Joins the cream-on-scrim group above rather
   than inventing a third hero colour: same shadow pair, same cream, dropped to
   .85 so it reads as a footnote to the button and never competes with the vow.
   It sits low in the hero where that gradient is at its strongest, so the
   measured 5.6:1 headroom holds even at the reduced alpha. */
.hero__price{
  margin:clamp(.9rem,2.2vh,1.35rem) 0 0;
  /* Own stacking context so the feather below can sit behind the text without
     escaping into the hero's own layer order. */
  position:relative;isolation:isolate;
  display:flex;flex-wrap:wrap;align-items:baseline;
  gap:.35rem .85rem;
  font-size:clamp(.86rem,1.35vw,1rem);
  line-height:1.5;
  color:rgba(251,246,240,.85);
  text-shadow:0 1px 3px rgba(20,13,9,.55), 0 2px 22px rgba(20,13,9,.45);
}
.hero__price b{font-family:var(--display);font-weight:600;color:#FBF6F0}
.hero__price-link{
  color:#FFA1E1;text-decoration:none;white-space:nowrap;
  border-bottom:1px solid rgba(255,161,225,.45);
  padding-bottom:.1em;
  transition:color .25s, border-color .25s;
}
.hero__price-link:hover{color:#fff;border-bottom-color:#fff}

/* ★ MEASURED, DO NOT DELETE. The hero's main scrim (.hero::after) runs
   bottom-left to top-right and has largely faded by the time it reaches this
   line, which sits over the bright blonde length of the hair — cream on that
   ground measured 3.03:1, under the 4.5:1 body-text floor, so the line was
   legible to me and marginal to everyone else. Moving it above the button only
   got to 3.28:1: the whole band is bright, so placement can't fix it.
   This is a local feather, not a plate — a radial that is dense under the text
   and gone before it reaches an edge, so nothing reads as a box dropped on the
   photograph (the same objection that killed the card version of the b/a).
   Re-measure if the hero photo ever changes. */
.hero__price::before{
  content:"";position:absolute;z-index:-1;
  /* Sized so every edge that a viewer can actually SEE fades to zero, and the
     one that doesn't fade is pushed off-screen. The hero is `overflow:clip`, so
     the left inset runs past the viewport edge on purpose: that boundary is
     clipped away, which buys the ellipse room to stay dense under the text
     instead of having to die symmetrically on both sides.
     ★ First attempt sized the ellipse past 100% in a tight box — still opaque
     at the bounds, so it painted a letterbox band across the photograph.
     Second attempt fixed the band but pulled the density off the right end of
     the line and contrast fell to 3.59:1. This holds density to ~88% and then
     drops, so the whole line is covered and nothing shows an edge. */
  inset:-1.9rem -4rem -2.1rem -6rem;
  background:radial-gradient(55% 52% at 45% 50%,
    rgba(20,13,9,.9) 0%, rgba(20,13,9,.85) 45%,
    rgba(20,13,9,.62) 72%, rgba(20,13,9,.26) 89%,
    rgba(20,13,9,0) 100%);
  pointer-events:none;
}
/* Retired for the Atelier Fifth direction — the script monogram is the hero's
   focal type now, and the two competed badly for the same space. Markup and all
   the rules below are kept intact so the marquee can be switched back on by
   deleting this one line. */
.hero__marquee{display:none}
.hero__marquee{
  position:absolute;inset-inline:0;top:14vh;z-index:1;
  overflow:hidden;pointer-events:none;
  /* FULL strength now. The headline it used to compete with is gone, so the
     wordmark is the hero's only type and carries the brand colour outright. */
  opacity:1;
}
.marquee__track{
  display:flex;width:max-content;white-space:nowrap;
  will-change:transform;
  /* 38s over a 6-copy track ~= 105px/s. The old 30s only looked slow because
     the shortened wordmark made each half tiny (~35px/s); duration alone is
     meaningless here — what matters is half-width / duration. */
  animation:marquee 38s linear infinite;
  /* Held until the webfont is in. translateX(-50%) is only seamless while the
     two halves are pixel-identical; if Bricolage swaps in mid-cycle the track
     width changes underneath the animation and the loop visibly jumps. */
  animation-play-state:paused;
}
.fonts-ready .marquee__track{animation-play-state:running}
/* the scale lives on the SPANS, never the track — the track's transform is the
   scroll animation and one element gets one transform property. The track's
   padding-block buys room for the stretch, otherwise the taller glyphs get
   sliced top and bottom by .hero__marquee's overflow:hidden. */
.marquee__track{padding-block:4.4vh}
.marquee__track span{
  padding-right:6vw;
  transform:scaleY(1.38);transform-origin:center;
  font-family:var(--display);font-weight:600;
  font-size:18vh;line-height:1;letter-spacing:-.04em;
  /* Same gradient as the headline. First and last stops are the SAME violet, so
     the seam where span 2 begins matches where span 1 ended.
     NB: these MUST stay inside this rule. A previous edit inserted the mobile
     font-size override by closing the rule early, which orphaned every
     declaration below — the whole gradient silently became invalid CSS and the
     wordmark rendered as flat ink. */
  background-image:linear-gradient(100deg,
    #7B2BE8 0%,#A855F7 12%,#C13BFF 24%,#F03DFF 36%,#FF3DBE 48%,
    #F03DFF 60%,#C13BFF 72%,#A855F7 86%,#7B2BE8 100%);
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
}
/* phones: 18vh is oversized on a narrow screen. Still leaves each half far
   wider than the viewport, so the seamless wrap is unaffected. */
@media (max-width:720px){
  .marquee__track span{font-size:13.5vh}
}
/* exactly one half-track, so the seam lands on identical pixels */
@keyframes marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){
  /* the one animation on the page that never ends — must actually stop */
  .marquee__track{animation:none;transform:none}
}

.hero__bg img,.hero__bg .hero__vid{
  /* BOTH layers are anchored identically. They were once positioned by different
     mechanisms — the still by flex, the loop by bottom:0 — and the composition
     jumped ~87px the instant the loop faded in. Assert they still match after
     ANY change here: Math.abs(vidRect.top - imgRect.top) < 1.

     Sized by WIDTH with height:auto, never the other way round. The box then
     inherits the file's exact aspect, so object-fit can never letterbox it and
     silently shift the framing. The svh arm is the real control — it is just
     "% of the hero's height" re-expressed as a width (x 3:4 for this file) —
     and the vw arm takes over on wide-but-short windows.

     THIS frame is a complete portrait: crown at 2.6%, hair ending ~92%, and she
     is centred inside the file. So unlike the previous tight crop it needs no
     horizontal nudge and no downward drop — the whole frame is shown, standing
     on the hero's floor. 69svh puts the box at ~92% of the hero's height, which
     clears the floating nav and lets her white top fade out into the page below
     the hair. Set --hero-nudge if a future frame is off-centre in its file. */
  /* 89svh = ~91% of the hero's height for THIS file's 1642x1672 aspect (x .982).
     Re-derive the svh arm whenever the source aspect changes, or she resizes. */
  --hero-nudge:0%;
  width:min(52vw,89svh);height:auto;
  max-width:none;max-height:none;
  position:absolute;left:50%;bottom:0;
  margin:0;
  transform:translate(calc(-50% + var(--hero-nudge)),0);
  filter:saturate(1.04) contrast(1.01);
  /* No ken-burns scale here. It was built for a full-bleed backdrop photo; on a
     standing cutout it visibly grows her, and it would drift the still out of
     register with the loop layered on top. The hair motion IS the movement. */
}
/* the dull layer: desaturates and dims everything OUTSIDE the hair window.
   backdrop-filter grades the real pixels — no AI touch-up of her work — and
   the mask cuts a clear opening over the hair. Browsers without support just
   fall through to the scrim. */
/* The dull-white grade is baked into the photo itself now (wall recolored,
   subject protected) — a live backdrop-filter would just gray the white. */
.hero__dull{display:none}

/* light hero: the photo IS the surface — no fades, per the user. The type
   flips to ink and carries its own contrast on the dull-white wall. */
.hero__scrim{display:none}
.hero__body{
  position:relative;z-index:4;
  width:100%;max-width:var(--maxw);margin:0 auto;
  padding:clamp(5.5rem,11vh,7.5rem) var(--pad) clamp(2rem,5vh,3.25rem);
  /* Both hero corners live in ONE row, bottom-aligned. They used to be a flow
     block and an absolutely-positioned quote, which left them 26px out of
     alignment and needing a magic number to reconcile. */
  display:flex;align-items:flex-end;justify-content:space-between;
  gap:clamp(1.5rem,5vw,4rem);
  pointer-events:none;              /* the hero media behind stays reachable */
}
.hero__body > *{pointer-events:auto}
.hero__col{max-width:36ch}
/* ── aurora: three slow-drifting blooms. transform/opacity only, so it
   stays on the compositor and costs nothing on scroll. ── */
.hero__aurora{
  display:none;   /* screen-blend glows are invisible over the white wall */
  position:absolute;inset:0;z-index:2;overflow:hidden;pointer-events:none;
  mix-blend-mode:screen;
}
.hero__aurora span{
  position:absolute;display:block;border-radius:50%;
  filter:blur(70px);will-change:transform;
}
/* pooled in the gap between the type and the photo, not behind the headline —
   sitting under the white lines it ate their contrast */
.hero__aurora span:nth-child(1){
  width:44vw;height:44vw;top:-14%;left:40%;
  background:radial-gradient(circle,rgba(255,61,190,.09),transparent 68%);
  animation:drift-a 22s ease-in-out infinite alternate;
}
.hero__aurora span:nth-child(2){
  width:34vw;height:34vw;bottom:-16%;left:6%;
  background:radial-gradient(circle,rgba(74,30,92,.62),transparent 70%);
  animation:drift-b 27s ease-in-out infinite alternate;
}
.hero__aurora span:nth-child(3){
  width:24vw;height:24vw;top:34%;left:52%;
  background:radial-gradient(circle,rgba(255,94,91,.04),transparent 68%);
  animation:drift-c 19s ease-in-out infinite alternate;
}
@keyframes drift-a{from{transform:translate3d(0,0,0) scale(1)}to{transform:translate3d(-7%,9%,0) scale(1.16)}}
@keyframes drift-b{from{transform:translate3d(0,0,0) scale(1.1)}to{transform:translate3d(11%,-8%,0) scale(.92)}}
@keyframes drift-c{from{transform:translate3d(0,0,0) scale(.9)}to{transform:translate3d(-13%,-11%,0) scale(1.22)}}

/* fine grain — the texture that reads "finished" rather than "flat CSS" */
.hero__grain{
  position:absolute;inset:0;z-index:3;pointer-events:none;
  opacity:.22;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
}

/* glass chip naming the hero photo's service — bottom right, over the hair */
/* credit line, not a badge: plain type so it reads as a caption and stays out
   of the contest between the headline and her */
.hero__tag{
  position:absolute;z-index:4;
  /* bottom-LEFT, aligned to the type column: bottom-right put it on her denim
     where dark caption text was unreadable */
  left:max(var(--pad),calc((100vw - var(--maxw))/2 + var(--pad)));
  right:auto;bottom:clamp(1.25rem,3.5vh,2.25rem);
  display:inline-flex;align-items:center;gap:.4rem;
  padding:0;border:0;border-radius:0;background:none;
  backdrop-filter:none;box-shadow:none;
  font-size:.66rem;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
  color:rgba(23,18,28,.36);
}
.hero__tag span{color:rgba(23,18,28,.52);font-weight:700}

/* Scoped with .hero deliberately: the base .hero__eyebrow rule is declared
   further down this file, so a bare .hero__eyebrow override here would tie on
   specificity and lose to it on source order. */
@media (max-width:560px){
  /* The chip can't hold one line at this width, and a two-line pill reads as
     a mistake. Keep the lit dot, drop the chrome — plain caps are cleaner. */
  .hero .hero__eyebrow{
    display:flex;padding:0;border:0;background:none;box-shadow:none;
    backdrop-filter:none;border-radius:0;
    font-size:.63rem;letter-spacing:.14em;line-height:1.5;
    gap:.35rem .55rem;color:rgba(23,18,28,.62);
  }
  .hero .hero__sep{display:none}
  /* three large blurs is a lot of fill rate for a phone */
  .hero .hero__aurora span{filter:blur(46px)}
  .hero .hero__aurora span:nth-child(3){display:none}
  .hero .hero__grain{opacity:.16}
}

/* Laptop-height screens: the pinned hero is capped at one viewport, so the
   type has to come down or the CTAs fall off the bottom edge. */
/* Every selector here is scoped with .hero on purpose: the base .thesis,
   .hero__retort, .hero__eyebrow and .hero__foot rules are declared FURTHER
   DOWN this file, so bare overrides would tie on specificity and lose on
   source order — the same trap already noted on .hero__eyebrow below. */
@media (min-width:941px) and (max-height:1000px){
  .hero .thesis{font-size:clamp(3rem,9vw,9rem)}
  .hero .hero__retort{font-size:clamp(1.05rem,2.2vw,1.6rem);margin-top:1.1rem}
  .hero .hero__eyebrow{margin-bottom:1.3rem}
  .hero .hero__foot{margin-top:1.4rem;padding-top:1.15rem}
  .hero__body{padding-top:5rem;padding-bottom:1.5rem}
}
@media (min-width:941px) and (max-height:840px){
  .hero .thesis{font-size:clamp(2.6rem,7.4vw,7.25rem)}
  .hero .hero__eyebrow{margin-bottom:.9rem}
  .hero .hero__foot{margin-top:1rem;padding-top:.85rem}
  .hero .hero__lede{font-size:.98rem;line-height:1.55}
  .hero__body{padding-top:4.5rem;padding-bottom:1.25rem}
}

@media (max-width:940px){
  .hero{min-height:100svh}
  .hero__body{padding-top:5.75rem;padding-bottom:3.25rem}
  /* phones: she sits along the bottom edge and the type moves to the top, so
     the two never share the same band of the screen */
  /* The body is just the CTA now, so park it on the floor — at flex-start it
     landed inside the wordmark band and collided with it. */
  .hero{align-items:flex-end}
  /* The .3 fade existed to keep the big headline readable over her. That
     headline is gone, so fading her only made her a ghost — full strength. */
  .hero__bg{opacity:1}
  /* the desktop drop would push a third of her off a static, auto-height hero.
     On phones she is just a faint backdrop: fit the width, sit on the floor. */
  /* NOT centred here. The frame is cut hard along its right edge (she runs to
     99.7% of it), so centring on a narrow viewport exposes that cut as a
     vertical seam mid-screen. Flush right hides it against the viewport edge;
     the left edge is safe because the frame carries ~12% white margin there. */
  .hero__bg img,.hero__bg .hero__vid{
    width:min(96vw,34rem);
    left:auto;right:0;transform:none;
  }
  .hero__tag{right:auto;left:1rem;bottom:1rem}

  /* Bring the creed BACK. It was display:none under 1080px for want of room
     beside the media, which meant her own line — "hair trauma heals… your new
     hair journey starts" — was invisible to the phone traffic that arrives from
     her Instagram, i.e. almost everyone. Restacked UNDER the CTA instead of
     beside it. Scoped to 940px and below on purpose: that is exactly where the
     hero is unpinned and auto-height (see the max-width:940px rule above), so
     the extra block grows the hero instead of pushing the CTA off a pinned
     100svh screen where it could never be scrolled to. */
  .hero__body{flex-direction:column;align-items:flex-start}
  .hero__creed{
    display:block;width:100%;max-width:34ch;
    text-align:left;
    margin-top:clamp(1.1rem,2.6vh,1.7rem);
  }
  .hero__creed p{font-size:clamp(1.02rem,4vw,1.3rem)}
  .hero__creed cite{margin-top:.55rem}
}
/* No pill: on the white hero the only things allowed to carry weight are the
   words and her. The eyebrow is plain caps with a single lit dot. */
.hero__eyebrow{
  display:inline-flex;align-items:center;gap:.55rem;flex-wrap:wrap;
  margin-bottom:clamp(1.35rem,3.5vh,2.25rem);
  padding:0;border:0;border-radius:0;background:none;
  backdrop-filter:none;box-shadow:none;
  font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;font-weight:600;
  color:rgba(23,18,28,.48);
  animation:rise .9s var(--ease) backwards;
}
.hero__pip{
  width:6px;height:6px;border-radius:50%;background:var(--orchid);
  box-shadow:0 0 10px 1px rgba(255,61,190,.85);
  animation:pip 2.6s ease-out infinite;
}
.hero__sep{
  width:1px;height:.85em;background:rgba(23,18,28,.22);
}

/* The hero is ONE statement now, so the headline carries the whole frame:
   bigger, and filled with the brand's violet→pink rather than ink. */
.thesis{
  font-family:var(--display);
  font-size:clamp(3.2rem,9.6vw,9.5rem);
  /* 600, not 800: at this size extra-bold reads loud and casual. Dropping the
     weight and opening the tracking a touch is what makes big type feel
     editorial rather than shouty. */
  font-weight:600;
  line-height:.88;
  letter-spacing:-.035em;
  text-wrap:balance;
  /* ONE continuous gradient across the whole headline, drifting slowly. First
     and last stop are the same violet so the loop has no seam. */
  background-image:linear-gradient(100deg,
    #7B2BE8 0%, #A855F7 12%, #C13BFF 24%, #F03DFF 36%, #FF3DBE 48%,
    #F03DFF 60%, #C13BFF 72%, #A855F7 86%, #7B2BE8 100%);
  /* 200% + travel to 200% is the ONLY seamless pair here. background-position
     percentages resolve against (container - image), so at size 200% one cycle
     moves exactly one tile width and the repeat lines up perfectly. At 240% it
     moved 1.4 tiles and snapped back visibly at the loop point. */
  background-size:200% 100%;
  background-position:0% 50%;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:thesisflow 6s linear infinite;
}
@keyframes thesisflow{
  from{background-position:0% 50%}
  to{background-position:200% 50%}
}
/* Each line is a clipping window; the inner span is what moves, so the type
   rises out of nothing instead of sliding over the background. */
.thesis__l{display:block;overflow:hidden;padding-bottom:.06em}
.thesis__l > span{
  display:block;
  animation:unmask 1.05s var(--ease) backwards;
  animation-delay:calc(var(--i) * .11s);
}
@keyframes unmask{
  from{opacity:0;transform:translateY(105%) rotate(2.5deg);filter:blur(9px)}
  60%{opacity:1;filter:blur(0)}
  to{opacity:1;transform:none;filter:blur(0)}
}
/* no text-shadow: the glyphs are transparent windows onto the gradient, so a
   shadow would paint a solid ghost of the letters underneath */
.thesis__l:not(.thesis__l--hit) > span{text-shadow:none}

/* ── the accent phrase: gradient fill that flows, with a bloom behind it ──
   This line opts out of the clipping mask so the 26px bloom isn't sliced off,
   and swaps the slide-up for a settle so nothing tracks over the copy below. */
.thesis__l--hit{overflow:visible}
.thesis__l--hit > span{animation-name:settle;animation-duration:1.15s}
@keyframes settle{
  from{opacity:0;transform:translateY(.18em) scale(.985)}
  to{opacity:1;transform:none}
}
.thesis__l--hit em{
  position:relative;
  display:inline-block;
  font-family:var(--serif);
  font-style:italic;
  font-weight:400;
  letter-spacing:-.01em;
  padding:0 .06em;

  /* Deliberately carries NO gradient of its own — it inherits the transparent
     fill so the h1's single moving gradient runs straight through it. Two
     separate gradients here made the last line read as a different element. */
}
/* The text-shaped bloom is OFF on the light hero — a 24px halo behind the
   phrase was the loudest thing on the screen and competed with her. */
.thesis__l--hit em::before{
  display:none;
  content:attr(data-text);
  position:absolute;left:.06em;top:0;z-index:-1;
  background-image:inherit;
  background-size:inherit;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  filter:blur(24px) saturate(1.9);
  opacity:.85;
  animation:flow 11s linear infinite;
  pointer-events:none;
}
@keyframes flow{to{background-position:230% 50%}}

/* the drawn strike: cancels the phrase "bad hair" itself */
.thesis__l--hit em::after{
  content:"";position:absolute;left:0;top:54%;z-index:1;
  height:clamp(3px,.42vw,6px);width:100%;
  background:#FF4E4B;
  border-radius:99px;
  /* no glow, no dark seam — on white the bar just needs to be a clean line */
  transform:scaleX(0);transform-origin:left center;
  animation:strike .9s var(--ease) 1.25s forwards;
}
@keyframes strike{to{transform:scaleX(1)}}

@keyframes rise{
  from{opacity:0;transform:translateY(.5em)}
  to{opacity:1;transform:none}
}

.hero__retort{
  font-family:var(--serif);font-style:italic;
  font-size:clamp(1.15rem,2.6vw,1.9rem);
  color:var(--ink);opacity:.66;margin-top:1.5rem;
  animation:rise .95s var(--ease) .62s backwards;
}

/* hugs the left column — a divider running across the whole photo cut it up */
.hero__foot{
  display:flex;flex-wrap:wrap;gap:clamp(1.25rem,3vw,2.25rem);
  align-items:flex-end;
  max-width:46rem;
  margin-top:clamp(1.75rem,4vh,2.5rem);
  padding-top:clamp(1.25rem,2.5vh,1.75rem);
}
.hero__lede{max-width:44ch;color:var(--ink);opacity:.78;line-height:1.65}
/* secondary action stays a hairline outline — no fill, no glass, so the pink
   CTA is the only button carrying weight */
.hero .btn--ghost{
  border-color:rgba(23,18,28,.22);color:var(--ink);
  background:none;backdrop-filter:none;box-shadow:none;
}
.hero .btn--ghost:hover{color:var(--ink);border-color:rgba(255,61,190,.65)}
.hero__cta{
  display:flex;flex-wrap:wrap;gap:.75rem;
  margin-top:clamp(2rem,5vh,3.25rem);
}

/* ══════════════════ the "too much" wall — SIGNATURE ══════════════════ */
.wall{
  /* ★ THE BONE FADES OUT AT THE BOTTOM (2026-08-25). This section owns the only
     light in this part of the page, and the bone→ink horizon below it now lives
     HERE rather than on whatever section follows. That is the durable place for
     it: an outgoing fade depends only on the section that owns the light, so
     reordering what comes next can never strand a light band mid-page again —
     which is exactly what happened when .story was moved in between .wall and
     .work and the blend was still sitting on .work::after.
     ★ The stops are the same easing curve that was on .work::after, reversed.
     Ten of them because this crosses ~220 luma and four visibly band.
     ★ The last stop must reach FULLY transparent: the body behind is ink, and
     anything short of 0 leaves a permanent haze over the section below. */
  background:linear-gradient(180deg,
    var(--bone) 0%,
    var(--bone) calc(100% - 11rem),
    rgba(246,241,244,.96) calc(100% - 9.9rem),
    rgba(246,241,244,.88) calc(100% - 8.5rem),
    rgba(246,241,244,.72) calc(100% - 7rem),
    rgba(246,241,244,.53) calc(100% - 5.7rem),
    rgba(246,241,244,.36) calc(100% - 4.4rem),
    rgba(246,241,244,.22) calc(100% - 3.2rem),
    rgba(246,241,244,.11) calc(100% - 2.1rem),
    rgba(246,241,244,.04) calc(100% - 1rem),
    rgba(246,241,244,0) 100%);
  color:var(--ink);
  padding:clamp(7rem,16vh,10.5rem) 0 clamp(4rem,10vh,7.5rem);
  overflow:hidden;
}
/* dusk band: the hero's night settles into the bone through the brand violet,
   so the dark→light cut reads as a horizon instead of a seam */
.wall::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  /* the hero is light now, so the curtain leads with a pink dawn breath
     instead of the old night-into-day ink ramp */
  background:linear-gradient(180deg,
      rgba(255,61,190,.14) 0%,
      rgba(255,161,225,.08) 45%,
      rgba(246,241,244,0) 100%)
    top/100% clamp(4rem,10vh,7rem) no-repeat;
  /* --dusk is scrubbed 0→1 by Motion as the section arrives; the default of 1
     means no JS = band simply sits in place */
  transform:translateY(calc(-5rem * (1 - var(--dusk,1))));
  opacity:calc(.3 + .7 * var(--dusk,1));
}
.wall__h{
  max-width:var(--maxw);margin:0 auto clamp(1.75rem,4vh,2.75rem);
  padding:0 var(--pad);
  font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
  font-weight:600;opacity:.45;
}
.wall__marquee{display:grid;gap:clamp(.1rem,.6vw,.5rem);user-select:none}
.wall__track{
  display:flex;gap:clamp(1.25rem,3vw,3rem);width:max-content;
  animation:slide 44s linear infinite;
}
.wall__track--rev{animation-direction:reverse;animation-duration:56s}
.wall__track span{
  position:relative;
  font-family:var(--display);
  font-size:clamp(2.4rem,8.5vw,7rem);
  font-weight:700;letter-spacing:-.04em;line-height:1.05;
  color:#C9BCC6;                      /* the words are muted... */
  white-space:nowrap;
}
.wall__track span::after{           /* ...and every one is struck out */
  content:"";position:absolute;left:-.02em;right:-.02em;top:52%;
  height:clamp(3px,.55vw,7px);
  background:var(--ember);border-radius:99px;
  opacity:.9;
}
@keyframes slide{to{transform:translateX(-50%)}}

.wall__quote{
  max-width:min(56rem,var(--maxw));margin:clamp(3rem,8vh,5.5rem) auto 0;
  padding:0 var(--pad);
}
.wall__quote p{
  font-family:var(--serif);
  font-size:clamp(1.6rem,4.2vw,3.1rem);
  line-height:1.16;letter-spacing:-.015em;
}
.wall__quote em{font-style:italic;color:var(--plum-2)}
.wall__quote-tail{
  font-family:var(--body)!important;
  font-size:1.02rem!important;line-height:1.65!important;
  letter-spacing:0!important;
  margin-top:1.75rem;max-width:52ch;opacity:.62;
}

/* ══════════════════ the work ══════════════════ */
/* rails bleed to the viewport edge, so the section carries no side padding;
   heads and rails share the same inset so the first card aligns with the text */
.work{
  /* no top padding: the intro veil opens on the wall's bone, so any gap here
     reads as a stray ink stripe between two light surfaces */
  padding:0 0 clamp(2.25rem,4.5vh,3.25rem);
  background:var(--ink);
}
/* ══════ .work's atmosphere — blooms only, no horizon ══════
   ★ THE BONE BAND THAT USED TO LIVE HERE IS GONE (2026-08-25). It existed to
   bridge .wall's bone into .work's ink back when .wall sat directly above.
   .story now sits between them, so .work follows a DARK section and a bone band
   here would paint a stray light stripe across the middle of the page. The
   horizon moved to .wall as an outgoing fade — see the note on .wall.
   ★ What stays is what was always .work__intro's own atmosphere: the violet
   bloom behind "My work!" and the pink one under it. Still painted on .work
   rather than on .work__intro, because a 324px box clips its own gradients at
   both edges — that lesson is unchanged and is why this lives here at all.
   ★ The mask stays too: it guarantees the layer dissolves before its own bottom
   edge, so the blooms can be retuned freely without ever drawing a line. */
.work::after{
  content:"";position:absolute;inset:0 0 auto 0;z-index:-1;pointer-events:none;
  height:clamp(30rem,64vh,44rem);
  background:
    radial-gradient(46rem 18rem at 16% 62%,rgba(154,43,240,.34),transparent 66%),
    radial-gradient(32rem 13rem at 42% 78%,rgba(255,61,190,.14),transparent 62%);
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 58%,rgba(0,0,0,.55) 78%,transparent 100%);
          mask-image:linear-gradient(180deg,#000 0%,#000 58%,rgba(0,0,0,.55) 78%,transparent 100%);
}
.work .sec-head{
  max-width:none;
  padding-inline:max(var(--pad),calc((100vw - var(--maxw))/2));
}

/* ── lookbook rails ── */
.railgroup{margin-bottom:clamp(2.75rem,6vh,4.25rem)}
.railgroup:last-child{margin-bottom:0}
.railgroup__head{
  display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;
  padding-inline:max(var(--pad),calc((100vw - var(--maxw))/2));
  margin-bottom:1.1rem;
}
/* quiet, editorial: a lit dot + small caps, not display type */
.railgroup__title{
  display:flex;align-items:center;gap:.6rem;
  font-family:var(--body);
  font-size:.8rem;font-weight:600;
  letter-spacing:.22em;text-transform:uppercase;line-height:1;
}
.railgroup__title::before{
  content:"";width:7px;height:7px;border-radius:50%;
  background:var(--orchid);box-shadow:0 0 9px rgba(255,61,190,.8);
}
.railgroup__range{font-size:.82rem;opacity:.5;margin-top:.5rem}
.railgroup__nav{display:flex;gap:.5rem}
.railbtn{
  width:2.6rem;height:2.6rem;border-radius:50%;
  display:grid;place-items:center;
  font-size:1.5rem;line-height:0;color:var(--bone);
  border:1px solid rgba(246,241,244,.2);
  background:linear-gradient(180deg,rgba(246,241,244,.07),rgba(246,241,244,.02));
  transition:border-color .25s,color .25s,transform .25s;
}
.railbtn:hover{border-color:var(--orchid);color:var(--orchid);transform:translateY(-1px)}

.rail{
  display:grid;grid-auto-flow:column;
  grid-auto-columns:clamp(17.5rem,27vw,25rem);
  gap:clamp(1.1rem,2.4vw,2.1rem);
  overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  scroll-padding-inline:max(var(--pad),calc((100vw - var(--maxw))/2));
  padding-inline:max(var(--pad),calc((100vw - var(--maxw))/2));
  padding-block:.25rem 1rem;
  scrollbar-width:none;
}
.rail::-webkit-scrollbar{display:none}

/* editorial: flat image, two-line museum placard below — the cut's name,
   then the price. No chrome on the photo itself. */
.tile{
  position:relative;margin:0;
  display:flex;flex-direction:column;
  scroll-snap-align:start;
  cursor:zoom-in;
}
.tile img{
  order:0;
  width:100%;aspect-ratio:3/4;object-fit:cover;
  background:var(--ink-2);
  transition:opacity .45s var(--ease),transform .6s var(--ease);
}
.tile::after{ /* the cut */
  order:1;
  content:attr(data-service);
  padding:.85rem .1rem 0;
  font-size:.7rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;
  line-height:1.35;
  color:rgba(246,241,244,.78);
  /* WRAP, don't truncate. `white-space:nowrap` + ellipsis suited the wide reel
     frames, but the Known-for grid is 165px per tile on a 393px phone and it
     chopped "Cool blonde balayage" to "COOL BLONDE BALAYA…". The cut's NAME is
     the one thing on the placard that has to survive. Two lines, then clamp. */
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  overflow:hidden;
  transition:color .3s;
}
.tile::before{ /* the price */
  order:2;
  content:attr(data-price);
  padding:.2rem .1rem 0;
  font-family:var(--display);
  font-size:.95rem;font-weight:700;letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;
  color:var(--orchid);
}
.tile:hover img,.tile:focus-within img{opacity:.88;transform:translateY(-4px)}
.tile:hover::after,.tile:focus-within::after{color:var(--bone)}

/* ── the reel-intro band: Higgsfield strands clip under the section title,
   melting into the ink the rails sit on ── */
/* A transition BAND, not a picture. It used to hold an abstract 3D-ribbon clip
   that read as a blurry blue wave — nothing to do with her, and it put a hard
   photographic edge between the wall's bone and this section's ink. Now the
   handoff is pure gradient, and the band is short enough to read as a deliberate
   seam rather than an empty hero. */
.work__intro{
  position:relative;
  display:flex;align-items:flex-end;
  /* Back to its original height (2026-08-25). It was raised on 2026-08-24 for
     one reason — to give the bone→ink horizon a long enough runway above the
     bottom-anchored heading — and that horizon has since moved to .wall. The
     extra height bought nothing else, so it goes back rather than lingering as
     unexplained slack. */
  min-height:clamp(15rem,36vh,21rem);
  margin-bottom:clamp(2.5rem,6vh,4rem);
  /* Kept from the old build: the band's top row used to antialias against
     .work's ink and drew a dark line. The atmosphere now lives on .work::after
     which starts at .work's own top edge, but this -1px still stops a subpixel
     rounding gap opening between .wall and .work at fractional zoom levels. */
  margin-top:-1px;
  /* ★ NO BACKGROUND HERE ANY MORE. It moved to .work::after — see the note
     there. Every hard edge in this area came from atmosphere being painted
     inside a 324px box: the top edge cut the dusk short, and the bottom edge
     sliced the violet bloom off mid-falloff (measured luma 44.6 → 23.4). A
     background can't escape its own element's box no matter what `overflow`
     says, so the only real fix was to paint it on something taller. */
}


.work__intro .sec-head{
  position:relative;z-index:2;
  width:100%;margin:0;
  padding-bottom:clamp(1.25rem,3.5vh,2rem);
}

@media (max-width:720px){
  .rail{grid-auto-columns:min(70vw,19rem)}
  .railgroup__nav{display:none}   /* swipe is the control on touch */
}

/* ══════════════════ story ══════════════════ */
.story{
  position:relative;isolation:isolate;overflow:hidden;
  display:grid;
  /* The balanced split, held everywhere under 1280px. The photo takes the
     larger share only where there is room for it — see the min-width rule
     below, and the measurement that forced it. */
  grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(2rem,5vw,4.5rem);
  align-items:center;
  padding:clamp(2.75rem,6vh,4.25rem) var(--pad) clamp(4rem,11vh,8rem);
  max-width:var(--maxw);margin:0 auto;
  background:
    radial-gradient(ellipse 70% 50% at 15% 40%,rgba(74,30,92,.5),transparent 70%);
}

.story__figures{position:relative}
.story__portrait{margin:0;border-radius:4px;overflow:hidden}
.story__portrait img{width:100%}
.story__inset{
  /* ★ RESIZED 2026-08-24 FOR A LANDSCAPE PORTRAIT (was min(44%,15rem) at
     -1.5rem/-2rem). Those numbers were composed against a 3:4 portrait, where a
     15rem block clipped a small corner. Tyce's replacement photo is 3:2, so the
     same block covered a third of the frame and sat squarely on her right arm.
     Smaller, and hung further past the corner so most of its area falls OUTSIDE
     the photo: it now overlaps roughly the bottom-right 18% x 23%, which is
     dress and background — never her face, hands or the forearm tattoo.
     If the portrait ever goes back to 3:4, restore the old values. */
  position:absolute;right:-1.75rem;bottom:-3rem;
  width:min(34%,11rem);margin:0;
  border:6px solid var(--ink);border-radius:4px;overflow:hidden;
  box-shadow:0 20px 50px rgba(0,0,0,.5);
}
.story__text .sec-head__h{margin-bottom:1.75rem}
.story__lead{
  font-family:var(--serif);
  font-size:clamp(1.35rem,2.8vw,2rem);
  line-height:1.28;margin-bottom:1.5rem;
}
.story__text p:not(.story__lead):not(.story__pull){
  opacity:.76;margin-bottom:1.15rem;max-width:52ch;line-height:1.7;
}
.story__text strong{color:var(--bone);font-weight:600}
.story__text em{font-style:italic;color:var(--orchid)}
.story__pull{
  margin-top:2.25rem;padding-top:1.5rem;
  border-top:1px solid rgba(255,61,190,.3);
  font-family:var(--display);
  font-size:clamp(1.15rem,2.2vw,1.6rem);
  font-weight:600;letter-spacing:-.02em;line-height:1.2;
  color:var(--orchid);
}
@media (max-width:860px){
  .story{grid-template-columns:1fr;gap:3.5rem}
  .story__figures{max-width:32rem}   /* raised with the landscape portrait — 26rem was capping it on tablets */
  .story__inset{right:-1rem;bottom:-1.5rem}
}

/* ★ THE PHOTO BECOMES THE WIDER COLUMN ONLY AT 1280px AND UP. Tyce asked for a
   bigger image after her landscape 3:2 portrait went in — at .9fr it rendered
   just 544x363 against a 594px-tall text column and left dead space beneath it.
   ★ 1280 IS A MEASURED FLOOR, NOT A ROUND NUMBER. Applied unconditionally, the
   1.2fr split squeezed the text column to 293px at 861px wide — about 33
   characters a line, materially worse than the .9fr it replaced. It only clears
   a sane measure once the viewport can afford it: 435px (~50 characters) at
   1280, 490px at 1440. If this ratio is ever pushed further, re-run the width
   sweep before believing it — the desktop screenshot looks fine either way. */
@media (min-width:1280px){
  .story{
    grid-template-columns:minmax(0,1.2fr) minmax(0,.8fr);
    gap:clamp(2rem,5vw,4.5rem);
  }
}

/* ══════════════════ vows ══════════════════ */
.vows{
  /* eases in from the story's ink, with a faint orchid horizon where the
     hairline rule used to sit — a glow reads as flow, a border reads as a cut */
  background:
    linear-gradient(180deg,var(--ink) 0%,rgba(23,18,28,0) 20%),
    radial-gradient(60rem 9rem at 50% 0,rgba(255,61,190,.13),transparent 70%),
    linear-gradient(168deg,var(--plum) 0%,var(--ink) 78%);
  padding:clamp(4rem,10vh,7rem) var(--pad);
}
/* .pains shares this grid: the two sections are one problem->promise argument
   and must line up column-for-column, so they get one layout rule, not two. */
.vows,
.pains{
  display:grid;
  grid-template-columns:minmax(0,.72fr) minmax(0,1.28fr);
  gap:clamp(1.5rem,5vw,4.5rem);
  /* STRETCH, not start. With align-items:start the left cell collapsed to the
     heading's own height, so position:sticky had nowhere to travel and the
     heading just pinned to the top leaving a dead column beneath it. */
  align-items:stretch;
  max-width:calc(var(--maxw) + 2 * 5.5rem);margin:0 auto;
}
.vows__side{position:relative}
/* the sticky element must be a CHILD of the stretched cell, never the cell */
.vows__sticky{position:sticky;top:clamp(5.5rem,20vh,9rem)}
.vows__h{
  font-family:var(--display);
  font-size:clamp(2rem,5.5vw,4.25rem);
  font-weight:700;letter-spacing:-.035em;line-height:.95;
}
.vows__sub{
  margin-top:clamp(1rem,2.4vh,1.6rem);
  max-width:24ch;
  font-size:.95rem;line-height:1.6;opacity:.52;
}
.vows__list{display:grid;gap:0}

/* ══════ the problem beat, above "In this chair" ══════
   Plain ink on purpose: .vows opens with `linear-gradient(180deg,var(--ink) 0%,
   transparent 20%)`, i.e. it already assumes ink above it, so a flat ink panel
   here hands off to that gradient with no visible seam. Do NOT give this its own
   gradient — you get two horizons a screen apart. */
.pains{
  background:var(--ink);
  padding:clamp(3.5rem,8vh,5.5rem) var(--pad) clamp(1rem,2vh,1.75rem);
  max-width:calc(var(--maxw) + 2 * 5.5rem);margin:0 auto;
}
/* Questions, not statements, so they are set lighter and larger than a vow's
   body copy but never as loud as .vows__key — the promise below has to win. */
.pains__list li{
  font-family:var(--display);
  font-size:clamp(1.15rem,2.2vw,1.55rem);
  font-weight:400;letter-spacing:-.015em;line-height:1.3;
  opacity:.72;
  padding:clamp(1.05rem,2.4vh,1.5rem) 0;
  max-width:30ch;
}
.pains__list li em{font-style:italic;color:var(--orchid);opacity:1}
@media (max-width:900px){
  .vows,.pains{grid-template-columns:1fr;gap:2rem}
  .vows__sticky{position:static}
  .vows__sub{max-width:none}
}
.vows__list li{
  padding:clamp(1.5rem,3.5vh,2.25rem) 0;
  border-top:1px solid rgba(246,241,244,.13);
  max-width:74ch;line-height:1.62;opacity:.8;
  font-size:1.03rem;
}
.vows__list li:last-child{border-bottom:1px solid rgba(246,241,244,.13)}
.vows__key{
  display:block;
  font-family:var(--display);
  font-size:clamp(1.2rem,2.6vw,1.8rem);
  font-weight:700;letter-spacing:-.025em;line-height:1.15;
  color:var(--bone);opacity:1;
  margin-bottom:.55rem;
}

/* ══════════════════ prices : clickable photo panels ══════════════════ */
.menuband{padding:clamp(4rem,10vh,7.5rem) var(--pad);background:var(--ink)}
.menuband::before{
  animation-duration:46s;animation-delay:-18s;
  background:
    radial-gradient(48rem 30rem at 12% 10%,rgba(74,30,92,.45),transparent 60%),
    radial-gradient(40rem 26rem at 90% 90%,rgba(255,61,190,.12),transparent 58%);
}
.menuband__grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:clamp(6px,.8vw,12px);
  max-width:var(--maxw);margin:0 auto;
}
.panel{
  position:relative;display:flex;flex-direction:column;justify-content:flex-end;
  min-height:clamp(28rem,70vh,40rem);
  overflow:hidden;text-decoration:none;color:var(--bone);
}
.panel__img{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;object-fit:cover;
  transition:transform .9s var(--ease);
}
.panel::before{ /* menu-card scrim */
  content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(to top,
    rgba(19,13,24,.93) 0%,rgba(19,13,24,.72) 38%,
    rgba(19,13,24,.22) 66%,rgba(19,13,24,.34) 100%);
  transition:background .5s var(--ease);
}
/* big vertical label, reads bottom-up like a spine */
.panel__label{
  position:absolute;top:1.15rem;left:1.1rem;z-index:2;
  writing-mode:vertical-rl;transform:rotate(180deg);
  font-family:var(--display);
  font-size:clamp(1.9rem,3.2vw,2.9rem);
  font-weight:700;letter-spacing:-.02em;line-height:1;
  color:var(--bone);
  text-shadow:0 2px 22px rgba(10,7,14,.6);
}
.panel .rates{position:relative;z-index:2;margin:0;padding:0 clamp(1.1rem,2vw,1.6rem)}
.panel__note{
  position:relative;z-index:2;
  padding:.6rem clamp(1.1rem,2vw,1.6rem) 0;
  font-size:.78rem;opacity:.55;
}
.panel__cta{
  position:relative;z-index:2;align-self:flex-start;
  margin:1.15rem clamp(1.1rem,2vw,1.6rem) clamp(1.1rem,2vw,1.5rem);
  display:inline-flex;align-items:center;gap:.45rem;
  padding:.65rem 1.3rem;border-radius:999px;
  font-size:.74rem;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
  border:1px solid rgba(246,241,244,.3);
  background:rgba(19,13,24,.35);backdrop-filter:blur(8px);
  transition:border-color .3s,background .3s,transform .3s;
}
.panel:hover .panel__img,.panel:focus-visible .panel__img{transform:scale(1.05)}
.panel:hover .panel__cta,.panel:focus-visible .panel__cta{
  border-color:var(--orchid);background:rgba(255,61,190,.18);transform:translateX(3px);
}
.panel:focus-visible{outline-offset:-3px}

.rates{margin:0}
.rate{
  display:flex;align-items:baseline;gap:.75rem;
  padding:.66rem 0;
  border-bottom:1px solid rgba(246,241,244,.16);
}
.rate dt{flex:1;font-size:.92rem}
.rate dd{
  margin:0;
  font-family:var(--display);font-size:1.05rem;font-weight:700;
  font-variant-numeric:tabular-nums;letter-spacing:-.02em;
  color:#FFA1E1;
}
.menuband__fine{
  max-width:var(--maxw);margin:1.75rem auto 0;
  font-size:.85rem;opacity:.5;
}
@media (max-width:900px){
  .menuband__grid{grid-template-columns:1fr}
  .panel{min-height:24rem}
}

/* ══════════════════ two chairs ══════════════════ */
.chairs{
  padding:clamp(4rem,10vh,7.5rem) var(--pad);
  background:var(--ink);
}
.chairs__grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,17.5rem),1fr));
  gap:clamp(1rem,2.5vw,1.75rem);
  max-width:var(--maxw);margin:0 auto;
}
.chair{
  display:flex;flex-direction:column;
  padding:clamp(1.5rem,3vw,2.25rem);
  background:var(--ink-2);
  border:1px solid rgba(246,241,244,.11);
  border-radius:6px;
  transition:border-color .35s var(--ease),transform .35s var(--ease);
}
.chair:hover{border-color:rgba(255,61,190,.45);transform:translateY(-3px)}
.chair__when{
  font-size:.68rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--orchid);font-weight:600;
}
.chair__where{
  font-family:var(--display);
  font-size:clamp(1.7rem,3.4vw,2.5rem);
  font-weight:700;letter-spacing:-.035em;line-height:1;
  margin:.65rem 0 1rem;
}
.chair__addr{font-style:normal;opacity:.68;line-height:1.55;font-size:.95rem}
.chair__hours{
  display:flex;align-items:center;gap:.5rem;
  margin:1.1rem 0 1.75rem;font-size:.85rem;opacity:.6;
}
.chair__dot{width:5px;height:5px;border-radius:50%;background:var(--clay);flex-shrink:0}
.chair__talk{opacity:.68;line-height:1.6;font-size:.95rem;margin:0 0 1.75rem}
.chair .btn{margin-top:auto}
.chair__via{
  margin-top:.85rem;font-size:.76rem;opacity:.42;line-height:1.5;
}
.chair--talk{background:transparent;border-style:dashed}
.chair.is-open{border-color:rgba(255,61,190,.55);background:var(--ink-3)}
.chair.is-open .chair__dot{background:var(--orchid);box-shadow:0 0 8px var(--orchid)}

.chairs__cancel{
  max-width:var(--maxw);margin:2.5rem auto 0;
  font-size:.88rem;opacity:.5;
}

/* ══════════════════ hair school ══════════════════ */
.school{
  background:var(--bone);
  color:var(--ink);
  padding:clamp(7rem,16vh,10.5rem) var(--pad) clamp(8.5rem,19vh,12rem);
}
/* same horizon treatment on both edges: the chairs' ink eases in through
   violet up top, and the bottom settles back into the dark the FAQ sits on */
.school::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    linear-gradient(180deg,
      var(--ink) 0%,rgba(35,20,44,.94) 26%,rgba(94,52,122,.4) 58%,
      rgba(255,61,190,.12) 78%,rgba(246,241,244,0) 100%)
      top/100% clamp(6rem,14vh,9rem) no-repeat,
    linear-gradient(0deg,
      var(--ink) 0%,rgba(35,20,44,.94) 26%,rgba(94,52,122,.4) 58%,
      rgba(255,61,190,.12) 78%,rgba(246,241,244,0) 100%)
      bottom/100% clamp(6rem,14vh,9rem) no-repeat;
  opacity:calc(.25 + .75 * var(--dusk,1));
}
.school .sec-head__note{opacity:.62}
.school__grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,17rem),1fr));
  gap:clamp(1.75rem,4vw,3rem);
  max-width:var(--maxw);margin:0 auto;
}
.lesson{padding-top:1.25rem;border-top:2px solid var(--ink)}
.lesson h3{
  font-family:var(--display);
  font-size:clamp(1.3rem,2.4vw,1.75rem);
  font-weight:700;letter-spacing:-.03em;margin-bottom:.9rem;
}
.lesson p{font-size:.95rem;line-height:1.65;opacity:.72;margin-bottom:.85rem}
.lesson em{font-style:italic;color:var(--plum-2);font-weight:500}
.lesson__kick{
  font-family:var(--serif)!important;font-style:italic;
  font-size:1.2rem!important;opacity:1!important;
  color:var(--plum-2);margin-top:1.25rem!important;
}

/* ══════════════════ faq ══════════════════ */
.faq{padding:clamp(4rem,10vh,7.5rem) var(--pad);max-width:var(--maxw);margin:0 auto}
.faq .sec-head__h{margin-bottom:2.75rem}
.faq__list{max-width:52rem}
.qa{border-top:1px solid rgba(246,241,244,.15)}
.qa:last-child{border-bottom:1px solid rgba(246,241,244,.15)}
.qa summary{
  display:flex;align-items:center;justify-content:space-between;gap:1.5rem;
  padding:1.4rem 0;cursor:pointer;list-style:none;
  font-family:var(--display);
  font-size:clamp(1.05rem,2vw,1.35rem);
  font-weight:600;letter-spacing:-.02em;line-height:1.25;
  transition:color .25s;
}
.qa summary::-webkit-details-marker{display:none}
.qa summary::after{
  content:"+";flex-shrink:0;
  font-size:1.5rem;font-weight:400;color:var(--orchid);
  transition:transform .35s var(--ease);
}
.qa[open] summary::after{transform:rotate(45deg)}
.qa summary:hover{color:var(--orchid)}
.qa__body{padding:0 0 1.6rem;max-width:60ch}
.qa__body p,.qa__body li{opacity:.72;line-height:1.7;font-size:.98rem}
.qa__body ul{display:grid;gap:.6rem}
.qa__body li{padding-left:1.1rem;position:relative}
.qa__body li::before{
  content:"";position:absolute;left:0;top:.7em;
  width:5px;height:5px;border-radius:50%;background:var(--orchid);
}
.qa__body strong{color:var(--bone);font-weight:600}

/* ══════════════════ closer ══════════════════ */
.closer{
  position:relative;overflow:hidden;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
  gap:clamp(2rem,5vw,4.5rem);align-items:center;
  padding:clamp(4rem,10vh,7rem) max(var(--pad),calc((100vw - var(--maxw))/2));
  /* fallback mesh — also what reduced-motion and no-JS visitors see */
  background:
    radial-gradient(48rem 30rem at 80% 16%,rgba(74,30,92,.6),transparent 62%),
    radial-gradient(40rem 26rem at 6% 96%,rgba(255,61,190,.12),transparent 60%),
    var(--ink);
}
/* Higgsfield silk loop — decorative, lazy-loaded by JS near the viewport */
.closer__loop{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;object-fit:cover;
  opacity:.45;
}
.closer__veil{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(180deg,rgba(23,18,28,.78),rgba(23,18,28,.5) 48%,rgba(23,18,28,.85));
}
.closer__img,.closer__text{position:relative;z-index:2}
@media (prefers-reduced-motion:reduce){.closer__loop{display:none}}
.closer__img{margin:0;border-radius:4px;overflow:hidden}
.closer__text h2{
  font-family:var(--display);
  font-size:clamp(2.2rem,6vw,4.75rem);
  font-weight:800;letter-spacing:-.04em;line-height:.93;
}
.closer__text p{margin:1.5rem 0 2.25rem;opacity:.7;max-width:38ch}
@media (max-width:800px){
  .closer{grid-template-columns:1fr}
  .closer__img{max-width:22rem}
}

/* ══════════════════ footer ══════════════════ */
.foot{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));
  gap:clamp(1.75rem,4vw,3rem);
  padding:clamp(3rem,7vh,5rem) var(--pad) 2rem;
  max-width:var(--maxw);margin:0 auto;
  border-top:1px solid rgba(246,241,244,.14);
}
.foot__mark{
  font-family:var(--display);font-size:2rem;font-weight:800;letter-spacing:-.04em;
}
.foot__tag{font-size:.82rem;opacity:.5;margin-top:.4rem;line-height:1.5}
.foot__col h2{
  font-size:.68rem;letter-spacing:.2em;text-transform:uppercase;
  font-weight:600;opacity:.45;margin-bottom:.9rem;
}
.foot__col p{font-size:.92rem;opacity:.72;line-height:1.6;margin-bottom:.55rem}
.foot__col a{text-decoration:none;border-bottom:1px solid rgba(255,61,190,.4);transition:color .25s}
.foot__col a:hover{color:var(--orchid)}
.foot__fine{font-size:.78rem!important;opacity:.42!important;margin-top:.9rem}
.foot__base{
  grid-column:1/-1;margin-top:1.5rem;padding-top:1.5rem;
  border-top:1px solid rgba(246,241,244,.1);
  font-size:.75rem;opacity:.4;
}

/* ══════════════════ mobile nav (built by main.js) ══════════════════ */
.navtoggle{
  display:grid;gap:5px;padding:.6rem;margin-left:.25rem;order:9;
}
.navtoggle span{
  display:block;width:22px;height:1.5px;background:var(--bone);
  transition:transform .3s var(--ease),opacity .3s var(--ease);
}
.navtoggle.is-x span:first-child{transform:translateY(3.25px) rotate(45deg)}
.navtoggle.is-x span:last-child{transform:translateY(-3.25px) rotate(-45deg)}

.navsheet{
  position:fixed;inset:0;z-index:99;
  background:rgba(23,18,28,.97);
  backdrop-filter:blur(18px);
  display:grid;place-items:center;
}
/* REQUIRED, not tidiness. main.js closes the sheet with `sheet.hidden = true`,
   but the hidden attribute only carries display:none in the UA stylesheet — any
   author `display` beats it. Without this rule the sheet NEVER hides, and every
   viewport under 1081px loads with the whole site behind an opaque menu (the
   min-width:1081px block below is the only reason desktop looked fine).
   Any element given a `display` must restate [hidden] if JS toggles .hidden. */
.navsheet[hidden]{display:none}
.navsheet nav{display:grid;gap:.5rem;text-align:center}
.navsheet a{
  font-family:var(--display);
  font-size:clamp(2rem,9vw,3.25rem);
  font-weight:700;letter-spacing:-.035em;line-height:1.1;
  text-decoration:none;padding:.35rem 1rem;
  transition:color .25s;
}
.navsheet a:hover{color:var(--orchid)}
/* MUST stay the exact complement of the `max-width:1300px` rule that hides
   .bar__nav. If these two breakpoints ever drift apart, the widths between them
   get NO navigation at all — inline nav hidden AND burger hidden. */
@media (min-width:1221px){
  .navtoggle,.navsheet{display:none}
}

/* ══════════════════ the work: showcase + reel ══════════════════
   Two halves. Four tall category panels edge-to-edge (each photo gets a whole
   column and a vertical word), then a slow-drifting ribbon of everything else.
   Replaces a 27-tile rail set, then a single-stage spotlight — the panels give
   the work scale, the ribbon gives it volume, without one fighting the other. */
.showcase{
  display:grid;grid-template-columns:repeat(4,1fr);
  gap:2px;margin-bottom:clamp(3rem,7vh,5rem);
}
.show{
  position:relative;display:block;overflow:hidden;
  height:clamp(30rem,80vh,52rem);
  text-decoration:none;color:var(--bone);
  isolation:isolate;
}
.show__img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transition:transform 1.1s var(--ease),filter .6s var(--ease);
  filter:saturate(.92) brightness(.82);
}
.show::after{                       /* legibility floor for the label */
  content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,rgba(23,18,28,.15) 0%,rgba(23,18,28,0) 38%,rgba(23,18,28,.72) 100%);
}
.show:hover .show__img,.show:focus-visible .show__img{transform:scale(1.06);filter:saturate(1) brightness(.9)}

/* the vertical word — the whole point of the layout */
.show__label{
  position:absolute;left:clamp(.9rem,1.6vw,1.6rem);bottom:clamp(4.6rem,9vh,6rem);z-index:2;
  writing-mode:vertical-rl;transform:rotate(180deg);
  font-family:var(--display);font-weight:600;
  font-size:clamp(2.9rem,5.2vw,5.2rem);
  line-height:1;letter-spacing:-.035em;
  text-transform:lowercase;
  text-shadow:0 2px 30px rgba(23,18,28,.5);
}
.show__pill{
  position:absolute;left:clamp(.9rem,1.6vw,1.6rem);bottom:clamp(1.5rem,3vh,2.2rem);z-index:2;
  display:inline-flex;align-items:center;gap:.45rem;
  padding:.5rem 1rem;border-radius:999px;
  background:rgba(246,241,244,.92);color:var(--ink);
  font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;font-weight:600;
  transition:background .3s var(--ease),gap .3s var(--ease);
}
.show:hover .show__pill{background:#fff;gap:.75rem}

/* ── the reel: a curved, drag-anywhere gallery ──
   A real horizontal scroller (so trackpad, shift-wheel, touch and keyboard all
   work for free) with the arc applied per-frame in JS. perspective on the
   scroller is what makes the rotateY read as a curve rather than a skew.

   NOTE: no cursor change on purpose — Brandon asked for drag without the
   pointer swapping to a grab hand. */
.reel__title{
  margin:0 var(--pad) clamp(.4rem,1vh,.9rem);
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.6rem,3.2vw,2.8rem);
  line-height:1;letter-spacing:-.03em;
}

.reel{
  position:relative;
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
  perspective:1400px;
  padding:clamp(2.2rem,5vh,3.6rem) 0 clamp(1rem,2.5vh,1.6rem);
}
.reel::-webkit-scrollbar{display:none}
.reel__track{
  display:flex;align-items:center;
  gap:clamp(.7rem,1.4vw,1.15rem);
  width:max-content;
  /* half a viewport of lead-in/out so the first and last frames can reach the
     centre of the arc instead of being stuck at the edge */
  padding:0 42vw;
  transform-style:preserve-3d;
}
.reel .tile{
  flex:0 0 auto;margin:0;
  width:clamp(11rem,24vw,22rem);
  border-radius:6px;overflow:hidden;
  transform-style:preserve-3d;
  will-change:transform;
  /* set by initReel; the transition only smooths the hover lift, the curve
     itself is written every scroll frame and must not lag */
  transition:box-shadow .4s var(--ease),filter .4s var(--ease);
  box-shadow:0 22px 46px -26px rgba(0,0,0,.75);
  filter:brightness(.86);
}
.reel .tile img{display:block;width:100%;aspect-ratio:3/4;object-fit:cover;pointer-events:none}
.reel .tile.is-front{filter:brightness(1);box-shadow:0 34px 66px -26px rgba(0,0,0,.85)}
/* while a drag is in progress, kill click-through on the frames */
.reel.is-dragging .tile{pointer-events:none}

.reel__hint{
  margin:clamp(1.1rem,2.6vh,1.7rem) var(--pad) 0;
  font-size:.68rem;letter-spacing:.2em;text-transform:uppercase;
  opacity:.38;
}
@media (max-width:860px){
  .showcase{grid-template-columns:repeat(2,1fr)}
  .show{height:clamp(20rem,52vh,28rem)}
}
@media (prefers-reduced-motion:reduce){
  .reel__track{animation:none}
  .reel{overflow-x:auto}
}

/* ══════════ brand gradient on every major heading ══════════
   One rule, so a new section heading inherits it automatically instead of
   needing a bespoke block. First and last stops are the SAME violet, so the
   sweep loops without a snap (see the seamless-loop maths on the wordmark).
   Deliberately NOT applied to small type — .wall__h (.72rem), .post__title,
   .menu__cat and .chair__where stay solid, because a gradient across a few
   hundred pixels of small text just reads as muddy colour. */
.sec-head__h,
.vows__h,
.reel__title,
.lbk__title{
  background-image:linear-gradient(100deg,
    #7B2BE8 0%,#A855F7 14%,#C13BFF 28%,#F03DFF 42%,#FF3DBE 50%,
    #F03DFF 58%,#C13BFF 72%,#A855F7 86%,#7B2BE8 100%);
  background-size:200% 100%;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:thesisflow 9s linear infinite;
}
@media (prefers-reduced-motion:reduce){
  .sec-head__h,.vows__h,.reel__title{animation:none;background-position:40% 50%}
}

/* ── the wall quote, given weight ──
   Bigger, and framed by an outline that tilts WITH it (the tilt is applied to
   this same element, so the frame turns as one object rather than the text
   sliding inside a static box). */
.wall__quote{
  position:relative;
  padding:clamp(2rem,4.5vw,3.4rem) clamp(1.8rem,4vw,3.2rem);
  border:1px solid rgba(23,18,28,.16);
  border-radius:3px;
  background:linear-gradient(150deg,rgba(255,255,255,.5),rgba(255,255,255,.14));
  backdrop-filter:blur(2px);
  box-shadow:0 30px 70px -45px rgba(74,30,92,.55);
}
/* a second, offset rule — the "outline that follows it" reads as depth once the
   block turns, because the two frames separate as it rotates */
.wall__quote::before{
  content:"";position:absolute;inset:-9px;z-index:-1;
  border:1px solid rgba(154,43,240,.22);
  border-radius:5px;
  transform:translateZ(-18px);
  pointer-events:none;
}
.wall__quote p{
  font-size:clamp(1.9rem,3.6vw,3.1rem);
  line-height:1.12;letter-spacing:-.02em;
}
.wall__quote .wall__quote-tail{
  font-family:var(--body);
  font-size:clamp(.98rem,1.25vw,1.14rem);
  line-height:1.62;letter-spacing:0;
  margin-top:clamp(1.1rem,2.4vh,1.7rem);
  opacity:.62;max-width:44ch;
}

/* cursor tilt: the transition is only present at REST, so returning to centre
   eases but tracking the pointer stays instant — a transition during tracking
   makes the element lag the cursor and feel broken. */
[data-tilt]{
  transform-style:preserve-3d;
  transition:transform .55s var(--ease);
  will-change:transform;
}
[data-tilt].is-tilting{transition:none}

/* ══════════════════ price menu ══════════════════
   Typeset like a real menu — leader dots, aligned prices, two columns — rather
   than three photo panels each carrying a long rate list. The photos were doing
   no work here: the WORK rails already show her results, so repeating them
   behind prices just made the section heavy. */
.menu{
  display:grid;gap:clamp(2.5rem,6vw,5rem);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr));
  max-width:var(--maxw);margin:0 auto;
}
.menu__cat{
  font-family:var(--serif);font-style:italic;font-weight:400;
  font-size:clamp(1.6rem,2.6vw,2.15rem);
  color:var(--orchid);
  margin-bottom:clamp(1.1rem,2.4vh,1.7rem);
}
.menu__list{display:grid;gap:.05rem}

/* the leader: a flexible dotted rule that eats whatever space is left, so the
   price always sits hard right no matter how long the service name runs */
.line{
  display:flex;align-items:baseline;gap:.6rem;
  padding:.62rem 0;
  border-bottom:1px solid rgba(246,241,244,.09);
}
.line__name{flex:0 1 auto;font-size:1rem;line-height:1.35}
.line__name em{
  display:block;font-style:normal;
  font-size:.76rem;letter-spacing:.11em;text-transform:uppercase;
  opacity:.5;margin-top:.16rem;
}
.line__dots{
  flex:1 1 auto;min-width:1.5rem;
  border-bottom:1px dotted rgba(246,241,244,.28);
  transform:translateY(-.28em);
}
.line__price{
  flex:0 0 auto;white-space:nowrap;
  font-family:var(--display);font-weight:600;
  font-size:1.05rem;letter-spacing:-.01em;
}
.line__price small{
  font-family:var(--body);font-weight:400;
  font-size:.6rem;letter-spacing:.14em;text-transform:uppercase;
  opacity:.45;margin-left:.28rem;
}
/* the second location reads as a variant, not a second product */
.line__alt{margin-left:.55rem;opacity:.62;font-size:.92em}

.menu__fine{
  margin-top:clamp(1.4rem,3vh,2rem);
  max-width:42ch;font-size:.92rem;line-height:1.62;opacity:.6;
}
.menu__fine strong{color:var(--orchid);font-weight:600}
.menu__cta{margin-top:clamp(1.5rem,3.4vh,2.2rem)}

/* ══════ the menu as tap-to-expand rows (2026-08-24) ══════
   Tyce's reference was Exhale House's collapsed price list. Mechanically this
   is the FAQ's .qa accordion — native <details>, no JS, "+" rotating to "x" —
   but the summary carries a price on the right, so a row has to read as a menu
   line first and a control second.
   ★ The dotted leader from .line is deliberately NOT reused. With a "+" at the
   end of the row the leader would run to the toggle instead of the price, which
   is exactly the wrong thing to point at. The price simply sits hard right and
   the "+" hangs outside it.
   ★ Rows are their own rules rather than extra selectors on .line: .line is
   still used elsewhere and a shared rule would couple them. */
.menu__list--acc{gap:0}
.svc{border-top:1px solid rgba(246,241,244,.15)}
.svc:last-child{border-bottom:1px solid rgba(246,241,244,.15)}
.svc summary{
  display:flex;align-items:baseline;gap:.75rem;
  /* More air than the first pass. Exhale's list breathes, and that is half of
     why it reads as calm. Also keeps every row well clear of the 44px touch
     minimum — measured at 60px, do not trim this back to save height. */
  padding:clamp(1.15rem,2.6vh,1.5rem) 0;
  cursor:pointer;list-style:none;
  transition:color .25s;
}
.svc summary::-webkit-details-marker{display:none}
/* The only (hover:hover) guard in the stylesheet, and it earns it: these rows
   are the first thing on the page you TAP rather than read, and iOS keeps :hover
   latched on a tapped element until you tap somewhere else — so an unguarded
   hover rule leaves rows glowing after you close them. The open row is coloured
   explicitly instead, so the highlight always means "this one is open". */
@media (hover:hover){
  .svc summary:hover{color:var(--orchid)}
}
.svc[open] summary{color:var(--orchid)}
.svc summary::after{
  content:"+";flex:0 0 auto;order:3;
  width:1ch;text-align:center;
  font-size:1.4rem;font-weight:400;line-height:1;color:var(--orchid);
  transition:transform .35s var(--ease);
}
.svc[open] summary::after{transform:rotate(45deg)}
/* ★ Set in caps and letterspaced, matching the reference Tyce sent. This is
   the single biggest reason their three-row list reads tidy: uniform caps turn
   a column of names into a LIST, where sentence case reads as prose that
   happens to be stacked. Weight drops to 400 to stop the caps shouting. */
.svc__name{
  flex:1 1 auto;
  font-size:.92rem;font-weight:400;
  text-transform:uppercase;letter-spacing:.085em;
  line-height:1.45;
}
.svc__name em{
  display:block;font-style:normal;
  font-size:.7rem;letter-spacing:.13em;text-transform:uppercase;
  opacity:.45;margin-top:.3rem;
}
.svc__price{
  flex:0 0 auto;order:2;white-space:nowrap;
  font-family:var(--display);font-weight:600;
  font-size:1.05rem;letter-spacing:-.01em;
}
/* ★ On a narrow screen the price drops UNDER the name, exactly as the reference
   does (`MINI CUSTOM COLOR` / `$375+` on two lines). Squeezing a long name and a
   price range onto one 320px line was forcing the name to wrap mid-phrase, which
   is the opposite of tidy. The "+" stays vertically centred against the pair. */
@media (max-width:520px){
  .svc summary{flex-wrap:wrap;align-items:center;row-gap:.3rem}
  .svc__name{flex:1 1 100%;order:1}
  .svc__price{order:2;flex:1 1 auto}
  .svc summary::after{order:3;align-self:center}
}

.svc__body{padding:0 0 1.3rem;max-width:54ch}
.svc__body p{margin:0;opacity:.72;line-height:1.7;font-size:.95rem}
.svc__body strong{color:var(--bone);font-weight:600}

/* The rows animate open where the browser can do it for free. Older engines
   just snap, which is the same behaviour the FAQ has always had. */
@supports (interpolate-size:allow-keywords){
  .svc{interpolate-size:allow-keywords}
  .svc::details-content{
    block-size:0;overflow:hidden;
    transition:block-size .38s var(--ease), content-visibility .38s allow-discrete;
  }
  .svc[open]::details-content{block-size:auto}
}
@media (prefers-reduced-motion:reduce){
  .svc summary::after{transition:none}
  .svc::details-content{transition:none}
}

/* ══════════════════ client love ══════════════════
   Real reviews. The lead quote is set large and carries [data-tilt] so it turns
   toward the cursor like the wall quote — the two biggest claims on the site
   (hers, then a client's) behave the same way on purpose. */
.love{
  position:relative;isolation:isolate;
  padding:clamp(4.5rem,11vh,8rem) var(--pad) clamp(5rem,12vh,8.5rem);
  max-width:var(--maxw);margin:0 auto;
}
.say{margin:0}
.say blockquote{margin:0}
.say figcaption{
  margin-top:clamp(.8rem,1.8vh,1.2rem);
  font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--orchid);opacity:.85;
}

/* the lead: a client saying the site's own thesis back to it */
.say--lead{
  margin:clamp(1rem,3vh,2rem) auto clamp(3rem,7vh,5rem);
  text-align:center;
  transform-style:preserve-3d;
}
/* the measure lives HERE, not on the figure: `ch` resolves against the element's
   own font-size, and the figure inherits 16px body type — 26ch on it collapsed
   this to a 210px ribbon. On the <p> the same unit reads the 3.6rem serif. */
.say--lead blockquote p{
  font-family:var(--serif);
  font-size:clamp(1.9rem,4.6vw,3.6rem);
  line-height:1.1;letter-spacing:-.02em;
  text-wrap:balance;
  max-width:24ch;margin-inline:auto;
}
.say--lead em{
  font-style:italic;
  background-image:linear-gradient(100deg,#C13BFF 0%,#FF3DBE 50%,#C13BFF 100%);
  background-size:200% 100%;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:thesisflow 9s linear infinite;
}
.say--lead figcaption{font-size:.74rem}

/* There are FOUR cards, so only 1, 2 or 4 columns are allowed — `auto-fit` was
   landing on 3 at laptop widths (the grid is 1130px inside --maxw + --pad, and
   4×17rem + gaps needs 1178px), which stranded the last card alone beside two
   empty slots. Explicit breakpoints instead. Revisit the counts if she sends a
   fifth review: an odd number orphans at 2-up the same way. */
.love__grid{
  display:grid;gap:clamp(1rem,2.4vw,1.9rem);
  grid-template-columns:1fr;
  align-items:start;
}
@media (min-width:34rem){.love__grid{grid-template-columns:repeat(2,1fr)}}
@media (min-width:100rem){.love__grid{grid-template-columns:repeat(4,1fr)}}
.love__grid .say{
  padding:clamp(1.4rem,3vw,2.1rem);
  border:1px solid rgba(246,241,244,.13);
  border-radius:4px;
  background:linear-gradient(155deg,rgba(246,241,244,.05),rgba(246,241,244,.015));
  transition:border-color .4s var(--ease),transform .4s var(--ease);
}
.love__grid .say:hover{border-color:rgba(255,61,190,.4);transform:translateY(-4px)}
.love__grid blockquote p{
  font-family:var(--serif);
  font-size:clamp(1.05rem,1.5vw,1.28rem);
  line-height:1.45;opacity:.9;
}
.love__grid em{font-style:italic;color:var(--orchid)}
/* stagger alternate cards so the row does not read as a flat strip — but only
   once there is more than one column, since in a single stack it just reads as
   an inconsistent gap */
@media (min-width:34rem){
  .love__grid .say:nth-child(even){transform:translateY(clamp(.8rem,2vh,1.6rem))}
  .love__grid .say:nth-child(even):hover{transform:translateY(calc(clamp(.8rem,2vh,1.6rem) - 4px))}
}

/* ══════════════════ known for ══════════════════
   Her two signatures, each given its own labelled row. Photos run larger than
   the reel's because this is the section she asked for by name — the point is
   that these read as a portfolio, not as filler. */
.known{
  position:relative;isolation:isolate;
  padding:clamp(4.5rem,11vh,8rem) var(--pad) clamp(5rem,12vh,8.5rem);
  max-width:var(--maxw);margin:0 auto;
}
.known__group{margin-top:clamp(2.5rem,6vh,4.25rem)}
.known__head{
  display:flex;align-items:baseline;gap:clamp(.9rem,2.5vw,2rem);flex-wrap:wrap;
  padding-bottom:clamp(.9rem,2vh,1.3rem);
  border-bottom:1px solid rgba(246,241,244,.14);
  margin-bottom:clamp(1.2rem,3vh,2rem);
}
.known__label{
  font-family:var(--serif);font-style:italic;font-weight:400;
  font-size:clamp(1.7rem,3.2vw,2.6rem);
  color:var(--orchid);line-height:1;
}
.known__note{flex:1 1 22ch;min-width:0;font-size:.96rem;line-height:1.6;opacity:.55}

/* rows size themselves from how many photos they hold, so adding a third mullet
   needs no CSS change — just the extra <figure> and data-count */
.known__row{display:grid;gap:clamp(.7rem,1.5vw,1.15rem)}
.known__row[data-count="2"]{grid-template-columns:repeat(2,1fr)}
.known__row[data-count="3"]{grid-template-columns:repeat(3,1fr)}
.known__row[data-count="4"]{grid-template-columns:repeat(4,1fr)}
/* SIX goes 3×2, not 6-across: six columns inside --maxw is ~172px a tile, which
   is smaller than the reel's thumbnails — the wrong direction for the one
   section she asked for by name. Three columns keeps them showcase-sized. */
.known__row[data-count="6"]{grid-template-columns:repeat(3,1fr)}
/* eight -> 4x2; the row keeps showcase-sized tiles instead of a thin strip */
.known__row[data-count="7"],
.known__row[data-count="8"]{grid-template-columns:repeat(4,1fr)}
.known .tile{
  margin:0;border-radius:6px;overflow:hidden;cursor:pointer;
  box-shadow:0 24px 50px -28px rgba(0,0,0,.8);
  transition:transform .5s var(--ease),box-shadow .5s var(--ease),filter .5s var(--ease);
  filter:brightness(.92);
}
.known .tile img{display:block;width:100%;aspect-ratio:3/4;object-fit:cover}
.known .tile:hover{transform:translateY(-8px);filter:brightness(1);box-shadow:0 34px 62px -26px rgba(0,0,0,.9)}

@media (max-width:860px){
  .known__row[data-count="3"],.known__row[data-count="4"],
  .known__row[data-count="6"],.known__row[data-count="7"],
  .known__row[data-count="8"]{grid-template-columns:repeat(2,1fr)}
}

/* ══════════════════ the blog ══════════════════
   Sits on the dark base after Hair School, so that section's existing violet
   horizon band still blends bone -> dark exactly as it did into the FAQ.
   Posts expand in place with <details> — no routing, no build step, and it
   still works with JS off, which is the whole reason not to use a script. */
.journal{
  position:relative;isolation:isolate;
  padding:clamp(5.5rem,13vh,9rem) var(--pad) clamp(6rem,14vh,9.5rem);
  max-width:var(--maxw);margin:0 auto;
}
.journal__list{
  display:grid;gap:clamp(1.6rem,3.4vw,2.75rem);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));
}
.post{
  padding-top:1.15rem;
  border-top:2px solid rgba(246,241,244,.22);
}
.post__meta{
  display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;
  margin-bottom:.9rem;
  font-size:.7rem;letter-spacing:.18em;text-transform:uppercase;
}
.post__tag{
  padding:.3rem .7rem;border-radius:999px;
  background:linear-gradient(120deg,rgba(255,61,190,.22),rgba(154,43,240,.22));
  border:1px solid rgba(255,61,190,.32);
  color:#FFA1E1;
}
.post__meta time{opacity:.5}
.post__title{
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.35rem,2.2vw,1.85rem);
  line-height:1.15;letter-spacing:-.02em;
  margin-bottom:.7rem;
}
.post__excerpt{font-size:1rem;line-height:1.62;opacity:.72}

/* the disclosure. summary::marker is removed in favour of a drawn caret so it
   can carry the brand colour and animate. */
.post__more{margin-top:1.1rem}
.post__more > summary{
  display:inline-flex;align-items:center;gap:.55rem;
  cursor:pointer;list-style:none;
  font-size:.74rem;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
  color:var(--orchid);
  transition:color .25s var(--ease);
}
.post__more > summary::-webkit-details-marker{display:none}
.post__more > summary::after{
  content:"";width:.42rem;height:.42rem;
  border-right:2px solid currentColor;border-bottom:2px solid currentColor;
  transform:rotate(45deg) translateY(-2px);
  transition:transform .25s var(--ease);
}
.post__more[open] > summary::after{transform:rotate(-135deg) translateY(-2px)}
.post__more > summary:hover{color:#FFA1E1}
.post__full{padding-top:1rem}
.post__full p{margin-bottom:.85rem;font-size:.98rem;line-height:1.68;opacity:.78}
.post__full p:last-child{margin-bottom:0}
.post__full em{color:var(--orchid);font-style:normal;font-weight:600}

/* ══════════════════ lightbox ══════════════════ */
.lb{
  border:0;padding:0;max-width:100vw;max-height:100vh;width:100%;height:100%;
  background:rgba(12,9,15,.96);color:var(--bone);
}
.lb::backdrop{background:rgba(12,9,15,.92)}
.lb__fig{
  margin:0;height:100%;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:1.25rem;padding:clamp(2.5rem,7vh,4.5rem) clamp(3.5rem,8vw,6rem);
}
.lb__fig img{
  max-width:100%;max-height:78vh;width:auto;
  object-fit:contain;border-radius:3px;
}
.lb__cap{display:flex;align-items:baseline;gap:.9rem;flex-wrap:wrap;justify-content:center}
.lb__service{font-size:.95rem;letter-spacing:.03em}
.lb__price{
  font-family:var(--display);font-weight:700;font-size:1.15rem;
  color:var(--orchid);font-variant-numeric:tabular-nums;
}
.lb__x{
  position:absolute;top:1rem;right:1.25rem;z-index:2;
  width:2.75rem;height:2.75rem;border-radius:50%;
  display:grid;place-items:center;font-size:1.1rem;
  background:rgba(246,241,244,.1);transition:background .25s;
}
.lb__x:hover{background:rgba(246,241,244,.22)}
.lb__nav{
  position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  width:3rem;height:3rem;border-radius:50%;
  display:grid;place-items:center;font-size:2rem;line-height:0;
  background:rgba(246,241,244,.08);transition:background .25s;
}
.lb__nav:hover{background:rgba(246,241,244,.2)}
.lb__nav--prev{left:.75rem}
.lb__nav--next{right:.75rem}
@media (max-width:560px){
  .lb__fig{padding:4rem 1rem 2rem}
  .lb__nav{width:2.5rem;height:2.5rem;font-size:1.5rem}
}

/* ══════════════════ before & after ══════════════════
   Her request. The AFTER is the layer on TOP, clipped from the LEFT edge to the
   handle — so the finished hair is what you meet, and dragging RIGHT uncovers
   where it started ("the after before the before"). Base CSS parks the handle at
   72%, which means with JS off you still get a legible, labelled split rather
   than a dead image. */
.ba{
  position:relative;isolation:isolate;
  padding:clamp(4.5rem,11vh,8rem) var(--pad) clamp(4rem,10vh,7rem);
  max-width:var(--maxw);margin:0 auto;
}
.ba__feature{margin:clamp(2rem,5vh,3.5rem) auto clamp(3rem,7vh,5rem);max-width:46rem}
/* side by side — same component, same job, so neither should read as secondary */
/* FIVE pairs -> 3 across (3+2). Two across would leave a single orphan on
   the last row. Straight to one column below 1024 so the middle sizes never
   land on the orphaning 2-up.
   Six half-columns rather than three whole ones: each pair spans 2, so the
   widths come out identical to a 3-up, but the last row can start on column 2
   and sit centred instead of hanging off the left edge. */
.ba__grid{
  display:grid;grid-template-columns:repeat(6,1fr);
  gap:clamp(1rem,2.6vw,2.25rem);
  align-items:start;
  margin-top:clamp(2rem,5vh,3.5rem);
}
.ba__grid .ba__feature{max-width:none;margin:0;grid-column:span 2}
/* 7 cards flow 3-up on the 6-col grid; the lone 7th is centered. (The old
   nth-child(4) half-column stagger was composed for the 5-card grid — with a
   third row it orphans card 6, so it's gone.) */
.ba__grid .ba__feature:nth-child(7){grid-column:3 / span 2}
@media (max-width:1024px){
  .ba__grid{grid-template-columns:1fr;gap:clamp(2rem,5vh,3rem)}
  .ba__grid .ba__feature,
  .ba__grid .ba__feature:nth-child(7){grid-column:auto}
}
.ba__compare{
  --split:72%;
  position:relative;overflow:hidden;border-radius:8px;
  aspect-ratio:3/4;
  cursor:ew-resize;
  /* pan-y, not none: a phone must still be able to scroll the page vertically
     with a finger that happens to land on the comparison. */
  touch-action:pan-y;
  user-select:none;-webkit-user-select:none;
  box-shadow:0 30px 70px -30px rgba(0,0,0,.85);
}
.ba__img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.ba__clip{position:absolute;inset:0;clip-path:inset(0 calc(100% - var(--split)) 0 0)}

.ba__tag{
  position:absolute;top:.9rem;z-index:3;
  padding:.35rem .7rem;border-radius:999px;
  font-size:.62rem;letter-spacing:.18em;text-transform:uppercase;font-weight:600;
  color:var(--bone);background:rgba(23,18,28,.6);
  border:1px solid rgba(246,241,244,.16);
  backdrop-filter:blur(8px);
  pointer-events:none;
}
.ba__tag--a{left:.9rem}
.ba__tag--b{right:.9rem}

.ba__handle{
  position:absolute;top:0;bottom:0;z-index:4;
  left:var(--split);transform:translateX(-50%);
  width:2.75rem;padding:0;border:0;background:none;cursor:ew-resize;
}
.ba__handle::before{        /* the seam */
  content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;margin-left:-1px;
  background:linear-gradient(180deg,rgba(255,255,255,.4),#fff 18%,#fff 82%,rgba(255,255,255,.4));
}
.ba__handle span{
  position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:2.5rem;height:2.5rem;border-radius:50%;
  background:linear-gradient(135deg,#FFA1E1,#FF3DBE 46%,#9A2BF0);
  box-shadow:0 6px 22px -6px rgba(255,61,190,.85),inset 0 1px 0 rgba(255,255,255,.5);
}
.ba__handle span::before,.ba__handle span::after{
  content:"";position:absolute;top:50%;left:50%;width:.4rem;height:.4rem;
  border-top:2px solid #17121C;border-right:2px solid #17121C;
}
.ba__handle span::before{transform:translate(-50%,-50%) translateX(-.34rem) rotate(-135deg)}
.ba__handle span::after {transform:translate(-50%,-50%) translateX(.34rem)  rotate(45deg)}
.ba__handle:focus-visible span{outline:2px solid var(--orchid);outline-offset:4px}

.ba figcaption{
  margin-top:clamp(.9rem,2vh,1.3rem);text-align:center;
  font-size:clamp(.86rem,1.1vw,.95rem);line-height:1.5;
  color:rgba(246,241,244,.62);
}
.ba figcaption strong{color:var(--bone);font-weight:600}
.ba figcaption span{color:var(--orchid);font-weight:600;white-space:nowrap}

/* Second pair: a plain labelled side-by-side. Those two frames were shot at
   different distances and angles, so a wipe across them reads as two photos
   spliced together rather than one head — the comparison has to be honest about
   that or it undermines the featured one above it. */
.ba__pair{
  display:grid;grid-template-columns:repeat(2,1fr);
  gap:clamp(.6rem,1.5vw,1rem);
  max-width:46rem;margin:0 auto;
}
.ba__pair figcaption{grid-column:1/-1}
.ba__side{position:relative;border-radius:8px;overflow:hidden}
.ba__side img{display:block;width:100%;aspect-ratio:3/4;object-fit:cover}
.ba__side .ba__tag{top:.7rem;left:.7rem;right:auto}

/* ══════════════════ hero: full-bleed studio spotlight ══════════════════
   Modelled on the reference page: two registered frames stacked full-bleed, the
   second revealed inside a soft radial mask that eases toward the pointer.
   The frames are shot with the subject in the RIGHT third against pure white,
   which is the whole reason black copy stays legible on the left over a
   full-bleed image — no scrim, no overlay, no card. */
.hero__reveal{
  position:absolute;inset:0;z-index:2;margin:0;
  overflow:hidden;pointer-events:none;
  /* The slow `heroBreathe` push was removed on Brandon's call — the hero holds
     perfectly still now, and the only motion on first scroll is the work panels
     rising. The photograph is brought CLOSER by cropping the source tighter
     rather than by scaling here: a transform would soften it, a crop keeps every
     pixel sharp. Adjust the crop in the build step, not with CSS scale.

     NO CSS filter either. The warm filmic grade is BAKED into hero-honey.*.webp,
     tuned to that specific photograph against the reference's measured values
     (mean RGB 113/92/73, R−B +40, saturation 40). A generic sepia() on top would
     double-grade it — that source was already warmer than the target, so adding
     warmth is exactly the wrong direction. One place to tune. */
}
.hero__rev{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;object-position:72% center;
  display:block;
}
.hero__rev--b{
  /* JS paints the radial-gradient mask. Until it does — and on touch, and with
     JS off — this layer stays hidden and the visitor simply sees frame A.
     Never a broken half-state. */
  opacity:0;
  -webkit-mask-size:100% 100%;mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
}
.hero--reveal-on .hero__rev--b{opacity:1}

/* The wordmark rides UNDER the hair: frame A is an alpha CUTOUT (her real
   rose after), so the letters scroll behind the hair and show through the
   transparent white page around it — the layering the marquee comment in the
   markup promises. Frame B is opaque white-backed, so wherever the reveal
   melts it in it covers the letters too, which is correct: the before is its
   own world. If frames ever go back to opaque, flip these with .hero__reveal. */
.hero__marquee{z-index:1}
.hero__body{z-index:4}

@media (max-width:940px){
  /* Centre HER on the screen, not the file. The frame is 16:9 and the phone
     hero is tall, so cover crops the width HARD — at 393x852 only ~26% of the
     file's width survives (a ~373px window on a 1440px frame), which is
     narrower than her hair is wide. So this number decides the entire mobile
     hero, and it MUST be re-measured whenever hero-honey.* is re-shot.

     Measured on the current photo (hero-honey, greenhouse profile shot): her
     hair spans 36-70% of the frame, her figure 36-71%, centre ~52%. 52% puts
     the fall of the hair and the balayage run in frame with a thin strip of
     green window on the right for depth.

     Was 74.5% until 2026-08-23 — correct for the OLD cutout-on-white hero,
     where she really did sit at 56-93%. That value survived the Aug-20 photo
     swap and cropped the new frame to the window frames and potted plants on
     its right edge, with a sliver of hair off the left. */
  .hero__rev{object-position:52% center}
  .hero__reveal::after{
    content:"";position:absolute;inset:0;
    background:linear-gradient(180deg,rgba(255,255,255,0) 38%,rgba(255,255,255,.86) 72%,#fff 100%);
  }
}

/* The hero copy is now a single left-hand column over the frame's white third,
   so the row must pack to the START. With space-between and one child it was
   fine, but the creed rejoining the column made it two and threw the block to
   the right, straight onto the hair. */
.hero__body{justify-content:flex-start}
.hero__col{max-width:min(44ch,46vw)}
.hero__creed{
  flex:none;max-width:34ch;text-align:left;
  margin-top:clamp(1.1rem,2.6vh,1.7rem);
}
.hero__creed p{font-size:clamp(1.3rem,2vw,1.9rem)}
@media (min-width:941px){ .hero__creed{display:block} }
@media (max-width:940px){
  /* 46vw is a desktop measure — on a 393px phone it squeezed the column to
     181px and clipped the CTA label to "START YOUR NEW". */
  .hero__col{max-width:none}
  .hero__creed{max-width:none}
}

/* ── touch devices: sweep between the two frames automatically ──
   There is no cursor to drive the spotlight, so the reveal would simply never
   happen. Instead the before/after wipes back and forth on a loop, with a
   travelling seam so it reads as one image being uncovered rather than two
   images cross-fading. Holds at each end so both states are actually legible. */
@media (hover:none),(pointer:coarse),(max-width:940px){
  .hero__rev--b{
    opacity:1;
    -webkit-mask-image:none;mask-image:none;
    /* 7.2s delay: the intro owns the first 7s. Without it the loop is already
       mid-cycle when .hero-intro drops and the handover visibly jumps. Its 0%
       keyframe fully clips the before, same as the intro's end state, so the
       two meet with no visual change. */
    animation:heroSweep 8s cubic-bezier(.65,0,.35,1) 7.2s infinite both;
  }
}
/* frame B is the BEFORE, so inset(0 100% 0 0) hides it and the AFTER shows */
@keyframes heroSweep{
  0%,10%   {clip-path:inset(0 100% 0 0)}
  45%,55%  {clip-path:inset(0 0 0 0)}
  90%,100% {clip-path:inset(0 100% 0 0)}
}
@keyframes heroIntroSeam{
  0%,18%{left:0;opacity:0}
  30%   {opacity:1}
  88%   {opacity:1}
  100%  {left:100%;opacity:0}
}
@keyframes heroSeam{
  0%,10%   {left:0;opacity:0}
  20%      {opacity:1}
  45%,55%  {left:100%;opacity:1}
  80%      {opacity:1}
  90%,100% {left:0;opacity:0}
}
/* a loop the visitor cannot stop is exactly what reduced-motion is for */
@media (prefers-reduced-motion:reduce){
  .hero__rev--b{animation:none;opacity:0}
  .hero__seam{animation:none;display:none}
}

/* tells the visitor the hero is interactive — it is not discoverable otherwise */
.hero__hint{
  display:flex;align-items:center;gap:.5rem;
  margin:clamp(1rem,2.4vh,1.5rem) 0 0;
  font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;
  color:rgba(23,18,28,.38);
}
.hero__hint span{
  width:6px;height:6px;border-radius:50%;background:var(--orchid);
  box-shadow:0 0 0 0 rgba(255,61,190,.6);
  animation:pip 2.6s ease-out infinite;flex-shrink:0;
}
/* touch gets the automatic sweep, so "hover" would be a lie there */
@media (hover:none),(pointer:coarse),(max-width:940px){ .hero__hint{display:none} }

/* second pair is now a swipe too — same component, slightly smaller so the
   first one still reads as the featured transformation */
/* was sized down when it sat underneath; in the grid both are equal */
.ba__feature--second{max-width:none;margin-top:0}

/* the lookbook hint speaks the right verb for the input device */
.reel__hint--touch{display:none}
@media (hover:none),(pointer:coarse),(max-width:940px){
  .reel__hint--fine{display:none}
  .reel__hint--touch{display:inline}
}

/* "Not ready to commit?" — a soft second door next to the before/afters, in the
   section's own voice rather than another loud pill button */
.ba__consult{
  margin:clamp(.6rem,1.6vh,1rem) 0 0;
  font-size:clamp(.9rem,1.15vw,1rem);
  color:rgba(246,241,244,.62);
}
.ba__consult a{
  color:var(--orchid);text-decoration:none;font-weight:600;
  border-bottom:1px solid rgba(255,61,190,.35);
  padding-bottom:1px;
  transition:color .25s var(--ease),border-color .25s var(--ease);
}
.ba__consult a:hover{color:#fff;border-color:#fff}

/* .ba__more and its position:static override lived here until 2026-08-24 —
   they styled the homepage "See all 6 transformations" pill, and Tyce then asked
   for the whole teaser to come off the page, so both rules had nothing left to
   style. Removed rather than left to rot. The footgun they existed to fix is
   still worth knowing: .show__pill is position:absolute and anchors to the
   nearest positioned ancestor, so it falls out of flow anywhere outside a
   .show card. */

/* "Book an appointment" needs 176px; a 360px phone bar has 117px for it once the
   wordmark and burger are placed. Short label below 560px — the aria-label keeps
   the full wording for screen readers. */
.btn__short{display:none}
@media (max-width:560px){
  .btn__full{display:none}
  .btn__short{display:inline}
}

/* ── intro: the transformation swipes, THEN the pointer takes over ──
   The hero opens holding on the BEFORE, a wipe clears it left-to-right to
   reveal her work, and from then on the cursor spotlight is the control.
   Frame B is the BEFORE, so inset(0 0 0 0) shows it and inset(0 0 0 100%)
   clears it — the end state matches the resting CSS exactly, so nothing moves
   when .hero-intro is dropped. */
@keyframes heroIntroSwipe{
  0%,18%   {clip-path:inset(0 0 0 0)}
  100%     {clip-path:inset(0 0 0 100%)}
}
.hero-intro .hero__rev--b{
  opacity:1;
  -webkit-mask-image:none;mask-image:none;
  animation:heroIntroSwipe 2.6s var(--ease) 3.6s both;
}


/* ── the wipe seam is a REAL element, not a pseudo ──
   .hero__reveal::after is already the mobile white wash that keeps the hero copy
   readable over the photo. Reusing it for the seam wiped the gradient out, and
   because the wash sets inset:0 the "2px" seam inherited left:0/right:0 and
   painted full-width. Three things needed a pseudo and only two exist. */
.hero__seam{
  position:absolute;top:0;bottom:0;z-index:5;
  width:2px;margin-left:-1px;left:0;
  display:none;pointer-events:none;
  background:linear-gradient(180deg,rgba(255,255,255,.4),#fff 18%,#fff 82%,rgba(255,255,255,.4));
  box-shadow:0 0 16px rgba(23,18,28,.3);
}
/* intro owns it first — 2 classes, so this beats the mobile loop rule below */
.hero-intro .hero__seam{display:block;animation:heroIntroSeam 2.6s var(--ease) 3.6s both}
/* mobile: after the intro, it rides the back-and-forth loop */
@media (hover:none),(pointer:coarse),(max-width:940px){
  .hero__seam{display:block;animation:heroSeam 8s cubic-bezier(.65,0,.35,1) 7.2s infinite both}
}
/* pointer takes over on desktop: no seam, the spotlight is the control */
.hero--reveal-on .hero__seam{display:none;animation:none}
@media (prefers-reduced-motion:reduce){ .hero__seam{display:none} }

/* phones: narrower frames so the ones either side are actually on screen */
@media (max-width:720px){
  .reel .tile{width:min(56vw,13rem)}
}

/* ══════════ standalone lookbook pages (lookbook-*.html) ══════════
   Bento treatment in the site's own voice: ink cards, tight gaps, glass
   chips, one giant over-photo title per page, one glass Book card in the
   grid. Photos keep the .tile data contract so the lightbox binds as-is;
   the museum placard is restyled here into overlay chips. */
/* The lookbook runs WIDER than the rest of the site: it is a gallery, and the
   1440 cap the marketing pages use left roughly a third of a large screen empty.
   Header and grid share these two tokens so they can never drift out of
   alignment — widen one and you widen both. */
.lbkpage{
  --lbk-maxw:min(100%,2000px);
  --lbk-pad:clamp(1rem,2.4vw,3.25rem);
}
.lbk__bar{
  display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  max-width:var(--lbk-maxw);margin:0 auto;
  padding:clamp(.9rem,2.2vh,1.4rem) var(--lbk-pad);
}
.lbk__back{font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;opacity:.65;text-decoration:none;color:inherit}
.lbk__back:hover{opacity:1}
.lbk__jump{display:flex;gap:clamp(.9rem,2vw,1.6rem)}
.lbk__jump a{
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;
  text-decoration:none;color:inherit;opacity:.55;
}
.lbk__jump a:hover{opacity:1}
.lbk__jump a[aria-current="page"]{opacity:1;color:var(--orchid)}
@media (max-width:640px){ .lbk__jump{order:3;flex-basis:100%} }

/* container-type lives HERE, on the mosaic's parent, not on the mosaic itself:
   an element cannot resolve container units against its own container, so
   100cqw inside .lbk__mosaic would silently fall back to the viewport and size
   the grid rows off 1440 instead of the 1296 the grid actually spans. This
   element's content box is exactly the mosaic's width, which is what we want. */
.lbk{max-width:var(--lbk-maxw);margin:0 auto;padding:clamp(.6rem,1.6vh,1.1rem) var(--lbk-pad) clamp(3rem,7vh,5rem);container-type:inline-size}

/* split hero: title panel on ink + a portrait photo cell, so the WHOLE cut
   reads instead of a wide band cropping the top of the head (Brandon's flag).
   The panel reuses the violet-bloom language from .work__intro. */
.lbk__hero{
  display:grid;grid-template-columns:1fr auto;
  gap:clamp(.4rem,.8vw,.55rem);
  /* taller cap on big screens: the hero photo is height-driven (3:4), so this is
     what lets it hold its own against the now-wider title panel */
  height:clamp(20rem,52vh,34rem);
  margin-bottom:clamp(.4rem,.8vw,.55rem);
}
.lbk__hero-panel{
  position:relative;overflow:hidden;border-radius:14px;
  background:
    radial-gradient(46rem 20rem at 16% 78%,rgba(154,43,240,.3),transparent 64%),
    radial-gradient(30rem 14rem at 40% 95%,rgba(255,61,190,.12),transparent 60%),
    var(--ink-2);
}
.lbk__hero-img{position:relative;overflow:hidden;border-radius:14px;height:100%;aspect-ratio:3/4}
.lbk__hero-img img{width:100%;height:100%;object-fit:cover;object-position:50% 20%}
.lbk__kicker{
  position:absolute;top:1.15rem;left:1.25rem;z-index:1;
  display:flex;align-items:center;gap:.55rem;margin:0;
  font-size:.72rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;
  color:var(--bone);
}
.lbk__kicker::before{
  content:"";width:7px;height:7px;border-radius:50%;
  background:var(--orchid);box-shadow:0 0 9px rgba(255,61,190,.8);
}
.lbk__title{
  /* right edge is bound too: the cap grew with the wider lookbook, and the
     longest title ("Curls & texture") would otherwise run out of the panel
     instead of wrapping */
  position:absolute;left:1.1rem;right:1.1rem;bottom:.55rem;z-index:1;margin:0;
  font-family:var(--display);font-weight:700;
  font-size:clamp(3.2rem,7vw,8.5rem);line-height:.85;letter-spacing:-.025em;
  text-wrap:balance;
}
.lbk__chip{
  position:absolute;right:1.15rem;bottom:1.15rem;z-index:1;
  padding:.6rem 1rem;border-radius:999px;
  border:1px solid rgba(246,241,244,.25);
  background:rgba(246,241,244,.1);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
  font-family:var(--display);font-size:.98rem;font-weight:700;
  font-variant-numeric:tabular-nums;color:var(--orchid);white-space:nowrap;
}

/* the mosaic: tight-gap bento of photo cards + one glass Book card */
.lbk__mosaic{
  display:grid;grid-template-columns:repeat(6,1fr);
  gap:clamp(.4rem,.8vw,.55rem);
  grid-auto-flow:dense;
}
.lbk__mosaic .tile{
  display:block;position:relative;overflow:hidden;border-radius:12px;
  grid-column:span 2;aspect-ratio:3/4;
}
.lbk__mosaic .tile img{
  position:absolute;inset:0;width:100%;height:100%;aspect-ratio:auto;
  object-fit:cover;
  transition:transform .55s var(--ease);
}
/* EVERY photo cell is 3:4 because EVERY source photo is portrait (3:4, a few
   4:5). A wide feature card cannot show a portrait whole: to stay row-aligned
   beside a 3:4 tile a 4-column card is forced to ~8:5, and `cover` then throws
   away half the image height — that is what cropped these to a scalp. So the
   feature keeps its place in the reading order but takes the same portrait
   shape as its neighbours; the grid is a tidy 3-across of whole cuts. */
.lbk__mosaic .lbk__card--lg{grid-column:span 2;aspect-ratio:3/4}
/* centred, not top-biased: with box and photo at the same ratio there is no
   vertical crop left to bias, and the 4:5 shots only ever lose a sliver of side */
.lbk__mosaic .lbk__card--lg img{object-position:50% 50%}
/* placard → overlay chips (service left, price right) */
.lbk__mosaic .tile::after{
  order:0;position:absolute;left:.6rem;bottom:.6rem;z-index:1;
  max-width:62%;padding:.42rem .7rem;border-radius:9px;
  border:1px solid rgba(246,241,244,.18);
  background:rgba(23,18,28,.42);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  color:var(--bone);
}
.lbk__mosaic .tile::before{
  order:0;position:absolute;right:.6rem;bottom:.6rem;z-index:1;
  padding:.42rem .7rem;border-radius:9px;
  border:1px solid rgba(246,241,244,.18);
  background:rgba(23,18,28,.42);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  font-size:.9rem;
}
/* mosaic hover: one effect — a slow settle-in zoom (replaces the placard lift) */
.lbk__mosaic .tile:hover img,.lbk__mosaic .tile:focus-within img{opacity:1;transform:scale(1.03)}

/* ══════════════════ editorial spread ══════════════════
   Photos vary in SIZE but never in ratio — every photo cell stays exactly 3:4,
   because these sources are all portrait and `cover` on any other ratio throws
   away image height (see the feature-card note above). The odd shapes are taken
   by .lbk__note type cards, which hold text and so can be any rectangle.

   ★ THE GRID IS GAPLESS, AND IT HAS TO BE. A double-size feature can only be
   exactly 3:4 while also being exactly two standard cells wide and tall if the
   gutter is zero: feature = (2w+g)/(2h+g), and setting that equal to w/h gives
   g·(h−w) = 0, so with h≠w the only solution is g=0. Any non-zero gap makes the
   feature a slightly different ratio from its neighbours, which is exactly the
   crop bug this section exists to avoid. Separation comes from the type cards
   and the rounded outer edge instead. */
/* The @supports block that actually applies this lives at the END of this
   section, after the fallback's own max-width:860px rules — it has the same
   specificity, so it only wins on source order. Putting it here instead let the
   old two-column mobile rules override it and the grid silently collapsed. */

/* ── type cards: her own words, set between the photos ── */
.lbk__note{
  /* fallback shape for the no-container-query grid; the @supports block below
     replaces it. It must be declared BEFORE that block, not after, or it wins
     on source order and flattens the spread. */
  grid-column:span 2;aspect-ratio:3/4;border-radius:12px;
  display:flex;flex-direction:column;justify-content:space-between;gap:1rem;
  padding:clamp(1.05rem,2.1vw,2.1rem);
  background:linear-gradient(200deg,rgba(246,241,244,.06),rgba(246,241,244,.015));
  box-shadow:inset 0 0 0 1px rgba(246,241,244,.08);
  color:var(--bone);
}
.lbk__note-eyebrow{
  font-size:.6rem;letter-spacing:.24em;text-transform:uppercase;
  opacity:.42;font-variant-numeric:tabular-nums;
}
.lbk__note-line{
  font-family:var(--serif);font-style:italic;font-weight:400;
  font-size:clamp(1.3rem,2.3vw,2.05rem);line-height:1.12;letter-spacing:-.005em;
  text-wrap:balance;
}
.lbk__note-line em{font-style:italic;color:var(--orchid)}
.lbk__note-body{
  font-size:.79rem;line-height:1.62;opacity:.58;max-width:36ch;
}
/* the glass Book card that closes the grid */
.lbk__book{
  grid-column:span 2;aspect-ratio:3/4;border-radius:12px;
  display:flex;flex-direction:column;justify-content:space-between;
  padding:clamp(1rem,2vw,1.5rem);
  border:1px solid rgba(246,241,244,.18);
  background:linear-gradient(200deg,rgba(246,241,244,.09),rgba(246,241,244,.03));
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
  text-decoration:none;color:var(--bone);
  transition:border-color .3s;
}
.lbk__book:hover,.lbk__book:focus-visible{border-color:var(--orchid)}
.lbk__book strong{
  font-family:var(--display);
  font-size:clamp(1.5rem,2.6vw,2.2rem);line-height:1.05;letter-spacing:-.01em;font-weight:700;
}
.lbk__book em{font-style:normal;font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;opacity:.6;display:block;margin-top:.6rem}
.lbk__book span{
  align-self:flex-end;width:3rem;height:3rem;border-radius:50%;
  border:1px solid rgba(246,241,244,.4);
  display:grid;place-items:center;
  transition:border-color .3s,color .3s;
}
.lbk__book span svg{transform:rotate(-45deg)}
.lbk__book:hover span{border-color:var(--orchid);color:var(--orchid)}

/* stagger the reveal down the grid (hiding still only ever lives under .js-anim) */
.lbk__mosaic .reveal:nth-child(2){transition-delay:.09s}
.lbk__mosaic .reveal:nth-child(3){transition-delay:.18s}
.lbk__mosaic .reveal:nth-child(4){transition-delay:.27s}
.lbk__mosaic .reveal:nth-child(5){transition-delay:.36s}
.lbk__mosaic .reveal:nth-child(6){transition-delay:.45s}
.lbk__mosaic .reveal:nth-child(7){transition-delay:.54s}
.lbk__mosaic .reveal:nth-child(n+8){transition-delay:.6s}

@media (max-width:860px){
  .lbk__hero{grid-template-columns:1fr;height:auto}
  .lbk__hero-panel{min-height:13.5rem;border-radius:12px}
  /* 3/4, not 4/5 — the hero shots are 3:4, and a 4:5 box cropped the top of
     the head off by ~6% for no reason */
  .lbk__hero-img{aspect-ratio:3/4;border-radius:12px}
  .lbk__mosaic{grid-template-columns:repeat(2,1fr)}
  .lbk__mosaic .tile{grid-column:span 1}
  /* stays full-width as the lead photo, but portrait so the whole cut reads */
  .lbk__mosaic .lbk__card--lg{grid-column:span 2;aspect-ratio:3/4}
  .lbk__mosaic .tile::after{max-width:58%;padding:.34rem .55rem;font-size:.62rem}
  .lbk__mosaic .tile::before{padding:.34rem .55rem;font-size:.8rem}
  .lbk__book{grid-column:span 1}
}

/* ══════════════════ editorial spread (the enhancement) ══════════════════
   Everything above this point is the fallback bento, and it is a correct layout
   on its own. This block upgrades it to the editorial spread wherever container
   queries exist. It MUST stay last in this section: it shares specificity with
   the max-width:860px rules above and only wins on source order.

   Photos vary in SIZE but never in ratio — every photo cell stays exactly 3:4,
   because these sources are all portrait and `cover` on any other ratio throws
   away image height (see the feature-card note further up). The odd shapes are
   taken by .lbk__note text cards, which can be any rectangle.

   ★ THE GRID IS GAPLESS, AND IT HAS TO BE. A double-size feature can be exactly
   3:4 AND exactly two standard cells wide and tall only when the gutter is zero:
   set (2w+g)/(2h+g) = w/h and it reduces to g·(h−w) = 0, so with h≠w the only
   solution is g = 0. Any non-zero gap makes the feature a slightly different
   ratio from its neighbours — the exact crop bug this section exists to avoid.
   Separation comes from the type cards and the rounded outer edge instead. */
@supports (container-type:inline-size){
  .lbk__mosaic{
    grid-template-columns:repeat(12,1fr);
    gap:0;
    /* one grid row == one grid column, so a span is a square unit and a
       3-col x 4-row block lands on an exact 3:4. The container itself is
       declared on .lbk — an element cannot resolve cq units against itself. */
    grid-auto-rows:calc(100cqw / 12);
    border-radius:14px;overflow:hidden;
  }
  /* the row span now fixes the height, so aspect-ratio has to stand down or the
     two compete and the cell overflows its track */
  .lbk__mosaic .tile{grid-column:span 3;grid-row:span 4;aspect-ratio:auto;border-radius:0}
  .lbk__mosaic .lbk__card--lg{grid-column:span 6;grid-row:span 8;aspect-ratio:auto}
  .lbk__mosaic .lbk__book{grid-column:span 3;grid-row:span 4;aspect-ratio:auto;border-radius:0}
  /* the closing CTA takes two frames: it gives the last word more weight, and
     it happens to consume exactly the one standard slot the grid would
     otherwise leave empty, so the spread ends on a flush edge */
  .lbk__mosaic .lbk__book--wide{grid-column:span 6}
  .lbk__note{grid-column:span 3;grid-row:span 4;aspect-ratio:auto;border-radius:0}
  .lbk__note--wide{grid-column:span 6;grid-row:span 4}
  /* standard tiles are 4-across here rather than 3, so the service and price
     chips have ~90px less room than the bento gave them and start to collide */
  .lbk__mosaic .tile::after{max-width:56%;padding:.34rem .55rem;font-size:.6rem}
  .lbk__mosaic .tile::before{padding:.34rem .55rem;font-size:.78rem}
  .lbk__mosaic .lbk__card--lg::after{max-width:62%;font-size:.66rem}

  @media (max-width:860px){
    .lbk__mosaic{grid-template-columns:repeat(6,1fr);grid-auto-rows:calc(100cqw / 6)}
    .lbk__mosaic .tile{grid-column:span 3;grid-row:span 4}
    .lbk__mosaic .lbk__card--lg{grid-column:span 6;grid-row:span 8}
    .lbk__mosaic .lbk__book{grid-column:span 6;grid-row:span 4}
    .lbk__note,.lbk__note--wide{grid-column:span 6;grid-row:span 3}
  }
}

@media (prefers-reduced-motion:reduce){
  .lbk__mosaic .tile img{transition:none}
}
