/* Laney PMU Studio - assets/site.css
   The whole design system for laneypmu.com. Brand book 2.1, August 2026.
   Quality pass, August 2026, against _build/DESIGN-SPEC.md.

   This file is the site's only stylesheet. It implements the shared class vocabulary and
   nothing else, so a page fragment that reaches for a name outside the list gets no styles
   and the build says so. The vocabulary, in full:

     page, band, kicker, kicker--quiet, display, headline, title, lede, note, small
     cards, card, card__label, card__text
     svc, svc__name, svc__desc, svc__price, svc__from, svc__dep
     sheet, sheet__row, sheet__name, sheet__desc, sheet__price, sheet__cur, sheet__dep,
     sheet__close, sheet__photos, sheet__frames
     guarantee, policies, policy, policy__n, policy__text
     timeline, step, step__day, step__note
     contra, contra__group, contra__list
     btn, btn--ghost, btn--quiet, actions, notify, notify__field, notify__msg
     spec, spec__item, pending, flag, mapping
     hero, hero__type, hero__device, hero__rule
     pagehead, pagehead__type, pagehead__mark
     shot, shot--brow-before, shot--brow-after, shot--lip-macro, shot--portrait,
     shot--portrait-dark, shot--interim
     stage, stage--healed, stage__line, pair, pair__rule

   And the drawing's own hooks, which the shell writes inside the SVG and which the build
   exempts from the class check because they label a drawing rather than a component:
     m-device, m-guide, m-guide--start|arch|tail, m-fill, m-outline, m-stroke, m-strokes,
     m-tick, m-tick--start|arch|tail, m-cap, m-cap--start|arch|tail, m-ticks

   Plus the shell's own names, which the build writes and fragments do not touch:
     skip, masthead, wordmark, wordmark__mark, wordmark--foot, lockup, lockup--mark,
     lockup--foot, topnav, menu, standing, sitefoot.

   Two behaviours are carried on attributes rather than class names, so the build's class
   allowlist does not have to grow for a layout switch:

     <section class="band">                        Ivory, the default. Every surface starts here.
     <section class="band" data-tone="wash">       a quiet band. Wash means "this part protects you"
     <section class="band" data-tone="paper">      Paper
     <section class="band" data-tone="charcoal">   dark band, Ivory text
     <section class="band" data-grid="12">         the band's children lay out on the 12 columns
     <div data-span="8">                           that child takes 8 of the 12
     <div data-span="full">                        that child bleeds edge to edge

   Three rules run through everything below and none of them are negotiable:
   Charcoal delivers the words and Champagne only decorates, buttons are squared with no
   shadow and no rounding, and body copy never drops below 16px.

   The three faces are loaded by the shell from Google Fonts, in one request:
   fonts.googleapis.com/css2?family=Montserrat...&family=Newsreader...&family=Playfair+Display...
   The stacks below name real fallbacks, so the page sets correctly before they arrive and
   still reads correctly if they never do.

   CONTENTS
     01  Tokens
     02  Reset and base
     03  Shell: masthead
     04  Shell: footer
     05  Layout: page, band, grid mode
     05b The page ground ornament
     05c Centring
     06  Type vocabulary
     07  Vertical rhythm
     08  Hero, desktop
     09  Hero, mobile
     10  Page header, the other eight pages
     11  The mapping device
     12  Motion
     13  Photography
     14  Cards
     15  Service block
     16  Price sheet
     17  The guarantee
     18  Policies
     19  The healing timeline
     20  Contraindications
     21  Spec list
     22  Buttons and the CTA system
     23  Notify capture
     24  Reduced motion and print
*/

/* ==========================================================================
   01  TOKENS
   ========================================================================== */

:root {
  color-scheme: light;

  /* Palette, book section 03. The roles are not interchangeable.
     Ivory 56, Paper 24, Wash 10, Gold line 5, Champagne 3, with Charcoal on top as text. */
  --ivory:         #F8F4EC;  /* page ground, every surface starts here */
  --paper:         #FFFFFF;  /* cards and anything handed over, nothing else */
  --charcoal:      #26221C;  /* all reading text, dark panels. 14.4 on Ivory */
  --charcoal-soft: #5C544A;  /* secondary text, captions, labels. 6.8 on Ivory */
  --champagne:     #C29A63;  /* DECORATIVE ONLY. 2.4 on Ivory, so never a sentence */
  --deep-gold:     #85622F;  /* the working accent: kickers, links, buttons. 5.1 on Ivory */
  --gold-line:     #D9C7A8;  /* construction guides and hairlines. Never text */
  --wash:          #F3EADA;  /* quiet bands and the guarantee panel */
  --rule:          #E6DCCB;  /* hairlines, table rules, borders. Never text */

  /* Three faces, one job each. Book section 04. */
  --display-face: 'Playfair Display', 'Iowan Old Style', Georgia, 'Times New Roman', Times, serif;
  --body-face:    'Newsreader', 'Iowan Old Style', Georgia, 'Times New Roman', Times, serif;
  --label-face:   'Montserrat', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI', sans-serif;

  /* THE SCALE, RAISED ONE STEP, AUGUST 2026.

     The whole ladder moved by a single factor of 1.09375, which is 16 to 17.5 on the body
     step. Every other step is that same multiple of what it was, so the intervals between
     the steps are unchanged and the pages move together rather than one rule at a time.
     Nothing here is a per page patch: raising a page's type means raising these numbers.

     Each clamp keeps its original ramp, that is the viewport range over which it grows,
     and only its two ends and its slope are rescaled. The comment on each line carries the
     old pair and the new one in pixels.

     Line heights below are ratios and are deliberately untouched, so the leading grows
     with the type and the copy stays as open as it was. */
  --fs-display: clamp(2.25rem, 1.506rem + 3.28vw, 3.375rem);      /* was 33 -> 50, now 36 -> 54 */
  --fs-hero:    clamp(2.25rem, 1.644rem + 2.37vw, 3.125rem);      /* was 33 -> 46, now 36 -> 50, see section 08 */
  --fs-title:   clamp(1.4375rem, 1.358rem + 0.375vw, 1.575rem);   /* was 21 -> 23, now 23 -> 25.2 */
  --fs-lede:    clamp(1.1625rem, 1.106rem + 0.28vw, 1.2969rem);   /* was 17 -> 19, now 18.6 -> 20.75 */
  --fs-body:    1.09375rem;                                       /* was 16, now 17.5 */
  --fs-small:   1rem;                                             /* was 13.5, then 14.75, now 16 */
  --fs-kicker:  0.75rem;                                          /* was 11, now 12 */

  /* THE SMALL STEP IS NOT ON THE 1.09375 RAMP, AND THAT IS DELIBERATE.
     Every other step above moved by the one factor. This one moved further, 13.5 to 16,
     because of what it actually carries. The small register is not captions: it is the
     sheet descriptions that decide which tier a client asks for, the sheet closing lines,
     every .note on every page, the field hints on the request form, and eleven of the
     paragraphs on the fit page. Those are the explanations, and the explanations were the
     thing named as hard to read. At 14.75 they would still have sat below the 16 that was
     called too small in the first place.

     16 against a 17.5 body is a narrow step, and the step is not what makes this register
     secondary. Position does: a note sits under the block it qualifies, a sheet
     description sits under the row it belongs to, a hint sits under its field. The type
     is a half step down to confirm that, not to carry it alone. Genuine labels are still
     a real step away at --fs-kicker, 12, tracked and uppercase, which is where the
     shorthand that is not read as a sentence lives. */

  /* The missing display step. The book's scale is Display 50/56, Headline 33/38,
     Title 23/30. This file used to implement 50 and 23 and skip 33, which forced every
     page after home to choose between a headline that shouts and a title that whispers. */
  --fs-headline: clamp(1.84375rem, 1.554rem + 0.92vw, 2.25rem);  /* was 27 -> 33, now 29.5 -> 36 */
  --lh-headline: 1.152;                                          /* 38 / 33   */

  /* THE SIX STEPS THAT USED TO BE TYPED INTO COMPONENTS.

     The scale above governed the prose and nothing else. The price sheet's name and its
     figure, the navigation, the two button labels and the mobile nav row were each written
     as a literal rem value inside the rule that used them, which is why a request to make
     the site bigger would have moved the paragraphs and left the prices, the nav and the
     controls exactly where they were. They are tokens now, raised by the same 1.09375, so
     the next time this is asked the whole page moves from one place.

     The two sheet steps are redeclared on .sheet itself at the narrow breakpoint in
     section 16, which is what a token is for: the component asks for the same name and
     gets the phone sized value. */
  --fs-name:   1.375rem;   /* the sheet row name.   was 20, now 22 */
  --fs-price:  1.5rem;     /* the sheet row figure. was 22, now 24 */
  --fs-nav:    0.875rem;   /* top nav and the numbered policy label. was 13, now 14 */
  --fs-nav-lg: 1.0625rem;  /* the nav panel row below 62em.         was 15.2, now 17 */
  --fs-btn:    0.8125rem;  /* every button label.                   was 12, now 13 */
  --fs-btn-sm: 0.75rem;    /* the masthead's compact call to action. was 11.2, now 12 */

  --lh-display: 1.06;
  --lh-title:   1.3;
  --lh-lede:    1.55;
  --lh-body:    1.625;
  --lh-small:   1.481;
  --lh-kicker:  1.4545;

  /* +2.5px, held in absolute terms across the raise. The kicker step went 11 to 12, and
     0.227em at 12px would have been 2.72px, so the longest kicker on the site would have
     grown by tracking as well as by size and gone looking for a second line at 320px.
     0.208em is the same +2.5px the book specifies, now expressed against 12. */
  --track-kicker: 0.208em;  /* +2.5 at 12px */
  --track-label:  0.14em;
  --track-btn:    0.18em;   /* wider than the kicker: a button label has no neighbours */

  /* Spacing scale. Base 8. Tight at the bottom where type sits, open at the top where
     bands sit, so type intervals are controllable and band intervals are decisive. */
  --s-1:  0.25rem;  /*   4 */
  --s-2:  0.5rem;   /*   8 */
  --s-3:  0.75rem;  /*  12 */
  --s-4:  1rem;     /*  16 */
  --s-5:  1.5rem;   /*  24 */
  --s-6:  2rem;     /*  32 */
  --s-7:  2.5rem;   /*  40 */
  --s-8:  3.5rem;   /*  56 */
  --s-9:  4.5rem;   /*  72 */
  --s-10: 6rem;     /*  96 */
  --s-11: 8rem;     /* 128 */

  /* Layout. 12 columns, 1200px max, 96px gutters collapsing to 24px on mobile.
     The clamp is that collapse: 24px at phone widths, 96px from roughly 1600px up. */
  --measure:  1200px;
  --gutter:   clamp(1.5rem, 6vw, 6rem);
  --gap:      1.5rem;
  --band-pad: clamp(2.5rem, 6.5vw, 6rem);   /* 40 -> 96 */

  /* One of the twelve at full measure: (1200 - 11 * 24) / 12 = 78px. This is the growth
     limit of a content column in grid mode, which is what caps the content region at
     1200px without a second wrapper element. It is not a layout value below 1200px:
     the columns shrink below it, which is the whole point of a fluid grid. */
  --col: calc((var(--measure) - 11 * var(--gap)) / 12);

  /* N of the twelve, expressed as a max-width against a full width content track.
     N columns measure 102N - 24 at full measure: 4 cols 384, 5 486, 6 588, 7 690, 8 792.
     Used where a component sits in a plain band rather than in grid mode. */
  --w-4: calc((100% + var(--gap)) / 3 - var(--gap));
  --w-6: calc((100% + var(--gap)) / 2 - var(--gap));
  --w-8: calc((100% + var(--gap)) * 2 / 3 - var(--gap));
  /* 10 of 12. The price sheet is a table, not running prose, so it is not bound by the
     reading measure that caps body copy. At 8 columns the interior pages stopped around
     790px and read as half loaded beside home, which fills its grid. */
  --w-10: calc((100% + var(--gap)) * 5 / 6 - var(--gap));

  /* THE READING MEASURE, NARROWED FOR CENTRED COPY.

     36em was a ranged left measure of roughly 72 characters. Centred copy cannot carry
     that: with both edges ragged the eye has to find the start of every line, and the
     longer the line the further it has to hunt. So the cap is now stated in the unit it
     was always describing, and it is the number a centred column can hold.

     ch tracks the element's own font size, so a lede at 20.75px and a note at 16px each
     get their own measure rather than the body's.

     50 AND NOT 60, AND THE NUMBER WAS MEASURED RATHER THAN GUESSED. One ch is the width of
     a zero, and a zero in Newsreader is a good deal wider than the average letter in a
     sentence. Measured against a lowercase sample in the built pages, 60ch set 72 to 82
     real characters per line, which is wider than the ranged left measure it was supposed
     to replace. 50ch sets 60 on a note, 62 on a body paragraph and 62 on a roman lede,
     which is the number the round 3 note asks for. */
  --text-measure: 50ch;

  --tap: 2.75rem;   /* 44px, the minimum tap target */

  /* Easing, named, so nobody types the bare keyword again. */
  --ease-out:  cubic-bezier(0.22, 0.61, 0.36, 1);   /* arrives and stops. Default. */
  --ease-move: cubic-bezier(0.40, 0.00, 0.20, 1);   /* colour and short travel     */
  --ease-draw: cubic-bezier(0.33, 0.00, 0.15, 1);   /* a hand pulling a line       */
  --ease-loop: cubic-bezier(0.65, 0.00, 0.35, 1);   /* a closed contour            */
  --ease-mark: cubic-bezier(0.34, 1.40, 0.64, 1);   /* the tick stamp. The only place
                                                       any overshoot is permitted. */
  --t-fast: 160ms;
  --t-mid:  200ms;

  /* Photography frame ratios. 4:5, 16:9 and 1:1 are the book, page 14.
     8:3 is the pair band, derived in section 13. */
  --ar-portrait: 4 / 5;
  --ar-macro:    16 / 9;
  --ar-square:   1 / 1;
  --ar-band:     8 / 3;
}

/* ==========================================================================
   02  RESET AND BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The masthead does not overlay the page any more, so this is air above a landing
     anchor rather than clearance under a sticky bar. #healed and friends. */
  scroll-padding-top: 2rem;
}

body {
  margin: 0;
  background: var(--ivory);
  color: var(--charcoal);
  font-family: var(--body-face);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: 400;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-wrap: break-word;
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { max-width: 100%; }
img { height: auto; display: block; }

/* Links in running copy. Deep gold is the only accent allowed to carry small text. */
a {
  color: var(--deep-gold);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { color: var(--charcoal); }

/* One focus ring for the whole site, and it is Charcoal, not Deep gold. The old ring drew
   Deep gold around a button whose ground turns Charcoal on focus, which is 2.9 to 1 and a
   pairing the book rejects outright. Charcoal holds 14.4 on Ivory, 15.8 on Paper and 13.3
   on Wash, so one ink clears AAA on every ground the site uses. */
:focus-visible {
  outline: 2px solid var(--charcoal);
  outline-offset: 3px;
}
.band[data-tone="charcoal"] :focus-visible,
.sitefoot :focus-visible,
.skip:focus-visible { outline-color: var(--ivory); }

::selection { background: var(--wash); color: var(--charcoal); }

strong, b { font-weight: 600; }  /* emphasis is 600, at most once per paragraph */
em, i { font-style: italic; }
hr { border: 0; border-top: 1px solid var(--rule); margin: var(--s-6) 0; }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--charcoal);
  color: var(--ivory);
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.9rem 1.25rem;
}
.skip:focus {
  left: 0;
  color: var(--ivory);
}

/* ==========================================================================
   03  SHELL: masthead, wordmark, top nav, standing tag
   ========================================================================== */

/* THE MASTHEAD IS NO LONGER STICKY, AND THE MARK IS WHY.
   The bar used to be about 73px tall and pinned to the top of every scroll. The approved
   artwork lockup cannot live in 73px: the book's own floor for the full lockup is 140px
   wide, page 05, which is 113px tall at the artwork's 1.238 ratio. Carrying the real mark
   means a bar of 158px, and 172px from 1200px up, and a strip that deep pinned to the top
   would take a fifth of a laptop viewport for the whole session, on a nine page site whose
   footer already carries every page. So the mark wins and the pin goes. */
.masthead {
  /* Positioned, and only for the z-index: the Services disclosure opens over the page
     below it, and z-index is ignored on a static element. This is what is left of the
     sticky rule and it is the half that was doing real work. */
  position: relative;
  z-index: 40;
  background: var(--ivory);
  border-bottom: 1px solid var(--rule);
}

/* The nav sits on the foot of the mark, not in the middle of the bar beside it. An image
   baseline is its bottom edge, so baseline alignment lands the nav labels within a few
   pixels of the PMU STUDIO line and the two halves of the masthead stand on one line.
   That is the same idea the hero is built on, where the type column and the drawing both
   stand on the dashed rule. Centring a 40px nav against a 142px mark aligns it to nothing
   and leaves the air in the wrong place, under the words rather than above them. */
.masthead__row {
  max-width: calc(var(--measure) + var(--gutter) * 2);
  margin-inline: auto;
  padding: 0.9rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 1.75rem;
  flex-wrap: wrap;
}
/* The one call to action on every page. Compact so the row holds one line at desktop. */
.masthead__cta {
  padding: 0.6rem 1rem;
  font-size: var(--fs-btn-sm);
  white-space: nowrap;
}

.masthead__nav {
  display: flex;
  align-items: center;
  gap: 1.25rem 2rem;
  flex-wrap: wrap;
}

/* The mark's own link. It holds the artwork, and it stays set in Playfair because the alt
   text is what a reader gets if the image cannot render: that fallback is the book's text
   lockup, page 04, which the book approves for a running header. Never tracked, never all
   caps. */
.wordmark {
  font-family: var(--display-face);
  font-weight: 400;
  font-size: clamp(1.35rem, 1.1rem + 1.1vw, 1.9rem);
  line-height: 1;
  color: var(--charcoal);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  /* 44px, so the link is a legal tap target even in the fallback case where it carries
     alt text rather than the mark. */
  min-height: var(--tap);
  /* Deliberately NOT nowrap. If the artwork cannot render, the alt text is laid out
     inside the box the image would have taken, and at that width the three words wrap
     onto three lines, which is how the book draws the text lockup on page 04. Held on
     one line it would simply be cut off at "Laney PMU". */
  /* inline-flex drops the whitespace text nodes between the three words, so the word
     spaces come back as a gap. One line, no tracking, no caps, book page 04. */
  gap: .26em;
  white-space: nowrap;
}
.wordmark em { font-style: italic; }
.wordmark:hover { color: var(--charcoal); }


/* The footer mark sits on Charcoal, so it takes the reversed master. */
.wordmark--foot {
  display: block;
  font-size: 1.35rem;
  min-height: 0;
  margin: 0 0 var(--s-4);
}

/* The approved artwork lockup at page scale. It appears once inside the page on the whole
   site, on the home page's Charcoal band, where it signs the page off rather than
   announcing it. 260px is the floor because below roughly that width the script L and the
   tracked caps stop resolving. */
.lockup {
  display: block;
  width: clamp(260px, 30vw, 400px);
  height: auto;
  margin: 0 0 var(--s-6);
}

