/* ============================================================
   BOMBAY HAIR PATCH — index.html stylesheet

   Lifted out of index.html verbatim. Nothing was rewritten in the move;
   this is the same CSS that was inline, so behaviour is unchanged.

   WHY IT MOVED: index.html had grown to 2,450 lines — 737 of CSS, 814 of
   JS and only ~900 of actual markup — and finding anything in it meant
   scrolling past two other languages. Markup, styling and behaviour now
   live in three files you can open independently.

   NOT MERGED WITH style.css (the blog's sheet) ON PURPOSE. The two share
   36 selectors and they do not all agree — h1 is --s5 here and --s4 there,
   for one — so a merge is a careful selector-by-selector job, not a
   copy-paste. It is on the fix list in README.md.
   ============================================================ */
/* ============================================================
   BOMBAY HAIR PATCH — design system
   ============================================================ */
:root{
  /* colour — deliberately near-monochrome. Emphasis comes from
     contrast and light, never from a decorative accent hue.

     One warm neutral family: hue runs 12° at the darkest to 36° at the
     lightest. That drift is intentional — darks sit redder, lights sit
     yellower — and it is what stops the greys reading as dead screen grey.
     Do not "correct" it to a single hue.

     Contrast ratios below are measured, not estimated. If you change a
     value here, re-measure: --muted and --muted-dk carry nearly all the
     body copy on the site (29 and 10 usages), at font-weight 300.

     ⚠ THESE TOKENS ARE MIRRORED IN style.css, WHICH THE BLOG PAGES USE.
     Change a value here and you must change it there too, or the blog
     drifts away from the studio it belongs to.

     They are duplicated on purpose rather than extracted to a shared file.
     This page's CSS is 33 KB inline; moving it out would put a
     render-blocking request in front of the largest-contentful paint on the
     one page that converts, and would mean a failed stylesheet leaves the
     page with no colours at all instead of no colours only on the blog.
     For fifteen lines of tokens, a loud comment is the cheaper guard than a
     refactor that makes the money page slower and more fragile. */
  --ink:        #151110;   /* espresso black — base. 16.82:1 with --bone   */
  --ink-2:      #1E1815;   /* raised surface                               */
  --ink-3:      #2A211C;   /* hover / inset                                */
  --stone:      #E8E1D7;   /* warm light chapter bg. 14.45:1 with --ink    */
  --stone-2:    #D9D0C3;
  --bone:       #F6F2EC;   /* type on dark                                 */
  /* Both muted tones were raised for readability, not to chase a badge.
     Thin (300-weight) light type on a dark ground halates, and WCAG does
     not model stroke weight — so the numbers alone understated it.
     --muted-dk was the worse offender: it outright FAILED on --stone-2
     (3.92:1) and cleared --stone by only 0.11. */
  --muted:      #A89C93;   /* secondary on dark.  7.00:1 on --ink   (AAA)  */
  --muted-dk:   #50463F;   /* secondary on light. 7.07:1 on --stone (AAA)
                              and 6.01:1 on --stone-2, which now passes    */

  /* Two kinds of border, and the difference is not cosmetic.

     --line / --line-dk are DECORATIVE: section dividers, hairline rules,
     the mesh motif. WCAG 1.4.11 does not apply to decoration, so these stay
     deliberately faint (~1.4:1) — that faintness is the whole look.

     --line-ui / --line-ui-dk are FUNCTIONAL: they draw the boundary of a
     real control — text inputs, ghost buttons. Those DO fall under 1.4.11
     and need 3:1 against what sits either side of them. Using --line for
     them left form fields at 1.49:1, i.e. an input you cannot locate.
     Keep the two sets separate; do not merge them to "tidy up". */
  --line:       rgba(246,242,236,.14);  /* decorative only */
  --line-dk:    rgba(21,17,16,.16);     /* decorative only */
  --line-ui:    #716860;   /* controls on dark:  3.22:1 fill, 3.44:1 page  */
  --line-ui-dk: #867B6A;   /* controls on light: 3.20:1 on --stone         */

  /* Tells the browser to render its OWN chrome dark: scrollbars, the popup
     a native <select> opens, autofill backgrounds, spinner buttons. Without
     it Chrome draws a light scrollbar down the side of a near-black page and
     the select popup opens white. Audited with the Web Interface Guidelines,
     which ask for it explicitly. It is re-declared in the light block rather
     than left to inherit, because the toggle can disagree with the OS. */
  color-scheme: dark;

  --wa:         #25D366;   /* the ONLY hue. WhatsApp brand, WhatsApp use
                              only, and never the sole carrier of meaning —
                              the green dot always sits beside a text label,
                              so nothing is lost to colour blindness.       */

  /* ---- REVIEW CARDS — A DELIBERATE EXCEPTION, ASKED FOR BY THE OWNER ----
     Rules 1 and 4 say near-monochrome with one accent. This block breaks
     both, on request and with reason: the reviews are what a visitor
     actually came to find, and warm-grey type on a warm-grey card read as
     "a box with some copy in it" rather than as somebody's testimony.

     THESE TOKENS DO NOT FLIP WITH THE THEME, and that is the whole idea —
     the card is a piece of paper, white under either theme. So everything
     drawn on it must be theme-independent too. Inside a review card use
     --paper-ink, NEVER --ink: --ink is cream in the light theme and would
     turn the quotes into white-on-white the moment somebody toggles.

     Measured against --paper:
       --paper-ink   18.76:1   the quote and the reviewer's name
       --paper-mut    7.85:1   "Google review" (AAA)
       --star         3.99:1   clears the 3:1 that 1.4.11 asks of a graphic
                               carrying meaning. Google's own amber sits at
                               about 2.8:1 on white and would have failed;
                               this is that same signal deepened until it
                               passes, which also keeps it inside the warm
                               hue family instead of shouting. */
  --paper:        #FFFFFF;
  --paper-ink:    #151110;
  --paper-mut:    #5A5048;
  --paper-line:   rgba(21,17,16,.12);
  --paper-shadow: rgba(21,17,16,.55);
  --star:         #A8760F;

  /* The base cutaways in #bases are pre-rendered artwork with a warm cream
     ground baked into the pixels, and no alpha to recolour. So the surface
     behind them cannot flip either — for the same reason --paper cannot.
     --stone was the obvious choice and is the wrong one: it is the
     ALTERNATE-band token, so it is cream on the dark theme but near-black on
     the light one, which would put a cream diagram on a black plate. Same
     bright hole, just inverted.
     #FBF5EC is sampled off the diagrams' own corners (~#FCF5EC / #FDF6EB),
     so the plate edge disappears into the artwork and only the border reads.
     Nothing but the image is ever drawn on this, so there is no type
     contrast to measure — the border is decoration around a figure. */
  --plate:        #FBF5EC;
  --plate-line:   rgba(21,17,16,.20);

  /* type */
  --display: "Bodoni Moda", "Didot", "Times New Roman", serif;
  --text:    "Archivo", "Helvetica Neue", Arial, sans-serif;

  /* scale */
  /* THE BOTTOM THREE STEPS ARE FLUID NOW, AND THAT WAS THE BUG.
     --s2 upward have always been clamp() with a vw term, so headings grew
     with the viewport. These three did not — they were fixed rem — so every
     line of body copy on the site stayed at the same pixel size whether you
     were on a 1080 laptop or a 27" monitor, while the headline beside it grew
     to 110px. That mismatch is why the copy kept reading as "too small": it
     was not small, it was static while everything around it scaled.

     EACH ONE IS TUNED TO BE IDENTICAL AT 1080px AND BELOW. Substitute 1080
     into any of the three and you get exactly the old value, so the phone
     layouts, the 1080 nav fit and every measured line-length are untouched.
     Growth only starts above that. At 1920 the body step reaches ~19px.

     Measures are in ch throughout, so they scale with the type and the line
     length stays where it was set. */
  --s-1: clamp(.78rem, .66rem + .18vw, .92rem);
  --s0:  clamp(.95rem, .80rem + .23vw, 1.12rem);
  --s1:  clamp(1.05rem, .88rem + .26vw, 1.25rem);
  --s2:  clamp(1.35rem, 1.1rem + .9vw, 1.9rem);
  --s3:  clamp(1.9rem, 1.3rem + 2.4vw, 3.1rem);
  --s4:  clamp(2.6rem, 1.4rem + 4.6vw, 5.4rem);
  --s5:  clamp(3.2rem, 1.2rem + 8.2vw, 9rem);

  /* layout */
  --pad:  clamp(1.25rem, 5vw, 5.5rem);
  --gap:  clamp(1.5rem, 3vw, 3rem);
  --max:  1680px;

  --ease: cubic-bezier(.22,.61,.36,1);
  --ease-io: cubic-bezier(.65,.05,.36,1);

  /* Composited values that used to be hardcoded rgba(). They had to become
     tokens or they would not flip with the theme — a nav scrim of
     rgba(21,17,16,.82) over a light page is a black bar. */
  --scrim:       rgba(21,17,16,.82);   /* nav backdrop, over blur      */
  --scrim-solid: rgba(21,17,16,.72);   /* pills sitting on photography */
  /* Text that sits on --scrim-solid. FIXED IN BOTH THEMES, deliberately,
     and this pairing is the whole reason it exists: --scrim-solid does not
     flip either, because the photograph underneath it does not flip. Using
     --bone here instead put dark type on a dark pill in the light theme and
     measured 1.04:1 — the "not a client" badge on the before/after frame was
     invisible, and that badge is one of the three places the page relies on
     to stay the right side of the Consumer Protection Act 2019.
     Anything drawn over photography takes this, never --bone. */
  --on-photo:    #F6F2EC;              /* 16.16:1 on --scrim-solid            */
  --veil:        rgba(246,242,236,.06);/* hover wash, light-on-dark    */
  --veil-alt:    rgba(21,17,16,.05);   /* hover wash on the alt surface*/
  --knob-ring:   rgba(246,242,236,.7);
  --knob-fill:   rgba(21,17,16,.35);
  --bone-hi:     #fff;                 /* solid button, hover          */
  --shadow:      rgba(0,0,0,.45);
  --shadow-lg:   rgba(0,0,0,.55);
  --nav-h:       4.6rem;               /* .nav is padding-sized, not fixed —
                                          this is the measured resting height,
                                          used to hang the services sheet
                                          below it on a phone. Matches
                                          style.css so both behave alike.   */
}

/* ============================================================
   LIGHT THEME

   The same palette, inverted. Not one new colour is invented here: every
   value below already existed as its opposite number, which is why both
   themes measure the same and why the brand does not change — it is the
   same ink and the same stone, swapped.

   Because every component already reads its colours from these tokens,
   redefining eleven of them flips the whole site. No component CSS is
   duplicated and no selector is themed by hand.

   ONE THING TO UNDERSTAND: .on-light means "the alternate surface", not
   literally "light". In the dark theme it is the stone band; in the light
   theme it is the dark band. The page keeps its light/dark rhythm either
   way, just starting from the other foot.

   Two values are NOT straight swaps, and both are measured:
     --line-ui    #796F60, because the straight swap (#867B6A) fell to
                  2.72:1 against the light surface and 1.4.11 needs 3.0.
     --scrim etc. re-tinted, or the nav becomes a black bar on a pale page.
   ============================================================ */
:root[data-theme="light"]{
  color-scheme: light;     /* see the note in :root                   */
  --ink:        #E8E1D7;   /* page.    14.45:1 with --bone            */
  --ink-2:      #D9D0C3;   /* raised surface                          */
  --ink-3:      #CEC4B5;   /* hover / inset                           */
  --stone:      #151110;   /* the ALTERNATE band is now the dark one  */
  --stone-2:    #1E1815;
  --bone:       #151110;   /* primary type is now ink                 */
  --muted:      #50463F;   /* 7.07:1 on --ink   (AAA)                 */
  --muted-dk:   #A89C93;   /* 7.00:1 on --stone (AAA)                 */
  --line:       rgba(21,17,16,.16);
  --line-dk:    rgba(246,242,236,.14);
  --line-ui:    #796F60;   /* 3.23:1 on surface, 3.80:1 on page       */
  --line-ui-dk: #716860;   /* 3.44:1 on the alt band                  */

  --scrim:       rgba(232,225,215,.86);
  --scrim-solid: rgba(21,17,16,.72);   /* still over photography      */
  --on-photo:    #F6F2EC;              /* still over photography      */
  --veil:        rgba(21,17,16,.05);
  --veil-alt:    rgba(246,242,236,.06);
  --bone-hi:     #000;
  --shadow:      rgba(21,17,16,.14);
  --shadow-lg:   rgba(21,17,16,.20);

  /* The review card stays white while the page turns cream, so white-on-cream
     is only a 1.30:1 step and the edge has to do the work the value contrast
     did in the dark theme. Twice the weight, and a lighter cast shadow so it
     reads as paper lifted off the page rather than a smudge under it. */
  --paper-line:   rgba(21,17,16,.26);
  --paper-shadow: rgba(21,17,16,.22);
}