/* THE SIGNATURE PANEL. The one place on this site where a line of type is centred.

   What was wrong: .lockup--mark centred the mark and nothing centred the sentence under
   it, so a centred mark stood over a ranged left line with the entire right half of a
   Charcoal panel empty. The mark was not the fault. The band was.

   Centring belongs to the band, so every child after the mark takes it, and the sentence
   takes a measure of its own as well or it runs the full 1200px and the centring is
   invisible. 34ch at the headline size holds this sentence to two lines that break on its
   own full stop, which is the composition: mark, then two centred lines under it.

   This is deliberately scoped to a Charcoal band carrying the mark. Body copy everywhere
   else on the site stays ranged left, which is what makes this panel read as a signature
   rather than as a page that cannot decide. */
.lockup--mark {
  margin-inline: auto;
  margin-bottom: 0;
}
.band[data-tone="charcoal"] > .lockup--mark ~ * {
  text-align: center;
  margin-inline: auto;
}
/* text-wrap: balance evens the lines out inside that measure, so the sentence breaks on
   its own full stop rather than leaving "The" hanging off the end of line one. Where it is
   not supported the max-width still holds the shape and the break is merely ordinary. */
.band[data-tone="charcoal"] > .lockup--mark ~ .headline,
.band[data-tone="charcoal"] > .lockup--mark ~ .display {
  max-width: 34ch;
  text-wrap: balance;
}
.band[data-tone="charcoal"] > .lockup--mark ~ .lede,
.band[data-tone="charcoal"] > .lockup--mark ~ p { max-width: 48ch; }
.band[data-tone="charcoal"] > .lockup--mark ~ .actions { justify-content: center; }
/* The mark carries no bottom margin of its own now, so the interval under it is set once,
   here, at the 40px step the rhythm uses before anything the reader has to weigh. */
.band[data-tone="charcoal"] > .lockup--mark + * { margin-top: var(--s-7); }

.topnav__list {
  display: flex;
  align-items: center;
  gap: 0.25rem 1.75rem;
  flex-wrap: wrap;
}

.topnav__item { position: relative; }

.topnav__link,
.menu__summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-family: var(--label-face);
  font-size: var(--fs-nav);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--charcoal);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.topnav__link:hover,
.menu__summary:hover { color: var(--deep-gold); }

.topnav__link[aria-current="page"] {
  color: var(--deep-gold);
  border-bottom-color: var(--deep-gold);
}

/* The Services grouping. A native disclosure: keyboard operable with Enter and Space,
   state announced natively rather than through an added aria attribute, and usable with
   scripting off.
   It is not a hover menu; a hover menu cannot be reached from a keyboard or a phone,
   and all three service pages hang off this control. */
.menu { position: relative; }

.menu__summary {
  cursor: pointer;
  gap: 0.5rem;
  list-style: none;
}
.menu__summary::-webkit-details-marker { display: none; }

.menu__summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform var(--t-fast) var(--ease-move);
}
.menu[open] .menu__summary::after { transform: translateY(0.05em) rotate(-135deg); }

.menu[open] .menu__summary { color: var(--deep-gold); }

.menu__list {
  position: absolute;
  top: calc(100% + 0.4rem);
  left: -0.9rem;
  min-width: 12.5rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: 0.35rem 0;
  z-index: 50;
}
.menu__list a {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 1.25rem;
  font-family: var(--label-face);
  font-size: var(--fs-nav);
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--charcoal);
  text-decoration: none;
}
.menu__list a:hover { background: var(--wash); color: var(--deep-gold); }
.menu__list a[aria-current="page"] { color: var(--deep-gold); }

/* The standing tag, book page 18. It sits on every page until booking opens, and it is
   the only place the hero's kicker must not repeat: see section 08. */
.standing {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--deep-gold);
  border: 1px solid var(--gold-line);
  padding: 0.5rem 0.75rem;
  white-space: nowrap;
}

/* --- the nav toggle -----------------------------------------------------
   The menu opens with no JavaScript at all and still works with scripting disabled.
   At and above 62em the button is hidden and the panel is laid out as an ordinary row,
   so the open state is irrelevant on a desktop. Below 62em the button becomes the Menu
   control and the panel opens under the masthead. The 960 is measured, not chosen by
   feel; see THE ROW FORM STARTS AT 960 below. */
/* The checkbox is the disclosure state. It is never shown; the label is the control.
   A details element cannot serve both breakpoints from one markup, because a closed
   details hides its own contents and no CSS can bring them back at desktop. That is not
   a theory: this nav was once built on details and the whole nav went invisible at
   desktop widths. Do not reintroduce it. */
.navtoggle__input { position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0; }
.navtoggle__button { display: none; }
/* Keyboard: the label has no focus of its own, so the ring is driven off the input. */
.navtoggle__input:focus-visible + .navtoggle__button {
  outline: 2px solid var(--deep-gold);
  outline-offset: 3px;
}

/* THE ROW FORM STARTS AT 62em, AND THE UNIT IS THE POINT.

   What was wrong: the row form used to take over at 861px, and for the next seventy odd
   pixels it did not fit. The five links took one line and the call to action dropped onto
   a second one underneath them, so the masthead stood three lines deep: mark, links,
   button. Nothing was broken and nothing overflowed. It simply looked unconsidered on a
   small laptop and on a landscape tablet, which is where a client resizing a window
   spends most of its time.

   THE MEASUREMENT, AND THE MISTAKE IN THE FIRST VERSION OF IT. The masthead row's content
   box is the viewport less two gutters, and --gutter is 6vw everywhere in this range, so
   the row has 0.88 of the viewport MINUS the classic scrollbar. That last term was missed
   the first time, because the measuring run hid scrollbars, and every figure came out
   15px optimistic. On Windows and Linux desktop Chrome the row actually has
   0.88 x vw - 15. The nav asks for 805.6px: a 453px link list, a 32px gap, and a 321px
   call to action. That puts the first fitting width at 933px, not the 916px first
   calculated, and the wrapping band at 861 to 932 rather than 861 to 915. Phones and
   macOS overlay their scrollbars and do lose the 15px, which is why the panel figures
   further down this file are right and only the row figures were wrong.

   WHY em AND NOT px, WHICH IS THE MORE IMPORTANT HALF. This site sets no root font size,
   so every token scales with the reader's browser default. Chrome's font size control
   offers Large at 20px, one click from the menu, and a reader who has asked for bigger
   type is exactly the reader this whole round of work is for. At a 20px root the nav needs
   about 1007px of content rather than 806, and a breakpoint written as 960px cannot know
   that: it hands such a reader the row form at 960px and puts the three line masthead
   straight back, from 960 all the way to 1160. A media query em is always the browser's
   default font size, not this document's, so 62em tracks the reader instead of guessing
   at them. It resolves to 992px at the standard 16px default, 1116px at 18, 1240px at 20
   and 1488px at 24, and the row fits with room to spare at every one of those.

   WHY 62 AND NOT 59. 933px is the zero slack width at a 16px root, and a nav balanced on
   nothing is the same fragility in a new place. 62em is 992px there, which leaves 52px,
   about 6 percent: enough for the metric differences between one platform's Montserrat
   and another's, and enough that a slightly longer button label does not reopen this.
   992px is also below every iPad in landscape, the narrowest of which is 1024px, so no
   tablet held sideways loses the row.

   WHY THE BREAKPOINT MOVED RATHER THAN THE TYPE OR THE GAPS. Closing the shortfall at
   861px by tightening would have meant cutting the link gap from 28px to about 18px and
   the nav gap from 32px to 20px, which squeezes the one row of type the client asked to
   make bigger, and it would have left the row balanced on nothing anyway: a sixth link or
   a longer button label puts it straight back onto two lines. Shrinking the call to action
   is worse still, since its label is already the smallest type on the page at 12px.

   WHAT DID NOT CHANGE. At 1200px and above the masthead is untouched, mark on one line
   and the whole nav row under it, exactly as before. At 390px the disclosure is untouched
   too. The only widths that behave differently are 861 up to 62em, which now get the Menu
   button instead of a three line masthead. */
@media (min-width: 62em) {
  /* Take the nav and its panel out of the layout entirely so the list is the flex
     item, exactly as it was before the toggle existed. Left as a flex item the wrapper
     collapses to zero width and every link wraps onto its own line. */
  /* The nav and the panel both drop out of the layout, so .topnav__list and
     the CTA become the flex items of .masthead__nav directly. Leaving any block wrapper
     in the chain gives the flex algorithm nothing with an intrinsic width to work from,
     and it shrinks the wrapper to zero so every link wraps onto its own row. */
  .topnav,
  .navtoggle__panel { display: contents; }
}

@media (max-width: 61.9375em) {
  /* The bar carries the wordmark and the menu button and nothing else, so it fits without
     wrapping and without shrinking either of them. The CTA lives inside the panel. */
  .masthead__row { padding-block: 0.7rem; flex-wrap: nowrap; gap: 1rem; }
  .masthead__nav { gap: 0.75rem; flex: 0 0 auto; }
  .navtoggle__panel .masthead__cta {
    display: flex;
    justify-content: center;
    margin-top: 0.9rem;
    width: 100%;
  }

  .navtoggle__button {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--tap);
    padding: 0.4rem 0.2rem;
    list-style: none;
    cursor: pointer;
    font-family: var(--label-face);
    font-size: var(--fs-btn-sm);
    font-weight: 600;
    letter-spacing: var(--track-kicker);
    text-transform: uppercase;
    color: var(--charcoal);
  }
  .navtoggle__button::-webkit-details-marker { display: none; }

  /* Three rules, drawn rather than iconographic, so it matches the hairline language
     the rest of the site is built from. */
  .navtoggle__bars,
  .navtoggle__bars::before,
  .navtoggle__bars::after {
    display: block;
    width: 20px;
    height: 1.5px;
    background: var(--charcoal);
    transition: transform var(--t-fast) var(--ease-move), opacity var(--t-fast) var(--ease-move);
  }
  .navtoggle__bars { position: relative; }
  .navtoggle__bars::before,
  .navtoggle__bars::after { content: ""; position: absolute; left: 0; }
  .navtoggle__bars::before { top: -6px; }
  .navtoggle__bars::after { top: 6px; }

  /* Open: the top and bottom rules cross and the middle one goes. */
  .navtoggle__input:checked + .navtoggle__button .navtoggle__bars { background: transparent; }
  .navtoggle__input:checked + .navtoggle__button .navtoggle__bars::before { top: 0; transform: rotate(45deg); }
  .navtoggle__input:checked + .navtoggle__button .navtoggle__bars::after { top: 0; transform: rotate(-45deg); }

  /* The panel drops out of the masthead row and spans the screen, so the links are a
     proper stacked list with real tap targets rather than a wrapped inline row. */
  /* Closed by default on a phone, opened by the checkbox. */
  .navtoggle__panel { display: none; }
  .navtoggle__input:checked ~ .navtoggle__panel {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    background: var(--ivory);
    border-bottom: 1px solid var(--rule);
    padding: 0.25rem var(--gutter) 1rem;
    z-index: 40;
  }
  .navtoggle__panel .topnav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  /* THE PANEL IS A COLUMN, NOT A BAR.

     This exists because the disclosure now holds up to 62em rather than 860px. At 950px
     the panel spans 821px of content, and without a cap the five separators and the call
     to action all stretched to that width: an 838px wide button carrying a 12px label,
     which is a stretched bar rather than a button, and hairlines running 780px to
     underline a six letter word. The cap makes the open sheet read at 950px exactly as
     it reads at 390px, one column of rows ranged left under the mark, in a wider bar.

     24rem is 384px, and it is a ceiling rather than a width. The panel's own content box
     is the viewport less two gutters, so below about 432px the panel is already narrower
     than the cap and this property does nothing at all: measured, the rows are 342px at
     390px and 312px at 360px both before and after it was added. The phone form is
     untouched by construction, not by luck. */
  .navtoggle__panel .topnav__list,
  .navtoggle__panel .masthead__cta { max-width: 24rem; }

  .navtoggle__panel .topnav__item + .topnav__item { border-top: 1px solid var(--rule-soft); }
  .navtoggle__panel .topnav__link {
    display: flex;
    width: 100%;
    min-height: 48px;
    font-size: var(--fs-nav-lg);
    letter-spacing: 0.02em;
  }
}

@media (prefers-reduced-motion: reduce) {
  .navtoggle__bars,
  .navtoggle__bars::before,
  .navtoggle__bars::after { transition: none; }
}

/* ==========================================================================
   04  SHELL: footer
   The site map. The top nav shows the book's four; all ten live here, so nothing in
   the inventory is more than one click away from anywhere.
   ========================================================================== */

.sitefoot {
  margin-top: auto;
  background: var(--charcoal);
  color: var(--ivory);
  padding: clamp(2.5rem, 5vw, 4rem) var(--gutter) 2rem;
}

.sitefoot__inner {
  max-width: var(--measure);
  margin-inline: auto;
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: 2.5rem var(--gap);
}

.sitefoot .wordmark { color: var(--ivory); }

/* The footer sign off is the same artwork as the masthead, reversed for a Charcoal
   ground and a little wider. Sized by width, height auto, so the mark is
   never stretched. The track matches the sizes attribute the shell writes exactly
   (210px below 1050, 20vw to 1300, 260px above), which is what makes the srcset pick the
   right candidate instead of the 400w file for a 42px box. */
.sitefoot .wordmark img.lockup--foot {
  height: auto;
  width: clamp(210px, 20vw, 260px);
  max-width: 100%;
}

/* The address, the hours and the licence line. Montserrat, because the book is
   unambiguous that Montserrat sets every number the studio publishes, and tabular so the
   street number and the opening hours sit on a column rather than drifting. Ivory, not
   Gold line: Gold line is a hairline ink and the book says it is never text. */
.sitefoot__line {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: 2;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--ivory);
}

.sitefoot__head {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--ivory);
  margin-bottom: 0.9rem;
}

.sitefoot__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--gap);
}
.sitefoot__list a {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  font-family: var(--body-face);
  font-size: var(--fs-small);
  color: var(--ivory);
  text-decoration: none;
}
/* Champagne carries no words here. It is used as a line under the current page, which is
   a legal decorative use, while the label itself stays Ivory at 13.6 to 1. */
.sitefoot__list a:hover {
  text-decoration: underline;
  text-decoration-color: var(--gold-line);
  text-underline-offset: 0.3em;
}
.sitefoot__list a[aria-current="page"] {
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--champagne);
  text-underline-offset: 0.3em;
}

.sitefoot__legal {
  max-width: var(--measure);
  margin: 2.5rem auto 0;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(217, 199, 168, 0.28);
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ivory);
}

@media (max-width: 700px) {
  .sitefoot__inner { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   05  LAYOUT: page and band
   The 12 column grid, 1200px max, 96px gutters collapsing to 24px.
   A band is full bleed so its ground can run edge to edge; its children sit in the
   1200px column.
   ========================================================================== */

.page {
  flex: 1 0 auto;
  display: block;
  /* The containing block for the ornament layer in section 05b. Nothing else depends on
     it, and z-index is deliberately left auto so .page does not become a stacking context
     and the masthead's own z-index 40 keeps working exactly as it did. */
  position: relative;
}

/* A fragment that puts loose content straight into the page still gets the gutters. */
.page > *:not(.band) {
  max-width: calc(var(--measure) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
  text-align: center;
}

/* ==========================================================================
   05b  THE PAGE GROUND ORNAMENT
   A botanical in the far corners of the reading area, drawn in Gold line at low opacity.

   FIVE CONSTRAINTS, AND EACH ONE IS A DECISION IN THE RULE BELOW.

   1. NO NETWORK REQUEST. The drawing is an inline SVG carried in a data URI, so it costs
      no connection, no round trip and no cache entry. One sprig is authored and used
      twice; the second copy is the same file rotated a half turn, which is why the two
      corners are the diagonal pair rather than all four. About 3.3KB of stylesheet.

   2. IT IS BEHIND EVERYTHING, ALWAYS. position: absolute takes it out of the flex flow,
      z-index: -1 puts it in the negative layer of the root stacking context, which paints
      after the canvas and before every block background and every line of text on the
      page. Nothing can be stacked between it and the reader because there is nothing
      between it and the reader. pointer-events: none for the same reason applied to input.

   3. IT IS CORNERS, NOT WALLPAPER. no-repeat, two layers, and the box is .page, so the
      two sprigs land at the top of the first band and the bottom of the last one. On a
      long page most of the scroll carries no ornament at all. It is also entirely covered
      wherever a band paints its own ground, so Wash, Paper and Charcoal bands are clean.

   4. LINE ART, NO FILLS, GOLD LINE ONLY. fill='none' on the root of the drawing, one
      stroke colour, and it is #D9C7A8 by value because a data URI cannot read a custom
      property. If --gold-line ever moves, this string moves with it.

   5. IT DOES NOT COST CONTRAST. Measured, not assumed: at 12 percent a fully covered
      stroke pixel composites to #F4EEE4 against Ivory, and Charcoal body copy over that
      pixel holds 13.8 to 1 against 14.4 on bare Ivory. Charcoal soft holds 6.5 against
      6.8. Those are worst case numbers, taken on the stroke itself rather than on the
      open ground between strokes, and both clear AAA and AA respectively. The opacity
      steps down again below 700px, where a narrow column and a page corner cannot help
      overlapping.
   ========================================================================== */

.page::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.12;
  /* 104px on a phone, 336px from about 1680px up. Small enough at the bottom of the
     range that it reads as a mark in the corner rather than as a background. */
  --orn-size:  clamp(6.5rem, 20vw, 21rem);
  --orn-inset: clamp(0.5rem, 1.6vw, 2.25rem);
  background-repeat: no-repeat;
  background-size: var(--orn-size) var(--orn-size);
  background-position:
    left var(--orn-inset) top var(--orn-inset),
    right var(--orn-inset) bottom var(--orn-inset);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 240 240%27 fill=%27none%27 stroke=%27%23D9C7A8%27 stroke-width=%271.4%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cg%3E%3Cpath d=%27M6 6C44 30 74 62 96 104C114 138 124 178 128 222%27/%3E%3Cpath d=%27M6 6C30 44 62 74 104 96C138 114 178 124 222 128%27/%3E%3Cpath d=%27M6 6C36 40 58 74 72 112%27/%3E%3Cg transform=%27translate(33 25) rotate(-14) scale(1)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(33 25) rotate(76) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(62 58) rotate(4) scale(0.94)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(62 58) rotate(94) scale(0.8)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(90 106) rotate(21) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(90 106) rotate(111) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(114 162) rotate(38) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(25 33) rotate(14) scale(1)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(25 33) rotate(104) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(58 62) rotate(-4) scale(0.94)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(58 62) rotate(86) scale(0.8)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(106 90) rotate(-21) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(106 90) rotate(69) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(162 114) rotate(-38) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(45 55) rotate(7) scale(0.66)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(45 55) rotate(97) scale(0.6)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Ccircle cx=%27128%27 cy=%27222%27 r=%274%27/%3E%3Ccircle cx=%27222%27 cy=%27128%27 r=%274%27/%3E%3Ccircle cx=%2772%27 cy=%27112%27 r=%273%27/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 240 240%27 fill=%27none%27 stroke=%27%23D9C7A8%27 stroke-width=%271.4%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cg transform=%27translate(240 240) scale(-1 -1)%27%3E%3Cpath d=%27M6 6C44 30 74 62 96 104C114 138 124 178 128 222%27/%3E%3Cpath d=%27M6 6C30 44 62 74 104 96C138 114 178 124 222 128%27/%3E%3Cpath d=%27M6 6C36 40 58 74 72 112%27/%3E%3Cg transform=%27translate(33 25) rotate(-14) scale(1)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(33 25) rotate(76) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(62 58) rotate(4) scale(0.94)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(62 58) rotate(94) scale(0.8)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(90 106) rotate(21) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(90 106) rotate(111) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(114 162) rotate(38) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(25 33) rotate(14) scale(1)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(25 33) rotate(104) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(58 62) rotate(-4) scale(0.94)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(58 62) rotate(86) scale(0.8)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(106 90) rotate(-21) scale(0.86)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(106 90) rotate(69) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(162 114) rotate(-38) scale(0.72)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(45 55) rotate(7) scale(0.66)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Cg transform=%27translate(45 55) rotate(97) scale(0.6)%27%3E%3Cpath d=%27M0 0c9-8 23-8 32 0-9 8-23 8-32 0z%27/%3E%3Cpath d=%27M2 0h27%27/%3E%3C/g%3E%3Ccircle cx=%27128%27 cy=%27222%27 r=%274%27/%3E%3Ccircle cx=%27222%27 cy=%27128%27 r=%274%27/%3E%3Ccircle cx=%2772%27 cy=%27112%27 r=%273%27/%3E%3C/g%3E%3C/svg%3E");
}

/* A 320px column and a page corner cannot avoid each other, so the ink comes down where
   the overlap is unavoidable. At 9 percent the same worst case stroke pixel holds 14.0
   to 1 under Charcoal. */
@media (max-width: 700px) {
  .page::before { opacity: 0.09; }
}

/* Decoration, and forced colours mode is not asking for decoration. */
@media (forced-colors: active) {
  .page::before { display: none; }
}

/* ==========================================================================
   05c  CENTRING
   Round 3: the content column centres in the viewport, the text centres inside it, and
   the measure comes in to sixty characters so a centred line is still findable.

   THIS IS A SYSTEM, NOT A SET OF PAGE OVERRIDES. Two declarations do the whole job for
   every band on every page, because a band already owns the content column: the band
   centres its text, and each of its children takes auto side margins so a box with a
   measure sits in the middle of the column instead of at its left edge.

   THE HOME HERO IS EXCLUDED BY NAME AND IT IS THE ONLY EXCLUSION. Its composition is
   approved: type on columns 1 to 5, the drawing bleeding off the right, both standing on
   one dashed rule. Centring that column would move the type off the rule's left end and
   leave the drawing unbalanced, so .band:not(.hero) is the selector everywhere below and
   the hero never sees any of it.

   WHERE AN INTERNAL ALIGNMENT SURVIVES, AND WHY. Four components keep their own
   alignment and are centred as blocks instead, because in each one the alignment is
   carrying information rather than taste. They are named individually further down, each
   with its reason. Everything else centres.
   ========================================================================== */

.band:not(.hero) { text-align: center; }

/* THE BOX, NOT JUST THE WORDS. A child of a band that carries a measure of its own has to
   move to the middle of the content column as well, or centred text just sits in a column
   that is still hard against the left edge.

   WIDTH 100 PERCENT IS LOAD BEARING HERE, AND IT IS NOT DECORATION.
   A band child is a grid item. Auto inline margins on a grid item switch off stretch, so
   the item is sized fit-content instead of filling its track, and then the auto margins
   share out what is left. For a paragraph that is invisible, because a paragraph's
   max-content is a single unwrapped line and always wider than the track. For anything
   else it is destructive: measured on the built pages, the dashed baseline in a page
   header collapsed from 1440px to 0, because an empty div's max-content is nothing, and
   the price sheet came in from 996px to 567px, because a table's max-content is the width
   of its widest row rather than the width of the grid. Declaring the width restores
   stretch, the max-width still caps it, and the auto margins centre what is left.

   THE SELECTOR IS A LIST OF ELEMENTS, NOT A STAR, AND THAT IS ALSO DELIBERATE. A universal
   child selector would outweigh the width every drawing on the site sets for itself: the
   page header mark is fit-content, the reduced device is capped at 282px and the signing
   off lockup is a clamp. All three are figures or images, none of them is in this list,
   and all three keep sizing themselves and are placed by their own rules. */
.band:not(.hero) > p,
.band:not(.hero) > h1,
.band:not(.hero) > h2,
.band:not(.hero) > h3,
.band:not(.hero) > h4,
.band:not(.hero) > ul,
.band:not(.hero) > ol,
.band:not(.hero) > div,
.band:not(.hero) > form,
.band:not(.hero) > .svc {
  width: 100%;
  margin-inline: auto;
}
/* One exception, and it is the reason the list above is a list. The page header's mark
   wrapper ships as a figure on the two pages that carry the reduced drawing and as a div
   on the six that carry the tick rail. The div form was picked up by the rule above and
   stretched to 1200px, which left a 110px rail sitting at the far left of an otherwise
   centred header. It sizes to what it holds, and the auto margins then centre it. */
.band:not(.hero) > .pagehead__mark { width: fit-content; }

/* Buttons and links group in the middle of the band rather than against its left edge. */
.band:not(.hero) .actions { justify-content: center; }

/* The from-price is a flex row of the word FROM and a figure, so it needs telling
   separately: text-align does not move flex children. */
.band:not(.hero) .svc__price { justify-content: center; }
/* The deposit chip pushed itself to the right end of the row with an auto left margin,
   which was correct beside a ranged left price and is wrong under a centred one. */
.band:not(.hero) .svc__dep { margin-inline: auto; }

/* The single field capture and its label centre as a group. */
.band:not(.hero) .notify { justify-content: center; }

/* Copy that carries a measure inside a component, rather than as a direct band child,
   centres in its own container the same way. The three display steps are in this list
   because a page header, a card and the words column of a stacked pair all hold a heading
   one level below the band, where the child rule above cannot reach it. */
.band:not(.hero) .display,
.band:not(.hero) .headline,
.band:not(.hero) .title,
.band:not(.hero) .lede,
.band:not(.hero) .note,
.band:not(.hero) .sheet__close,
.band:not(.hero) .notify__msg,
.band:not(.hero) .spec__item p,
.band:not(.hero) .step__note,
.band:not(.hero) .guarantee p,
.band:not(.hero) .card__text,
.band:not(.hero) .svc__desc,
.band:not(.hero) .policy__text { margin-inline: auto; }

/* --- INTERNAL ALIGNMENT KEPT, CASE 1: THE PRICE SHEET -------------------
   A price table with every cell centred is worse than one with the name on the left and
   the figure on the right. The whole argument of this component is in section 16: the
   figures form a column, the same column across three pages, and the eye reads down it.
   Centre every cell and there is no column left to read, only eleven rows of drifting
   text. So the sheet keeps its interior exactly as it was, name and description ranged
   left, price and deposit ranged right, and the SHEET is what centres: it has a measure,
   it is a band child, and the auto margins above put it in the middle of the column.

   The closing line is the exception inside the exception. It is a sentence rather than a
   row, it sits under the rules rather than on them, and it takes the page's centring. */
.sheet { text-align: left; }
.sheet__close { text-align: center; }

/* --- INTERNAL ALIGNMENT KEPT, CASE 2: THE HEALING TIMELINE --------------
   The timeline is built on a Gold line rail down its left edge with a Deep gold cross
   tick where each step meets it. The rail IS the component; centring the steps would
   leave every line floating a random distance from the tick that points at it. The block
   centres in the column, the steps stay on their rail. */
.timeline { text-align: left; }

/* --- INTERNAL ALIGNMENT KEPT, CASE 3: THE CONTRAINDICATIONS LIST --------
   Every item hangs off a dash drawn at its own left edge, and the fit page carries more
   than fifteen of them. A hanging mark with centred text beside it is a mark pointing at
   nothing. The two column grid centres; the items stay left of their dashes. */
.contra { text-align: left; }

/* --- INTERNAL ALIGNMENT KEPT, CASE 4: THE FORMS --------------------------
   A label centred over a full width field does not sit over anything, and a validation
   message centred under one does not point at the control it belongs to. Both forms on
   the site, the request form and the notify capture, keep their labels, hints, fields and
   error slots ranged left, and the form itself centres in the band. The notify capture's
   own row is centred by the flex rule above, which is a different axis. */
.band:not(.hero) form { text-align: left; margin-inline: auto; }
/* The status line is the one thing inside a form that is not a control and not a label.
   It is a sentence addressed to the reader, it appears on both forms, and on home it sits
   outside the form element entirely, so it is centred in both places rather than reading
   as left on one page and centred on the other. */
.band:not(.hero) .notify__msg { text-align: center; }

/* --- SIDE BY SIDE IMAGE AND TEXT PAIRS STACK, CENTRED --------------------
   .figrow is the row component. Two of its uses are a photograph beside a block of words:
   the artist band on home and the portrait band on about. Those stack now, image over
   words, both centred, which is what the round 3 note asks for.

   The other uses of .figrow are rows of FRAMES: three procedure illustrations, or a
   before and after pair of them. Those are not an image beside text, they are a set of
   things being compared with each other, and a comparison that stacks is not a
   comparison. So the selector asks the row what it holds: :has(> :not(figure)) is true
   only of a row carrying something that is not a frame, which is the words column.

   display: block rather than a one column grid, deliberately. The two pages that carry
   this pattern set grid-template-columns from their own fragments, one of them off an id,
   and an id outweighs anything this file can write short of an important declaration.
   Taking the row out of grid formatting entirely makes those declarations inert without
   a specificity fight and without touching a file this stylesheet does not own. */
.figrow[data-cols="2"]:has(> :not(figure)) {
  display: block;
  text-align: center;
}
.figrow[data-cols="2"]:has(> :not(figure)) > * {
  margin-inline: auto;
}
.figrow[data-cols="2"]:has(> :not(figure)) > * + * {
  margin-top: var(--s-7);
}
/* The frame stops being a column and becomes a portrait standing over its own words, so
   it takes a portrait's width rather than the whole 1200px column. */
.figrow[data-cols="2"]:has(> :not(figure)) > figure {
  max-width: min(24rem, 100%);
}
.figrow[data-cols="2"]:has(> :not(figure)) > :not(figure) {
  max-width: var(--text-measure);
}
/* The frame wrapper centres, but a display:block image inside it does not inherit that,
   so the portrait sat left of the words that surround it. */
.figrow[data-cols="2"]:has(> :not(figure)) img {
  margin-inline: auto;
}
.figrow[data-cols="2"]:has(> :not(figure)) > .shot,
.figrow[data-cols="2"]:has(> :not(figure)) > div:has(> img) {
  max-width: min(24rem, 100%);
}

/* --- HEADINGS DO NOT ORPHAN A WORD --------------------------------------
   text-wrap: balance evens the line lengths across a heading, so a title never leaves one
   word alone on a last line. It is applied at the type vocabulary rather than per page,
   which is what makes it true of the service page titles, the sheet row names and every
   h2 a fragment writes. Where it is unsupported the heading simply breaks as it did.

   Running copy takes pretty rather than balance: balance is capped at a handful of lines
   by every engine that implements it and is the wrong tool for a paragraph, while pretty
   is exactly the tool for the last line of one. */
p, li, dd, figcaption,
.lede, .note, .small, .svc__desc, .sheet__desc, .sheet__close,
.card__text, .policy__text, .step__note, .notify__msg { text-wrap: pretty; }

h1, h2, h3, h4,
.display, .headline, .title,
.svc__name, .sheet__name, .card__label,
.kicker, .kicker--quiet { text-wrap: balance; }

/* The default band. One content track, no column gap, so the whole thing is exactly
   100 percent wide at every viewport including 320px. */
.band {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [content-start] min(100% - var(--gutter) * 2, var(--measure)) [content-end]
    minmax(var(--gutter), 1fr) [full-end];
  padding-block: var(--band-pad);
}
.band > * { grid-column: content; }
.band > [data-span="full"] { grid-column: full; }

/* Grid mode. Opt in with data-grid="12" when a band holds two components side by side,
   for example the brows price sheet at 8 columns and a portrait at 4.

   It is deliberately not applied below 880px. Fourteen tracks with a 24px column gap
   carry 13 gaps, which is 312px of fixed space before a single column is drawn, so on a
   320px screen it could only overflow. Below 880px the band stays one column and the
   spans below collapse to full width, which is the correct phone layout anyway. */
@media (min-width: 880px) {
  .band[data-grid="12"] {
    grid-template-columns:
      [full-start] minmax(calc(var(--gutter) - var(--gap)), 1fr)
      [content-start] repeat(12, [c] minmax(0, var(--col))) [content-end]
      minmax(calc(var(--gutter) - var(--gap)), 1fr) [full-end];
    column-gap: var(--gap);
    /* One interval in grid mode. The typed rhythm in section 07 governs a stack of type;
       a row of components is governed by the row gap, because a margin on the second item
       of a row would push it out of line with the first. */
    row-gap: var(--s-7);
  }
  .band[data-grid="12"] > * { grid-column: content; margin-top: 0; }
  .band[data-grid="12"] > [data-span="full"]  { grid-column: full; }
  /* READ THIS BEFORE USING A NUMERIC SPAN BELOW.
     "full" and "12" resolve to named lines, so they are DEFINITE placements and they are
     safe. Every numeric span from 1 to 11 resolves to "span N" with no start line, so the
     item is AUTO PLACED, and auto placement seats the first item of a row at track one,
     which is the left gutter, not at content-start. The whole row then sits one track left
     of the type above it and the first item is a gutter wider than its neighbours. It is
     invisible at the measure, where the gutter is 0, and grows with the viewport.
     A row of components therefore goes in one content-column child that divides itself.
     See .figrow in section 13, which is what the illustration rows use. */
  .band[data-grid="12"] > [data-span="1"]     { grid-column: span 1; }
  .band[data-grid="12"] > [data-span="2"]     { grid-column: span 2; }
  .band[data-grid="12"] > [data-span="3"]     { grid-column: span 3; }
  .band[data-grid="12"] > [data-span="4"]     { grid-column: span 4; }
  .band[data-grid="12"] > [data-span="5"]     { grid-column: span 5; }
  .band[data-grid="12"] > [data-span="6"]     { grid-column: span 6; }
  .band[data-grid="12"] > [data-span="7"]     { grid-column: span 7; }
  .band[data-grid="12"] > [data-span="8"]     { grid-column: span 8; }
  .band[data-grid="12"] > [data-span="9"]     { grid-column: span 9; }
  .band[data-grid="12"] > [data-span="10"]    { grid-column: span 10; }
  .band[data-grid="12"] > [data-span="11"]    { grid-column: span 11; }
  .band[data-grid="12"] > [data-span="12"]    { grid-column: content; }
  /* A component that has been given an explicit span drops its own measure cap: the
     track is the measure now, which is the point of having a grid. */
  .band[data-grid="12"] > [data-span] { max-width: none; }
}

.band[data-tone="wash"]     { background: var(--wash); }
.band[data-tone="paper"]    { background: var(--paper); }
.band[data-tone="charcoal"] { background: var(--charcoal); color: var(--ivory); }

/* Two ivory bands in a row need a line between them. A change of ground is its own
   separation, so the rule is dropped on either side of a toned band. */
.page > .band + .band { border-top: 1px solid var(--rule); }
.page > .band[data-tone] + .band,
.page > .band + .band[data-tone] { border-top: 0; }

/* A hero and a page header already end on the dashed baseline, and that measured line is
   the boundary. A solid hairline a band's padding below it would be the same edge drawn
   twice, so the band that follows either one draws none. This lives here rather than as an
   inline border-top:0 in ten fragments. */
.page > .hero + .band,
.page > .pagehead + .band { border-top: 0; }

/* A hairline separated pair reads as two parts of one section rather than as two
   sections, so the two facing paddings come down to three quarters. This fires in
   exactly the case the border above fires in, which is two untoned bands. The hero and
   the page header are excluded: they set their own asymmetric padding. */
.page > .band:not([data-tone], .hero, .pagehead) + .band:not([data-tone]) {
  padding-top: calc(var(--band-pad) * 0.75);
}
.page > .band:not([data-tone], .hero, .pagehead):has(+ .band:not([data-tone])) {
  padding-bottom: calc(var(--band-pad) * 0.75);
}

/* A dark band leaves an optical after image, so the band under it needs more air than a
   hairline transition does. */
.page > .band[data-tone="charcoal"] + .band {
  padding-top: calc(var(--band-pad) * 1.25);
}

/* On Charcoal: the two Playfair steps may take Champagne, which is the one place besides
   the logo where Champagne is allowed to be type. It holds 6.1 to 1 at those sizes, and
   the home page's signing off line under the reversed mark is set in it by name. Small
   text stays Ivory, because Champagne never carries a sentence. */
.band[data-tone="charcoal"] .display,
.band[data-tone="charcoal"] .headline { color: var(--champagne); }
/* Everything else on a Charcoal band is Ivory, at 13.6 to 1.

   This list is long on purpose. Deep gold is 1.9 to 1 on Charcoal and Charcoal soft is
   1.4, and both are pairings the book rejects outright, so every class carrying either ink
   has to be named here or it goes dark on a dark band. Naming them is this file's job: a
   fragment should never have to ship a style block to make a label readable. */
.band[data-tone="charcoal"] .title,
.band[data-tone="charcoal"] .lede,
.band[data-tone="charcoal"] .note,
.band[data-tone="charcoal"] .small,
.band[data-tone="charcoal"] .kicker,
.band[data-tone="charcoal"] .kicker--quiet,
.band[data-tone="charcoal"] .card__label,
.band[data-tone="charcoal"] .card__text,
.band[data-tone="charcoal"] .svc__name,
.band[data-tone="charcoal"] .svc__desc,
.band[data-tone="charcoal"] .svc__price,
.band[data-tone="charcoal"] .svc__from,
.band[data-tone="charcoal"] .svc__dep,
.band[data-tone="charcoal"] .sheet__name,
.band[data-tone="charcoal"] .sheet__desc,
.band[data-tone="charcoal"] .sheet__price,
.band[data-tone="charcoal"] .sheet__cur,
.band[data-tone="charcoal"] .sheet__dep,
.band[data-tone="charcoal"] .sheet__close,
.band[data-tone="charcoal"] .policy__n,
.band[data-tone="charcoal"] .policy__text,
.band[data-tone="charcoal"] .step__day,
.band[data-tone="charcoal"] .step__note,
.band[data-tone="charcoal"] .contra__list li,
.band[data-tone="charcoal"] .spec__item p,
.band[data-tone="charcoal"] .mapping figcaption,
.band[data-tone="charcoal"] .stage__line,
/* Last in the list, and it has to be: the healed claim's own Deep gold rule weighs exactly
   as much as this one, so source order is what stops it painting 1.9 to 1 on a dark band.
   The claim is carried by weight here instead of by the accent. */
.band[data-tone="charcoal"] .stage--healed .stage .stage__line.pending,
.stage__line.pending {
  display: block;
  background: none;
  border: 0;
  padding: 0;
  vertical-align: baseline;
  line-height: var(--lh-kicker);
}
.stage__line:first-child { color: var(--ivory); }

/* The second line of a stage bar stays one step quieter, in the hairline ink. This is the
   single place Gold line meets type, and it holds 9.8 to 1 on Charcoal. */
.band[data-tone="charcoal"] .stage__line + .stage__line { color: var(--gold-line); }
.band[data-tone="charcoal"] .timeline { border-left-color: rgba(217, 199, 168, 0.45); }
.band[data-tone="charcoal"] .step::before { background: var(--gold-line); }
.band[data-tone="charcoal"] .step { border-bottom-color: rgba(217, 199, 168, 0.3); }
.band[data-tone="charcoal"] .contra__group { border-top-color: rgba(217, 199, 168, 0.3); }
.band[data-tone="charcoal"] .contra__list li::before { background: rgba(217, 199, 168, 0.45); }
/* A link on Charcoal keeps an Ivory label and takes a Champagne underline, so the accent
   is a line rather than a sentence. */
.band[data-tone="charcoal"] a {
  color: var(--ivory);
  text-decoration-color: var(--champagne);
  text-underline-offset: 0.24em;
}
.band[data-tone="charcoal"] .card { background: transparent; border-color: rgba(217, 199, 168, 0.3); }
.band[data-tone="charcoal"] .sheet,
.band[data-tone="charcoal"] .sheet__row,
.band[data-tone="charcoal"] .spec__item,
.band[data-tone="charcoal"] .policy { border-color: rgba(217, 199, 168, 0.3); }

/* ==========================================================================
   06  TYPE VOCABULARY
   ========================================================================== */

/* Kicker and caption, 11/16 Montserrat tracked +2.5, Deep gold. The label that opens
   almost every block on this site. */
.kicker {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--deep-gold);
}

/* The quiet cut, for a caption or a second label that must not compete.
   INK: Charcoal soft, kept. This is a label by definition, twelve tracked uppercase
   characters that name the block under them. Nobody reads it as a sentence. */
.kicker--quiet {
  color: var(--charcoal-soft);
  font-weight: 500;
}

/* Display, 33 to 50 on 1.06. One thought per headline, sentence case, full stop allowed.
   Never tracked, never all caps. Home only, at the top of the scale. */
.display {
  font-family: var(--display-face);
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  font-weight: 400;
  letter-spacing: -0.005em;
  color: var(--charcoal);
  max-width: 24ch;
}
.display em { font-style: italic; }  /* one emphasised word, in the manner of the tagline */

/* Headline, 27 to 33 on 1.152. The step the book has and this file used to skip. It is
   the h1 of the other eight pages, so a page header can never be mistaken for the home
   hero: two tiers, both from the book. */
.headline {
  font-family: var(--display-face);
  font-size: var(--fs-headline);
  line-height: var(--lh-headline);
  font-weight: 400;
  letter-spacing: -0.005em;
  color: var(--charcoal);
  max-width: 28ch;
}
.headline em { font-style: italic; }

.title {
  font-family: var(--display-face);
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  font-weight: 400;
  color: var(--charcoal);
  max-width: 28ch;
}

/* The standfirst. Roman in a body band, italic in a page header, because the book allows
   exactly one italic sentence on a page and the page header is where it belongs.

   INK: Charcoal, not Charcoal soft. The lede is the sentence most likely to be the only
   one a visitor reads on a page, so it is running copy by any definition and it takes the
   reading ink. 14.4 to 1 on Ivory. */
.lede {
  font-family: var(--body-face);
  font-size: var(--fs-lede);
  line-height: var(--lh-lede);
  color: var(--charcoal);
  max-width: var(--text-measure);
}

/* Running copy sits at the body size by default; .note and .small are the half step down.
   Nothing that is read as a sentence is set below 16px anywhere on this site. The only
   type under 16 is --fs-kicker at 12, which is tracked uppercase shorthand, and the
   button labels.

   INK: Charcoal. A note is smaller copy, not quieter copy. It carries the sentences about
   what is included, what is decided at consultation and what the illustrations are, and a
   serif at 16px is exactly where a soft ink stops holding its edges. Size is what makes
   it secondary; colour is not asked to do it twice. */
.note {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--charcoal);
  max-width: var(--text-measure);
}
.small {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
}