/* The theme swap is the one colour change on the page that a person actually
   triggers, so it is worth easing — but only the two properties that carry
   it, and not for anyone who asked for less motion. */
body,.site-head,.nav,.chapter,.frame,.btn{
  transition:background-color .45s var(--ease), color .45s var(--ease),
             border-color .45s var(--ease);
}
@media(prefers-reduced-motion:reduce){
  body,.site-head,.nav,.chapter,.frame,.btn{transition:none}
}

/* ---- theme toggle ---------------------------------------------
   Sits in the nav beside the CTA. It is a real button with
   aria-pressed, not an icon with a click handler, so a screen reader
   announces both what it does and which state it is in. */
.theme-btn{
  display:inline-flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:999px; flex:none;
  border:1px solid var(--line-ui); color:var(--bone);
  transition:border-color .35s var(--ease), background .35s var(--ease);
}
.theme-btn:hover{border-color:var(--bone); background:var(--veil)}
.theme-btn svg{width:17px; height:17px; display:block}
/* one icon per state, swapped by the attribute rather than by JS touching
   style — the DOM stays declarative */
.theme-btn .icon-dark{display:none}
:root[data-theme="light"] .theme-btn .icon-dark{display:block}
:root[data-theme="light"] .theme-btn .icon-light{display:none}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; scroll-behavior:smooth}
body{
  margin:0; background:var(--ink); color:var(--bone);
  font-family:var(--text); font-weight:300; font-size:var(--s1);
  line-height:1.62; letter-spacing:.005em;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
  /* THE FIX FOR THE BROKEN "H".
     Bodoni Moda ships a variable optical-size axis (opsz 6–96), and browsers map
     it to font-size automatically. At the hero's 144px that selects opsz ≈ 96 —
     the display cut, where Didone thick/thin contrast is at its most extreme.
     Measured on the rendered page: the stems came out ~10px and the crossbar of
     the capital H came out 1px. A 1px hairline at that scale does not survive
     screen rendering, so "Hair" read as "| |air".

     Pinning a text-weight optical size keeps the face elegant but gives the thin
     strokes enough body to hold together. It is set here on body rather than per
     heading because font-variation-settings inherits, and Archivo has no opsz
     axis so it simply ignores the declaration. 'wght' is deliberately NOT set,
     so font-weight still controls weight everywhere. */
  font-optical-sizing:none;
  font-variation-settings:"opsz" 22;
}
img{display:block; max-width:100%; height:auto}
a{color:inherit; text-decoration:none}
button{font:inherit; color:inherit; background:none; border:0; cursor:pointer}
:focus-visible{outline:2px solid var(--bone); outline-offset:4px; border-radius:2px}
/* The bone ring is 16.82:1 on the dark chapters and 1.16:1 on the stone ones —
   near-white on near-white. It was invisible across all five .on-light sections,
   including the arrow-key solution tablist and every FAQ summary, so a keyboard
   user had no idea where they were. Flip it to ink on light: 14.45:1.
   Specificity (0,2,0) beats the bare pseudo-class above, so this wins. */
.on-light :focus-visible{outline-color:var(--ink)}

/* Skip link (WCAG 2.4.1 Bypass Blocks). Parked off-screen by transform so it
   keeps its place in the focus order — display:none or visibility:hidden would
   make it unfocusable, which is the usual way this control gets broken. */
.skip{
  position:fixed; top:0; left:0; z-index:100;
  padding:.85rem 1.35rem; background:var(--bone); color:var(--ink);
  font-size:var(--s0); border-radius:0 0 2px 0;
  transform:translateY(-120%); transition:transform .2s var(--ease);
}
.skip:focus{transform:translateY(0)}
/* <main> is not focusable by default, so the skip link would move the reading
   position without moving focus. tabindex="-1" on it (see markup) fixes that. */
main:focus{outline:none}
form{margin:0}
::selection{background:var(--bone); color:var(--ink)}