/* The champagne hairline lead in. Ornament, so it is Champagne and it carries no words. */
.flag::before {
  content: "";
  display: inline-block;
  width: 1.75rem;
  height: 1px;
  margin-right: 0.7rem;
  vertical-align: 0.28em;
  background: var(--champagne);
}

/* PENDING. Book page 02: where a fact is not confirmed, the surface says so rather than
   inventing one. This is that tag, and it is meant to be visible. */
.pending {
  display: inline-block;
  font-family: var(--label-face);
  /* The kicker step, 11px, which is the smallest size the book's scale defines. This tag
     was set at 10px, below the bottom of the scale, on a label the book wants read. */
  font-size: var(--fs-kicker);
  line-height: 1.6;
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  color: var(--deep-gold);
  background: var(--wash);
  border: 1px solid var(--gold-line);
  padding: 0.2rem 0.5rem;
  vertical-align: 0.1em;
}

/* ==========================================================================
   07  VERTICAL RHYTHM
   The old file gave every direct child of a band the same 24px gap, so the interval
   between a label and its headline was identical to the interval between a paragraph and
   a call to action. A page where every gap is the same number reads as a form.

   Six distinct intervals is a rhythm. Reading the table: 12 between two notes, 16 inside
   a thought, 24 between a label and its thought, 40 before anything the reader has to act
   on or scan, 72 when a new label starts a new movement.

   Only direct children are governed here: a paragraph inside a card or a policy is the
   component's business, not this rule's, which is why every selector is a child selector.
   ========================================================================== */

.band > p { max-width: var(--text-measure); }

.band > * + *                       { margin-top: var(--s-5); }  /* 24, the fallback */
.band > .kicker + *                 { margin-top: var(--s-5); }  /* 24 */
.band > .display + .lede            { margin-top: var(--s-5); }  /* 24 */
.band > .headline + .lede           { margin-top: var(--s-4); }  /* 16 */
.band > .title + p                  { margin-top: var(--s-4); }  /* 16 */
.band > p + p                       { margin-top: var(--s-4); }  /* 16 */
.band > .note + .note               { margin-top: var(--s-3); }  /* 12 */
.band > .lede + *                   { margin-top: var(--s-7); }  /* 40, the biggest inside a band */
.band > * + .cards,
.band > * + .sheet,
.band > * + .timeline,
.band > * + .contra,
.band > * + .spec,
.band > * + .policies,
.band > * + .pair,
.band > * + .shot,
.band > * + .guarantee,
.band > * + .notify,
.band > * + .actions                { margin-top: var(--s-7); }  /* 40 */
/* Last, so it wins on source order against every rule above at equal specificity:
   a kicker after anything at all is a new movement, including after a lede. */
.band > * + .kicker                 { margin-top: var(--s-9); }  /* 72 */

/* ==========================================================================
   08  HERO, DESKTOP
   Twelve columns at 880px and above. Type in columns 1 to 5, an open channel at column 6
   holding nothing on purpose, and the device from column 7 off the right edge of the page.

   The single organising idea: both columns are bottom aligned onto one dashed rule that
   crosses the whole viewport. The device's own baseline, which the book calls the ground
   truth the guides stand on, is promoted out of the SVG and becomes a full bleed hairline.
   The three guides descend onto it and the type column stands on it. The brand's sentence
   is that a shape is measured before it is permanent; the layout draws the measurement
   under the sentence.

   Below 880px this is a different composition, not a narrower one. See section 09.
   ========================================================================== */

.hero {
  padding-top: clamp(3rem, 7vw, 6.5rem);      /*  48 -> 104 */
  padding-bottom: clamp(2.5rem, 5vw, 4.5rem); /*  40 ->  72 */
}
/* Works whether the hero is the band itself or a full bleed child of one. */
.band > .hero { grid-column: full; }

.hero__rule {
  position: relative;
  height: 1px;
  /* Dash 1 on 5, which is the book's exact baseline specification and matches what the
     SVG was drawing before that path was lifted out of it. */
  background: repeating-linear-gradient(to right, var(--gold-line) 0 1px, transparent 1px 6px);
}

/* The three tick rail rides the right end of the baseline on the five document pages.
   Its viewBox is centred on y 0 and 14 units tall, so dropping it 7px above the rule's own
   half pixel puts the crossing point exactly on the hairline, and the 6 unit arms overhang
   symmetrically without the 1px rule growing to contain them. No transform is used, so the
   reduced motion block's transform reset cannot knock it off the line.

   The right edge tracks the content column rather than the viewport: below the cap that is
   one gutter in, and above it the content is centred at 1200px, which is what the max()
   picks between. The rail sits where the page's right edge is, not where the screen's is. */
.hero__rule > .m-ticks {
  position: absolute;
  right: max(var(--gutter), calc((100% - var(--measure)) / 2));
  top: calc(50% - 7px);
  width: 110px;
  height: 14px;
}

/* The device fills its track. The 38rem cap that .mapping carries is lifted in section 11,
   after that rule, because the hero device element carries both class names and the two
   would otherwise be decided by source order rather than by intent. */

/* The vertical rhythm inside the type column. Five different intervals, tightest at the
   top, loosest before the action: a label belongs to the thing under it, a thought is
   followed by its explanation, and then reading stops and deciding starts. */
.hero__type > * + *                  { margin-top: var(--s-5); }  /* 24 */
.hero__type > .display + .lede       { margin-top: var(--s-5); }  /* 24 */
.hero__type > * + .actions           { margin-top: var(--s-7); }  /* 40 */

/* The hero h1 takes the scale, and the column is its measure: no character cap, the type
   column is what decides where the line turns. */
.hero .display {
  font-size: var(--fs-hero);
  max-width: none;
}

/* THE ORPHAN CEILING, AND WHY THE HERO HEADLINE NEEDS ONE.

   Measured, not guessed. In Playfair at 50px "Measured before" sets 375.45px, so the
   string costs 7.51px of width for every 1px of font size. The hero type column is five
   of the twelve plus four gaps, which is 309px at 880, 361px at 1024 and 426px at 1200.
   Divide: the widest size that still holds "Measured before" on one line is 41px at 880,
   48px at 1024, 57px at 1200.

   The token alone hands the headline 47px at 880 and 50px at 1024. Both are over the
   line, and over the line the balancer has no two line solution left, so it sets three
   and the first of them is the single word "Measured". That is the orphan, on the largest
   heading on the site, and it appears at every width from 880 up to about 1090.

   min() is the whole fix. calc(5vw - 5px) is the column's own limit with roughly 2px of
   headroom held across the band: 39px at 880, 46px at 1024, 50px at 1100. Above 1100 it
   exceeds the token and the token wins, so 1200 and 1440 render at 50px exactly as they
   did. Below 880 this rule does not exist: the mobile hero gives the headline the full
   content width, where the whole sentence sets on one line, and capping it there would
   shrink type for a break that cannot happen.

   The ceiling is the column, so it is written against the viewport the column is cut
   from rather than as a second hand-tuned clamp that would drift the next time the scale
   moves. If the hero ever goes from five columns to six, this line changes with it. */
@media (min-width: 880px) {
  .hero .display { font-size: min(var(--fs-hero), calc(5vw - 5px)); }
}

/* THE RESULT LINE. One 16px sentence under the 19px lede, saying what the argument is
   for. It is the only unclassed paragraph in the hero, so it is addressed as one rather
   than by growing the vocabulary for a single line of copy. Body size, body ink, capped
   to the reading measure: quieter than the lede because it is smaller, not because it is
   greyer. The 24px above it comes from the interval table below; desktop needs nothing
   else, and the mobile block gives it its place in the order. */
.hero__type > .lede + p {
  max-width: var(--text-measure);
}

@media (min-width: 880px) {
  .hero {
    display: grid;
    grid-template-columns:
      [full-start] minmax(calc(var(--gutter) - var(--gap)), 1fr)
      [content-start] repeat(12, [c] minmax(0, var(--col))) [content-end]
      minmax(calc(var(--gutter) - var(--gap)), 1fr) [full-end];
    column-gap: var(--gap);
    grid-template-rows: [top] auto [base] auto [bottom];
  }
  .hero > * + * { margin-top: 0; }

  .hero__type   { grid-column: c 1 / span 5; grid-row: top / base; align-self: end;
                  /* The action stands 32px above the measured line rather than on it,
                     while the drawing's guides land exactly on it. */
                  padding-bottom: var(--s-6); }
  .hero__device { grid-column: c 6 / content-end; grid-row: top / base; align-self: end; }
  .hero__rule   { grid-column: full-start / full-end; grid-row: base; }
}

/* At full measure the device bleeds off the right edge of the page. The book says guides
   always run past the shape, top and bottom; a construction drawing that runs off the
   sheet is the same grammar extended one step, and it is the only honest use for the
   600px the old hero abandoned. */
@media (min-width: 1200px) {
  .hero__device { grid-column: c 7 / full-end; }
}

/* ==========================================================================
   09  HERO, MOBILE. A DIFFERENT COMPOSITION
   Below 880px. On desktop the device is a large drawing standing beside the type. On
   mobile it is a thin measured band lying between the claim and its explanation. Same
   drawing, opposite role, one grammar.

   Order: kicker, headline, device full bleed, dashed rule, lede, primary, tertiary.
   Words come first. The drawing physically separates the claim from its explanation,
   which is what a measurement does. The band running to both screen edges is the
   strongest form available on a 390px screen and it costs nothing.
   ========================================================================== */

@media (max-width: 879px) {
  .hero {
    padding-top: var(--s-7);
    padding-bottom: var(--s-7);
  }

  /* display: contents lets the four type elements become children of the hero grid, so
     the drawing can be lifted between the headline and the lede without the fragment
     carrying two type blocks. .hero__type is a plain div with no role to lose.

     Every one of them then has to be placed by hand. They are grandchildren of the band in
     the DOM, so the band's own rule for placing its children in the content column does
     not reach them, and an unplaced grid item auto flows into a gutter track. */
  .hero__type { display: contents; }
  .hero__type > * { grid-column: content; }

  .hero__type > .kicker  { order: 1; }
  .hero__type > h1,
  .hero__type > .display { order: 2; }
  .hero__device          { order: 3; grid-column: full; margin-top: var(--s-6); }
  .hero__rule            { order: 4; grid-column: full; margin-top: 0; }
  .hero__type > .lede    { order: 5; }
  /* The result line takes the lede's own order, and a tie resolves in DOM order, so it
     lands under the lede rather than jumping to the top of the hero the way an unordered
     child of an ordered flow does. */
  .hero__type > .lede + p { order: 5; }
  .hero__type > .actions { order: 6; }

  /* The interval table, read in visual order. The margins fall on the DOM neighbour,
     which after ordering lands on the right pair every time:
       kicker to h1 16, h1 to the device band 32, device band to rule 0,
       rule to lede 32, lede to the primary 32, primary to tertiary 16. */
  .hero__type > * + *                { margin-top: var(--s-4); }  /* 16 */
  .hero__type > .display + .lede,
  .hero__type > h1 + .lede           { margin-top: var(--s-6); }  /* 32 */
  .hero__type > * + .actions         { margin-top: var(--s-6); }  /* 32 */

  /* The primary goes full width on a phone only. A 180px slab floating in a 342px column
     is the same isolation problem the desktop hero had; a full width bar is a control,
     not an ornament. The secondary stays a rule link so a phone screen never carries two
     solid slabs. */
  .hero .actions {
    flex-direction: column;
    align-items: flex-start;
    row-gap: var(--s-4);
    width: 100%;
  }
  .hero .actions > .btn {
    width: 100%;
    min-height: 3rem;   /* 48px */
  }
  /* A full width bar is flush with both edges of the column, so there is nowhere for the
     champagne tail to run past to. It is dropped rather than allowed to hang off the
     screen. */
  .hero .actions > .btn::after { content: none; }

  /* The crop, book page 12. A 390px phone is nominally above the book's 320px full
     threshold, but the 36 hairstrokes are 1.2 viewBox units wide and at that scale render
     at about 1.27px, which aliases into grey mush and reads as dirt. The book anticipated
     exactly this, so the 160 to 320 treatment applies on grounds of legibility: guides
     and outline stay, strokes, ticks and labels go. The wash fill stays. The dashed
     baseline is dropped from the drawing because .hero__rule under the band is the
     baseline now. */
  .hero__device .m-strokes,
  .hero__device .m-stroke,
  .hero__device .m-tick,
  .hero__device .m-cap { display: none; }
}

/* ==========================================================================
   10  PAGE HEADER, THE OTHER EIGHT PAGES
   One family with home, at one third the mass. The page header keeps the hero's
   organising idea, the shared dashed baseline, and reduces the drawing to a fragment of
   itself. Total height 260 to 320px on desktop, against the hero's 520 to 640px.
   ========================================================================== */

.pagehead {
  padding-top: clamp(2.5rem, 5vw, 4rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
}
.band > .pagehead { grid-column: full; }

.pagehead__type > * + *            { margin-top: var(--s-4); }  /* 16 */
.pagehead__type > * + .lede        { margin-top: var(--s-4); }  /* 16 */

/* The book: standfirst is serif italic 19 in Charcoal soft, and it is the only italic
   sentence on the page. That is true only if .lede stays roman everywhere else. */
.pagehead .lede { font-style: italic; }

/* The mark half of the header. It carries one of two tiers of the book's own size ladder,
   and which one is resolved from the page, not from this file:

     reduced device   brows, lip-blush, eyeliner. 160 to 320px on the ladder, so guides
                      plus outline, no strokes and no labels. 282px here.
     the three ticks  pricing, healing, fit, about, visit. Below 160px on the ladder, so
                      three 12px crosses and nothing else. That tier ships as a small SVG
                      rail, either inside .hero__rule or as this figure at its own width.

   The ticks are NOT evenly spaced. On a 96px rail they sit at x 0, 60 and 96, because the
   book puts the arch guide at 62.5 percent of the span and 62.5 percent of 96 is 60.
   Nobody will consciously notice. It is the reason the mark feels right on five otherwise
   plain pages. The rail's own geometry sets those positions; this file only places it. */
/* The mark shrinks to whatever it holds and is capped at the reduced device's 282px. It
   has to be fit-content rather than a flat width, because the wrapper carries either tier:
   a reduced device, which has no intrinsic width and takes the cap, or a 110px tick rail,
   which does. A flat 282px would leave a tick rail floating 172px inside the column edge
   it is supposed to sit on. */
.pagehead__mark {
  width: fit-content;
  max-width: min(282px, 100%);
  align-self: end;
}
/* A drawing dropped straight into a header without its .pagehead__mark wrapper is still a
   mark, so it is placed and sized as one. Without this it is just another band child and
   it spans the whole content column at roughly four times its intended mass, which is the
   wallpaper the book forbids. */
.pagehead > .m-device {
  width: min(282px, 100%);
  align-self: end;
}
.pagehead__mark > .m-ticks { width: 110px; height: 14px; }

/* ROUND 3, AND THIS IS THE ONE STRUCTURAL CHANGE THE CENTRING NOTE COSTS.

   The header used to be the hero's composition at a third of the mass: type on columns 1
   to 7, the mark on 10 to 12, the two of them facing each other across an empty column 8
   and 9. Centring the type inside columns 1 to 7 would have centred it inside the left
   half of the page and left the right half exactly as empty as the note complains about.

   So the header becomes one centred stack: type across the full column, the mark centred
   under it, the dashed baseline under that. The organising idea survives intact, because
   the organising idea was never the two columns; it was that everything in the header
   stands on one measured line. It still does. It is now a column standing on it rather
   than a pair of columns, and the drawing sits under its own heading instead of beside it.

   Three rows, not two: [top] is the type, [mid] is the mark, [base] is the rule. */
@media (min-width: 880px) {
  .pagehead {
    display: grid;
    grid-template-columns:
      [full-start] minmax(calc(var(--gutter) - var(--gap)), 1fr)
      [content-start] repeat(12, [c] minmax(0, var(--col))) [content-end]
      minmax(calc(var(--gutter) - var(--gap)), 1fr) [full-end];
    column-gap: var(--gap);
    grid-template-rows: [top] auto [mid] auto [base] auto [bottom];
  }
  .pagehead > * + * { margin-top: 0; }
  .pagehead__type { grid-column: content; grid-row: top / mid;
                    padding-bottom: var(--s-6); }
  .pagehead__mark,
  .pagehead > .m-device { grid-column: content; grid-row: mid / base;
                          align-self: end; justify-self: center; }
  .pagehead .hero__rule { grid-column: full-start / full-end; grid-row: base;
                          margin-top: var(--s-6); }
  /* A headline running the full 1200px column would be a centred line nobody can scan
     back to the start of. It keeps its own measure and centres inside the column. */
  .pagehead .headline { max-width: 20ch; }
}

@media (max-width: 879px) {
  .pagehead__mark,
  .pagehead > .m-device { justify-self: center; margin-top: var(--s-6); }
  .pagehead .hero__rule { grid-column: full; margin-top: var(--s-5); }
}

/* THE TICK RAIL COMES TO THE MIDDLE, ON THE PAGE HEADERS ONLY.
   Six of the pages carry the smallest tier of the mark, which is three crosses on a 110px
   rail, and the shell writes it inside the dashed baseline rather than as a separate mark.
   Section 08 rides it on the right hand end of the rule, tracking the content column's
   right edge. Under a centred header that leaves one 110px object hanging off the right of
   an otherwise symmetrical stack, which is the shape of the complaint rather than the fix.
   So it centres on these pages. The home hero's rail is untouched: it belongs to a
   composition that is ranged left on purpose and the hero is not centred.
   left rather than a transform, so the reduced motion block cannot reach it. */
.pagehead .hero__rule > .m-ticks {
  right: auto;
  left: calc(50% - 55px);
}

/* ==========================================================================
   11  THE MAPPING DEVICE
   The construction drawing that replaces the photograph. Reproduced from the exported
   files, never sketched freehand per surface, never over photography, never rotated.

   Anti wallpaper, book page 12: the device or any fragment of it appears once per page,
   in the page header, and nowhere else on that page. It is never a background, never
   behind running text, never repeated, never tiled, never below full opacity, never
   rotated, never mirrored. It animates on home only. Seven of the ten pages carry no
   drawing at all beyond three 12px crosses, and that ratio is what keeps it a signature
   instead of a pattern.

   The inks are exposed as custom properties so an inline SVG can reference them from a
   style attribute, for example style="stroke:var(--device-guide)". A presentation
   attribute cannot read a custom property, so the style attribute is the way in.
   ========================================================================== */

.mapping {
  --device-base:    var(--rule);        /* the dashed baseline */
  --device-guide:   var(--gold-line);   /* start, arch, tail */
  --device-outline: var(--deep-gold);   /* the mapped outline, 1.5px */
  --device-stroke:  var(--charcoal);    /* hairstrokes, 1.2px, round caps */
  /* INK: Charcoal soft, kept. START, ARCH and TAIL are annotations inside a construction
     drawing. They label the geometry; they are not copy and nobody reads them in sequence. */
  --device-label:   var(--charcoal-soft);
  /* The Start, Arch and Tail labels inside the drawing ask for var(--label) from a style
     attribute. Aliasing it here puts them on the same declared Montserrat stack as every
     other label on the site rather than on the SVG's own inline fallback. */
  --label: var(--label-face);
  max-width: 38rem;
}

/* The two placed instances lift that cap and take their width from their grid track
   instead. This sits here, immediately after .mapping, rather than in sections 08 and 10
   where the rest of their layout lives: the elements carry both class names, both
   selectors weigh the same, and source order would otherwise decide. The hero device in
   particular is 690px and wider, so a 608px cap would silently cancel the bleed. */
.hero__device,
.hero__device .mapping,
.pagehead__mark,
.pagehead__mark .mapping { max-width: none; }

.mapping svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
/* INK: Charcoal soft, kept. A genuine figcaption, and the shortest one on the site. */
.mapping figcaption {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 500;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--charcoal-soft);
  margin-top: var(--s-4);
}
/* No figcaption on the home hero device. The Start, Arch and Tail labels inside the
   drawing are the caption; a second one under it would be the same word twice.
   .mapping figcaption is reserved for the page header fragments. */