/* ---- mesh motif: the lace base, rendered as structure ------- */
.mesh{
  position:absolute; inset:0; pointer-events:none; opacity:.5;
  background-image:
    repeating-linear-gradient(to right, var(--line) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 34px);
  -webkit-mask-image:radial-gradient(120% 80% at 50% 40%, #000 20%, transparent 78%);
          mask-image:radial-gradient(120% 80% at 50% 40%, #000 20%, transparent 78%);
}
.on-light .mesh{
  background-image:
    repeating-linear-gradient(to right, var(--line-dk) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(to bottom, var(--line-dk) 0 1px, transparent 1px 34px);
}

/* ---- typography roles -------------------------------------- */
h1,h2,h3,h4{margin:0; font-weight:400; font-family:var(--display); letter-spacing:-.015em}
h1{font-size:var(--s5); line-height:.95}
h2{font-size:var(--s4); line-height:1.02}
h3{font-size:var(--s3); line-height:1.1}
p{margin:0 0 1.1em}
p:last-child{margin-bottom:0}
.lede{font-size:var(--s2); line-height:1.42; font-weight:300; max-width:24ch}
.body{max-width:62ch; color:var(--muted)}
.on-light .body{color:var(--muted-dk)}
.small{font-size:var(--s-1); letter-spacing:.06em; color:var(--muted)}
.on-light .small{color:var(--muted-dk)}
.italic{font-family:var(--display); font-style:italic}

/* ---- layout ------------------------------------------------ */
section{position:relative}
/* the nav is fixed, so anchor jumps must stop clear of it */
:target, section[id], main[id]{scroll-margin-top:clamp(4.5rem, 9vh, 6.5rem)}
.wrap{max-width:var(--max); margin:0 auto; padding-left:var(--pad); padding-right:var(--pad)}
/* Section rhythm. This was clamp(5rem, 11vh, 10rem) — 99px a side on a
   900px-tall screen, so every boundary between two sections was 198px of
   nothing, and roughly 3,000px of a 17,000px page was empty padding.
   Reported as "unwanted space", and it was. */
.chapter{padding-block:clamp(4rem, 8.5vh, 7.5rem)}

/* AND WHERE TWO SECTIONS SHARE A GROUND, THE BOUNDARY IS COLLAPSED.
   Eight of the thirteen boundaries on this page are dark-to-dark or
   light-to-light. Nothing marks those but the gap itself, so paying two full
   measures of padding is pure page length — the top one is doing no work.

   Where the ground CHANGES, both sides keep their full padding on purpose:
   there the colour edge is the divider, and type set hard against a colour
   change looks like a mistake rather than a decision. That is why this is a
   sibling rule and not simply a smaller number. */
.chapter:not(.on-light) + .chapter:not(.on-light),
.chapter.on-light + .chapter.on-light{
  padding-top:clamp(2rem, 4.5vh, 3.75rem);
}
.on-light{background:var(--stone); color:var(--ink)}
.rule{height:1px; background:var(--line); border:0; margin:0}
.on-light .rule{background:var(--line-dk)}

.cols{display:grid; gap:var(--gap); grid-template-columns:1fr}
@media(min-width:900px){
  .cols-2{grid-template-columns:minmax(0,5fr) minmax(0,7fr)}
  .cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media(min-width:640px) and (max-width:899px){
  .cols-3,.cols-4{grid-template-columns:repeat(2,minmax(0,1fr))}
}

/* section heading pair: kicker sits under the heading, not above,
   so the eye lands on the statement first */
.head h2{max-width:16ch}
.head .kicker{
  display:block; margin-top:1.4rem; padding-top:1.1rem;
  border-top:1px solid var(--line); max-width:34ch;
  font-size:var(--s0); color:var(--muted);
}
.on-light .head .kicker{border-color:var(--line-dk); color:var(--muted-dk)}

/* ---- buttons ----------------------------------------------- */
.btn{
  display:inline-flex; align-items:center; gap:.65rem;
  padding:1.05rem 1.85rem; border-radius:999px;
  font-size:var(--s0); font-weight:400; letter-spacing:.01em;
  border:1px solid transparent; position:relative;
  /* NOTE: transform is deliberately NOT transitioned here. The magnetic
     effect is driven by GSAP quickTo; a CSS transition on transform would
     fight it and make the button feel laggy / double-eased. */
  transition:background .45s var(--ease), color .45s var(--ease), border-color .45s var(--ease);
}
.magnetic{will-change:transform}
.btn-solid{background:var(--bone); color:var(--ink)}
.btn-solid:hover{background:var(--bone-hi)}
/* A ghost button's border is the only thing drawing the control, so it is a
   functional boundary, not a divider — --line left it at 1.44:1. */
.btn-ghost{border-color:var(--line-ui); color:var(--bone)}
.btn-ghost:hover{border-color:var(--bone); background:var(--veil)}
.on-light .btn-solid{background:var(--ink); color:var(--bone)}
.on-light .btn-ghost{border-color:var(--line-ui-dk); color:var(--ink)}
.on-light .btn-ghost:hover{border-color:var(--ink); background:var(--veil-alt)}
.btn .dot{width:7px;height:7px;border-radius:50%;background:var(--wa);flex:none}

/* ---- .btn-swap: label slides out, icon slides in -------------
   The label leaves to the right and the icon arrives from the left, so the
   button reads as one object turning over rather than two things crossfading.

   The reference implementation moved both by a FIXED 10rem. That breaks:
   a button narrower than 10rem leaves the icon short of centre, and one wider
   than 10rem never pushes the label out of view, so the two overlap mid-hover.
   Percentages are relative to each element's OWN width, so the icon — which is
   stretched edge to edge by inset:0 — always travels exactly one button width
   and lands dead centre, at any size and in any language.

   .btn already sets position:relative, which the absolute icon needs.
   overflow:hidden does not clip :focus-visible, because an outline is painted
   outside the border box. */
.btn-swap{overflow:hidden; justify-content:center}
.btn-swap-label{display:block; transition:transform .5s var(--ease)}
.btn-swap-icon{
  position:absolute; inset:0; display:grid; place-items:center;
  font-size:1.6em; line-height:1; transform:translateX(-100%);
  transition:transform .5s var(--ease);
}
/* HOVER ONLY, and the earlier version of this comment argued the opposite.
   It said keyboard users should get the same affordance, so the swap also ran
   on :focus-visible. Driving it in a real browser showed what that costs:
   close the modal with Escape, focus returns to the trigger by design,
   :focus-visible matches, and the primary call to action sits there as a
   wordless "📅" until focus moves somewhere else.

   Hover and focus are not equivalent states. Hover is transient and the
   visitor can undo it by moving the mouse; focus persists, and it is exactly
   the state in which somebody most needs to read what the button says.
   Keyboard users are not losing an affordance here — :focus-visible already
   draws a 2px outline at line 294. */
.btn-swap:hover .btn-swap-label{transform:translateX(150%)}
.btn-swap:hover .btn-swap-icon{transform:translateX(0)}

@media(prefers-reduced-motion:reduce){
  /* The global reduce rule collapses the duration to .01ms, which would make
     the label snap away and the icon snap in — a jump-cut is exactly what
     somebody who asked for less motion is avoiding. Hold the label still and
     drop the icon instead: the button keeps working, nothing moves. */
  .btn-swap-icon{display:none}
  .btn-swap:hover .btn-swap-label{transform:none}
}

/* underline link */
.ul{position:relative; padding-bottom:2px}
.ul::after{content:""; position:absolute; left:0; right:0; bottom:0; height:1px; background:currentColor; transform:scaleX(0); transform-origin:right; transition:transform .5s var(--ease)}
.ul:hover::after{transform:scaleX(1); transform-origin:left}

/* ---- image frames + shot-brief placeholders ---------------- */
.frame{position:relative; overflow:hidden; background:var(--ink-2)}
.on-light .frame{background:var(--stone-2)}
.frame img{width:100%; height:100%; object-fit:cover; display:block}
.frame .ph{
  position:absolute; inset:0; display:flex; flex-direction:column;
  justify-content:flex-end; padding:1.15rem;
  background:
    radial-gradient(90% 60% at 30% 20%, var(--veil), transparent 70%),
    var(--ink-2);
}
.on-light .frame .ph{background:radial-gradient(90% 60% at 30% 20%, var(--veil-alt), transparent 70%), var(--stone-2)}
.frame .ph span{font-size:.72rem; line-height:1.45; letter-spacing:.04em; color:var(--muted); max-width:30ch}
.on-light .frame .ph span{color:var(--muted-dk)}
.r-portrait{aspect-ratio:3/4}
/* The "look for this door" photo in the booking column. Capped narrow so it
   reads as a wayfinding aid beside the address, not as a gallery image. */
/* The scan block that replaced the door photograph. Side by side rather than
   stacked: the code only has to be big enough for a phone camera, and the
   leftover width is exactly where the two lines telling you what to do with
   it belong. Under 26rem it stacks, because a 7rem code beside two lines of
   text is neither a readable code nor a readable line. */
.book-scan{
  display:flex; align-items:center; gap:clamp(1rem,3vw,1.4rem);
  margin:2.2rem 0 0; padding-top:1.6rem; border-top:1px solid var(--line);
}
/* --plate, not --stone: a camera reads dark-on-light, so this square must not
   follow the theme to a dark ground or it stops scanning. Same reasoning as
   the modal — see --plate in :root. :has(img) holds the plate back until
   there is an image, because the placeholder caption is light type and would
   otherwise sit on cream at about 1.2:1. */
.book-qr{
  flex:none; width:clamp(5.5rem,22vw,7.5rem); aspect-ratio:1;
  border:1px solid var(--line); border-radius:4px;
}
.book-qr:has(img){background:var(--plate); border-color:var(--plate-line); padding:.4rem}
/* contain, never cover: .frame img crops to fill, and a cropped QR loses the
   corner finder squares, which is the part a camera looks for first. */
.book-qr img{object-fit:contain}
/* Same shape as .step h3 and .trust-item h3: the base h3 rule would put
   display-serif metrics on a four-word label, so the text face, the weight
   and the letter-spacing are all restated. */
.book-scan-copy h3{font-family:var(--text); font-weight:400; font-size:var(--s0); line-height:1.35; letter-spacing:normal; margin:0 0 .3rem}
.book-scan-copy p{font-size:var(--s-1); font-weight:300; color:var(--muted); line-height:1.5; margin:0}
.book-scan-link{margin-top:.45rem !important}
@media(max-width:26rem){
  .book-scan{flex-direction:column; align-items:flex-start}
}
.r-square{aspect-ratio:1/1}
.r-wide{aspect-ratio:16/10}
.r-macro{aspect-ratio:5/4}

/* ============================================================
   NAV
   ============================================================ */
.nav{
  position:fixed; inset:0 0 auto 0; z-index:80;
  display:flex; align-items:center; justify-content:space-between;
  gap:1rem; padding:1.15rem var(--pad);
  transition:padding .5s var(--ease), background .5s var(--ease), border-color .5s var(--ease);
  border-bottom:1px solid transparent;
}
.nav.stuck{
  padding-block:.75rem; background:var(--scrim);
  -webkit-backdrop-filter:blur(16px); backdrop-filter:blur(16px);
  border-bottom-color:var(--line);
}
.brand{display:flex; flex-direction:column; line-height:1; z-index:2}
.brand b{font-family:var(--display); font-weight:400; font-size:1.22rem; letter-spacing:.01em}
.brand i{font-style:normal; font-size:.63rem; letter-spacing:.16em; color:var(--muted); margin-top:.42rem}
/* WAS var(--s0) — 15.2px, in --muted. That is a caption size doing a primary
   navigation's job, and on a 27" screen it read as fine print. --s1 is 16.8px
   and the gap comes in from 2.1rem to compensate, so the row is no wider than
   it was: one item lighter (see below) plus tighter gaps buys the size.

   The underline is the one idea worth taking from a pill navbar — it marks
   where you are without a coloured chip. It uses the same scaleX-from-the-side
   gesture as .ul everywhere else on the site, so it is not a new mechanic. */
.nav-links{display:none; gap:1.7rem; font-size:var(--s1); color:var(--muted)}
.nav-links a{position:relative; transition:color .35s var(--ease)}
.nav-links a::after{
  content:""; position:absolute; left:0; right:0; bottom:-5px; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:right;
  transition:transform .45s var(--ease);
}
.nav-links a:hover{color:var(--bone)}
.nav-links a:hover::after{transform:scaleX(1); transform-origin:left}
/* the page you are on stays marked, not just hovered */
.nav-links a[aria-current]{color:var(--bone)}
.nav-links a[aria-current]::after{transform:scaleX(1)}
.nav-right{display:flex; align-items:center; gap:.9rem; z-index:2}
.nav .btn{padding:.72rem 1.3rem; font-size:var(--s-1)}

.burger{display:flex; flex-direction:column; gap:5px; padding:.7rem .2rem; z-index:2}
.burger i{display:block; width:26px; height:1px; background:var(--bone); transition:transform .5s var(--ease), opacity .3s}
.menu-open .burger i:nth-child(1){transform:translateY(6px) rotate(45deg)}
.menu-open .burger i:nth-child(2){opacity:0}
.menu-open .burger i:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* MOVED BELOW THE .burger BLOCK, AND THAT IS THE WHOLE FIX. This media query
   used to sit ABOVE `.burger{display:flex}`. A media query adds no
   specificity, so the later plain rule won and display:none never applied —
   the burger has been visible at every width, sitting next to the full
   desktop navigation. Keep it after the block it overrides. */
@media(min-width:1080px){ .nav-links{display:flex} .burger{display:none} }

.overlay{
  position:fixed; inset:0; z-index:70; background:var(--ink);
  display:flex; flex-direction:column; justify-content:center;
  padding:6rem var(--pad) 2rem; visibility:hidden; opacity:0;
  /* visibility is stepped, not eased: it flips to visible instantly on open
     (otherwise the links stay unfocusable for the length of the fade) and is
     held back until after the fade on close. */
  transition:opacity .5s var(--ease), visibility 0s linear .5s;
}
.menu-open .overlay{
  visibility:visible; opacity:1;
  transition:opacity .5s var(--ease), visibility 0s linear 0s;
}
.overlay ol{list-style:none; margin:0; padding:0}
.overlay li{overflow:hidden}
.overlay li a{
  display:block; font-family:var(--display); font-size:clamp(2rem,9vw,3.4rem);
  line-height:1.22; padding:.1em 0; transform:translateY(105%);
}
.menu-open .overlay li a{transform:translateY(0); transition:transform .7s var(--ease)}
.overlay li:nth-child(1) a{transition-delay:.05s}
.overlay li:nth-child(2) a{transition-delay:.10s}
.overlay li:nth-child(3) a{transition-delay:.15s}
.overlay li:nth-child(4) a{transition-delay:.20s}
.overlay li:nth-child(5) a{transition-delay:.25s}
.overlay li:nth-child(6) a{transition-delay:.30s}
.overlay li:nth-child(7) a{transition-delay:.35s}
.overlay li:nth-child(8) a{transition-delay:.40s}
.overlay-foot{margin-top:3rem; padding-top:1.5rem; border-top:1px solid var(--line); display:flex; flex-wrap:wrap; gap:1.5rem 2.5rem; font-size:var(--s0); color:var(--muted)}

/* ============================================================
   HERO
   ============================================================ */
.hero{min-height:100svh; display:flex; align-items:flex-end; position:relative; overflow:hidden}
/* No .hero-media / .hero-veil any more. The photographic walk-in is gone, so
   the hero needs no scrim: the headline sits on flat --ink and is legible at
   every width by construction rather than by tuning. Everything that used to
   balance text against a moving photograph — the overscan box, the brightness
   filter, the two-axis veil, the opaque-first-frame rule — went with it. */

/* The studio sign, and the copy, as two grid cells — never as an overlay.
   One column on phones with the mark on top; two from 1100px, mark on the
   right. `minmax(0,1fr) auto` lets the copy column shrink so the headline
   rewraps instead of running under the mark, which is the failure an absolute
   overlay could not prevent. Because the mark sits BESIDE the type and never
   behind it, the hero needs no scrim at all. */
.hero-grid{display:grid; gap:clamp(1.6rem,4vw,4rem); align-items:center}
.hero-mark{
  display:block; width:clamp(88px,20vw,132px); aspect-ratio:1;
  border-radius:50%; overflow:hidden; background:var(--ink-2);
}
.hero-mark img{width:100%; height:100%; object-fit:cover; display:block}
@media(min-width:1100px){
  .hero-grid{grid-template-columns:minmax(0,1fr) auto}
  /* explicit placement, so the DOM order (mark first, for phones) does not
     decide the desktop layout */
  .hero-copy{grid-column:1; grid-row:1}
  .hero-mark{grid-column:2; grid-row:1; width:clamp(180px,17vw,290px)}
}
/* 7rem of top padding is right on a laptop and expensive on a phone, where
   it is 112px of nothing above the fold. The floor only has to clear the
   fixed header (~4.5rem), so 5rem is safe and buys back two lines — enough
   to bring the free-consultation line above the fold on a 390x844 screen,
   which is the whole reason that line exists. */
.hero .wrap{position:relative; z-index:3; width:100%; padding-bottom:clamp(2.5rem,7vh,5rem); padding-top:clamp(5rem,12vh,7rem)}
/* A grid item defaults to min-width:auto, meaning it refuses to shrink below
   its own min-content width. Below 1100px .hero-grid has no explicit column
   template, so the copy column took that floor and came out 518px wide inside
   a 390px viewport — and .hero{overflow:hidden} then cropped the result in
   silence. The eyebrow lost its last word, the WhatsApp button lost its label,
   and nothing anywhere reported an overflow, because there was no scroll to
   report: scrollWidth still read 390.
   min-width:0 releases the floor so the column tracks the viewport and the
   text rewraps. The desktop template already does this with minmax(0,1fr) —
   this is the same fix for the single-column case. */
.hero-copy{min-width:0}
.hero .eyebrow{font-size:var(--s0); color:var(--muted); margin-bottom:clamp(1.5rem,4vh,2.6rem)}
/* Measured at 390px, not estimated: "have to explain." wants 354px of the
   350px available — it misses a single line by FOUR PIXELS, and pays for it
   by wrapping to three. The line's natural width is a constant 6.91em, so the
   size at which it fits is solved rather than guessed: 6.91 x F <= 100vw - 40
   gives F <= 14.47vw - 5.8px. 14vw - 6px takes that with about 4% in hand,
   which is enough to absorb font-render variance between engines.

   min() rather than a media query, because this is a constraint and not a
   breakpoint. The two expressions cross at 434px: above it --s5 is smaller
   and nothing changes at all, so every desktop size is untouched by
   construction rather than by a number somebody has to keep in sync. Below
   it the headline tracks the screen — which also rescues phones under 390px,
   where --s5 hits its 3.2rem floor and stops shrinking while the screen
   carries on getting narrower. */
.hero h1{max-width:13ch; font-size:min(var(--s5), calc(14vw - 6px))}
.hero-sub{margin-top:1.9rem; max-width:46ch; color:var(--muted); font-size:var(--s1)}
.hero-cta{display:flex; flex-wrap:wrap; gap:.85rem; margin-top:2.4rem}
.hero-foot{
  display:flex; flex-wrap:wrap; align-items:center; gap:.8rem 2.2rem;
  margin-top:clamp(2.5rem,6vh,4.5rem); padding-top:1.3rem;
  border-top:1px solid var(--line); font-size:var(--s-1); color:var(--muted);
}
/* .scrollcue lived here — a "Scroll" label and a draining hairline pinned to
   the bottom right of the hero from 1080px up. Removed with its markup and its
   tween; see the note at the end of the hero in index.html. */

/* ---- the opening strip ------------------------------------
   Four 16:9 frames under the hero. 2x2 on a phone, 4-across from 760px:
   four across a 390px screen gives 95px frames, and at that size these
   stop being pictures and become texture. Most of this site's traffic is
   on a phone, so the phone case is the one that decides the breakpoint.

   Not a .chapter — it is a tight band, and .chapter's padding-block would
   put a full section's air around 200px of photograph. The hero already
   carries its own bottom padding, hence the small top value here.

   1px gap over --line, figures on --ink: the same hairline-divider device
   .macro, .steps and .quotes already use. --line is DECORATIVE here, which
   is the correct family — these are dividers, not control boundaries. */
.strip{ padding-block:clamp(1.5rem,4vh,2.75rem) clamp(2.5rem,6vh,4rem) }
.strip-line{
  font-family:var(--display); font-size:var(--s2); line-height:1.25;
  color:var(--bone); max-width:26ch;
  margin:0 0 clamp(1.4rem,3.5vh,2.2rem);
}
.strip-row{ display:grid; grid-template-columns:repeat(2,1fr); gap:1px; background:var(--line) }
@media(min-width:760px){ .strip-row{ grid-template-columns:repeat(4,1fr) } }
.strip-row figure{ margin:0; background:var(--ink) }
.strip-row img{ width:100%; height:auto; aspect-ratio:16/9; object-fit:cover; display:block }

/* line-mask reveal */
.reveal-line{display:block; overflow:hidden}
.reveal-line > span{display:block; will-change:transform}

/* ============================================================
   STATEMENT — the one centred moment in the whole site
   ============================================================ */
/* The one centred moment, so it keeps more air than a normal chapter — but
   18vh was 162px a side, and the gap below it into the services grid measured
   261px once the next section's own padding was added. */
.statement{padding-block:clamp(4.5rem,12vh,9rem); text-align:center}
.statement .big{
  font-family:var(--display); font-size:var(--s4); line-height:1.14;
  max-width:19ch; margin:0 auto; letter-spacing:-.02em;
}
.statement .big em{font-style:italic}
.statement .word{display:inline-block; will-change:transform,opacity}
.statement .body{margin:2.6rem auto 0; max-width:52ch; text-align:left}
@media(min-width:760px){ .statement .body{text-align:center} }

/* ============================================================
   BEFORE / AFTER
   ============================================================ */
/* 4:5 portrait, and capped in width. It used to be 16/11, widening to 16/8 on
   desktop — a 1296x648 letterbox. The subject here is one man's head and his
   hairline, so a 2:1 slot can only do one of two bad things: crop the top of
   the head off, or strand a small face in a wide empty band. Nothing in the
   component was broken; the frame was simply the wrong shape for its content.
   Capping the width keeps the head large rather than small-in-a-billboard, and
   4:5 is the ratio the shot brief now asks for, so photograph and frame agree. */
.ba{position:relative; user-select:none; touch-action:pan-y; overflow:hidden;
    aspect-ratio:4/5; max-width:600px; margin-inline:auto;
    background:var(--ink-2); cursor:ew-resize}
.ba-layer{position:absolute; inset:0}
.ba-layer .frame{position:absolute; inset:0}
.ba-after{clip-path:inset(0 0 0 var(--x, 50%))}
.ba-tag{
  position:absolute; top:1.1rem; z-index:4; font-size:var(--s-1);
  letter-spacing:.12em; color:var(--on-photo); background:var(--scrim-solid);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  padding:.45rem .9rem; border-radius:999px;
}
.ba-tag.l{left:1.1rem} .ba-tag.r{right:1.1rem}
/* Sits above the wipe layers (z-index 4, same as the Before/After tags) so it
   is legible whichever side of the divider is showing. Centred at the bottom,
   clear of the knob at the vertical middle. */
.ba-note{
  position:absolute; left:50%; bottom:1.1rem; translate:-50% 0; z-index:4;
  font-size:var(--s-1); letter-spacing:.08em; color:var(--on-photo);
  background:var(--scrim-solid);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  padding:.45rem .9rem; border-radius:999px; white-space:nowrap; max-width:calc(100% - 2rem);
}
@media(max-width:520px){ .ba-note{font-size:.68rem; letter-spacing:.04em} }
.ba-handle{
  position:absolute; top:0; bottom:0; left:var(--x,50%); z-index:5;
  width:1px; background:var(--bone); transform:translateX(-.5px); pointer-events:none;
}
.ba-knob{
  position:absolute; top:50%; left:50%; translate:-50% -50%;
  width:62px; height:62px; border-radius:50%;
  border:1px solid var(--knob-ring); background:var(--knob-fill);
  -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  display:grid; place-items:center; gap:0;
}
.ba-knob::before{content:"‹  ›"; font-size:1rem; letter-spacing:.2em; color:var(--bone)}
.ba-range{position:absolute; inset:0; opacity:0; width:100%; height:100%; margin:0;
  cursor:ew-resize; touch-action:pan-y}
/* tracks the capped .ba above, so the hint sits under its left edge rather
   than out at the page gutter */
.ba-hint{margin:1rem auto 0; max-width:600px; font-size:var(--s-1); color:var(--muted)}

/* ============================================================
   SOLUTION SELECTOR
   ============================================================ */
.spec{display:grid; grid-template-columns:auto 1fr; gap:.55rem 1.4rem; font-size:var(--s0); border-top:1px solid var(--line-dk); padding-top:1.1rem; margin-top:.4rem}
.spec dt{color:var(--muted-dk)}
.spec dd{margin:0}

/* ============================================================
   SYSTEMS INDEX (hover-image list, no cards)
   ============================================================ */
.index{border-top:1px solid var(--line)}
.index a{
  display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:1.2rem;
  padding:clamp(1.4rem,3vw,2.4rem) 0; border-bottom:1px solid var(--line);
  transition:color .4s var(--ease);
}
.index .n{font-size:var(--s-1); color:var(--muted); width:2.2em}
.index .t{font-family:var(--display); font-size:var(--s3); line-height:1; letter-spacing:-.02em;
  transition:opacity .4s var(--ease)}
.index .d{font-size:var(--s0); color:var(--muted); max-width:30ch; display:none}
@media(min-width:900px){
  .index a{grid-template-columns:auto minmax(0,1fr) minmax(0,26ch) auto}
  .index .d{display:block}
}
.index a:hover .t{opacity:.55}
.index .go{font-size:var(--s-1); color:var(--muted)}
.hover-img{
  position:fixed; z-index:60; width:250px; aspect-ratio:3/4; pointer-events:none;
  opacity:0; overflow:hidden; display:none;
  /* no CSS transition — GSAP quickTo/tweens drive x, y, scale and opacity */
}
@media(min-width:1080px) and (pointer:fine){ .hover-img{display:block} }

/* ============================================================
   WORK — the permissioned client frames

   REPLACED "CRAFT — sticky storytelling", whose .craft / .craft-sticky /
   .craft-item rules went with the section they styled. Dead selectors are
   harmless to a browser and misleading to the next person reading this file,
   so they were removed rather than left.

   The grid itself is .cols.cols-4, which already steps 1 -> 2 -> 4 columns and
   is what #trustGrid uses. On a phone that is one 3:4 portrait per row at the
   full content width, which is the only size a face or a hairline is actually
   legible at. Two-up would put each frame at about 165px on a 390px screen.

   Nothing here animates, by request, so there is no opacity:0 start state and
   nothing that can fail to arrive. .frame and .r-portrait already carry the
   placeholder and the 3:4 ratio.
   ============================================================ */
#work .work-grid{list-style:none; margin:0; padding:0}

/* ============================================================
   PROCESS — a genuine sequence, so numbering is earned
   ============================================================ */
/* An <ol>, so the list reset is not optional: a browser's default
   padding-inline-start would push the first column off the grid and the
   markers would sit in the gap. See the note in the markup for why this is a
   list at all. */
.steps{
  list-style:none; margin:0; padding:0;
  display:grid; gap:1px; background:var(--line); border-block:1px solid var(--line);
}
@media(min-width:760px){ .steps{grid-template-columns:repeat(3,1fr)} }
@media(min-width:1200px){ .steps{grid-template-columns:repeat(6,1fr)} }
.step{background:var(--ink); padding:1.8rem 1.4rem 2.4rem}
.step .n{font-family:var(--display); font-size:1.5rem; color:var(--muted); display:block; margin-bottom:1.6rem}
.step h3{font-family:var(--text); font-weight:400; font-size:var(--s1); line-height:1.35; letter-spacing:normal; margin:0 0 .55rem}
.step p{font-size:var(--s0); color:var(--muted); margin:0}

/* ============================================================
   HAIRLINE MACRO STRIP
   ============================================================ */
.macro{display:grid; gap:1px; background:var(--line)}
@media(min-width:760px){ .macro{grid-template-columns:repeat(3,1fr)} }
.macro figure{margin:0; background:var(--ink)}
.macro figcaption{padding:1.1rem 1.2rem 1.6rem; font-size:var(--s0); color:var(--muted)}

/* ============================================================
   TRUST
   ============================================================ */
/* #trustGrid is a <ul>. Without the reset the browser's default
   padding-inline-start pushes the first column out of the grid and drops
   markers into the gap — same reason .steps needs one. */
#trustGrid{list-style:none; margin:0; padding:0}
.trust-item{padding-top:1.2rem; border-top:1px solid var(--line-dk)}
.trust-item h3{font-family:var(--text); font-weight:400; font-size:var(--s1); line-height:1.35; letter-spacing:normal; margin:0 0 .5rem}
.trust-item p{font-size:var(--s0); color:var(--muted-dk); margin:0}

/* ============================================================
   TESTIMONIALS
   ============================================================ */
/* ---- the review rail ----------------------------------------
   Replaced .quotes / .quote / .quotes--one, which are gone: nothing on any
   page used them once the single quote moved into the rail.

   Two effects rebuilt from scratch in CSS. The originals were React +
   Tailwind + Framer Motion components, and this site has no build step, no
   npm and no framework — so the BEHAVIOUR is reproduced, not the code:
     · an infinite marquee, from one keyframe over a track cloned in JS
     · a hold-to-pin lift, from perspective + translateZ + a drawn pin stem
   The original pin is a violet-purple-blue gradient. That is not available
   here: --wa is the only accent on this site and it means WhatsApp. The pin
   is drawn in --bone and --line-ui instead, which is the same gesture in
   this palette. */

/* The rating line. --bone on the numeral so the 4.9 carries, --muted on the
   sentence so it does not shout — the link is the thing to click. */
.rev-stat{
  display:flex; align-items:baseline; flex-wrap:wrap; gap:.25rem .7rem;
  margin-top:1.5rem; font-size:var(--s0); color:var(--muted);
}
.rev-stat-n{ font-family:var(--display); font-size:var(--s2); line-height:1; color:var(--bone) }
.rev-stat-s{ color:var(--bone); letter-spacing:.12em; font-size:var(--s0) }

/* DEFAULT IS THE NO-JAVASCRIPT STATE: a centred wrapped row, everything
   visible, nothing clipped. Module 8 adds .is-live and only then does it
   become a nowrap marquee. Written this way round on purpose — if the
   module never runs, the reviews are still readable. */
.rail{ padding-block:clamp(1.7rem,4vh,2.7rem); border-block:1px solid var(--line) }
.rail-track{
  list-style:none; margin:0; padding-inline:var(--pad);
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:clamp(.8rem,2vw,1.3rem);
}
.rail.is-live{ overflow:hidden }
/* Fade the two ends so cards enter and leave instead of being guillotined.
   Behind @supports because without mask-image the rail simply hard-clips,
   which is a downgrade, not a break. */
@supports (mask-image:linear-gradient(#000,#000)){
  .rail.is-live{ mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent) }
}
/* align-items IS THE WHOLE REASON THE CARDS WERE DIFFERENT HEIGHTS.
   This said `center`, which makes every <li> size to its own content and
   then centres it — so a two-line review became a short card, a seven-line
   one became a tall card, and the row read as ragged junk. `stretch` gives
   every <li> the height of the tallest, and .rev-card's height:100% fills
   it. The quote is line-clamped and the attribution is pinned with
   margin-top:auto, so the extra room lands as even white space at the
   bottom of the shorter cards rather than as drifting text.

   It also matches the no-JS fallback, where .rail-track never set
   align-items at all and therefore already stretched. One behaviour now,
   not two. */
.rail-track.is-live{
  flex-wrap:nowrap; justify-content:flex-start; width:max-content;
  padding-inline:0; align-items:stretch;
  animation:rail-scroll var(--rail-dur,64s) linear infinite;
}
/* Cloned once by module 8, so one full pass is exactly -50%. Runs from -50%
   to 0 so the cards travel rightwards, matching the reference. */
@keyframes rail-scroll{ from{transform:translate3d(-50%,0,0)} to{transform:translate3d(0,0,0)} }
.rail-track.is-paused{ animation-play-state:paused }

.rev{ flex:0 0 auto; perspective:1000px }
.rev-card{
  position:relative; width:clamp(260px,76vw,380px); height:100%;
  display:flex; flex-direction:column;
  padding:clamp(1.5rem,3.4vw,2rem);
  /* WHITE PAPER, NOT A TINTED PANEL. An earlier pass used --stone here so the
     card would flip with the theme, and it was still too close in value to
     the page to register as a separate object — the reviews read as a box
     with copy in it. White is the request and it is also the right answer:
     a review is a thing somebody else wrote, and it should look like it was
     laid on the page rather than painted into it.

     Because --paper does not flip, nothing inside may use a flipping token.
     See the --paper block in :root. */
  background:var(--paper); color:var(--paper-ink);
  border:1px solid var(--paper-line); border-radius:4px;
  /* Two layers: a tight one that draws the edge, and a wide soft one that
     lifts the sheet. The lift is what makes five white cards on a near-black
     page read as the loudest thing in the section, which is the point. */
  box-shadow:0 1px 2px var(--paper-line), 0 16px 34px -16px var(--paper-shadow);
  transform-style:preserve-3d;
  transition:transform .5s var(--ease), box-shadow .5s var(--ease), border-color .5s var(--ease);
}
/* The lift. rotateX is kept to 5deg on purpose: this card is text, and past
   about 8deg the far edge starts to smear and the review gets harder to
   read, which defeats the point of stopping to read it. */
.rev-card.is-pin{
  transform:translate3d(0,0,44px) rotateX(5deg) scale(1.015);
  box-shadow:0 1px 2px var(--paper-line), 0 34px 60px -18px var(--paper-shadow);
  border-color:var(--paper-line);
}
/* the pin: a stem that draws up out of the card, and its head */
.rev-card::before,.rev-card::after{ content:""; position:absolute; left:50%; pointer-events:none }
.rev-card::before{
  bottom:100%; width:1px; height:0; background:linear-gradient(to top,var(--line-ui),transparent);
  transition:height .45s var(--ease);
}
.rev-card::after{
  bottom:calc(100% + 15px); width:5px; height:5px; margin-left:-2px;
  border-radius:50%; background:var(--bone);
  opacity:0; transform:scale(.3); transition:opacity .45s var(--ease), transform .45s var(--ease);
}
.rev-card.is-pin::before{ height:16px }
.rev-card.is-pin::after{ opacity:1; transform:scale(1) }
/* THE OFFSET IS NEGATIVE ON PURPOSE. A positive offset draws the ring just
   OUTSIDE the card — which is the near-black page, where a --paper-ink ring
   is invisible. Insetting it puts the ring on the white sheet at 18.76:1,
   and it stays right in both themes because both the ring and the surface
   under it are theme-independent. This is the third time rule 7 has come up
   in this component; the shape of the bug is always "the ring is measured
   against the wrong surface". */
.rev-card:focus-visible,
.rev-card :focus-visible{ outline:2px solid var(--paper-ink); outline-offset:-4px }

/* The rating is the fastest signal that this is a review rather than a pull
   quote, so it gets the one hue and enough size to register at a glance.
   aria-label on the element carries the value for anyone who cannot see the
   colour, so the gold is never the only thing saying "five stars". */
.rev-stars{ margin:0 0 1rem; font-size:var(--s1); letter-spacing:.18em; color:var(--star) }
/* Clamped to seven lines so every card is a predictable height and the rail
   cannot be knocked out of shape by one long review. Pinning releases it. */
.rev-card blockquote{
  margin:0; font-family:var(--display); font-size:var(--s1); line-height:1.55;
  color:var(--paper-ink); letter-spacing:-.005em;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:7; overflow:hidden;
}
.rev-card.is-pin blockquote{ -webkit-line-clamp:99; overflow:visible }

/* THIS IS SCRIPT COVERAGE, NOT A THIRD TYPEFACE. Bodoni Moda carries no
   Devanagari, so a Hindi review set in var(--display) falls back glyph by
   glyph to whatever the OS happens to offer — on Windows that is Nirmala UI
   arriving mid-sentence next to Bodoni, which looks like a rendering fault
   rather than a second language. Naming the stack makes the fallback a
   decision instead of an accident.

   Devanagari hangs matras above and below the headline stroke, so it needs
   more leading than Latin at the same size, and the negative tracking that
   suits Bodoni closes up the conjuncts. Both are reset here. */
.rev-card blockquote:lang(hi){
  font-family:"Noto Sans Devanagari","Nirmala UI","Kohinoor Devanagari",
              "Devanagari Sangam MN",sans-serif;
  line-height:1.65; letter-spacing:normal;
}
/* The rule is structural, not decorative: it marks where the person's words
   stop and the attribution starts. Without it the name reads as the last
   line of the quote, which is a large part of why these looked like plain
   boxes of copy. margin-top:auto pins the whole footer to the bottom so the
   rule lands at the same height on every card in the row. */
.rev-card cite{
  display:block; margin-top:auto; padding-top:1.1rem;
  border-top:1px solid var(--paper-line);
  font-family:var(--text); font-style:normal; font-weight:500;
  font-size:var(--s-1); color:var(--paper-ink); line-height:1.5;
}
/* An earlier pass faded this with opacity:.78, which composited to about
   4.19:1 — under the 4.5 this size needs. Hierarchy now comes from two
   measured tokens rather than from dimming one: the NAME is --paper-ink at
   18.76:1 and set in the body face at 500 so a real person is the heaviest
   thing in the footer, the SOURCE is --paper-mut at 7.85:1 and light. */
.rev-src{ display:block; margin-top:.15rem; font-weight:300; color:var(--paper-mut) }
.rev-hint{ margin-top:1.3rem; font-size:var(--s-1); color:var(--muted) }

/* Reduced motion: no marquee at all. Back to the wrapped row, which is the
   same thing the no-JavaScript path gets — one fallback, not two. The pin
   still works, because a lift on deliberate touch is not ambient motion. */
@media(prefers-reduced-motion:reduce){
  .rail.is-live{ overflow:visible; mask-image:none }
  .rail-track.is-live{
    flex-wrap:wrap; justify-content:center; width:auto;
    padding-inline:var(--pad); animation:none; transform:none;
  }
  .rev-card{ transition-duration:.01ms }
}

/* ============================================================
   FAQ
   ============================================================ */
.faq{border-top:1px solid var(--line-dk)}
.faq details{border-bottom:1px solid var(--line-dk)}
.faq summary{
  list-style:none; cursor:pointer; padding:1.5rem 3rem 1.5rem 0; position:relative;
  font-size:var(--s2); font-family:var(--display); letter-spacing:-.01em; line-height:1.25;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:""; position:absolute; right:.4rem; top:2.35rem; width:14px; height:1px;
  background:currentColor;
}
.faq summary::before{
  content:""; position:absolute; right:.4rem; top:2.35rem; width:14px; height:1px;
  background:currentColor; transform:rotate(90deg); transition:transform .45s var(--ease);
}
.faq details[open] summary::before{transform:rotate(0)}
.faq .ans{padding:0 0 1.7rem; max-width:64ch; color:var(--muted-dk); font-size:var(--s0)}

/* ============================================================
   BOOKING
   ============================================================ */
.book{display:grid; gap:var(--gap)}
@media(min-width:1000px){ .book{grid-template-columns:minmax(0,5fr) minmax(0,6fr)} }
.field{display:block; margin-bottom:1.1rem}
.field span{display:block; font-size:var(--s-1); color:var(--muted); margin-bottom:.5rem}
/* --line-ui, not --line: a text field's border is what tells you a text field
   is there. At --line it measured 1.49:1 against its own fill. */
.field input,.field select,.field textarea{
  width:100%; padding:.95rem 1rem; font:inherit; font-size:var(--s0); color:var(--bone);
  background:var(--ink-2); border:1px solid var(--line-ui); border-radius:2px;
  transition:border-color .35s var(--ease), background .35s var(--ease);
}
.field textarea{min-height:104px; resize:vertical}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--bone); background:var(--ink-3); outline:none}
.field select{appearance:none; background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%); background-position:calc(100% - 20px) 22px, calc(100% - 14px) 22px; background-size:6px 6px; background-repeat:no-repeat}
.two{display:grid; gap:0 1rem}
@media(min-width:560px){ .two{grid-template-columns:1fr 1fr} }
.form-note{font-size:var(--s-1); color:var(--muted); margin-top:.4rem}
.book-aside dl{display:grid; grid-template-columns:auto 1fr; gap:.5rem 1.6rem; margin:1.6rem 0 0; font-size:var(--s0); padding-top:1.4rem; border-top:1px solid var(--line)}
.book-aside dt{color:var(--muted)} .book-aside dd{margin:0}

/* ============================================================
   FOOTER + STICKY BAR
   ============================================================ */
footer{padding-top:clamp(4rem,9vh,7rem); padding-bottom:calc(clamp(2rem,5vh,4rem) + env(safe-area-inset-bottom))}
.foot-top{display:grid; gap:var(--gap); padding-bottom:3rem}
/* 3fr, not the 5fr this was: the first column held a display-serif wordmark of
   the studio name and needed the width for it. The wordmark is gone (the name
   was already in the header on every page) and only a 34ch sentence is left, so
   the extra two units were pure empty space at the widest end of the footer.
   .foot-mark, which styled that wordmark, went with it. */
@media(min-width:900px){ .foot-top{grid-template-columns:minmax(0,3fr) repeat(3,minmax(0,2fr))} }
.foot-col h3{margin:0 0 1rem; font-family:var(--text); font-weight:400; font-size:var(--s-1); line-height:1.4; color:var(--muted); letter-spacing:.06em}
.foot-col ul{list-style:none; margin:0; padding:0; display:grid; gap:.6rem; font-size:var(--s0)}
/* The footer address: one link over three lines, so it cannot use .ul — see
   the note beside it in index.html.

   It carries a RESTING underline, unlike every other link on the site, because
   the others are a word or two that read as links from their position while
   this is a block of plain address text that otherwise gives no sign it can be
   pressed. --line-ui, not --line: this is the affordance of a real control, so
   1.4.11 applies and it is measured at 3.2:1 or better in both themes, where
   --line would be ~1.4:1 and invisible. Hard rule 5 is the reason these are
   two separate tokens.

   inline-block so the three lines stay one block; the underline still wraps
   per line because text-decoration is not a positioned pseudo-element. */
.foot-addr{
  display:inline-block; color:inherit; line-height:1.65;
  text-decoration:underline; text-decoration-thickness:1px;
  text-underline-offset:3px; text-decoration-color:var(--line-ui);
  transition:text-decoration-color .4s var(--ease);
}
.foot-addr:hover,
.foot-addr:focus-visible{text-decoration-color:currentColor}
@media(prefers-reduced-motion:reduce){ .foot-addr{transition:none} }
.foot-bottom{display:flex; flex-wrap:wrap; gap:.8rem 2rem; justify-content:space-between; padding-top:1.6rem; border-top:1px solid var(--line); font-size:var(--s-1); color:var(--muted)}

.bar{
  position:fixed; left:0; right:0; bottom:0; z-index:75;
  display:grid; grid-template-columns:1fr 1fr; gap:1px;
  background:var(--line); border-top:1px solid var(--line);
  transform:translateY(101%); transition:transform .55s var(--ease);
  padding-bottom:env(safe-area-inset-bottom);
}
.bar.show{transform:translateY(0)}
.bar a{display:flex; align-items:center; justify-content:center; gap:.55rem; padding:1.05rem .5rem; font-size:var(--s0); background:var(--ink-2)}
.bar a.primary{background:var(--bone); color:var(--ink)}
@media(min-width:1080px){ .bar{display:none} }
/* room for the sticky conversion bar so it never covers the footer */
@media(max-width:1079px){ body{padding-bottom:4.2rem} }


/* ============================================================
   THE ROOM — the studio in three photographs.

   This was a scroll-pinned "reel": three chapters stacked in one box,
   each clip-wiped up over the one before it. Two things were wrong with
   it and both are why it is gone.

   1. Mid-wipe it composited two photographs and two headlines on top of
      each other. A half-revealed chapter reads as a rendering fault, not
      as a transition — you see two rooms spliced across one frame and
      two serif headlines colliding.
   2. The frame was forced landscape — height:54vh inside a 66fr column
      is roughly 1.7:1 — while every source photograph is portrait (3:4)
      or square. The room was centre-cropped to a horizontal band, which
      is why it never looked like a room.

   Now it is a static triptych at 4:5, close to what the camera actually
   shot, so the frames crop gently instead of butchering. No pin, no
   wipe, no progress rails, no motion. Nothing here can strand content:
   there is nothing to reveal.

   Structure deliberately matches .macro / .steps / .quotes — the 1px
   grid gap over --line is this site's existing device for a row of
   related panels. A new one would be decoration.
   ============================================================ */

/* ============================================================
   PRIVACY + CLOSING
   ============================================================ */
/* Two columns of prose, not four icon cards. The brief asked for editorial
   rows rather than the icon-card kit, and this section is read, not scanned. */
.privacy-list{display:grid; gap:1px; background:var(--line); border-block:1px solid var(--line)}
@media(min-width:760px){ .privacy-list{grid-template-columns:repeat(2,minmax(0,1fr))} }
.privacy-item{background:var(--ink); padding:clamp(1.6rem,3vw,2.4rem)}
.privacy-item h3{
  font-family:var(--text); font-weight:400; font-size:var(--s1);
  line-height:1.35; letter-spacing:normal; margin:0 0 .55rem;
}
.privacy-item p{margin:0; font-size:var(--s0); color:var(--muted); max-width:46ch}


/* ---- editorial blur reveal --------------------------------
   The fourth animation type. Fade-and-rise is the site's workhorse; this one
   is for a pull-quote moment where the sentence IS the content. Blur resolving
   to sharp reads as a lens finding focus, which is slower and more
   expensive-feeling than a slide.

   NOTHING USES IT RIGHT NOW. #closing was its only caller and that section is
   gone; #promise takes the word-by-word stagger in module 6c instead. The
   machinery is left in place because it costs nothing idle — the selector in
   module 6f-2 simply matches no elements — and because the reasoning below is
   the part worth keeping. Add data-anim="blur" to one line and it works.

   Kept off everything else on purpose: a blur filter on a large element is
   the most expensive thing here to composite, and on a mid-range Android
   phone — which is most of this studio's traffic — using it widely would
   cost frames. Two elements is affordable. Twenty would not be.

   No rule is needed here: the generic [data-anim]{opacity:0} already hides
   it, and the three motion fallbacks already un-hide it. The blur only ever
   exists while GSAP is driving it, so a stranded blur is not possible. */

/* ============================================================
   SERVICES DROPDOWN

   Progressive enhancement: the markup ships a plain link to services.html
   and menu.js swaps in the button. With scripting off the link still works,
   so the fifteen services are never unreachable.

   Desktop: a card anchored under the button, right-aligned so it cannot run
   off the viewport. Phone: a full-width sheet hung under the fixed header,
   because a 420px dropdown on a 390px screen is just a worse full screen.
   On a phone it also carries the page links — below 1080px .site-nav and
   .nav-links are display:none and the blog and services pages have no
   burger, so without this those pages had no navigation at all.
   ============================================================ */
.svcmenu{position:relative}
.svcmenu-link{font-size:var(--s0); color:var(--muted)}
.svcmenu-link:hover{color:var(--bone)}

.svcmenu-btn{
  display:inline-flex; align-items:center; gap:.5rem;
  padding:.72rem 1.15rem; border-radius:999px;
  border:1px solid var(--line-ui);          /* functional border: WCAG 1.4.11 */
  color:var(--bone); font-size:var(--s-1); letter-spacing:.01em;
  white-space:nowrap;
  transition:border-color .35s var(--ease), background .35s var(--ease);
}
.svcmenu-btn:hover{border-color:var(--bone); background:var(--veil)}
.svcmenu-btn svg{width:10px; height:10px; flex:none;
  transition:transform .35s var(--ease)}
.svcmenu[data-open] .svcmenu-btn{border-color:var(--bone); background:var(--veil)}
.svcmenu[data-open] .svcmenu-btn svg{transform:rotate(180deg)}

.svcmenu-panel{
  position:absolute; z-index:95; right:0; top:calc(100% + .7rem);
  width:min(420px, calc(100vw - 2rem));
  max-height:min(70vh, 560px); overflow-y:auto;
  background:var(--ink-2); border:1px solid var(--line-ui); border-radius:3px;
  box-shadow:0 24px 60px var(--shadow-lg);
  /* fade and rise, the same entrance the rest of the site uses */
  opacity:0; transform:translateY(-8px);
  transition:opacity .3s var(--ease), transform .3s var(--ease);
}
.svcmenu[data-open] .svcmenu-panel{opacity:1; transform:translateY(0)}

.svcmenu-list{list-style:none; margin:0; padding:.4rem}
.svcmenu-item + .svcmenu-item{border-top:1px solid var(--line)}
.svcmenu-term{
  width:100%; text-align:left; display:flex; align-items:baseline; gap:.7rem;
  padding:.85rem .8rem; font-size:var(--s0); line-height:1.35;
  color:var(--bone); border-radius:2px;
  transition:background .25s var(--ease);
}
.svcmenu-term:hover{background:var(--ink-3)}
/* the +/- is generated, so the markup carries no decorative glyphs for a
   screen reader to read out — aria-expanded already says the same thing */
.svcmenu-term::before{
  content:"+"; flex:none; width:1em; color:var(--muted); line-height:1;
}
.svcmenu-term[aria-expanded="true"]{color:var(--bone); background:var(--ink-3)}
.svcmenu-term[aria-expanded="true"]::before{content:"\2013"}

/* grid-template-rows 0fr -> 1fr animates to an auto height without JS
   measuring it. Engines too old for it just show the row open, which is
   the right way to fail: the text is readable either way. */
.svcmenu-def{
  display:grid; grid-template-rows:0fr;
  transition:grid-template-rows .35s var(--ease);
}
.svcmenu-term[aria-expanded="true"] + .svcmenu-def{grid-template-rows:1fr}
.svcmenu-def > p{
  overflow:hidden; margin:0;
  font-size:var(--s0); line-height:1.6; color:var(--muted);
}
.svcmenu-term[aria-expanded="true"] + .svcmenu-def > p{padding:0 .8rem 1rem 2.5rem}

.svcmenu-foot{
  padding:.9rem 1.2rem 1.1rem; border-top:1px solid var(--line-ui);
  display:grid; gap:.9rem;
}
.svcmenu-all{font-size:var(--s0); color:var(--bone);
  text-decoration:underline; text-underline-offset:3px}
.svcmenu-nav{display:none}

@media(max-width:1079px){
  /* below this width the panel is the only navigation on the blog and
     services pages, so it carries the page links too */
  .svcmenu-nav{
    display:flex; flex-wrap:wrap; gap:.75rem 1.4rem;
    padding-top:.9rem; border-top:1px solid var(--line); font-size:var(--s0);
  }
  .svcmenu-nav a{color:var(--muted); transition:color .35s var(--ease)}
  .svcmenu-nav a:hover{color:var(--bone)}
}

/* The Services button costs 105px of header, which pushed "Book consultation"
   and the burger off the right edge at 390px — measured, not guessed. Book
   comes out below 640px rather than the button: nothing is lost, because the
   hero has its own Book CTA, the overlay menu lists one, and the sticky .bar
   carries one as soon as you scroll. The burger has no such stand-in. */
@media(max-width:639px){
  .nav-right > .btn-solid{display:none}

  /* HIDING BOOK WAS NOT ENOUGH, AND THE BURGER WAS STILL BEING CLIPPED.
     Measured at 390px: --pad leaves 350px of content. The brand's second line,
     "Hair replacement studio, Mumbai", is 30 characters at .63rem tracked out
     to .16em — about 210px on its own. Services (105) + theme (40) + burger
     (30) plus gaps needs another 200. That is ~429px in 350, and because body
     carries overflow-x:hidden the surplus is not scrolled to, it is silently
     cut off — taking the theme toggle and the burger with it. The entire
     navigation was unreachable on a phone.

     The subtitle goes rather than the burger. At 10px tracked to .16em it is
     barely legible on a phone anyway, the brand name above it still says who
     this is, and the same words are in the footer and the <title>. */
  .brand i{display:none}

  /* Dropping the subtitle recovered ~210px and it was still about 44px short,
     because "Bombay Hair Patch" at 1.22rem is itself ~175px. Rather than cut
     another control, the remaining shortfall is taken in small amounts from
     four places nobody will notice:

       brand 1.22 -> 1.06rem   ~23px      gaps .9 -> .55rem        ~21px
       Services padding trim   ~16px      nav padding 20 -> 16px    ~8px

     Measured total at 390px: 346px of content in 358px available. The burger
     and the theme toggle are both reachable again. */
  .nav{gap:.55rem; padding-inline:1rem}
  .brand b{font-size:1.06rem}
  .nav-right{gap:.55rem}
  .svcmenu-btn{padding:.72rem .85rem}
}

@media(max-width:719px){
  /* full-width sheet with bigger type and taller rows, so it is genuinely
     readable on a phone rather than a shrunken desktop menu */
  .svcmenu{position:static}
  .svcmenu-panel{
    position:fixed; left:.6rem; right:.6rem; width:auto;
    top:calc(var(--nav-h) + .8rem);
    /* stop above the sticky .mobile-bar, or the last item hides behind it */
    max-height:calc(100dvh - var(--nav-h) - 6.2rem);
  }
  .svcmenu-term{padding:1rem .9rem; font-size:var(--s1)}
  .svcmenu-def > p{font-size:var(--s1); line-height:1.62}
  .svcmenu-term[aria-expanded="true"] + .svcmenu-def > p{padding:0 .9rem 1.1rem 2.7rem}
  .svcmenu-foot{padding:1rem 1.1rem 1.2rem}
}

/* Motion fallback. The panel must still open and close — only the
   animation goes. opacity/transform are reset so nothing is left faded. */
@media(prefers-reduced-motion: reduce){
  .svcmenu-panel{transition:none; opacity:1; transform:none}
  .svcmenu-def{transition:none}
  .svcmenu-btn svg{transition:none}
}


/* ============================================================
   THE RANGE — fifteen services with photographs (#systems)

   Structure is the site's existing hairline grid: gap:1px over --line, the
   same device .macro, .steps and .quotes already use. A new
   card treatment — border, radius, shadow — would be decoration, and it
   would make fifteen of them look like a pricing table.

   Three across, because that is what the alternating entrance reads against.
   Two on a phone rather than one: fifteen full-width cards is four thousand
   pixels of scrolling, and this section exists to show breadth quickly.

   No numbering here, although services.html numbers its list. Fifteen
   services are not a sequence — you do not do them in order — so numbers
   would encode something untrue. On the glossary page they are a finding
   aid for a long list; here the pictures do that job.
   ============================================================ */
.range{
  display:grid; gap:1px; background:var(--line);
  border-block:1px solid var(--line);
}
@media(min-width:600px){ .range{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media(min-width:960px){ .range{grid-template-columns:repeat(3,minmax(0,1fr))} }

.range-item{
  display:block; background:var(--ink);
  padding:clamp(1rem,2.4vw,1.6rem);
}
/* No background change on hover. --ink-2 against --ink is a soft raise in the
   dark theme, but in the light theme it is stone-2 against stone — visibly
   darker, and fifteen of them read as grey blocks dropping in and out. The
   picture coming up to full is a clearer signal and only touches the card
   you are actually pointing at. */

.range-item img{
  width:100%; height:auto; aspect-ratio:4/3; object-fit:cover;
  border-radius:2px; background:var(--ink-2);
  /* knocked back so fifteen pictures do not shout over the words they
     illustrate; hover brings the one you are looking at back up */
  filter:saturate(.92) brightness(.92);
  transition:filter .45s var(--ease);
}
.range-item:hover img{filter:saturate(1) brightness(1)}

.range-item h3{
  font-family:var(--text); font-weight:400; font-size:var(--s1);
  line-height:1.3; letter-spacing:normal; color:var(--bone);
  margin:1rem 0 .45rem;
}
.range-item p{
  margin:0; font-size:var(--s0); line-height:1.58; color:var(--muted);
  max-width:38ch;
}
.range-more{margin-top:1.8rem; font-size:var(--s0)}

/* ---- on a phone: one column, and every word kept ------------
   The blurb used to be display:none below 520px. That was the right call at
   FIFTEEN cards — two columns leaves a ~175px text column, about twenty
   characters a line, and fifteen full-width rows is unscrollable either way.

   At six it is affordable, so nothing is hidden any more. This is the same
   shape #how and services.html use: the picture keeps the heading company
   on the first row, the sentence takes the full width underneath at a real
   measure. Three sections, one mobile pattern — which is worth more than
   the pixels it costs, because a visitor only has to learn it once. */
@media(max-width:599px){
  .range-item{
    display:grid;
    grid-template-columns:clamp(96px,28vw,132px) minmax(0,1fr);
    column-gap:1rem; row-gap:.6rem; align-items:center;
  }
  .range-item img{grid-column:1; grid-row:1; aspect-ratio:1}
  .range-item h3{grid-column:2; grid-row:1; margin:0; font-size:var(--s1)}
  .range-item p{grid-column:1 / -1; grid-row:2; max-width:none}
}

/* ---- the focus effect's safety net ---------------------------
   Module 6j scrubs the row's picture down to .25 and its words to .82 when
   the row is away from the centre of the screen. That runs entirely in JS,
   and module 6 is skipped wholesale under reduced motion or a dead GSAP CDN
   — but if a tween is ever interrupted mid-flight, these leave the content
   readable rather than stranded at a quarter opacity.

   The children are listed as well as the card. Only resetting .range-item
   would miss the img and the text, which is where 6j actually writes. */
.no-motion .range-item,
.no-motion .range-item img,
.no-motion .range-item h3,
.no-motion .range-item p{opacity:1 !important; transform:none !important}
@media(prefers-reduced-motion:reduce){
  .range-item,
  .range-item img,
  .range-item h3,
  .range-item p{opacity:1 !important; transform:none !important}
}


/* ============================================================
   HOW A PATCH IS MADE — #how

   Four photographs, four sentences, with pins marking the thing each
   sentence is about. Behind FEATURES.headDiagram: with the flag off the
   whole section is display:none and module 6k never runs.

   THE PINS REFLOW RATHER THAN SHRINK. On desktop they sit on the picture,
   in the empty third every one of these photographs was composed to leave.
   On a phone that empty third is about 120px wide — four labels in it would
   be unreadable — so the same markup becomes a wrapped caption row under
   the image instead. Same HTML, no duplication, nothing too small to read.
   ============================================================ */
.patch-diagram{display:none}
.has-head-diagram .patch-diagram{display:block}

.pd-steps{list-style:none; margin:0; padding:0; display:grid;
  gap:clamp(2rem,5vh,3.2rem)}
.pd-step{display:grid; gap:clamp(1rem,2.5vw,1.5rem)}

/* ---- the photograph, and the pins over it ---- */
.pd-shot{position:relative}
.pd-shot img{
  width:100%; height:auto; aspect-ratio:1; object-fit:cover;
  border-radius:3px; background:var(--ink-2);
}

.pd-dots{list-style:none; margin:0; padding:0}
.pd-dot{position:absolute; left:var(--x); top:var(--y);
  display:flex; align-items:center; gap:.55rem; transform:translate(-50%,-50%)}
.pd-pin{
  width:9px; height:9px; border-radius:50%; flex:none;
  background:var(--bone);
  /* a ring, so the pin holds against both the dark stone and the pale
     display head — --scrim-solid exists for exactly this, marks sitting
     on top of photography */
  box-shadow:0 0 0 2px var(--scrim-solid);
}
/* --s0, not --s-1. At 12.5px over a photograph these were reported as
   unreadable, and they were: a label on a picture has no column of text
   around it to help the eye, so it needs to be BIGGER than body copy, not
   smaller. 15px with more padding, and the same step up again on a phone,
   where most of this site is read. */
.pd-tag{
  font-size:var(--s0); line-height:1.15; white-space:nowrap;
  color:var(--on-photo); background:var(--scrim-solid);
  padding:.5rem .75rem; border-radius:2px; font-weight:400;
  -webkit-backdrop-filter:blur(5px); backdrop-filter:blur(5px);
}

/* ---- the words ---- */
.pd-n{
  font-family:var(--display); font-size:var(--s-1); letter-spacing:.06em;
  color:var(--muted); margin:0 0 .7rem;
}
.pd-say h3{
  font-family:var(--text); font-weight:400; font-size:var(--s2);
  line-height:1.25; letter-spacing:normal; margin:0;
}
.pd-body{margin:0; color:var(--muted); max-width:52ch;
  font-size:var(--s1); line-height:1.62}

/* ---- two up, picture over words -----------------------------
   It used to be four full-width rows with the words beside the picture,
   and that measured badly: the text block was 164px tall against a 560px
   picture, so 396px of every row sat empty — 1,530px across the four,
   half the section.

   Stacking the words under the picture spends that width instead of
   wasting it, and two per row halves the run. The pictures come out
   BIGGER than they were, not smaller.

   THESE RULES COME AFTER THE BASE ONES ON PURPOSE. aspect-ratio:4/3 and
   aspect-ratio:1 have the same specificity, so whichever is written last
   wins — the first attempt put this block above the base rule and the
   square silently kept applying. */
@media(min-width:600px){
  .pd-steps{
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:1px; background:var(--line); border-block:1px solid var(--line);
  }
  .pd-step{background:var(--ink); padding:clamp(1.1rem,2.2vw,1.7rem)}
  /* 4:3 rather than square. Same width, so the picture loses none of its
     presence, but a quarter less height — four times over. */
  .pd-shot img{aspect-ratio:4/3}
  /* the display head is the one tall subject in the set, so it holds its
     top rather than centring and losing the stand that makes it read as
     an object rather than a person */
  .pd-step:nth-child(3) .pd-shot img{object-position:50% 34%}
  .pd-say h3{margin-top:.9rem}
}

/* ---- on a phone: nothing hidden ------------------------------
   Two columns on a 390px screen leaves a 142px text column, about
   SEVENTEEN characters a line. That is not a reading measure, so the
   sentence would have to be cut — and cutting it loses the answer the
   step exists to give.

   Instead this is the layout services.html and the range grid already
   use: the picture keeps the heading company on the first row, then the
   sentence and the pin captions take the full width underneath at a
   proper measure. Every word survives.

   display:contents on .pd-shot lets the image and the pin captions be
   placed as separate rows of this grid while staying one element for
   the absolute positioning that desktop needs. Without it the captions
   are stuck in a 117px column under the thumbnail and wrap to four
   lines, which is what made that column 216px tall. */
@media(max-width:599px){
  .pd-step{
    /* 148px on a 390px phone. The row height is set by the picture, so
       every pixel here costs the section 4px overall — cheap enough that
       the pictures should not be smaller than they need to be. */
    grid-template-columns:clamp(120px,38vw,168px) minmax(0,1fr);
    column-gap:1rem; row-gap:.75rem; align-items:center;
  }
  .pd-shot{display:contents}
  .pd-shot img{grid-column:1; grid-row:1}
  .pd-say  {grid-column:2; grid-row:1}
  .pd-body {grid-column:1 / -1; grid-row:2}
  .pd-dots {grid-column:1 / -1; grid-row:3;
    display:flex; flex-wrap:wrap; gap:.5rem .9rem}
  .pd-dot{position:static; transform:none}
  .pd-tag{
    background:none; -webkit-backdrop-filter:none; backdrop-filter:none;
    padding:0; color:var(--muted); font-size:var(--s1);
  }
}

/* ---- motion fallbacks -------------------------------------
   Module 6k writes opacity and transform on these. Module 6 as a whole is
   skipped under reduced motion or a dead GSAP CDN, but if a tween is ever
   interrupted these leave the content readable rather than stranded. */
.no-motion .pd-shot, .no-motion .pd-say, .no-motion .pd-body,
.no-motion .pd-dot, .no-motion .pd-shot img{
  opacity:1 !important; transform:none !important;
}
@media(prefers-reduced-motion:reduce){
  .pd-shot, .pd-say, .pd-body, .pd-dot, .pd-shot img{
    opacity:1 !important; transform:none !important;
  }
}

/* ============================================================
   SCRIPT COVERAGE — Devanagari and Gujarati

   NOT A THIRD AND FOURTH TYPEFACE, and rule 6 is not being broken. Bodoni
   Moda and Archivo carry no Devanagari and no Gujarati at all, so
   "English, हिन्दी, ગુજરાતી" was already rendering in three different faces:
   Archivo for the Latin, then whatever the OS happened to offer for each of
   the other two — Nirmala UI and Shruti on Windows, something else on a Mac,
   something else again on Android. Three unplanned faces inside one h3.

   Naming the stacks makes the fallback a decision instead of an accident. It
   is the same fix already applied to the Hindi review card, generalised: this
   heading is not the only place it happens, the footer on every page carries
   the same three scripts.

   letter-spacing is reset because the display face runs -.015em tracking, and
   negative tracking closes up Devanagari and Gujarati conjuncts until they
   misread. These selectors target the span directly, so they beat the
   inherited font-family from any parent heading rule. */
/* Marathi shares the script, so it shares the stack. Grouped rather than
   duplicated: without a :lang(mr) rule at all, मराठी falls through to the
   display serif, which carries no Devanagari — so it renders in whatever the
   system substitutes and sits visibly wrong beside हिन्दी on the same line. */
:lang(hi), :lang(mr){
  font-family:"Noto Sans Devanagari","Nirmala UI","Kohinoor Devanagari",
              "Devanagari Sangam MN",sans-serif;
  letter-spacing:normal;
}
:lang(gu){
  font-family:"Noto Sans Gujarati","Shruti","Gujarati Sangam MN",
              "Nirmala UI",sans-serif;
  letter-spacing:normal;
}

/* ---- #activities, the six-frame grid ----------------------------------
   .cols.cols-3 already steps 1 -> 2 -> 3 columns, so six frames land as
   6 / 3x2 / 2x3 and never leave an orphan at any breakpoint. On a phone that
   is one 3:4 portrait at full content width, which is the only size a crown
   or a parting is actually legible at.

   The list reset is needed for the same reason #trustGrid and .steps need
   one: this is a <ul> and the browser's default padding would push the first
   column out of the grid. */
#activities .act-grid{list-style:none; margin:0; padding:0}
#activities .act-grid h3{
  margin:1rem 0 .35rem; font-family:var(--text); font-weight:500;
  font-size:var(--s1); line-height:1.35; letter-spacing:normal; color:var(--bone);
}
#activities .act-grid p{margin:0; font-size:var(--s0); color:var(--muted); max-width:34ch}

/* The sliced cells are 418x627 — a true 2:3. .r-portrait is 3:4, which would
   have cropped about 11% off the bottom of every frame and taken the
   shoulders out of the swimming and shower shots. Matching the source ratio
   means the six are shown as they were composed. */
#activities .act-grid .frame{aspect-ratio:2/3}
/* --gap is up to 48px, which is right between columns and too much between a
   caption and the picture below it. Rows are pulled in so each frame reads
   with its own label rather than floating between two. */
@media(min-width:640px){
  #activities .act-grid{row-gap:clamp(1.8rem,3.5vw,2.6rem)}
}

/* MOBILE GETS TWO COLUMNS, NOT ONE. .cols collapses to a single column below
   640px, which for six 2:3 portraits meant roughly 3,200px of stacked
   photographs — the section became a scroll rather than a glance, and the
   sixth activity was three screens from the first.

   Two-up puts them at about 165px wide on a 390px phone. That is small for a
   detailed picture and exactly right for these: every frame is one head from
   behind against a simple ground, so it survives the size, and the whole grid
   becomes three rows you can take in.

   The caption steps down with it. At 165px a 34ch measure wraps to six lines
   and the label stops belonging to the picture above it. */
@media(max-width:639px){
  #activities .act-grid{
    grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:clamp(.6rem,2.5vw,1rem);
    row-gap:1.6rem;
  }
  #activities .act-grid h3{margin-top:.7rem; font-size:var(--s0)}
  #activities .act-grid p{font-size:var(--s-1); line-height:1.5}
}

/* ---- the 1080-1199 squeeze --------------------------------------------
   The nav links switch on at 1080px, and at exactly that width the header
   has to hold: brand + six links + Services + theme + Book + burger. It did
   not. The brand broke onto three lines and "Book consultation" wrapped into
   the burger — measured in a real 1080px viewport, not guessed.

   The brand's second line goes, for the same reason it goes on a phone: at
   .63rem tracked to .16em it is the widest single element in the header and
   the least load-bearing. Below 1200px the brand name alone identifies the
   studio, and the same words sit in the footer.

   nowrap on the links and the button because a two-line nav item is worse
   than a slightly tighter row — it changes the header's height and pushes
   everything under it. */
@media(max-width:1199px){
  .brand i{display:none}
}
.nav-links a,
.site-nav a,
.nav .btn,
.head-actions .btn{white-space:nowrap}

/* ---- #work, the permissioned before/after pairs -----------------------
   3:1, because that is what the source photographs can actually give. The
   usable band is from below the crown to above the eyes, and in these shots
   that is shallow — a single frame holding both heads came out at 6.5:1,
   which is 54px tall on a phone. Split into halves, each crops cleanly to
   3:1 and the pair reads as a comparison rather than a baked-in seam.

   The two halves sit side by side from 520px and stack below it. Stacked is
   better than two 165px-wide frames: a hairline needs width to read, and on
   a phone before-above-after is the more natural reading order anyway. */
.r-work{aspect-ratio:3/1}
.work-pair{display:grid; gap:.55rem; grid-template-columns:1fr}
@media(min-width:520px){ .work-pair{grid-template-columns:1fr 1fr} }
.work-pair figure{margin:0}
.work-pair figcaption{
  margin-top:.5rem; font-size:var(--s-1); color:var(--muted-dk); line-height:1.4;
}
.work-pair figcaption b{font-weight:500; color:var(--ink)}
#work .work-grid > li + li{margin-top:0}

/* ---- #bases, the lace vs silk-top comparison -------------------------
   TWO CARDS, NOT A TABLE. A real <table> would be the semantically obvious
   choice and the wrong one here: at 390px a two-column comparison table
   either scrolls sideways or crushes both columns to nothing. Two stacked
   cards each carrying the same five <dt> labels let a phone read one build
   fully, then the other, and the shared labels do the comparing.

   <dl> because that is exactly what this is — five terms, each with a
   description, twice. Screen readers announce the pairing. */
.base-cmp{align-items:start}
.base-card{
  padding-top:1.3rem; border-top:1px solid var(--line);
}
.base-card h3{
  font-family:var(--text); font-weight:500; font-size:var(--s2);
  line-height:1.3; letter-spacing:normal; margin:0;
}
.base-sub{margin:.35rem 0 1.4rem; font-size:var(--s0); color:var(--muted)}
.base-card dl{margin:0; display:grid; grid-template-columns:1fr; gap:0}
@media(min-width:520px){
  /* term left, description right — the labels line up down the column so the
     two cards can be read across as well as down */
  .base-card dl{grid-template-columns:minmax(0,7rem) minmax(0,1fr); column-gap:1.2rem}
}
.base-card dt{
  padding-top:.85rem; font-size:var(--s0); color:var(--bone);
  border-top:1px solid var(--line);
}
.base-card dd{
  margin:0; padding-top:.85rem; padding-bottom:.85rem;
  font-size:var(--s0); color:var(--muted); line-height:1.55;
}
@media(min-width:520px){
  .base-card dd{border-top:1px solid var(--line)}
}
@media(max-width:519px){
  .base-card dd{padding-top:.2rem; padding-bottom:1rem}
}
/* The buyer's-advice line. --copper is Academy-only, so this takes the same
   left rule in --line-ui, which is a real boundary token and passes 3:1. */
.base-note{
  margin-top:clamp(2.2rem,5vw,3.2rem); padding-left:1.1rem;
  border-left:2px solid var(--line-ui);
  max-width:72ch; font-size:var(--s1); color:var(--muted);
}
.base-note b{color:var(--bone); font-weight:500}

/* The two cutaways sit on their own fixed-light plate — see --plate in :root
   for why it must not flip with the theme.

   The two source crops are different shapes (326x254 and 334x290), and the
   cards sit side by side, so left unconstrained the silk diagram would push
   its card taller than the lace one and the two headings would stop lining
   up. A fixed plate height with object-fit:contain settles that: both
   drawings letterbox inside the same box at their own aspect ratio. contain,
   not cover — these are technical drawings, and cropping one to fill the box
   would slice the layer numbers off the edge. */
.base-fig{
  background:var(--plate); border:1px solid var(--plate-line); border-radius:3px;
  padding:clamp(.7rem,2vw,1.1rem); margin-bottom:1.4rem;
}
.base-fig img{
  width:100%; height:clamp(170px,22vw,250px); object-fit:contain; display:block;
}
/* The cap is deliberate. Both sources are ~330px wide, so at 250px tall they
   are drawn at roughly native size; pushing the box any larger would only
   upscale a 330px JPEG slice and soften it. The cream either side is the
   plate doing its job, not a gap. */

/* ============================================================
   BOOKING MODAL

   z-index 90 clears everything already on the page: the menu overlay (70),
   the sticky mobile bar (75) and the assistant launcher. A modal that the
   sticky bar punched through would be worse than no modal.

   The panel is NOT [data-anim] and never starts at opacity:0 in the reveal
   sense — it lives behind [hidden], so there is no way for it to strand
   itself invisible if the animation system fails.
   ============================================================ */
.modal[hidden]{display:none}
.modal{
  position:fixed; inset:0; z-index:90;
  display:grid; place-items:center; padding:var(--pad);
}
.modal-veil{
  position:absolute; inset:0; background:var(--scrim);
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  opacity:0; transition:opacity .4s var(--ease);
}
.modal-card{
  position:relative; width:min(100%, 29rem);
  max-height:calc(100dvh - 2 * var(--pad)); overflow:auto;
  /* Stop scroll chaining. Without contain, reaching the end of this panel
     hands the gesture to the page underneath, so the visitor scrolls the
     site behind a dialog they have not dismissed. */
  overscroll-behavior:contain;
  background:var(--ink-2); border:1px solid var(--line);
  border-radius:14px; padding:clamp(1.5rem, 4.5vw, 2.3rem);
  box-shadow:0 24px 60px var(--shadow-lg);
  opacity:0; transform:translateY(14px) scale(.96);
  transition:opacity .45s var(--ease), transform .45s var(--ease);
}
.modal.is-open .modal-veil{opacity:1}
.modal.is-open .modal-card{opacity:1; transform:none}

.modal-x{
  position:absolute; top:.8rem; right:.8rem; width:2.4rem; height:2.4rem;
  display:grid; place-items:center; border-radius:50%;
  border:1px solid var(--line-ui); background:transparent; color:var(--bone);
  transition:background .35s var(--ease), border-color .35s var(--ease);
}
.modal-x:hover{background:var(--veil); border-color:var(--bone)}
.modal-x svg{width:15px; height:15px}

.modal-head{
  font-family:var(--display); font-size:var(--s2);
  line-height:1.12; letter-spacing:-.01em; margin:0 0 .7rem;
  padding-right:2.4rem;              /* clear of the close button */
}
.modal-lede{
  font-size:var(--s0); font-weight:300; color:var(--muted);
  line-height:1.6; margin:0 0 clamp(1.2rem,3.5vw,1.7rem);
}

/* The scan target. --plate is the fixed-light surface (see :root): a QR code
   is read by a camera looking for dark-on-light, so this one square cannot be
   allowed to flip to a dark ground with the theme — it would stop scanning.
   :has(img) holds the plate back until there is actually an image, because
   the empty-state caption is light type and would sit on cream at roughly
   1.2:1. Engines without :has() simply leave the code on --ink-2, where a QR
   with its own white quiet zone still scans. */
.modal-qr{
  width:min(15rem, 70%); aspect-ratio:1; margin:0 auto clamp(1.1rem,3vw,1.5rem);
  border:1px solid var(--line); border-radius:8px;
}
.modal-qr:has(img){background:var(--plate); border-color:var(--plate-line); padding:.8rem}
/* contain, never cover: .frame img crops to fill, and a cropped QR is an
   unreadable QR — the finder squares in the corners are the first thing lost. */
.modal-qr img{object-fit:contain}

.modal-note{
  font-size:var(--s-1); font-weight:300; color:var(--muted);
  line-height:1.55; text-align:center; margin:0 0 clamp(1.2rem,3.5vw,1.6rem);
}
.modal-actions{display:flex; flex-wrap:wrap; gap:.75rem; justify-content:center}
.modal-actions .btn{padding:.85rem 1.5rem}

/* Under 26rem the two actions stop fitting side by side and wrap to a ragged
   second row. Full width each is tidier than that. */
@media(max-width:26rem){
  .modal-actions{flex-direction:column}
  .modal-actions .btn{width:100%; justify-content:center}
}
.modal-open{overflow:hidden}

/* ---- hero rating + offer -----------------------------------
   Two conversion elements, deliberately placed at opposite ends of the hero:
   proof before the headline, risk-reversal under the buttons. */

.hero-rating{
  display:inline-flex; align-items:center; gap:.55rem;
  /* The bottom margin is the load-bearing one. Without it the rating sits on
     the headline's cap height and reads as a kicker attached to the <h1>
     rather than as its own piece of evidence. */
  margin:.9rem 0 1.1rem; color:var(--muted); text-decoration:none;
}
/* Sized to be read across a room, not squinted at. --s3 stops well short of
   the headline's --s5, so the rating is unmissable without competing with the
   thing it sits above. The number takes the display face because it is set
   like a figure rather than like copy — and Bodoni's numerals are the reason
   this reads as a score instead of as a word. */
.hero-rating-n{
  font-family:var(--display); font-size:var(--s3); font-weight:400;
  line-height:1; letter-spacing:-.01em; color:var(--bone);
}
/* --star is the review-rail token, reused rather than duplicated — this is
   the same signal doing the same job, so a second gold would be a second
   accent for no reason. Measured: 4.70:1 on --ink (dark) and 3.07:1 on the
   light theme's page colour. The light figure clears the 3:1 that WCAG
   1.4.11 asks of a meaningful graphic, and it only has to be a graphic at
   all — the sentence beside it carries "4.9" in text, so nothing is lost if
   the stars are missed entirely. */
.hero-rating-stars{
  display:inline-flex; gap:.1em; color:var(--star);
  font-size:var(--s2); line-height:1;
}
.hero-rating:hover .hero-rating-n{color:var(--bone-hi)}

.hero-offer{
  list-style:none; margin:1.7rem 0 0; padding:0; max-width:min(100%, 42rem);
  display:grid; gap:1.3rem 2.6rem;
  /* auto-fit rather than a hard two-up: at the width where the second column
     would squeeze "No charge to come in" onto three ragged lines, the pair
     stacks instead. Nothing to maintain at a breakpoint.
     The measure has to be wide enough to HOLD two tracks or auto-fit can
     never place them — 46ch could not fit 2 x 15rem plus the gap, so the
     pair silently stacked at every width and the rule looked broken. */
  grid-template-columns:repeat(auto-fit, minmax(18rem, 1fr));
}
.hero-offer li{display:flex; flex-direction:column; gap:.2rem}
.hero-offer-k{font-size:var(--s2); color:var(--bone); line-height:1.2}
.hero-offer-v{font-size:var(--s1); font-weight:300; color:var(--muted); line-height:1.45}

/* The hero's vertical rhythm was tuned on a laptop, where there is height to
   spare. On a 390x844 phone the same gaps push the free-consultation line —
   the one piece of copy most likely to turn a reader into a booking — just
   past the fold. A media query rather than more vh terms in the clamps:
   making a clamp smaller at 844px while holding it at 1000px needs a negative
   intercept (calc(11.5vh - 4.7rem)), which is unreadable and would have to be
   re-solved by hand every time a gap changes. Desktop is untouched. */
@media(max-width:700px){
  .hero .eyebrow{margin-bottom:1.2rem}
  .hero-rating{margin:.7rem 0 .9rem}
  .hero-sub{margin-top:1.3rem}
  .hero-cta{margin-top:1.7rem}
  .hero-offer{margin-top:1.3rem}
}

/* ---- guideline sweep: touch + heading rag --------------------
   From a Web Interface Guidelines audit.

   touch-action:manipulation drops the ~300ms delay a mobile browser waits to
   see whether a tap is the first half of a double-tap-to-zoom. Scoped to tap
   targets only: the before/after slider sets its own touch-action (pan-y, so
   a vertical swipe still scrolls the page while a horizontal drag works the
   handle) and must not be overridden.

   text-wrap:balance evens the rag on multi-line headings. NOT on h1 — the
   hero headline is hand-broken into .reveal-line spans, and balancing each
   span separately would fight a break chosen on purpose.

   No exception for headings carrying an explicit <br>. A first pass added one,
   blaming balance for re-ragging #basesHead; toggling the property live and
   comparing line counts showed the break was the natural wrap either way.
   Only #howHead and #basesHead contain a <br> and balance moves neither. */
a, button, summary, [role="button"]{touch-action:manipulation}
h2, h3{text-wrap:balance}

/* ---- #book scan panel ---------------------------------------
   Replaced the six-field form. The code is the big element on a laptop and
   the button is the one that matters on a phone, so both are given room
   rather than the code being decoration beside small print.

   Centred, unlike the form it replaced: a single column of code, heading,
   sentence, buttons reads as one instruction. The form needed left
   alignment because a label must sit above its field; nothing here is a
   field. */
.book-panel{
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:0;
}
/* --plate again, for the reason given at :root — a camera reads dark on
   light, so this square must not follow the theme to a dark ground. */
.book-panel-qr{
  width:min(100%, 20rem); aspect-ratio:1;
  background:var(--plate); border:1px solid var(--plate-line); border-radius:6px;
  padding:clamp(.9rem,2.5vw,1.4rem);
}
.book-panel-qr img{object-fit:contain}   /* never cover: a cropped QR loses its finder squares */

.book-panel-head{
  font-family:var(--text); font-weight:400; font-size:var(--s2);
  line-height:1.25; letter-spacing:normal; margin:clamp(1.4rem,3.5vw,2rem) 0 .5rem;
}
.book-panel-sub{
  font-size:var(--s0); font-weight:300; color:var(--muted);
  line-height:1.6; max-width:36ch; margin:0;
}
.book-panel-cta{
  display:flex; flex-wrap:wrap; gap:.8rem; justify-content:center;
  margin-top:clamp(1.4rem,3.5vw,1.9rem);
}
.book-panel-note{
  font-size:var(--s-1); font-weight:300; color:var(--muted);
  line-height:1.55; max-width:34ch; margin:1.1rem 0 0;
}

/* On a phone the code is the least useful thing on the panel — you cannot
   scan the screen you are holding — so it shrinks and the buttons, which do
   work, come first in the eye. It is not hidden: people photograph a code to
   send to someone else, and someone on a tablet beside a laptop still scans. */
@media(max-width:700px){
  .book-panel-qr{width:min(58%, 11rem)}
  .book-panel-cta .btn{width:100%; justify-content:center}
}

/* ---- footer social row --------------------------------------
   Brand colours live ONLY here, and only on hover. The palette rule says one
   accent (--wa) — this is the documented exception, and it is narrow on
   purpose: at rest every mark is --muted like the text around it, so the
   footer stays calm. The colour is feedback, not decoration.
   Hover and focus-visible both trigger it; a keyboard user gets the same
   answer from the page as somebody with a mouse. */
.social{list-style:none; margin:0; padding:0; display:grid; gap:.15rem}
.social-link{
  display:inline-flex; align-items:center; gap:.6rem;
  padding:.4rem .55rem; margin-left:-.55rem;   /* optical: text lines up with the column above */
  border-radius:6px; color:var(--muted);
  transition:color .3s var(--ease), background .3s var(--ease), transform .3s var(--ease);
}
.social-mark{
  flex:none; display:grid; place-items:center; width:22px; height:22px;
  transition:transform .35s var(--ease);
}
.social-mark svg{width:100%; height:100%; display:block}

.social-link:hover{color:var(--bone); background:var(--veil); transform:translateX(3px)}
.social-link:hover .social-mark{transform:scale(1.12)}
.social-link:focus-visible{color:var(--bone); background:var(--veil)}
.on-light .social-link:hover,
.on-light .social-link:focus-visible{color:var(--ink); background:var(--veil-alt)}

/* The one place brand hues are allowed. Applied to the mark only — never to
   the label, which stays readable type rather than becoming a logo. */
.social-link:hover [data-brand], .social-link:hover .social-mark{color:inherit}
.social-link[data-brand="wa"]:hover  .social-mark{color:#25D366}
.social-link[data-brand="ig"]:hover  .social-mark{color:#E1306C}
.social-link[data-brand="fb"]:hover  .social-mark{color:#1877F2}
.social-link[data-brand="yt"]:hover  .social-mark{color:#FF0033}
.social-link[data-brand="th"]:hover  .social-mark{color:var(--bone)}
.social-link[data-brand="x"]:hover   .social-mark{color:var(--bone)}
.social-link[data-brand="pin"]:hover .social-mark{color:#E60023}
.on-light .social-link[data-brand="th"]:hover .social-mark,
.on-light .social-link[data-brand="x"]:hover  .social-mark{color:var(--ink)}

@media(prefers-reduced-motion:reduce){
  /* The colour change stays — it is information, not motion. The travel goes. */
  .social-link:hover{transform:none}
  .social-link:hover .social-mark{transform:none}
}