.hero__device figcaption,
.hero__device .mapping figcaption { display: none; }

/* ==========================================================================
   12  MOTION
   Book page 12: "On the website the guides may draw on once, 600ms ease out, on first
   view. Nothing loops, nothing floats."

   The choreography below draws on once, on first view, never loops, and nothing floats.
   It extends the book's clause from the guides to the rest of the drawing because the
   book's own anatomy is an ordered procedure: baseline, then guides, then outline, then
   hairstrokes, then labels and markers. Animating that order is not decoration. It is the
   business argument played at reading speed: the shape is measured before it is permanent.

   Type never animates. Nothing fades the headline in. The words are readable at frame one.

   SCOPE. The sequence runs on the home page hero only. It is scoped on ".hero .mapping",
   which is the one selector both markup shapes satisfy: the shell's own partial writes
   figure.mapping[data-device="hero"], and the home fragment writes its own
   figure.mapping.hero__device around a bare svg. Scoping on the data attribute alone
   silently matched neither of those on the day this was written, and a motion sequence
   that quietly does not run is worse than one that is scoped a little more broadly.

   It still cannot leak. The other eight pages put their drawing in .pagehead, not .hero,
   the build refuses to emit the hero device anywhere but home, and every rule below also
   requires data-drawn, which only the device's own observer ever sets. Every other
   instance of the device, or a fragment of it, is static.

   THE MARKUP CONTRACT, which the shell publishes and this file relies on.
     pathLength="1"     on every path the sequence draws. The geometry is normalised, so a
                        draw on is stroke-dasharray: 1 with stroke-dashoffset going 1 to 0
                        on all forty paths, and nothing measures a length at runtime.
     m-guide--start     the guides, the ticks and the labels are named rather than left to
     m-tick--start      document order, so the draw order can be START, TAIL, ARCH without
     m-cap--start       depending on how the SVG happens to be authored.
     data-motion="on"   set on the figure, and on its .hero ancestor, by the script that
                        travels with the device. Every pre draw state below is scoped to
                        it, so a reader with scripting off gets a finished drawing rather
                        than an empty box.
     data-drawn="true"  set once, on first intersection, threshold 0.25, rootMargin
                        "0px 0px -10% 0px", then unobserved. Once per page load. No scroll
                        linked motion of any kind. It lands on the figure and on the .hero,
                        so the rule can be gated without :has(); the :has() form is kept
                        beside it so the sequence still runs if a fragment carries its own
                        script and sets the attribute only on the figure.
     The observer is never attached when prefers-reduced-motion matches, so in practice
     data-drawn is never set for a reader who has asked for stillness.
   ========================================================================== */

@keyframes device-draw    { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes device-stroke  { from { stroke-dashoffset: 1; opacity: 0; }
                            to   { stroke-dashoffset: 0; opacity: 1; } }
@keyframes device-fill-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes device-tick-in { from { opacity: 0; transform: scale(0.6); }
                            to   { opacity: 1; transform: scale(1); } }
@keyframes device-cap-in  { from { opacity: 0; transform: translateY(4px); }
                            to   { opacity: 1; transform: none; } }
@keyframes rule-draw      { from { clip-path: inset(0 100% 0 0); }
                            to   { clip-path: inset(0); } }

/* --- the pre draw state, only ever with scripting present ---------------- */

.hero .mapping[data-motion="on"] .m-guide path,
.hero .mapping[data-motion="on"] .m-outline,
.hero .mapping[data-motion="on"] .m-strokes path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
/* The stroke group already carries opacity .82 inline. The paths animate to 1 rather than
   to .82, so the finished hairstroke reads at exactly .82 and not at .82 squared. */
.hero .mapping[data-motion="on"] .m-strokes path { opacity: 0; }
.hero .mapping[data-motion="on"] .m-fill { opacity: 0; }
.hero .mapping[data-motion="on"] .m-tick path { opacity: 0; }
.hero .mapping[data-motion="on"] .m-cap text { opacity: 0; }

.hero[data-motion="on"] > .hero__rule,
.hero:has(.hero .mapping[data-motion="on"]) > .hero__rule {
  clip-path: inset(0 100% 0 0);
}

/* --- the sequence, desktop. Total 5470ms --------------------------------- */

.hero[data-drawn="true"] > .hero__rule,
.hero:has(.hero .mapping[data-drawn="true"]) > .hero__rule {
  animation: rule-draw 520ms var(--ease-out) 0ms both;
}

.hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path {
  animation: device-draw 420ms var(--ease-draw) both;
}
/* Order is START, TAIL, ARCH. Not left to right, and this is the most important decision
   in the sequence. The book puts the arch guide at 62.5 percent of the span, so the arch
   is derived from the two ends rather than placed. Drawing the two ends first and the arch
   last, after a 60ms hold, is the whole proprietary claim in one gesture. Left to right
   would say the three lines are decoration. That 60ms is the only pause anywhere in the
   sequence, and it reads as a calculation. Do not add pauses elsewhere. */
.hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--start { animation-delay: 520ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--tail  { animation-delay: 880ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--arch  { animation-delay: 1300ms; }

.hero .mapping[data-motion="on"][data-drawn="true"] .m-outline {
  animation: device-draw 960ms var(--ease-loop) 1720ms both;
}
/* The fill arrives at 2240ms, while the outline is still travelling to 2680ms, so the
   shape gains body rather than switching on. */
.hero .mapping[data-motion="on"][data-drawn="true"] .m-fill {
  animation: device-fill-in 640ms var(--ease-out) 2240ms both;
}

/* Hairstrokes, head to tail, which is the order the paths are authored in and the order
   the work happens in the chair: near vertical at the head, laying down toward the tail.
   58ms apart is the rate at which 36 strokes read as a hand working rather than as a
   slideshow or a flicker. The phase runs 2680 to 4890, which is the longest single
   movement on the site and should be: it is the part the studio charges for. */
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path {
  animation: device-stroke 180ms var(--ease-move) both;
}
/* A paint containment hint on the stroke group, and it is here instead of will-change on
   purpose: will-change would promote about forty layers for a single 2.2 second phase. */
.hero .mapping .m-strokes { contain: paint; }

.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(1)  { animation-delay: 2680ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(2)  { animation-delay: 2738ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(3)  { animation-delay: 2796ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(4)  { animation-delay: 2854ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(5)  { animation-delay: 2912ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(6)  { animation-delay: 2970ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(7)  { animation-delay: 3028ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(8)  { animation-delay: 3086ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(9)  { animation-delay: 3144ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(10) { animation-delay: 3202ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(11) { animation-delay: 3260ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(12) { animation-delay: 3318ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(13) { animation-delay: 3376ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(14) { animation-delay: 3434ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(15) { animation-delay: 3492ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(16) { animation-delay: 3550ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(17) { animation-delay: 3608ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(18) { animation-delay: 3666ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(19) { animation-delay: 3724ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(20) { animation-delay: 3782ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(21) { animation-delay: 3840ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(22) { animation-delay: 3898ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(23) { animation-delay: 3956ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(24) { animation-delay: 4014ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(25) { animation-delay: 4072ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(26) { animation-delay: 4130ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(27) { animation-delay: 4188ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(28) { animation-delay: 4246ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(29) { animation-delay: 4304ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(30) { animation-delay: 4362ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(31) { animation-delay: 4420ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(32) { animation-delay: 4478ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(33) { animation-delay: 4536ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(34) { animation-delay: 4594ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(35) { animation-delay: 4652ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-strokes path:nth-child(36) { animation-delay: 4710ms; }

/* The tick easing is the only overshoot on the site. A tick is a stamp; everything else
   arrives and stops. transform-box: fill-box makes the scale happen about the tick's own
   centre rather than about the SVG origin. The ticks follow the guides' order, start,
   tail, arch, so the arch is marked last for the same reason it is drawn last. */
.hero .mapping[data-motion="on"][data-drawn="true"] .m-tick path {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: device-tick-in 240ms var(--ease-mark) both;
}
.hero .mapping[data-motion="on"][data-drawn="true"] .m-tick path.m-tick--start { animation-delay: 4890ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-tick path.m-tick--tail  { animation-delay: 4980ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-tick path.m-tick--arch  { animation-delay: 5070ms; }

/* Labels trail their ticks by 120ms, so each mark is named just after it is made. The 4px
   travel is in viewBox units, which is what an SVG transform speaks. */
.hero .mapping[data-motion="on"][data-drawn="true"] .m-cap text {
  transform-box: fill-box;
  animation: device-cap-in 280ms var(--ease-out) both;
}
.hero .mapping[data-motion="on"][data-drawn="true"] .m-cap text.m-cap--start { animation-delay: 5010ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-cap text.m-cap--tail  { animation-delay: 5100ms; }
.hero .mapping[data-motion="on"][data-drawn="true"] .m-cap text.m-cap--arch  { animation-delay: 5190ms; }

/* --- the sequence, mobile. Total 1880ms, five moving parts instead of 41 ---
   Below 880px there are no hairstrokes, no ticks and no labels, so the guide stagger drops
   from 360 to 220 and the arch hold is removed. One third the running time, one eighth the
   animated elements, same order and same argument. */
@media (max-width: 879px) {
  .hero[data-drawn="true"] > .hero__rule,
  .hero:has(.hero .mapping[data-drawn="true"]) > .hero__rule {
    animation-duration: 380ms;
  }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path { animation-duration: 300ms; }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--start { animation-delay: 380ms; }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--tail  { animation-delay: 600ms; }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-guide path.m-guide--arch  { animation-delay: 820ms; }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-outline {
    animation-duration: 620ms;
    animation-delay: 1120ms;
  }
  .hero .mapping[data-motion="on"][data-drawn="true"] .m-fill {
    animation-duration: 480ms;
    animation-delay: 1400ms;
  }
}

/* ==========================================================================
   13  PHOTOGRAPHY
   The brand's entire argument is that it publishes healed results nobody else publishes,
   so the photographs carry the page rather than decorate it. Five published frames at
   launch, deliberately small, whole faces where whole faces exist.

   Ground rules, book pages 13, 14 and 18:
     1. No photograph appears in a hero or above the first band boundary on any page.
        Photography enters the page only lower, healed and labelled.
     2. Nothing is ever written on the pixels. All labelling lives in the caption bar,
        outside the image.
     3. The lockup never sits over photography, at any opacity.
     4. Every published photograph carries a stage bar, without exception. The label is
        the claim, so an unlabelled image is an unmade claim wearing a made one.
     5. Both panes of a before and after pair carry their own bar. A pair with one bar is
        how a before frame gets read as a result.

   The markup contract for every image inside .shot:
     width and height attributes carrying the file's true intrinsic pixels, so the box is
     reserved before the bytes arrive; loading="lazy" and decoding="async", which rule 1
     makes safe on every image on this site; and an accurate sizes attribute.
   ========================================================================== */

/* A ROW OF EQUAL FRAMES, and the reason it is a component rather than a set of spans.

   The obvious way to lay three illustrations across a band is to give each figure
   data-span="4" inside a data-grid="12" band. That is wrong, and it is wrong in a way that
   only shows above the measure, which is why it can be shipped without being seen.

   The grid mode track list in section 05 is fourteen tracks: a gutter, the twelve, and a
   gutter. Numeric data-span rules resolve to "grid-column: span N" with no start line, so
   the item is AUTO PLACED, and auto placement seats the first item of a row at track one,
   which is the LEFT GUTTER. On a 1905px viewport that gutter is 328px, so the first frame
   started at the window edge, was 250px wider than its neighbours, and dragged the whole
   row a full track left of the text above it. Every following frame inherited the offset.

   A row of frames therefore sits on the content column as ONE child and divides itself.
   The division is its own business, the band never sees it, and no auto placement is
   involved at any width. Below 880px it collapses to a single column, which is the same
   place the grid mode spans collapse to and the correct layout for a phone. */
.figrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-7) var(--gap);
}
@media (min-width: 880px) {
  .figrow[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .figrow[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.shot {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--ar, var(--ar-portrait));
  background: var(--wash);   /* a slow image is a quiet band, never white */
}
.shot > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--ox, 50%) var(--oy, 50%);
  display: block;
}

/* --- the crops. Every number below is measured from the file, not estimated ---

   brow-pair-macro.jpg, 1280 x 1584. The strongest asset on the site: a vertical composite
   with the frame seam at y 855, the natural brow band at y 210 to 380, the hairstroke brow
   band at y 1030 to 1250, and another studio's "bmsh" watermark at y 1412 to 1516.

   Both panes are cut from the one file at 8:3. Window height is 1280 / (8/3) = 480px and
   the pannable range is 1584 - 480 = 1104px, so --oy is a percentage of 1104.

   BEFORE at 4 percent  opens the window at y 44 to 524. The brow sits at 35 to 70 percent
                        of the pane and the closed eye at y 580 is excluded.
   AFTER  at 78 percent opens the window at y 861 to 1341. The brow sits at 35 to 81
                        percent, the seam at 855 is excluded by 6px, and the watermark's
                        top edge at y 1412 is 71px below the window's bottom edge.

   The watermark is removed by geometry, never by masking and never by stretching. Do not
   raise --oy above 78 percent without recomputing: at 82 percent the window bottom reaches
   1385 and the margin falls to 27px.

   The eye is dropped from both panes, which is the stronger composition and is also what
   makes the crop safe. Full width is retained in both: the book is explicit that framing a
   brow tightly enough to remove ethnicity is a lie told with a lens, so the temple, the
   hairline and the skin stay in frame. */
.shot--brow-before { --ar: var(--ar-band); --ox: 50%; --oy: 4%; }
.shot--brow-after  { --ar: var(--ar-band); --ox: 50%; --oy: 78%; }

/* The lip illustrations, 1408 x 768. The portrait source this rule was written for
   was retired on 2026-08-29, and these are already framed on the mouth, philtrum to
   chin, so there is nothing to pan to. At 1.83 they are slightly wider than the 16:9
   band, so cover trims a little width and the vertical position is inert. Centre on
   both axes and let the ratio do the work. */
.shot--lip-macro { --ar: var(--ar-macro); --ox: 50%; --oy: 50%; }

/* portrait-neutral.jpg, 1280 x 1920. Closest to the book's target style: grey seamless,
   sharp even light, skin texture intact, brows clearly legible.
   Window 1280 x 1600, pannable range 320px. 96 percent opens y 307 to 1907, which puts
   the eyes at 40 percent of the frame and the chin at 68 percent, and keeps the hand at
   the chin in. Faces are shown whole where consent allows; macro crops are for detail,
   not for hiding who the client is. */
.shot--portrait { --ar: var(--ar-portrait); --ox: 50%; --oy: 96%; }

/* portrait-black-b.jpg, 1280 x 1564. Editorial, direct gaze, near black ground.
   The square crop is head and shoulders and it drops the red pendant at y 1450, whose
   colour is the only note in the file outside the palette. */
.shot--portrait-dark { --ar: var(--ar-square); --ox: 50%; --oy: 0%; }

/* laney-portrait.png, 532 x 666. The interim portrait, about page only. The file's own
   aspect is 0.7988, which is 4:5 within a pixel, so the crop is the full frame.
   266px is exactly 2x the 532px source, and anything wider is soft on a retina screen.
   It is also right editorially: an interim image should be modest and sit on a Paper
   panel beside the PENDING tag, not carry a page. */
.shot--interim { --ar: var(--ar-portrait); --ox: 50%; --oy: 50%; max-width: 266px; }

/* --- the stage bar -----------------------------------------------------
   Book page 14 anatomises it as stage, then service and tier, then location. Location is
   dropped on the website because the footer carries the address on every page. Two lines.

   The bar takes no ground of its own, so on the default Ivory band it is Ivory, exactly as
   the book describes, and on a Paper panel or a Wash band it is that ground instead. */
.stage {
  background: none;
  padding-top: var(--s-3);
}
.stage__line {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
/* The stage. Charcoal soft by default, because BEFORE is not a claim, DAY 1 is not a
   result, and STAGE / PENDING is the book's own tag for a fact not yet confirmed. */
.stage .stage__line.pending,
.stage__line.pending {
  display: block;
  background: none;
  border: 0;
  padding: 0;
  vertical-align: baseline;
  line-height: var(--lh-kicker);
}
/* INK: Charcoal soft, kept. The stage bar is the case the ink was reserved for. */
.stage__line:first-child {
  font-weight: 600;
  color: var(--charcoal-soft);
}
/* Deep gold only on HEALED / WEEK 6 and HEALED / WEEK 8. The book says the label is the
   claim, and gold is the working accent, so it marks the one claim the studio actually
   makes. Do not put this class on a file whose stage the owner has not confirmed: the
   studio's entire position rests on that label being true. */
.stage--healed .stage .stage__line.pending,
.stage__line.pending {
  display: block;
  background: none;
  border: 0;
  padding: 0;
  vertical-align: baseline;
  line-height: var(--lh-kicker);
}
.stage--healed .stage__line:first-child { color: var(--deep-gold); }

/* The service and tier line. INK: Charcoal soft, kept. Second line of a stage bar. */
.stage__line + .stage__line {
  font-weight: 500;
  color: var(--charcoal-soft);
  margin-top: var(--s-1);
}

/* A hairline only where the image edge is not already the separation. On Ivory the edge
   does the work, and the surface ratio allots Gold line and Rule 5 percent between them,
   so a hairline is not spent where an edge already exists. */
.band[data-tone="paper"] .stage,
.band[data-tone="wash"] .stage,
.card .stage {
  border-top: 1px solid var(--rule);
}

/* --- the pair ----------------------------------------------------------
   Book page 14: before and after sit side by side, same crop, same light, split by a 1px
   Rule line. The dividing line is a real 1px grid track, not a border on a child, so it is
   exactly 1px and can never double. It spans the image row only, so the two caption bars
   remain separate objects and a pane can never borrow its neighbour's label. */
.pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: 0;
}
.pair > figure {
  display: grid;
  grid-template-rows: subgrid;   /* the shot lands in row 1, the stage bar in row 2 */
  grid-row: span 2;
}
.pair > figure:first-of-type { grid-column: 1; }
.pair__rule { grid-column: 2; grid-row: 1; background: var(--rule); }
.pair > figure:last-of-type  { grid-column: 3; }

@supports not (grid-template-rows: subgrid) {
  .pair { grid-template-rows: auto; }
  .pair > figure { display: block; grid-row: auto; }
  .pair__rule { grid-row: auto; }
}

/* Below 720px the pair stacks and the rule becomes a horizontal hairline between the two.
   Each pane keeps its own bar, so a stacked pair still cannot be misread. */
@media (max-width: 719px) {
  .pair { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; }
  /* Both :first-of-type and :last-of-type have to be named here. The two column rule that
     sends the second pane to column 3 outweighs a bare .pair > figure, so without this the
     stacked pane would keep asking for a column that no longer exists and the grid would
     grow an implicit one to hold it. */
  .pair > figure,
  .pair > figure:first-of-type,
  .pair > figure:last-of-type { display: block; grid-row: auto; grid-column: 1; }
  .pair__rule { grid-column: 1; grid-row: auto; height: 1px; margin-top: var(--s-5); }
  .pair > figure:last-of-type { margin-top: var(--s-5); }
}

/* Full bleed is permitted in exactly one circumstance on this site: the photograph's own
   ground is black and it sits inside a Charcoal band, so there is no visible edge to
   bleed. Everywhere else a photograph is an object on a page, not a background, because a
   full bleed beauty photograph is the exact register the book refuses. The stage bar for
   a bled photograph stays inside the 1200px column. */
.band[data-tone="charcoal"] > .shot[data-span="full"] { background: var(--charcoal); }

/* ==========================================================================
   14  CARDS
   Paper only for cards. Three across on the 12 column grid, four columns each.
   Paper is also the ground for an artifact, which the book defines as cards, artifacts,
   anything handed over. On this site exactly one photograph qualifies: the interim
   portrait, which is a document rather than a published result.
   ========================================================================== */

.cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap);
}

.card {
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: clamp(1rem, 2vw, 1.5rem);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.card__label {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--deep-gold);
}

.card__text {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--charcoal);
  max-width: none;
}

/* A photo panel: the image and its bar both sit inside the Paper border, and the card's
   own gap is dropped so the bar's 12px is the only interval between them. */
.card > .shot + .stage { margin-top: calc(var(--s-3) * -1); }

@media (max-width: 860px) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .cards { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   15  SERVICE BLOCK
   The three from-price teasers. A .svc stands alone or sits inside .cards.
   Every price on this site is Montserrat and every column of prices lines up.
   ========================================================================== */

.svc {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  text-decoration: none;
}
.cards > .svc {
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: clamp(1rem, 2vw, 1.5rem);
}

/* THE CARD SPLIT IN TWO, SO THE LINK STYLING MOVED WITH THE LINK.

   .svc is still an anchor on home, where the whole block is one link and carries no
   picture and no button. On services.html it is an <article> holding .svc__body, the
   link to the service page, and .svc__cta, the link to the request form. Everything
   that was true of .svc as an anchor is therefore restated on .svc__body rather than
   deleted, and .svc keeps its own rules for home. */
.svc__body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  text-decoration: none;
  color: inherit;
  flex: 1 0 auto;
}

/* The button is pinned to the foot of the card by the auto margin, so three cards
   carrying three different lengths of sentence still line their buttons up. Without
   it the shortest card's button floats halfway up the row and the set reads ragged. */
.svc__cta {
  margin-top: auto;
  width: 100%;
  justify-content: center;
}

.svc__name {
  font-family: var(--display-face);
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  font-weight: 400;
  color: var(--charcoal);
}

/* Brought up to the sheet's system: 20px Montserrat 600, tabular and lining, so the home
   page's from-prices and the three sheet pages share one figure treatment. */
.svc__price {
  font-family: var(--label-face);
  font-size: var(--fs-name);
  font-weight: 600;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--charcoal);
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-top: var(--s-1);
}

/* The measure comes back on. A .svc inside .cards sits in a 336px column and never
   reaches it, which is why it was switched off. On home the three services are full width
   children of a band, and a centred line running the whole 1200px column is the thing the
   narrow measure exists to prevent. */
.svc__desc {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--charcoal);
  max-width: var(--text-measure);
  flex: 1 0 auto;
}

/* The word FROM next to a from-price, and the deposit line. Labels, not figures, but they
   sit in a figure column so they take the same numeric treatment.
   INK: Charcoal soft, kept. Four tracked uppercase characters qualifying a number. */
.svc__from,
.svc__dep {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--charcoal-soft);
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.svc__dep { margin-left: auto; }

/* ==========================================================================
   16  PRICE SHEET
   The most read content on the site. What made it plain was not the hairlines: it was that
   the number was smaller than the name, that the numbers did not form a column, and that
   the rows were cramped.

   NO ROW IS HIGHLIGHTED, AND THAT IS ENFORCED HERE, NOT REQUESTED.
   Book page 21: tinting the top tier reads as steering, and four equal hairlines say the
   tier is a diagnosis, not an upsell. So there is deliberately no modifier on .sheet__row,
   no attribute selector on a row, no nth-child or first-child or last-child styling of a
   row beyond the border reset on the last one, no background, tint, border weight change
   or font weight change on any single row, and no hover state on a row at all, including
   the cursor. The rows are not interactive; making them look interactive is the same
   upsell pattern wearing a different costume.
   ========================================================================== */

.sheet {
  border-top: 1px solid var(--rule);
  /* 8 of the 12 columns, which is 792px at full measure. This puts the sheet on the grid
     rather than near it: the old 56rem cap was 896px, or 8.8 columns of nothing. */
  /* Widens to 10 of 12 once there is room for it, so the deposit column sits further
     into the grid instead of leaving the right third of a wide screen empty. */
  max-width: var(--w-8);
}

.sheet__row {
  display: grid;
  /* The price track is fixed, so the digits sit at the same x position on the brows sheet,
     the lip sheet and the eyeliner sheet. Three pages, one column. That cross page
     alignment is the detail that says one studio, one sheet, and it costs one value. */
  grid-template-columns: minmax(0, 1fr) 9rem;
  column-gap: var(--s-6);
  align-items: baseline;
  padding-block: var(--s-5);   /* 24 top and bottom, up from 18.4 */
  border-bottom: 1px solid var(--rule);
}

.sheet__name {
  font-family: var(--display-face);
  font-size: var(--fs-name);   /* was 20, now 22 */
  line-height: 1.35;
  font-weight: 400;
  color: var(--charcoal);
}

/* INK: Charcoal. This is the sentence that decides which tier a client asks for, so it is
   the most closely read prose on the site's most read page. It is set small because it
   belongs to the row above it, not because it is optional. */
.sheet__desc {
  grid-column: 1;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--charcoal);
  max-width: 46ch;
  margin-top: var(--s-3);
}

/* Prices always align in clean columns; that reads as honesty. Book page 09. That
   sentence is a specification, and the two track grid below is what satisfying it costs.

   22px against a 20px serif name: the number wins on size and the serif wins on colour
   mass, so neither dominates the row. Not hidden, not shouted. */
.sheet__price {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  font-family: var(--label-face);
  font-size: var(--fs-price);   /* was 22, now 24 */
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--charcoal);
  white-space: nowrap;
  text-align: right;
}

/* Right aligning $688 and $1,088 lines up the last digit but leaves the dollar signs
   ragged, so the eye reads trailing text rather than a column. Splitting the cell puts
   every dollar sign on one left edge and every digit group on one right edge.

   The digit track is a FIXED 4.75ch, not a minmax. That is the whole point: an auto track
   grows for $1,088 and shrinks for $688, which right aligns the digits correctly but then
   drags the dollar sign along with it and leaves the currency ragged again, which is the
   defect this grid exists to remove. 4.75ch holds the widest published figure, four
   tabular digits plus a comma, which measures about 4.47ch in Montserrat 600; ch is used
   rather than pixels so the track tracks the font size on the small screen step below.

   The rule is conditional so a sheet row that has not been given a currency span still
   sets correctly as plain right aligned text. */
/* The book sets prices as solid Montserrat units. An earlier two column grid right
   aligned the digits inside a 4.75ch track, which stranded the dollar sign about two
   characters from a three digit price. The price is one unit, right aligned; tabular
   numerals keep the right edges lining up down the column. */
.sheet__price:has(> .sheet__cur) {
  display: block;
  white-space: nowrap;
}
/* INK: Charcoal. The dollar sign is not a caption and not a label. It is the first
   character of a 22px figure, and setting it two shades lighter than the digits beside it
   was the exact "washed out" effect on the exact number the page exists to publish. One
   ink, one unit. It is still the lighter weight, which is what keeps the digits leading. */
.sheet__cur {
  color: var(--charcoal);
  font-weight: 500;
  text-align: left;
}

/* The deposit prints beside the price, every row, per policy 02.
   INK: Charcoal soft, kept. "DEP $200" is a tracked uppercase label under a figure, and it
   has to stay a step behind the price or the row grows a second headline. */
.sheet__dep {
  grid-column: 2;
  justify-self: end;
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 500;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--charcoal-soft);
  white-space: nowrap;
  margin-top: var(--s-2);
  text-align: right;
}

/* The closing line under a sheet. Every published sheet has one.
   INK: Charcoal. It is a paragraph, and it carries the fact that the consultation is
   included, which is a thing the studio wants read rather than skimmed. */
.sheet__close {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--charcoal);
  max-width: 52ch;
  margin-top: var(--s-6);
}

/* THE 8 COLUMN CAP HOLDS ONLY WHERE THERE ARE 8 COLUMNS TO HOLD IT TO.
   --w-8 exists to seat the sheet on the 12 column grid, and that grid does not apply below
   880px: section 05 is explicit that fourteen tracks cannot be drawn on a narrow screen.
   Between 620 and 880 the cap was therefore aligning the sheet to a grid that was not
   there, and costing it a third of a column it could have used. At 768 that left 296px for
   the name track, 9px short of holding "Severe dark-lip neutralization" on one line, so
   three rows of the lip sheet broke with a single word on a second line. Dropping the cap
   gives the name 544px and all three set on one line. This is the same argument the 620
   block below already makes, applied at the width where it actually starts being true. */
@media (max-width: 879px) {
  .sheet { max-width: none; }
}

/* The price column never collapses. Dropping the price below the name and left aligning it
   turns the sheet into a list and destroys the one thing the sheet is for. At 360px this
   leaves about 184px for the name, so the longest name wraps to three lines. A price list
   that stays a price list on a phone is worth three lines of wrap. */
@media (max-width: 619px) {
  /* --w-8 is 8 of the 12 columns OF ITS CONTAINER, and its container here is the content
     column, not the page. That is right at 1200px, where it resolves to 792px and puts the
     sheet on the grid. It is wrong on a phone: at 390px the content column is 342px, so the
     cap resolved to 220px and the sheet gave up a third of the screen it did not have. The
     name track fell to 92px, "Soft Powder Brows" wrapped to three lines and its description
     to six, and the one page a client reads for the number became the hardest to read.

     Below this breakpoint there is no 12 column grid left to sit on, so the cap has nothing
     to align to and is dropped. The sheet takes the content column, which is what the
     column is for. The fixed price track above then has 214px to work against instead of
     92px, and the longest published name wraps to two lines rather than four.
     The cap itself now comes off at 880 rather than at 620; see the block above this one. */

  /* THE NAME TRACK BUYS BACK WHAT THE RAISED SCALE COSTS IT.
     At 320px the content column is 272px, and a 6.5rem price track with a 24px gap left
     144px for the name. At the raised name step "Youthful Natural Hairstroke Brows" went
     to four lines and every brow row ended on the orphaned word "Brows", which is exactly
     the shape of the round 3 note about single words on a second line.

     The price track was carrying slack it did not need: the widest published figure is
     $1,088, which measures about 71px at the raised price step, so 5.9rem holds it with
     23px to spare. Taking that slack and half the gap gives the name 162px, and measured
     on the built pages that is the difference between four lines and two on the longest
     name, and between two and one on the shortest. */
  .sheet__row {
    grid-template-columns: minmax(0, 1fr) 5.9rem;
    column-gap: var(--s-4);
  }
  /* The two steps come down for the narrow column, by redeclaring the tokens on the
     component rather than by overriding the rules that read them. The name lands at 19
     rather than 20: measured against the 162px track above, 19 is the largest step at
     which the longest name still sets in two lines. */
  .sheet { --fs-name: 1.1875rem; --fs-price: 1.375rem; }   /* was 18 and 20, now 19 and 22 */

  /* THE DESCRIPTION TAKES THE WHOLE ROW ON A PHONE, AND THE DEPOSIT GOES UNDER THE PRICE.

     The price column never collapses, and the block above is right that it must not. But
     the description was paying for the price track and it should never have been. It sat
     in column 1 of the second grid row, so on a phone it was reading in the width left
     over after a fixed 94px number track and a 16px gap. Measured on the built pricing
     page: 202px at 360px and 232px at 390px, which is 21 to 29 characters a line. That is
     about half the measure prose needs, on the sentence this file's own note calls the
     one that decides which tier a client asks for. The top row ran to six lines at 360px.
     Beside it, in the same grid row, sat the deposit: an 82px string in a 94px track, with
     the 110px to the left of it empty for the full four to six lines of the description.
     A phone screen cannot afford a dead rectangle that size, and the copy could not afford
     the lines it was costing.

     Three explicit placements fix it, and only below 620px. The name and the price keep
     row one, so the number column is exactly as it was. The deposit moves to row two of
     the number column, which is where it belongs anyway: it is the second fact about the
     price, not a note beside the description. The description takes row three across both
     tracks, so it reads at the full width of the sheet.

     Measured after: 312px and 40 to 48 characters a line at 360px, and the six line row
     comes down to four. Nothing at 620px and above moves, and the 46ch cap on the
     description still holds the measure at the top of this range so the sentence does not
     run wider than it should at 619px. */
  .sheet__dep  { grid-row: 2; }
  .sheet__desc { grid-column: 1 / -1; grid-row: 3; }

  /* Every published deposit is "DEP $100" or "DEP $200", which is 82px and sits inside the
     94px track. One row does not: the lip lightening row prints "PER SESSION / DEP $100",
     which is 198px. nowrap held it on one line, justify-self: end anchored its right edge,
     and the surplus ran leftwards across whatever shared that grid row.

     Wrapping is still the correct release rather than a wider track, and it is still
     needed now that the deposit has a row of its own: the track is fixed on purpose so the
     digits column holds across the three sheets, and widening it for one row would drag
     the other eleven off the column to solve a problem they do not have. The two halves of
     the longest deposit stack inside the track instead, still right aligned. */
  .sheet__dep { white-space: normal; }
}

/* --- THE PHOTOGRAPHS UNDER A ROW ------------------------------------------
   Added 2026-09-28: a row that has photographs of its own service carries a small "+"
   that opens them. A row with no photograph of its own service has no "+" rather than a
   borrowed one, and nothing marks its absence.

   THE ROW IS STILL NOT INTERACTIVE, AND NO ROW IS WEIGHTED. The rules at the top of this
   section stand: no tint, no weight, no hover and no cursor on a row. Only the summary is
   a control, it is the same size, ink and weight on every row that has one, and it takes
   the site's link ink and the site's one focus ring, like every other control on the page.

   A NATIVE <details>, NOT A SCRIPT. It opens with Enter and Space, announces its own state
   and works with scripting off. Its images are loading="lazy", so a browser running scripts
   fetches none of them until a reader opens the row and the page still loads as a document.
   With scripting off, browsers ignore loading="lazy" (the HTML spec requires it), so there
   every menu photograph downloads with the page while the panels stay closed.

   AN OPEN PANEL STOPS ITS BAND'S ARRIVAL. The band arrival under MOTION ends at "cover
   16%", a point that moves with the band's height. Opened, these panels make a sheet band
   two to three times taller, and the band would sit half transparent while it is being
   read. So a band holding an open panel does not animate at all; the rule sits with the
   arrival, under MOTION, 1. ARRIVAL.

   THE FRAMES ARE SHOWN WHOLE. Every tile is the same 3:4 box on a Wash ground with
   object-fit contain, so a tall three panel composite, a landscape pair and a single
   portrait all sit in one box shape without a crop. Most lip befores are small insets near
   an edge of the frame, and a cover crop would cut exactly the part that makes the photo a
   before and after. Each frame carries a stage line saying where the before is.

   Phones: one row of tiles that scrolls sideways and snaps, with the next tile showing at
   the edge so it reads as more. 620px and up, where the sheet itself changes layout: a
   small grid that wraps. */
.sheet__photos {
  grid-column: 1 / -1;
  margin-top: var(--s-3);
}

.sheet__photos > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: var(--tap);          /* 44px, the minimum tap target */
  cursor: pointer;
  list-style: none;
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--deep-gold);
}
.sheet__photos > summary::-webkit-details-marker { display: none; }
.sheet__photos > summary:hover { color: var(--charcoal); }

/* The sign. One square in a Gold line hairline, with the plus drawn in the summary's own
   ink as two bars. Open, the upright bar folds to nothing and the plus reads as a minus.
   Drawn rather than typed, so the two states sit on the same centre and the same weight. */
.sheet__photos > summary::before {
  content: "";
  flex: 0 0 auto;
  width: 1.375rem;
  height: 1.375rem;
  border: 1px solid var(--gold-line);
  background:
    linear-gradient(currentColor, currentColor) center / 0.625rem 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 0.625rem no-repeat;
  transition: background-size var(--t-fast) var(--ease-move);
}
.sheet__photos[open] > summary::before {
  background-size: 0.625rem 1.5px, 1.5px 0;
}

.sheet__frames {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(78%, 17rem);
  gap: var(--s-4);
  margin-top: var(--s-3);
  padding-bottom: var(--s-3);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
}
.sheet__frames > figure {
  min-width: 0;
  scroll-snap-align: start;
}
.sheet__frames img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: contain;
  background: var(--wash);
}

@media (min-width: 620px) {
  .sheet__frames {
    grid-auto-flow: row;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: var(--s-5) var(--s-4);
    padding-bottom: 0;
    overflow-x: visible;
    scroll-snap-type: none;
  }
}

/* Tracking on Chinese labels, as on the kicker: wide tracking splits the characters. */
:lang(zh-Hans) .sheet__photos > summary { letter-spacing: 0.06em; }

/* A printed sheet is the rows, the deposits, the guarantee and the policies. */
@media print {
  .sheet__photos { display: none; }
}

/* ==========================================================================
   17  THE GUARANTEE
   Bank sentence 02, verbatim, on every pricing surface. It is the answer to the fear the
   prices raise, so it sits with them and not in a policy footnote. One of the two legal
   Champagne appearances on a page: a 2px edge, carrying no words.
   ========================================================================== */

.guarantee {
  background: var(--wash);
  border-left: 2px solid var(--champagne);
  padding: clamp(1.25rem, 2.6vw, 1.75rem) clamp(1.25rem, 2.6vw, 2rem);
  max-width: 46rem;
}
/* The auto margins are declared here rather than only in the centring section, because the
   guarantee is the one component on the site that ships as a bare section under .page
   instead of inside a band, so the band scoped rule cannot reach it and the sentence sat
   against the panel's left padding while the panel itself was centred. */
/* :not(.kicker) IS LOAD BEARING, AND THE BUG IT FIXES WAS VISIBLE ON TWO PAGES.

   The panel ships with its label as an h2 on brows, lip-blush and eyeliner, where it is a
   labelled landmark, and as a p on pricing and on visit, where it is not. Both are right.
   But a bare `.guarantee p` is (0,1,1) and `.kicker` is (0,1,0), so on the two p pages
   this rule was reaching the label and beating its own class: measured at 1440, the
   label set at 20.75px in Charcoal with 4.32px of tracking, against 12px in Deep gold
   with 2.5px everywhere else on the site. The tracking scaled with it because the token
   is in em. That put a 20.75px uppercase line directly above a 20.75px sentence, so the
   panel read as two headings and the label stopped looking like a label.

   Excluding the class is the fix rather than raising the kicker's specificity, because
   the kicker is the site's smallest and most reused vocabulary item and it should not
   have to defend itself against every component that styles a paragraph. */
.guarantee p:not(.kicker) {
  font-size: var(--fs-lede);
  line-height: 1.6;
  color: var(--charcoal);
  max-width: 48ch;
  margin-inline: auto;
}
.guarantee .kicker { margin-bottom: var(--s-3); }

/* The guarantee ships as a bare section rather than a .band, so section 06's
   `.page > *:not(.band)` reached it first. That selector is (0,2,0) against this block's
   (0,1,0), so it was overriding BOTH declarations above: max-width became 1392px instead
   of 46rem and padding-inline became a 96px gutter instead of the authored clamp. The
   Wash ground stretched to 1392px and the 2px Champagne edge landed 96px OUTSIDE the
   1200px content column, so on brows, lip-blush, eyeliner and pricing the one panel that
   answers the fear the prices raise was the only element on the page not on the column.

   This restores the authored panel at (0,2,0) and later in the file, so it wins the tie.

   ROUND 3: it used to start on the column's left edge, with the surplus width falling off
   its right. Under the centring system it takes equal margins instead, so the panel sits
   in the middle of the page like everything above and below it. The max() floor is still
   there in the shorthand's own way: the cap subtracts two gutters, so the panel can never
   reach an edge. */
.page > .guarantee {
  max-width: min(46rem, 100% - var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 2.6vw, 2rem);
}

/* ==========================================================================
   18  POLICIES 01 TO 04
   Identical across all three services, printed once per pricing surface.
   ========================================================================== */

.policies {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gap);
}

.policy {
  border-top: 1px solid var(--rule);
  padding-top: 0.9rem;
}

.policy__n {
  display: block;
  font-family: var(--label-face);
  font-size: var(--fs-nav);
  font-weight: 600;
  letter-spacing: var(--track-label);
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--deep-gold);
  margin-bottom: 0.45rem;
}

.policy__text {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--charcoal);
  max-width: none;
}

@media (max-width: 860px) { .policies { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .policies { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   19  THE HEALING TIMELINE
   Day 01 to week 6, on a gold construction rail. The day numbers are Montserrat and are
   never restyled per surface.
   ========================================================================== */

.timeline {
  border-left: 1px solid var(--gold-line);
  padding-left: clamp(1.25rem, 3vw, 2rem);
  max-width: 46rem;
}

.step {
  position: relative;
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0.35rem var(--gap);
  padding: 1.05rem 0;
  border-bottom: 1px solid var(--rule);
}
.step:last-child { border-bottom: 0; }

/* The cross tick where the rail meets the step, in Deep gold, as on the mapping device. */
.step::before {
  content: "";
  position: absolute;
  left: calc(clamp(1.25rem, 3vw, 2rem) * -1);
  top: 1.55rem;
  width: 0.75rem;
  height: 1px;
  background: var(--deep-gold);
}

.step__day {
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  font-weight: 600;
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums lining;
  font-feature-settings: "tnum" 1, "lnum" 1;
  color: var(--deep-gold);
}

.step__note {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--charcoal);
  max-width: 46ch;
}

@media (max-width: 560px) {
  .step { grid-template-columns: minmax(0, 1fr); }
  .step::before { top: 1.4rem; }
}

/* ==========================================================================
   20  CONTRAINDICATIONS
   Permanent makeup is not for everyone. This is the page that says so plainly, so it is
   set to be read, not skimmed past.
   ========================================================================== */

.contra {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-6) var(--gap);
  max-width: 56rem;
}

.contra__group {
  border-top: 1px solid var(--rule);
  padding-top: var(--s-4);
}
.contra__group .kicker { margin-bottom: var(--s-3); }

.contra__list li {
  position: relative;
  padding-left: 1.5rem;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--charcoal);
  max-width: 44ch;
}
.contra__list li + li { margin-top: 0.6rem; }
/* Rule, not Champagne. This dash used to be Champagne on every item, and the fit page
   carries fifteen or more, which is roughly five times Champagne's 3 percent allowance and
   turns a clinical page gold. Champagne is jewellery, not paint. */
.contra__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.8em;
  width: 0.6rem;
  height: 1px;
  background: var(--rule);
}

@media (max-width: 700px) { .contra { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   21  SPEC LIST
   Address, hours, what to bring, what happens on the day. Label above, fact below.
   ========================================================================== */

.spec {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--gap);
  max-width: 56rem;
}

.spec__item {
  border-top: 1px solid var(--rule);
  padding: 1.1rem 0;
}
.spec__item .kicker { margin-bottom: var(--s-2); }
.spec__item p { max-width: 40ch; }

@media (max-width: 620px) { .spec { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   22  BUTTONS AND THE CTA SYSTEM
   The book's constraints are fixed: Deep gold ground, Paper Montserrat labels, squared
   corners, no shadow, no rounding, hover darkens to Charcoal, 44px minimum. Everything
   considered has to happen inside those.

   Which variant, and it is a rule rather than a preference:
     PRIMARY   .btn        the thing the studio wants next. One per band, one above the
                           fold per page. See pricing, Notify, Join the notify list.
     SECONDARY .btn--ghost an equally valid alternative route. At most two per band, never
                           alone in a band. Fit and contraindications, The healing
                           timeline, cross links between service pages.
     TERTIARY  .btn--quiet a continuation of something already being read. Unlimited, but
                           never the only action in a band.

   Label case: write the label in the fragment in sentence case and let text-transform do
   the capitals. The DOM text stays readable, the accessible name is normal language, and
   a screen reader does not spell out a short all caps string.
   ========================================================================== */

/* Any group of two or more actions. This also fixes a live defect: two ghost buttons
   inside one paragraph with no gap rule touch each other. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--s-6);
  row-gap: var(--s-4);
}

/* 12px in a 44px bar, not 13px. The old button filled its box and read as a stubby brick.
   Dropping the label to 12 and widening the horizontal padding from 25.6 to 36 makes the
   label roughly 180px wide inside a 44px bar: more ground per unit of type, which is what
   premium resolves to in a control. The label is smaller and the object is larger.
   12 + 16 + 16 is 44 exactly, so line-height 1 is deliberate. */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 1rem 2.25rem;
  /* Absorb the trailing letter-space, so the tracking does not visually pull the label
     left inside its own box. */
  padding-right: calc(2.25rem + 0.18em);
  background: var(--deep-gold);
  color: var(--paper);
  border: 1px solid var(--deep-gold);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--label-face);
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  /* Wider than the kicker's 0.14em because the label is short and isolated and has no
     neighbours to set its rhythm. */
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  appearance: none;
  transition: background-color var(--t-fast) var(--ease-move),
              border-color     var(--t-fast) var(--ease-move),
              color            var(--t-fast) var(--ease-move);
}

/* The rule motif. The device's grammar is a line that runs past the shape it measures, so
   a 24px Champagne hairline continues from the button's bottom right edge and retracts as
   the ground darkens on hover. Champagne is decorative and carries no words, so this is a
   legal use. One hairline, one transitioned property, and it gives the mandated rectangle
   an asymmetry without touching its shape.

   Never place a primary button flush against the right edge of its container: the tail
   needs 24px of clearance. */
.btn::after {
  content: "";
  position: absolute;
  left: 100%;
  bottom: 0;
  width: 1.5rem;
  height: 1px;
  background: var(--champagne);
  transition: width var(--t-mid) var(--ease-out);
}
.btn:hover::after { width: 0; }

.notify .btn::after,                       /* joined to a field, no room to run past  */
.band[data-tone="charcoal"] .btn::after,   /* champagne on charcoal is for display type */
.btn--ghost::after,
.btn--quiet::after,
.btn[disabled]::after { content: none; }

/* Focus must not equal hover. The old file made them identical, so a keyboard user could
   not tell one from the other, and then rang a Charcoal button in Deep gold at 2.9 to 1.
   The ground does not change on focus; only the ring appears. */
.btn:hover {
  background: var(--charcoal);
  border-color: var(--charcoal);
  color: var(--paper);
}
.btn:focus-visible {
  outline: 2px solid var(--charcoal);
  outline-offset: 3px;
  background: var(--deep-gold);
  border-color: var(--deep-gold);
  color: var(--paper);
}
.btn:focus-visible:hover {
  background: var(--charcoal);
  border-color: var(--charcoal);
}
.btn:active { background: var(--charcoal); transform: none; }   /* nothing floats */

/* Paper on Charcoal soft is 7.4 to 1, so the disabled and sending states stay legible. */
.btn[disabled] {
  background: var(--charcoal-soft);
  border-color: var(--charcoal-soft);
  color: var(--paper);
  cursor: default;
}

.btn--ghost {
  background: transparent;
  color: var(--charcoal);
  border-color: var(--charcoal);
}
.btn--ghost:hover,
.btn--ghost:focus-visible {
  background: var(--charcoal);
  border-color: var(--charcoal);
  color: var(--paper);
}

.band[data-tone="charcoal"] .btn--ghost {
  color: var(--ivory);
  border-color: var(--gold-line);
  background: transparent;
}
.band[data-tone="charcoal"] .btn--ghost:hover,
.band[data-tone="charcoal"] .btn--ghost:focus-visible {
  background: var(--ivory);
  border-color: var(--ivory);
  color: var(--charcoal);
}

/* Tertiary. No horizontal padding, so it aligns hard to the column edge rather than
   sitting a padding's width inside it. Deep gold on Ivory is 5.1 to 1, which clears AA at
   12px. The Gold line underline is a line ink used as a line, which is legal; it is never
   the text colour. */
.btn--quiet {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0.9rem 0;
  background: none;
  border: 0;
  color: var(--deep-gold);
  font-family: var(--label-face);
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  text-decoration: underline;
  text-decoration-color: var(--gold-line);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.55em;
  cursor: pointer;
  appearance: none;
  transition: color var(--t-fast) var(--ease-move),
              text-decoration-color var(--t-fast) var(--ease-move);
}
.btn--quiet:hover {
  color: var(--charcoal);
  text-decoration-color: var(--charcoal);
}
.band[data-tone="charcoal"] .btn--quiet { color: var(--ivory); }
.band[data-tone="charcoal"] .btn--quiet:hover {
  color: var(--ivory);
  text-decoration-color: var(--ivory);
}

/* At the narrowest widths the horizontal padding comes in, so the longest label, THE
   HEALING TIMELINE, still fits a 272px content column without overflowing it. */
@media (max-width: 480px) {
  .btn {
    padding-inline: 1.5rem;
    padding-right: calc(1.5rem + 0.18em);
  }
}

/* ==========================================================================
   23  NOTIFY CAPTURE
   The only channel on the site. There is no phone number and no email address anywhere
   here, deliberately; this form is what replaces them. Wash is its band, because Wash has
   one fixed meaning across ten pages: this is the part that protects you.

   .notify__field is the input itself. Any input inside .notify is styled the same way, so
   the markup can carry the class or not.
   ========================================================================== */

.notify {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.5rem;
  max-width: 30rem;
}

.notify__field,
.notify input {
  flex: 1 1 14rem;
  min-height: var(--tap);
  padding: 0.7rem 0.9rem;
  background: var(--paper);
  color: var(--charcoal);
  border: 1px solid var(--rule);
  border-radius: 0;
  font-family: var(--body-face);
  font-size: var(--fs-body);   /* 17.5px, comfortably over the 16px at which a phone zooms the page on focus */
  line-height: 1.4;
}
/* The notify field's label. .notify is a wrapping flex row of field plus button, so the
   label has to claim a whole row of its own or it lands to the left of the box and eats
   the width the field needs. flex-basis 100 percent does that at every width, including
   the one where the button has already wrapped below the field. No ghost text is used
   anywhere on the site: a label that vanishes the moment a person types is a label that
   is missing when it is wanted. */
.notify__label {
  flex: 0 0 100%;
  margin-bottom: -0.15rem;
}
/* A tighter offset on a field than on a button, so a focused input does not collide with
   the button sitting 8px away from it. */
.notify__field:focus-visible,
.notify input:focus-visible {
  outline: 2px solid var(--charcoal);
  outline-offset: 2px;
  border-color: var(--charcoal);
}
/* align-self: stretch makes the button's height track the field exactly, rather than both
   independently hitting the 44px floor and drifting apart when the field wraps. */
.notify .btn {
  flex: 0 0 auto;
  align-self: stretch;
}

/* INK: Charcoal. This is a live region. It carries the confirmation that a request went
   through and it carries the validation message when one did not, and a message a person
   is waiting for is the last string on the site that should be set in the quiet ink. */
.notify__msg {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--charcoal);
  max-width: 40ch;
  margin-top: var(--s-3);
}

.band[data-tone="charcoal"] .notify__msg { color: var(--ivory); }

/* ==========================================================================
   24  REDUCED MOTION AND PRINT
   ========================================================================== */

/* Nothing loops and nothing floats, and for a reader who has asked for stillness, nothing
   moves at all. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  /* The device renders complete and immediately, which is not the same thing as running
     the sequence very fast. The global block above, applied to this sequence, would
     produce a single frame flash of 41 elements appearing at once, which is worse than
     the animation it is trying to suppress. These selectors are more specific, so they
     win among the important declarations and the drawing is simply finished.

     The observer is also not attached at all when prefers-reduced-motion matches, so in
     practice data-drawn is never set. This block is the belt to that braces. */
  .mapping [class^="m-"],
  .mapping [class^="m-"] > * {
    animation: none !important;
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* The finished hairstroke reads at 0.82, which the book sets. The important declaration
     above has already cleared the inline 0.82 off the group, so it is restored here on
     the paths themselves rather than being applied twice. */
  .mapping .m-stroke path,
  .mapping .m-strokes path { opacity: 0.82 !important; }

  .hero__rule { clip-path: none !important; animation: none !important; }
}

/* The price sheets get printed. A5 or half letter, Paper stock. The drawing, the
   photographs and every control come off: a printed sheet is the four rows, the deposits,
   the guarantee and the policies. */
@media print {
  :root { --gutter: 0.5cm; --band-pad: 0.6cm; }
  body { background: #fff; color: #000; }
  /* .lockup is here because both of its placements take the reversed master, which is
     Ivory artwork for a Charcoal panel, and print drops the panel. Ivory on white paper
     is a blank rectangle above a sentence, so the mark comes off with the ground. */
  .skip, .masthead, .sitefoot__nav, .notify, .btn, .btn--quiet, .actions,
  .hero__device, .hero__rule, .pagehead__mark, .shot, .pair, .lockup { display: none; }
  /* The corner botanical is a screen ornament. On paper it is two grey smudges in the
     margins of a price sheet, so it comes off with the drawing and the photographs. */
  .page::before { display: none; }
  .band, .band[data-tone] { background: none; page-break-inside: avoid; }
  .band[data-tone="charcoal"] { color: #000; }
  .sitefoot { background: none; color: #000; padding: 0.6cm 0.5cm; }
  .sitefoot__line, .sitefoot__legal { color: #000; }
  .sheet__row { page-break-inside: avoid; }
  a { color: #000; text-decoration: none; }
}


/* ==========================================================================
   Wide screen balance. Interior pages were stopping around 790px at 1440px while
   home filled its 12 column grid, so they read as half loaded. The sheet is tabular,
   so widening it costs no legibility.
   ========================================================================== */
@media (min-width: 1100px) {
  .sheet { max-width: var(--w-10); }
}


/* ==========================================================================
   MOTION, AND WHY THERE IS ANY.

   Jacqueline's note on 2026-09-07 was that the design reads flat next to the
   competition. She is describing something real. The site was built to be a
   technical document rather than a spa, and the discipline that produced the
   restraint also produced a page where nothing at all responds to a reader.
   Every competitor in this market has a gallery that reacts to a hand.

   The answer is not to join them. Five of six local studios use the same soft
   lifestyle motion: long cross fades, drifting parallax, images that swell under
   the cursor. That is the look the book rejects on page 27, and copying it here
   would cost the one thing this brand has.

   So the motion is built from what the brand already does. The home hero draws
   its mapping device in the order the procedure happens: guides, then start and
   tail, then the arch, then the strokes. That is the site's existing statement
   about motion, and it says movement is allowed when it is measurement. Three
   things follow from it.

   ONE. Things arrive rather than appear. A band rises 12px as it enters. That
   is one third of the distance the eye needs to register a change and it reads
   as settling, not as sliding.

   TWO. A photograph answers a hand. A result frame lifts its image 3.5 percent
   under a cursor and 2 percent under a thumb. The frame does not move and the
   caption does not move: the picture moves inside a fixed window, which is what
   a photograph does under a loupe.

   THREE. Nothing bounces, nothing fades in over a second, nothing parallaxes,
   and no text animates. Words are read, not performed.

   THE SCALE DIRECTION IS A SAFETY RULE, NOT A TASTE. Every hover here scales the
   image UP inside an overflow hidden box, never down. Scaling up shrinks the
   visible source window, so a crop stays at least as tight as it was measured to
   be. brow-pair-macro.jpg carries another studio's mark 71px below the bottom of
   its window, and scaling down by even 3 percent would walk it into frame. Do
   not add a scale under 1 to any .shot on this site.

   REDUCED MOTION IS OBEYED EVERYWHERE, and it is obeyed by not starting rather
   than by animating to the same place faster.
   ========================================================================== */

/* --- 1. ARRIVAL ------------------------------------------------------------

   Scroll driven, with no script and no observer, on the browser's own view()
   timeline. The whole block sits inside @supports, and the hidden state is
   declared INSIDE it too. That ordering is the important part: where view()
   is not supported, the rule never applies, so the page renders exactly as it
   did before rather than leaving its content at opacity 0 forever. A missing
   animation must fail to a visible page, never to a blank one. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* The band. One rule, every page, every band, including the ones already in
       the viewport at load: for those the entry range is already behind the
       scroll position, so they start and stay at the final frame. */
    .page > .band {
      animation: riseIn linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 16%;
    }

    /* "cover 16%" is a share of the band's own height plus the viewport, so the end of
       the arrival moves down as a band grows. The price menu's photograph panels make a
       sheet band two to three times taller when they are open, which would leave its
       headings, prices and photographs half transparent in the reading zone. A band
       holding an open panel is already being read, so it simply does not animate. */
    .page > .band:has(.sheet__photos[open]) {
      animation: none;
    }

    /* The frames inside it get their own timeline rather than a delay. A delay
       on a scroll driven animation is measured in range, not in seconds, so a
       stagger written that way drifts with the height of whatever is above it.
       Giving each figure its own view() lets the scroll do the staggering: a
       pair's two frames enter together, and the pair below enters when it is
       reached. That is the honest stagger and it costs nothing. */
    .pair figure,
    .figrow figure,
    .cards > *,
    .spec li {
      animation: riseIn linear both;
      animation-timeline: view();
      animation-range: entry 2% cover 14%;
    }
  }
}

@keyframes riseIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* --- 2. THE PHOTOGRAPH ANSWERS ---------------------------------------------

   .shot is already position relative with overflow hidden, and .shot > img is
   already the full box under object-fit cover, so a transform on the image moves
   the picture inside a window that does not move. Nothing here changes layout,
   so nothing here can reflow a page.

   The transition lives on the image unconditionally and the transform is applied
   only under a real hover device, so a touch screen never inherits a hover state
   it cannot leave. */
.shot > img {
  transition: transform 700ms cubic-bezier(0.2, 0.6, 0.2, 1);
  will-change: transform;
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  figure:hover > .shot > img,
  .svc:hover .shot > img,
  .shot:hover > img {
    transform: scale(1.035);
  }
  /* The service cards are links, so they may lift. A result frame may not: it is
     evidence sitting on a page, and evidence that floats is decoration. */
  .svc { transition: transform 320ms cubic-bezier(0.2, 0.6, 0.2, 1), box-shadow 320ms ease; }
  .svc:hover { transform: translateY(-3px); }
}

/* Touch has no hover, and Jacqueline will be looking at this on a phone. The
   press state is the whole of the feedback there: a smaller lift, ended by the
   finger leaving rather than by a timer. */
@media (prefers-reduced-motion: no-preference) {
  /* The anchor follows the image in the DOM, so a sibling selector pointing at it
     would never match. The press is read on .shot, which contains both. */
  .shot:active > img { transform: scale(1.02); }
}

/* --- 3. TAP OR CLICK TO ENLARGE --------------------------------------------

   Built on :target, so it needs no script. The shell writes one transparent
   anchor inside every result frame and one lightbox per frame at the end of the
   page; see the enlarge pass in _build/build.mjs for why it is generated there
   rather than typed into eleven fragments.

   The anchor is INSIDE .shot rather than wrapped around it, which matters twice.
   Two selectors in this file reach .shot as a direct child, .card > .shot + .stage
   and the figrow centring rule, and a wrapper would have broken both. And the
   service cards are themselves anchors, so wrapping would have nested a link
   inside a link. Placed inside, the overlay changes no existing selector at all.

   CLOSING GOES TO #_ AND THAT IS DELIBERATE. An empty href returns the reader to
   the top of the page, which loses their place. #_ matches no element on any page
   here, so :target stops matching, the overlay closes, and the scroll position is
   untouched. */
.zoom {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: zoom-in;
  /* No paint of its own. The frame already reads as a photograph; a hover tint
     over evidence would be us editing what the reader sees. */
  background: transparent;
}
.zoom:focus-visible {
  outline: 2px solid var(--deep-gold);
  outline-offset: -4px;
}

.lightbox { display: none; }

.lightbox:target {
  display: grid;
  position: fixed;
  inset: 0;
  z-index: 90;
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in srgb, var(--charcoal) 97%, transparent);
  /* The blur is what makes the page behind stop being readable. At 97 percent the
     ground is dark enough, but ivory type on a dark scrim still reads through at a
     few percent, and a legible nav behind an enlarged photograph pulls the eye off
     the photograph. Two pixels is enough to turn words back into texture. Where it
     is unsupported the overlay is simply a shade darker and nothing is lost. */
  backdrop-filter: blur(2px);
  /* Belt and braces. The overlays are written outside main precisely so the rule in
     section 04 that shapes every non band child of main cannot reach them, but an
     overlay that inherits a max-width fails by letting the page show down both edges,
     which reads as a rendering fault rather than as a style. Stated here as well, it
     cannot happen a second time. */
  max-width: none;
  margin: 0;
  text-align: initial;
}
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .lightbox:target { background: rgba(38, 34, 28, 0.97); }
}

/* The scrim is a link covering the whole overlay, so anywhere outside the
   picture closes it. It sits under the frame in the stacking order. */
.lightbox__scrim {
  position: absolute;
  inset: 0;
  cursor: zoom-out;
}

.lightbox__frame {
  position: relative;
  z-index: 1;
  max-width: min(100%, 1100px);
  margin: 0;
  text-align: center;
}
/* THE BOX MAY GET WIDER THAN ITS RATIO. IT MAY NEVER GET TALLER.
   width sets the size and max-height caps it, so when the cap binds the box ends up
   wider than its declared --ar. cover then scales to the width and the visible slice
   of source gets shorter, which moves further inside the panel rather than past it.
   A max-width or a fixed height here would invert that and walk a crop across a seam.
   The long note in the enlarge pass in _build/build.mjs works the numbers. */
.lightbox__shot {
  width: min(1080px, 92vw);
  max-height: 78vh;
  background: var(--wash);
}
.lightbox__cap {
  margin-top: var(--s-3);
  font-family: var(--label-face);
  font-size: var(--fs-kicker);
  line-height: var(--lh-kicker);
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--ivory);
}
.lightbox__close {
  position: absolute;
  top: clamp(0.75rem, 3vw, 1.5rem);
  right: clamp(0.75rem, 3vw, 1.5rem);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--s-4);
  font-family: var(--label-face);
  font-size: var(--fs-btn-sm);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  color: var(--ivory);
  text-decoration: none;
  border: 1px solid color-mix(in srgb, var(--ivory) 45%, transparent);
}
.lightbox__close:hover,
.lightbox__close:focus-visible { border-color: var(--ivory); }

@media print {
  .lightbox, .zoom { display: none; }
}

/* Booking pop-up palette: Laney's charcoal on ivory, with her own faces. */
:root {
  --bk-ink: var(--charcoal); --bk-soft: var(--charcoal-soft); --bk-line: var(--rule); --bk-bg: var(--paper);
  --bk-wash: var(--ivory); --bk-accent: var(--charcoal); --bk-on-accent: var(--paper);
  --bk-display: var(--display-face); --bk-body: var(--body-face); --bk-label: var(--label-face);
  --bk-radius: 2px; --bk-scrim: rgba(38, 34, 28, .5);
}

/* ------------------------------------------------------------------------------------------
   Booking pop-up (assets/book.js v2). One question per screen, over whatever page the reader
   is on. This block is identical on both sites; each site maps the --bk-* variables above it
   onto its own palette, so no colour value is written here.
   ------------------------------------------------------------------------------------------ */
html.bkm-open { overflow: hidden; }
.bkm { position: fixed; inset: 0; z-index: 1000; }
.bkm[hidden] { display: none; }
.bkm__scrim { position: absolute; inset: 0; background: var(--bk-scrim); }
.bkm__panel {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--bk-bg); color: var(--bk-ink); font-family: var(--bk-body);
}
@media (min-width: 720px) {
  .bkm__panel {
    inset: auto; left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: min(580px, calc(100vw - 48px)); height: min(720px, calc(100vh - 64px));
    border-radius: var(--bk-radius); box-shadow: 0 24px 80px rgba(0, 0, 0, .25);
  }
}
.bkm__top { display: flex; align-items: center; gap: 12px; padding: 14px 16px 10px; }
.bkm__title { flex: 1; font-family: var(--bk-label); font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--bk-soft); }
.bkm__count { font-family: var(--bk-label); font-size: 12px; letter-spacing: .06em; color: var(--bk-soft); white-space: nowrap; }
.bkm__back, .bkm__close {
  width: 44px; height: 44px; border: 0; background: transparent; color: var(--bk-ink);
  font-size: 24px; line-height: 1; cursor: pointer; border-radius: var(--bk-radius);
}
.bkm__back:hover, .bkm__close:hover { background: var(--bk-wash); }
.bkm__track { height: 3px; background: var(--bk-line); }
.bkm__bar { height: 100%; width: 0; background: var(--bk-accent); transition: width .3s ease; }

.bkm__body { flex: 1; overflow-y: auto; padding: 32px 24px 24px; -webkit-overflow-scrolling: touch; }
@media (min-width: 720px) { .bkm__body { padding: 40px 44px 28px; } }
.bkm__body--in { animation: bkm-in .28s ease both; }
@keyframes bkm-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .bkm__body--in { animation: none; } .bkm__bar { transition: none; } }

.bkm__q { font-family: var(--bk-display); font-weight: 400; font-size: clamp(26px, 6vw, 34px); line-height: 1.15; margin: 0 0 10px; }
.bkm__q:focus { outline: none; }
/* The optional "how did you hear" question under the email field is labelled in the same
   class as the question that labels the name, phone and email fields. It is a <label>, not a
   heading, so it needs the heading's block display to take the same spacing. */
label.bkm__q { display: block; }
.bkm__hint { color: var(--bk-soft); font-size: 15px; line-height: 1.5; margin: 0 0 22px; }
.bkm__lead { font-size: 19px; line-height: 1.4; margin: 6px 0 14px; }

/* Services: one tap chooses and moves on. */
.bkm__list { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.bkm__group { font-family: var(--bk-label); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--bk-soft); margin: 18px 0 4px; }
.bkm__group:first-child { margin-top: 0; }
.bkm__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%;
  min-height: 60px; padding: 14px 18px; text-align: left; cursor: pointer;
  background: var(--bk-bg); color: var(--bk-ink); border: 1px solid var(--bk-line); border-radius: var(--bk-radius);
  font: inherit; transition: border-color .15s ease, background .15s ease;
}
.bkm__opt:hover { border-color: var(--bk-ink); background: var(--bk-wash); }
.bkm__opt.is-on { border-color: var(--bk-ink); box-shadow: inset 0 0 0 1px var(--bk-ink); }
.bkm__opt-name { font-size: 17px; line-height: 1.3; }
.bkm__opt-meta { font-family: var(--bk-label); font-size: 12px; letter-spacing: .04em; color: var(--bk-soft); white-space: nowrap; }

/* Days: a calendar-like grid of tappable dates. */
.bkm__days { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 8px; }
.bkm__day {
  display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 12px 6px; min-height: 84px;
  background: var(--bk-bg); color: var(--bk-ink); border: 1px solid var(--bk-line); border-radius: var(--bk-radius); cursor: pointer; font: inherit;
}
.bkm__day:hover { border-color: var(--bk-ink); }
.bkm__day.is-on { background: var(--bk-accent); color: var(--bk-on-accent); border-color: var(--bk-accent); }
.bkm__day-dow, .bkm__day-mon { font-family: var(--bk-label); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; }
.bkm__day-num { font-family: var(--bk-display); font-size: 26px; line-height: 1.1; }

/* Times */
.bkm__times { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
.bkm__time {
  min-height: 52px; padding: 10px; font-family: var(--bk-label); font-size: 15px; letter-spacing: .02em;
  background: var(--bk-bg); color: var(--bk-ink); border: 1px solid var(--bk-line); border-radius: var(--bk-radius); cursor: pointer;
}
.bkm__time:hover { border-color: var(--bk-ink); }
.bkm__time.is-on { background: var(--bk-accent); color: var(--bk-on-accent); border-color: var(--bk-accent); }

/* Typed answers: one big field, like a form that asks one thing at a time. */
.bkm__input {
  display: block; width: 100%; padding: 12px 2px; font-family: var(--bk-body); font-size: 24px;
  color: var(--bk-ink); background: transparent; border: 0; border-bottom: 2px solid var(--bk-line); border-radius: 0;
}
.bkm__input:focus { outline: none; border-bottom-color: var(--bk-ink); }
/* A select left on its empty first choice ("Optional") is muted, so the placeholder does not
   read as an answer already given. The choices in its list keep the full ink. */
.bkm__input--empty { color: var(--bk-soft); }
.bkm__input--empty option { color: var(--bk-ink); }
.bkm__agree { display: flex; gap: 12px; align-items: flex-start; font-size: 18px; line-height: 1.4; cursor: pointer; }
.bkm__check { width: 22px; height: 22px; margin-top: 3px; flex: 0 0 auto; accent-color: var(--bk-accent); }

.bkm__review { display: grid; grid-template-columns: auto 1fr; gap: 12px 20px; margin: 8px 0 0; font-size: 17px; }
.bkm__review dt { font-family: var(--bk-label); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--bk-soft); padding-top: 4px; }
.bkm__review dd { margin: 0; }
.bkm__tick { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-size: 28px; background: var(--bk-accent); color: var(--bk-on-accent); margin-bottom: 18px; }

.bkm__foot { padding: 12px 16px 16px; border-top: 1px solid var(--bk-line); display: flex; flex-direction: column; gap: 8px; }
@media (min-width: 720px) { .bkm__foot { padding: 14px 44px 22px; } }
.bkm__foot[hidden] { display: none; }
.bkm__status { margin: 0; min-height: 0; font-size: 14px; line-height: 1.4; color: var(--bk-soft); }
.bkm__status:empty { display: none; }
.bkm__status[data-kind="error"] { color: var(--bk-ink); font-weight: 600; }
.bkm__next {
  width: 100%; min-height: 54px; border: 0; cursor: pointer; border-radius: var(--bk-radius);
  background: var(--bk-accent); color: var(--bk-on-accent);
  font-family: var(--bk-label); font-size: 13px; letter-spacing: .16em; text-transform: uppercase;
}
.bkm__next[hidden] { display: none; }
.bkm__next:disabled { opacity: .55; cursor: default; }
.bkm__back:focus-visible, .bkm__close:focus-visible, .bkm__opt:focus-visible, .bkm__day:focus-visible,
.bkm__time:focus-visible, .bkm__next:focus-visible { outline: 2px solid var(--bk-ink); outline-offset: 2px; }

/* ---------------------------------------------------------------------------
   CHINESE. The /zh/ pages, added 2026-09-21.
   ---------------------------------------------------------------------------
   THE ONE THING THAT HAS TO CHANGE IS THE ITALIC. Playfair Display carries no Chinese
   glyphs, so the browser substitutes a system face and SLANTS it mechanically. A synthesised
   slant on Chinese type reads as a rendering fault rather than as emphasis, and Chinese
   typography does not use italics for emphasis in any case. Everything that marks these
   lines out otherwise, their size, their position, their weight, is untouched; only the
   manufactured slant comes off. The sister site carries the identical rule.

   The wordmark keeps its italic: "Laney PMU Studio" is Latin on every page in both
   languages, so the real italic is available and correct there. */
:lang(zh-Hans) em,
:lang(zh-Hans) i,
:lang(zh-Hans) .display em,
:lang(zh-Hans) .headline em,
:lang(zh-Hans) .pagehead .lede { font-style: normal; }
.wordmark em, .wordmark:lang(zh-Hans) em { font-style: italic; }

/* Latin runs inside Chinese copy, mostly the studio's own name and service tiers, keep the
   Latin face rather than falling through to the CJK fallback, which sets them too wide. */
:lang(zh-Hans) .wordmark { font-family: var(--font-display, 'Playfair Display', Georgia, serif); }

/* LONG-FORM SPEC ITEMS. .spec is built for one or two short lines an item, where the band's
   centring reads as deliberate. The four brow descriptions Jacqueline supplied on 2026-09-22
   run to three paragraphs each, and centred multi-line prose is simply hard to read: every
   line starts in a different place, so the eye has to hunt for the next one.
   This modifier left-aligns the prose inside the item while leaving the item, and the band,
   centred as the book requires. It changes nothing about the existing .spec blocks. */
/* One column: two columns of three paragraphs each is a wall of type, not a comparison.
   The block is centred in the band, as the book requires, and everything INSIDE it starts on
   the same left edge. margin-inline and max-width are reset on the children because the
   centred-band rules give paragraphs an auto margin and a narrow measure, which is right for
   a one-line spec item and wrong for a paragraph: it left every line starting in a different
   place, which is the thing that made this section hard to read in the first place. */
.spec--prose {
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  text-align: left;
  max-width: 40rem;
  margin-inline: auto;
}
.spec--prose .spec__item { text-align: left; }
.spec--prose .spec__item > * {
  text-align: left;
  margin-inline: 0;
  max-width: none;
}
.spec--prose .spec__item p + p { margin-top: var(--s-2, 0.75rem); }

/* ===========================================================================
   CHINESE TYPE SIZES
   ===========================================================================
   The same correction made on the salon site on 2026-09-22, from Jacqueline's
   note that Chinese booking text reads too small on a phone.

   The reason is the same in both places: the scale was drawn for Latin, where a
   tracked 11px uppercase label is a legible caption. A Chinese character at 11px
   carries far more stroke in the same box, so it fills in; and letter-spacing,
   which opens up Latin caps, pushes Chinese characters into separate islands so
   a four-character label stops reading as one word.

   This site's own body scale is already generous (--fs-body 17.5px), so only the
   small labels and the booking pop-up need lifting. Everything is behind
   :lang(zh-Hans); the English pages are untouched.
   --------------------------------------------------------------------------- */

:root:lang(zh-Hans) {
  --fs-small:  1.0625rem;   /* 17px, was 16 */
  --fs-kicker: 0.875rem;    /* 14px, was 12 */
}

:lang(zh-Hans) .kicker,
:lang(zh-Hans) .btn,
:lang(zh-Hans) .topnav__link { letter-spacing: 0.06em; }

/* The booking pop-up, appended inside the page so it inherits :lang from <html>. */
:lang(zh-Hans) .bkm__title,
:lang(zh-Hans) .bkm__count      { font-size: 14px; letter-spacing: 0.04em; }
:lang(zh-Hans) .bkm__group      { font-size: 14px; letter-spacing: 0.04em; }
:lang(zh-Hans) .bkm__opt-name   { font-size: 18px; }
:lang(zh-Hans) .bkm__opt-meta   { font-size: 14px; letter-spacing: 0; }
:lang(zh-Hans) .bkm__hint       { font-size: 16px; }
:lang(zh-Hans) .bkm__day-dow,
:lang(zh-Hans) .bkm__day-mon    { font-size: 13px; letter-spacing: 0.02em; }
:lang(zh-Hans) .bkm__review     { font-size: 18px; }
:lang(zh-Hans) .bkm__review dt  { font-size: 14px; letter-spacing: 0.04em; }
:lang(zh-Hans) .bkm__status     { font-size: 15px; }
:lang(zh-Hans) .bkm__agree      { font-size: 17px; }
