/* =============================================================
   Karrina Crystal PMU & Skin
   Core stylesheet
   Osborn Online - Daniel Osborn Consulting Limited

   Contents
   01  Font faces
   02  Design tokens
   03  Reset and base
   04  Typography
   05  Layout primitives
   06  Skip link
   07  Header and navigation
   08  Buttons
   09  Hero
   10  Section furniture
   11  Cards and grids
   12  Stats, award, results and timeline
   13  Reviews
   14  FAQ
   15  Gallery
   16  CTA band
   17  Forms
   18  Breadcrumbs
   19  Footer
   20  Mobile action bar
   21  Motion and reveals
   22  Utilities

   Colour roles
   -------------
   The raw palette is declared once in :root and never touched again.
   Components only ever reference the role tokens (--c-bg, --c-text,
   --c-heading and so on). Dark sections remap the roles rather than
   redefining the palette, so nothing needs a dark-specific override.
   ============================================================= */


/* 01  Font faces
   Files from Fontsource, Latin subset only.
   npm names: @fontsource/bodoni-moda, @fontsource/jost
   Drop the woff2 files straight into /assets/fonts/ keeping the
   Fontsource filenames as written below.
   ------------------------------------------------------------ */

@font-face {
  font-family: "Bodoni Moda";
  src: url("/assets/fonts/bodoni-moda-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Bodoni Moda";
  src: url("/assets/fonts/bodoni-moda-latin-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Jost";
  src: url("/assets/fonts/jost-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Jost";
  src: url("/assets/fonts/jost-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}


/* 02  Design tokens
   ------------------------------------------------------------ */

:root {

  /* Raw palette. Contrast ratios quoted against --paper unless stated. */
  --paper:      #fbf9f6;  /* page background, warm white                */
  --shell:      #f2ece5;  /* alternating section tint, champagne        */
  --stone:      #e4dad0;  /* hairlines, borders, image wells            */
  --espresso:   #2a211c;  /* dark section background                    */
  --ink:        #17130f;  /* headings and solid buttons     17.5:1      */
  --body-ink:   #3f3630;  /* body copy                      11.2:1      */
  --muted-ink:  #6b5e55;  /* meta, captions, labels          5.9:1      */
  --brass:      #a8895c;  /* decorative rules only on light             */
                          /* usable as text on --espresso    4.8:1      */
  --brass-ink:  #7a5f35;  /* brass-toned text on light       5.7:1      */
  --error:      #9a2e1c;  /* form errors                     7.2:1      */

  /* Colour roles. Remapped by .section--dark, never edited elsewhere. */
  --c-bg:        var(--paper);
  --c-text:      var(--body-ink);
  --c-heading:   var(--ink);
  --c-muted:     var(--muted-ink);
  --c-rule:      var(--stone);
  --c-accent:    var(--brass);
  --c-accent-tx: var(--brass-ink);
  --focus-ring:  var(--ink);
  --btn-bg:      var(--ink);
  --btn-fg:      var(--paper);
  --btn-bg-hov:  var(--body-ink);

  /* Type families */
  --font-display: "Bodoni Moda", "Didot", "Bodoni MT", "Times New Roman", serif;
  --font-body: "Jost", "Century Gothic", "Avenir Next", "Segoe UI", system-ui, sans-serif;

  /* Fluid type scale. Interpolates between 360px and 1600px viewports.
     Rescaled after testing at 1920 and 1440. The previous top end ran
     to 84px on the h1, which pushed "Effortless beauty," onto two lines
     and made the hero content taller than the viewport regardless of
     what min-height was set to. 64px puts it back on one line.
     Steps 1 to 4 move with it so the ramp stays proportional - an h1 at
     64px above an h2 at 60px would read as a mistake. */
  --step--1: clamp(0.875rem, 0.857rem + 0.081vw, 0.9375rem);  /* 14 - 15 */
  --step-0:  clamp(1.0625rem, 1.044rem + 0.081vw, 1.125rem);  /* 17 - 18 */
  --step-1:  clamp(1.1875rem, 1.133rem + 0.242vw, 1.375rem);  /* 19 - 22 */
  --step-2:  clamp(1.375rem, 1.266rem + 0.484vw, 1.75rem);    /* 22 - 28 */
  --step-3:  clamp(1.625rem, 1.444rem + 0.806vw, 2.25rem);    /* 26 - 36 */
  --step-4:  clamp(1.875rem, 1.548rem + 1.452vw, 2.875rem);   /* 30 - 46 */
  --step-5:  clamp(2.5rem, 2.064rem + 1.935vw, 3.75rem);      /* 40 - 60 */

  /* Spacing scale */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs:  0.75rem;
  --space-s:   1rem;
  --space-m:   1.5rem;
  --space-l:   2rem;
  --space-xl:  3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;

  /* Fluid section rhythm.
     Trimmed roughly 20%. The hero is not affected - it carries its own
     padding-block on .hero__content - and neither is the header, which
     is sized by --header-h. */
  --section-y:    clamp(3rem, 1.984rem + 4.516vw, 6.5rem);      /* 48 - 104 */
  --section-y-lg: clamp(3.75rem, 2.371rem + 6.13vw, 8.5rem);    /* 60 - 136 */

  /* Measure and containers */
  --gutter:      clamp(1.25rem, 0.887rem + 1.613vw, 2.5rem);    /* 20 - 40   */
  /* Fluid rather than a flat 1200px. A fixed container fills 83% of a
     1440 laptop but only 62% of a 1920 monitor, which is why the same
     page reads as comfortable on one and marooned on the other. This
     holds 1200 up to about 1460, then grows with the viewport to a
     1408 ceiling, so a large screen fills 73% instead.

     Reading measure is protected separately - see the split column
     caps in section 05 - so the extra width lands on the imagery
     rather than stretching lines of text. */
  --wrap:        clamp(75rem, 82vw, 88rem);   /* 1200 - 1408                 */
  --wrap-wide:   90rem;    /* 1440px, wide media bands                       */
  --wrap-narrow: 42rem;    /* 672px, long-form prose                         */

  /* Chrome */
  --header-h: 4.25rem;     /* 68px, mobile                                   */
  --mobile-bar-h: 3.875rem;/* 62px                                           */
  --radius: 0;             /* square throughout, matches the Didone logo     */
  --rule-w: 1px;

  /* Motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 0.18s;
  --dur: 0.35s;
  --dur-slow: 0.7s;

  /* Elevation. Warm-tinted, never neutral grey. */
  --shadow-s: 0 1px 2px rgba(23, 19, 15, 0.05);
  --shadow-m: 0 8px 28px rgba(23, 19, 15, 0.08);
  --shadow-l: 0 20px 60px rgba(23, 19, 15, 0.1);
}

@media (min-width: 64em) {
  :root {
    --header-h: 5.25rem; /* 84px, desktop */
  }
}

/* Dark context. Applied to a section, footer or any wrapper.
   Remaps roles only, so every component inside adapts on its own. */
.section--dark,
.is-dark {
  --c-bg:        var(--espresso);
  --c-text:      #e8e0d8;
  --c-heading:   var(--paper);
  --c-muted:     #bdb0a4;
  --c-rule:      rgba(232, 224, 216, 0.18);
  --c-accent:    var(--brass);
  --c-accent-tx: var(--brass);
  --focus-ring:  var(--paper);
  --btn-bg:      var(--paper);
  --btn-fg:      var(--ink);
  --btn-bg-hov:  var(--shell);

  background-color: var(--c-bg);
  color: var(--c-text);
}

/* One step deeper than --section--dark, for a dark band sitting directly
   below another one. Only the background changes, so every other role
   token carries over. Contrast improves rather than degrades: brass goes
   from 4.8:1 on espresso to 5.6:1 on ink. */
.section--dark.section--deep {
  --c-bg: var(--ink);
}


/* Background rhythm
   -----------------
   No two adjacent bands share a background. Two light tones alternate,
   then two dark ones close the page.

     hero            shell      (--c-bg remapped on .hero)
     welcome         paper      .section
     treatments      shell      .section .section--tint
     about           paper      .section
     reviews         shell      .section .section--tint
     faq             paper      .section
     closing cta     espresso   .cta-band .section--dark
     footer          ink        .site-footer .section--dark .section--deep

   Interior pages open with .page-hero, which is already shell, so the
   first .section below it must be untinted to keep the alternation.

   Treatments and reviews stay on the tint deliberately: the cards and
   review boxes are paper, and they need a tinted band behind them to
   lift off. Flipping those to paper would flatten both components. */


/* 03  Reset and base
   ------------------------------------------------------------ */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--space-m));
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  min-height: 100svh;
  background-color: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: 400;
  line-height: 1.7;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Room for the sticky mobile action bar so the footer is never covered.
   Matches the bar height exactly. See section 20 on why no safe-area
   inset is involved. */
@media (max-width: 63.99em) {
  body {
    padding-bottom: var(--mobile-bar-h);
  }
}

img,
picture,
video,
svg {
  display: block;
  max-width: 100%;
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

ul[class],
ol[class] {
  list-style: none;
  padding: 0;
}

:target {
  scroll-margin-top: calc(var(--header-h) + var(--space-m));
}

/* Single focus treatment for the whole site. Never removed, only remapped
   through --focus-ring so it stays visible on dark sections too. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

::selection {
  background-color: var(--ink);
  color: var(--paper);
}


/* 04  Typography
   ------------------------------------------------------------ */

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.08;
  color: var(--c-heading);
  letter-spacing: -0.005em;
  text-wrap: balance;
}

h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-3); line-height: 1.14; }
h4 { font-size: var(--step-2); line-height: 1.2; }
h5 { font-size: var(--step-1); line-height: 1.3; }

/* h6 carries the small wide-tracked label role, so it takes the body face. */
h6 {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--c-muted);
}

p {
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

strong,
b {
  font-weight: 500;
}

em,
i {
  font-style: italic;
}

small {
  font-size: var(--step--1);
}

/* Display italic, used sparingly for a single accent word in a heading. */
.accent {
  font-style: italic;
}

/* Standfirst under an h1 or h2. */
.lead {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--c-text);
  max-width: 34ch;
}

.lead--wide {
  max-width: 52ch;
}

/* Wide-tracked label with a short leading rule. Site signature. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--c-accent-tx);
  margin-bottom: var(--space-s);
}

.eyebrow::before {
  content: "";
  flex: 0 0 auto;
  width: 2.25rem;
  height: 1px;
  background-color: var(--c-accent);
}

.eyebrow--centred {
  justify-content: center;
}

.eyebrow--centred::after {
  content: "";
  flex: 0 0 auto;
  width: 2.25rem;
  height: 1px;
  background-color: var(--c-accent);
}

/* Mobile only. At 14px with 0.18em tracking the full string runs about
   376px, which will not hold one line inside a 390px viewport once the
   gutters and the leading rule are accounted for. 11px at 0.14em with a
   shorter rule brings it to roughly 282px against 292px available on a
   360px phone, so it fits from there up. Deliberately scoped with
   max-width so the desktop treatment is untouched. */
@media (max-width: 29.99em) {
  .eyebrow {
    font-size: 0.6875rem;
    letter-spacing: 0.14em;
    gap: var(--space-2xs);
  }

  .eyebrow::before,
  .eyebrow--centred::after {
    width: 1.25rem;
  }
}

/* Intermediate step for tablet and small desktop. At 1024 the split
   hero halves the column to about 396px of usable width while the full
   string at 14px/0.18em measures 377px. That is a 5% margin, thin
   enough that a slightly wider render wraps it. 13px at 0.16em brings
   it to roughly 342px and takes the risk out. */
@media (min-width: 30em) and (max-width: 71.99em) {
  .eyebrow {
    font-size: 0.8125rem;
    letter-spacing: 0.16em;
  }

  .eyebrow::before,
  .eyebrow--centred::after {
    width: 1.75rem;
  }
}

/* Numbered marker for treatment sections, set in the display face. */
.index-num {
  display: block;
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1;
  color: var(--c-accent-tx);
  margin-bottom: var(--space-2xs);
}

/* Prose block, used inside policy and terms pages. */
.prose {
  max-width: var(--wrap-narrow);
}

.prose > * + * {
  margin-top: var(--space-s);
}

.prose h2 {
  font-size: var(--step-3);
  margin-top: var(--space-xl);
}

.prose h3 {
  font-size: var(--step-2);
  margin-top: var(--space-l);
}

.prose ul,
.prose ol {
  padding-left: 1.25rem;
}

.prose li + li {
  margin-top: var(--space-2xs);
}

.prose a {
  color: var(--c-heading);
  text-decoration: underline;
}

.prose a:hover {
  color: var(--c-accent-tx);
}


/* 05  Layout primitives
   ------------------------------------------------------------ */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap--wide { max-width: var(--wrap-wide); }
.wrap--narrow { max-width: calc(var(--wrap-narrow) + (var(--gutter) * 2)); }

.section {
  padding-block: var(--section-y);
}

.section--lg { padding-block: var(--section-y-lg); }
.section--tight { padding-block: calc(var(--section-y) * 0.6); }
.section--tint { background-color: var(--shell); }
.section--flush-top { padding-top: 0; }
.section--flush-bottom { padding-bottom: 0; }

/* Vertical rhythm helper. One rule, applied to any container. */
.stack > * + * { margin-top: var(--space-s); }
.stack--m > * + * { margin-top: var(--space-m); }
.stack--l > * + * { margin-top: var(--space-l); }
.stack--xl > * + * { margin-top: var(--space-xl); }

/* Two-column split. Stacks below 40em.

   Top-aligned, not centred. Centring only looks right when both columns
   are a similar height; the moment the text runs longer than the image,
   the photograph floats in the middle of the row with dead space above
   and below it and reads as a mistake. Sharing a top edge reads as
   deliberate at any difference, which is why editorial layouts do it. */
.split {
  display: grid;
  gap: var(--space-xl);
  align-items: start;
}

@media (min-width: 56em) {
  .split {
    grid-template-columns: 1fr 1fr;
    gap: clamp(2.5rem, 1rem + 4vw, 5rem);
  }

  /* The text column is capped in rem, not set as a fraction. With
     fractional columns a wider container stretches the copy with it -
     at a 1408 wrap that runs to 81 characters a line. Capping holds the
     measure at about 69 characters on every desktop size. */
  .split--wide-left  { grid-template-columns: minmax(0, 38rem) 1fr; }
  .split--wide-right { grid-template-columns: 1fr minmax(0, 38rem); }

  /* The image is capped as well, and this matters as much as the text
     cap. Leaving the media column as a bare 1fr hands it every pixel
     the container gains, so between 1440 and 1920 the image inflates
     from roughly 444px to 720px while the text stays at 608 - the
     balance inverts and the photograph swamps the copy.

     Capping the frame rather than the track keeps 1440 exactly as it
     was, because at that width the track is narrower than the cap and
     the cap never applies. Only wide screens see any difference, and
     there the surplus becomes gutter instead of image. */
  .split > .frame,
  .split > .split__media {
    max-width: 36rem;
  }

  /* Surplus sits between the columns rather than outside them, so the
     image still meets the container edge. */
  .split--wide-left > .frame,
  .split--wide-left > .split__media  { margin-left: auto; }
  .split--wide-right > .frame,
  .split--wide-right > .split__media { margin-right: auto; }
}

/* Every split is written text-first in the DOM, so stacked it always
   reads heading, copy, image - the same way down the whole page. This
   class moves the image back to the first grid cell once there are two
   columns, which is what alternates the sides at desktop.

   Doing it the other way round, alternating the markup itself, is what
   produced two images back to back when adjacent sections stacked. */
/* 56em, not 40em. Below the two-column breakpoint the split is stacked,
   and moving the image to the first cell there would put a photograph
   above its own heading - and bring back two images in a row wherever
   adjacent sections alternate. */
@media (min-width: 56em) {
  .split__media--first { order: -1; }

  /* The image centres in the row. The text does not.

     Once every image became a fixed square, a tall text column left the
     photograph hanging at the top with a lot of dead space beneath it.
     Centring the whole row would fix that but also drag the copy down
     away from its own heading, and on a split where both columns are
     text - contact, for one - it would misalign two blocks that should
     start together. Aligning only the media column does the one thing
     needed.

     The media column is either a bare .frame or a .split__media wrapper
     holding a frame plus something else, so both are targeted. Direct
     children only, or a frame nested inside a wrapper would be sized
     twice. */
  .split > .frame,
  .split > .split__media {
    align-self: center;
  }
}

/* ============================================================
   TABLET, 640 to 896

   Not a squeezed desktop and not a stretched phone. This range gets its
   own composition, built from the same parts.

   Three moves:

   1. The type scale steps up. Body goes from 17.7px to 20px, headings
      in proportion. That is what lets copy run the full width of the
      container without the line getting too long - at 20px a 755px
      measure is 75 characters, inside the band. Capping the text
      instead was the mistake: it left a strip of dead paper down the
      right of every block, which reads as an accident rather than a
      margin.

   2. Photographs break out to the viewport edges. A contained image
      with a gutter either side reads as an oversized block floating in
      a column; the same image edge to edge reads as a band, which is
      what the hero already does. Nothing is cropped to achieve it -
      they stay square.

   3. Section padding grows, 68px to 80px. Without it the larger type
      just makes everything feel tighter rather than more generous.

   The result alternates bleed image, contained text, bleed image,
   contained text - the same rhythm as the desktop bands, composed for
   a portrait screen.
   ============================================================ */
@media (min-width: 40em) and (max-width: 55.99em) {
  :root {
    --step--1: 1rem;         /* 16 */
    --step-0:  1.25rem;      /* 20 */
    --step-1:  1.4375rem;    /* 23 */
    --step-2:  1.75rem;      /* 28 */
    --step-3:  2.125rem;     /* 34 */
    --step-4:  2.625rem;     /* 42 */
    --step-5:  3.25rem;      /* 52 */

    --section-y:    5rem;    /* 80  */
    --section-y-lg: 6.5rem;  /* 104 */
  }

  .split {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
  }

  /* Full width. No cap, so no leftover margin. */
  .split > *:not(.frame):not(.split__media) {
    max-width: none;
  }

  /* Out to the viewport edges. The wrap is full width at this size, so
     cancelling its gutter is all that is needed. */
  .split > .frame,
  .split > .split__media {
    max-width: none;
    margin-inline: calc(var(--gutter) * -1);
  }

  /* A band, not a square.

     Full bleed is right for the width and wrong for the height: at
     810px a square photograph stands 810px tall, which is 75% of the
     viewport, so each one swallows a screen on its own. 4:3 brings it
     to 608px - a little over half - which reads as a band rather than a
     panel.

     4:3 rather than something wider because it is the gentlest crop
     that still does the job. object-fit crops from the original file
     rather than from the square version, so nothing compounds: the
     landscape sources lose nothing at all, and a square one loses 25%
     of its height against 33% at 3:2.

     Position biased slightly above centre. Cropping height on a
     head-and-shoulders shot takes from the top and bottom equally at
     dead centre, and the top is where the face is. */
  .split > .frame > img,
  .split > .split__media .frame > img {
    aspect-ratio: 4 / 3;
    object-position: center 40%;
  }

  /* The offset rule has nowhere to sit behind a bled image - its
     negative right inset would push past the viewport and produce a
     horizontal scrollbar. It returns at desktop. */
  .frame::before {
    display: none;
  }

  /* The award sits under a bled photograph on the home page, so its
     gutter has to go back on. */
  .split__media > .frame + * {
    margin-inline: var(--gutter);
  }

  /* Leads are capped in ch, so the larger type makes them narrower in
     absolute terms, not wider - .lead would sit at 340px in a 755px
     container and leave 415px of nothing beside it. At 20px the full
     container is 65 characters, so the cap has no work left to do.

     .measure keeps its cap. It carries small print - timing caveats and
     compliance notes - where a shorter line is right, and the text is
     brief enough that the margin never reads as a gap. */
  .lead,
  .lead--wide {
    max-width: none;
  }
}

/* Media column wrapper.
   Only needed where the column carries something besides the image -
   currently the award badge sitting under Karrina's portrait. A bare
   .frame remains a valid media column, and the split rules target both. */
.split__media > .frame + * {
  margin-top: var(--space-xl);
}

/* Framed image well. Thin brass offset rule, the second site signature.

   Square, like every other photographic container on the site. Left to
   their natural ratios these came out at wildly different heights - a
   4:3 room shot beside a 3:4 brow shot beside a 4:5 portrait - so the
   same slot looked a different size on every page. Consistency is worth
   more than preserving each file's shape, and it is the same call
   already made for .result and the gallery. */
.frame {
  position: relative;
}

.frame > img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  position: relative;
  z-index: 1;
}

.frame::before {
  content: "";
  position: absolute;
  inset: var(--space-m) calc(var(--space-m) * -1) calc(var(--space-m) * -1) var(--space-m);
  border: var(--rule-w) solid var(--c-accent);
  z-index: 0;
}

.frame--left::before {
  inset: var(--space-m) var(--space-m) calc(var(--space-m) * -1) calc(var(--space-m) * -1);
}

/* The offset rule is proportional to the image it sits behind. On the
   narrow tablet column a 24px offset reads as heavy, so the tighter
   value carries all the way up to the desktop breakpoint. */
@media (max-width: 55.99em) {
  .frame::before {
    inset: var(--space-s) calc(var(--space-s) * -1) calc(var(--space-s) * -1) var(--space-s);
  }

  .frame--left::before {
    inset: var(--space-s) var(--space-s) calc(var(--space-s) * -1) calc(var(--space-s) * -1);
  }
}

/* Decorative divider with a centred diamond. */
.rule-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-s);
  color: var(--c-accent);
}

.rule-mark::before,
.rule-mark::after {
  content: "";
  flex: 1 1 auto;
  max-width: 6rem;
  height: 1px;
  background-color: currentColor;
}

.rule-mark span {
  width: 5px;
  height: 5px;
  background-color: currentColor;
  transform: rotate(45deg);
}


/* 06  Skip link
   ------------------------------------------------------------ */

.skip-link {
  position: absolute;
  top: 0;
  left: var(--gutter);
  z-index: 200;
  transform: translateY(-120%);
  background-color: var(--ink);
  color: var(--paper);
  padding: var(--space-xs) var(--space-m);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease);
}

.skip-link:focus {
  transform: translateY(0);
}


/* 07  Header and navigation
   ------------------------------------------------------------ */

/* Solid paper header at all times. The logo is pure black, so a
   transparent-over-hero header would be illegible over photography. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background-color: var(--paper);
  border-bottom: var(--rule-w) solid var(--c-rule);
  transition:
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

/* Slides out of view on scroll down, returns on scroll up.
   Gated on .js so that without JavaScript the header simply stays put
   rather than being stranded off-screen. The reduced-motion block in
   section 21 collapses the transition to instant rather than removing
   the behaviour, which is the right accommodation - the movement is
   what gets reduced, not the function. */
.js .site-header[data-hidden="true"] {
  transform: translateY(-100%);
}

.site-header.is-stuck {
  box-shadow: var(--shadow-m);
}

/* Slim contact strip. Desktop only, keeps the phone number permanently
   visible without stealing mobile viewport height. */
.topbar {
  display: none;
  background-color: var(--espresso);
  color: #e8e0d8;
  --focus-ring: var(--paper);
}

@media (min-width: 64em) {
  .topbar { display: block; }
}

.topbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  min-height: 2.5rem;
  font-size: var(--step--1);
  letter-spacing: 0.06em;
}

.topbar__list {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  margin: 0;
}

.topbar__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

.topbar__item svg {
  width: 0.9375rem;
  height: 0.9375rem;
  flex: 0 0 auto;
  color: var(--brass);
}

.topbar a {
  text-decoration: none;
}

.topbar a:hover {
  color: var(--brass);
}

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  min-height: var(--header-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  line-height: 0;
}

.brand img {
  width: auto;
  height: 2.375rem;
}

@media (min-width: 64em) {
  .brand img { height: 3.125rem; }
}

.site-header.is-stuck .brand img {
  height: 2.125rem;
  transition: height var(--dur) var(--ease);
}

@media (min-width: 64em) {
  .site-header.is-stuck .brand img { height: 2.625rem; }
}

/* Desktop navigation */
.nav {
  display: none;
}

@media (min-width: 64em) {
  .nav {
    display: flex;
    align-items: center;
    gap: clamp(1.25rem, 0.4rem + 1.6vw, 2.25rem);
  }
}

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 0.4rem + 1.6vw, 2.25rem);
  margin: 0;
}

.nav__link {
  position: relative;
  display: inline-block;
  padding-block: var(--space-2xs);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  white-space: nowrap;
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--brass);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease);
}

.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__link[aria-current="page"]::after {
  transform: scaleX(1);
}

/* Dropdown
   The trigger is a real button with aria-expanded, so it works from the
   keyboard without JavaScript having to fake anything. The panel is
   hidden with visibility rather than display so it can transition, and
   inert is applied by main.js so it is never a hidden tab stop. */

.nav__item {
  position: relative;
}

.nav__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding-block: var(--space-2xs);
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  position: relative;
}

.nav__trigger::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--brass);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease);
}

.nav__item[data-open="true"] .nav__trigger::after,
.nav__trigger:hover::after,
.nav__trigger:focus-visible::after,
.nav__trigger[data-section-current="true"]::after {
  transform: scaleX(1);
}

.nav__caret {
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease);
}

.nav__item[data-open="true"] .nav__caret {
  transform: translateY(1px) rotate(225deg);
}

.nav__menu {
  position: absolute;
  top: calc(100% + var(--space-2xs));
  left: 50%;
  transform: translate(-50%, -0.35rem);
  min-width: 15rem;
  margin: 0;
  padding: var(--space-2xs) 0;
  background-color: var(--paper);
  border: var(--rule-w) solid var(--stone);
  box-shadow: var(--shadow-m);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease),
    visibility var(--dur-fast) var(--ease);
}

.nav__item[data-open="true"] .nav__menu {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.nav__menu a {
  display: block;
  padding: var(--space-2xs) var(--space-m);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: none;
  text-decoration: none;
  color: var(--body-ink);
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.nav__menu a:hover,
.nav__menu a:focus-visible {
  background-color: var(--shell);
  color: var(--ink);
}

.nav__menu a[aria-current="page"] {
  color: var(--brass-ink);
}

/* Divider above the "all treatments" link at the foot of the menu. */
.nav__menu li.nav__menu-all {
  margin-top: var(--space-2xs);
  padding-top: var(--space-2xs);
  border-top: var(--rule-w) solid var(--stone);
}

/* Mobile toggle */
.nav-toggle {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 2.75rem;
  height: 2.75rem;
  margin-right: calc(var(--space-2xs) * -1);
  flex: 0 0 auto;
}

@media (min-width: 64em) {
  .nav-toggle { display: none; }
}

.nav-toggle__bar {
  display: block;
  width: 1.375rem;
  height: 1px;
  background-color: var(--ink);
  transition: transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

/* Mobile panel. Full height, slides from the right. */
.nav-panel {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(24rem, 100%);
  z-index: 150;
  display: flex;
  flex-direction: column;
  background-color: var(--paper);
  border-left: var(--rule-w) solid var(--c-rule);
  transform: translateX(100%);
  transition: transform var(--dur) var(--ease);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.nav-panel[data-open="true"] {
  transform: translateX(0);
}

@media (min-width: 64em) {
  .nav-panel { display: none; }
}

.nav-panel__head {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: var(--header-h);
  padding-inline: var(--gutter);
  border-bottom: var(--rule-w) solid var(--c-rule);
}

.nav-panel__body {
  flex: 1 1 auto;
  padding: var(--space-l) var(--gutter);
}

.nav-panel__list {
  margin: 0;
}

/* Direct children only. As a descendant selector this matched every li
   inside the panel, including the treatment sub-items two levels down,
   which drew a rule across each one and made the submenu read as a
   table. */
.nav-panel__list > li + li {
  border-top: var(--rule-w) solid var(--c-rule);
}

.nav-panel__link {
  display: block;
  padding-block: var(--space-s);
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: 1.2;
  text-decoration: none;
  color: var(--ink);
  transition: color var(--dur-fast) var(--ease);
}

.nav-panel__link:hover,
.nav-panel__link[aria-current="page"] {
  color: var(--brass-ink);
}

/* Mobile sub-navigation.
   Native details and summary, so it opens from the keyboard and works
   with JavaScript disabled. No aria-expanded needed, the element
   carries that semantic itself. */

.nav-panel__group summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  padding-block: var(--space-s);
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: 1.2;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}

.nav-panel__group summary::-webkit-details-marker {
  display: none;
}

.nav-panel__group summary:hover {
  color: var(--brass-ink);
}

.nav-panel__group-icon {
  position: relative;
  flex: 0 0 auto;
  width: 0.875rem;
  height: 0.875rem;
  color: var(--brass-ink);
}

.nav-panel__group-icon::before,
.nav-panel__group-icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 1px;
  background-color: currentColor;
  transition: transform var(--dur) var(--ease);
}

.nav-panel__group-icon::after {
  transform: rotate(90deg);
}

.nav-panel__group[open] .nav-panel__group-icon::after {
  transform: rotate(0deg);
}

/* Indent lives on the links, not on this list. The reset carries
   ul[class] { padding: 0 } at (0,1,1) specificity, which beats a plain
   class at (0,1,0), so padding-left here is silently discarded and the
   links end up flush against the rule. Padding the link also makes the
   whole indented strip tappable. */
.nav-panel__sublist {
  margin: 0 0 var(--space-s);
  border-left: var(--rule-w) solid var(--stone);
  list-style: none;
}

.nav-panel__sublink {
  display: block;
  /* 44px minimum tap target: 12px + 12px around a ~20px line box. */
  padding: var(--space-xs) var(--space-s) var(--space-xs) var(--space-m);
  font-size: var(--step-0);
  line-height: 1.4;
  text-decoration: none;
  color: var(--body-ink);
}

.nav-panel__sublink:hover,
.nav-panel__sublink[aria-current="page"] {
  color: var(--brass-ink);
}

.nav-panel__foot {
  padding: var(--space-l) var(--gutter) var(--space-xl);
  border-top: var(--rule-w) solid var(--c-rule);
}

.nav-panel__meta {
  margin-top: var(--space-m);
  font-size: var(--step--1);
  color: var(--c-muted);
}

.nav-panel__meta a {
  text-decoration: none;
  color: var(--ink);
}

.nav-panel__meta a:hover {
  color: var(--brass-ink);
}

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 140;
  background-color: rgba(23, 19, 15, 0.5);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease), visibility var(--dur) var(--ease);
}

.nav-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
}

/* Locks the page behind the open panel without losing scroll position.
   Shared with the lightbox, which needs the same thing - a modal
   dialog does not reliably stop the document scrolling behind it. */
body.is-nav-open,
body.is-modal-open {
  overflow: hidden;
}


/* 08  Buttons
   ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: 3rem;
  padding: var(--space-xs) var(--space-l);
  border: var(--rule-w) solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

.btn svg {
  width: 1.0625rem;
  height: 1.0625rem;
  flex: 0 0 auto;
}

.btn--solid {
  background-color: var(--btn-bg);
  color: var(--btn-fg);
  border-color: var(--btn-bg);
}

.btn--solid:hover {
  background-color: var(--btn-bg-hov);
  border-color: var(--btn-bg-hov);
  transform: translateY(-1px);
  box-shadow: var(--shadow-m);
}

.btn--solid:active {
  transform: translateY(0);
  box-shadow: none;
}

.btn--outline {
  background-color: transparent;
  color: var(--c-heading);
  border-color: var(--c-heading);
}

.btn--outline:hover {
  background-color: var(--c-heading);
  color: var(--c-bg);
  transform: translateY(-1px);
}

.btn--outline:active {
  transform: translateY(0);
}

/* Quiet text link with a persistent underline that extends on hover. */
.btn--text {
  min-height: 0;
  padding: 0;
  color: var(--c-heading);
  border: 0;
  position: relative;
}

.btn--text::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.4em;
  height: 1px;
  background-color: var(--c-accent);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease);
}

.btn--text:hover::after {
  transform: scaleX(0.6);
}

.btn--sm {
  min-height: 2.5rem;
  padding-inline: var(--space-m);
  letter-spacing: 0.12em;
}

.btn--block {
  display: flex;
  width: 100%;
}

/* Header CTA hides on narrow screens, the sticky bar covers it instead. */
.btn--header {
  display: none;
}

@media (min-width: 64em) {
  .btn--header { display: inline-flex; }
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
}

.btn-row--centred {
  justify-content: center;
}


/* 09  Hero
   ------------------------------------------------------------ */

.hero {
  /* Champagne rather than paper, so the hero does not share a background
     with the Welcome section directly below it. Changing the hero rather
     than Welcome keeps Treatments and Reviews on the tint, which is what
     the paper cards and review boxes need behind them. See the
     background rhythm note in section 02. */
  --c-bg: var(--shell);
  position: relative;
  background-color: var(--c-bg);
  overflow: hidden;
}

.hero__inner {
  display: grid;
  gap: 0;
}

/* The split starts at 40em, not 56em.

   Tablet portrait is roughly 3:4 and so is the hero photograph, which
   means a vertical column suits it and a stacked banner does not. Every
   ratio tried while stacked was a compromise: tall enough to keep the
   composition meant eating half the viewport, short enough to leave room
   meant a letterbox strip. Splitting instead gives the image a portrait
   container that matches the source almost exactly.

   The same range also catches phones in landscape, where a stacked hero
   filled the entire screen with the image. */
@media (min-width: 40em) {
  .hero__inner {
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }
}

/* Viewport fill is gated on a landscape viewport, not on width.

   On a landscape screen the image column is wider than it is tall, so
   stretching the hero to the fold crops the photograph vertically -
   harmless, and what the desktop design wants.

   On a portrait screen the same rule makes the column tall and narrow.
   At 810px the column goes to 0.53 against a 0.75 source and cover
   crops 29% off the WIDTH, which on a two-faces-side-by-side photo cuts
   into both of them. Portrait viewports let the content set the height
   instead, which keeps the column near the source ratio. */
/* Portrait tablet. Without this the hero is sized purely by its content
   and comes out around 530px on a 1080 screen, so the next section
   shows above the fold and the hero reads as a strip rather than an
   opening.

   62svh rather than a full fill, deliberately. The image column is
   405px wide here, and forcing it much taller makes cover crop the
   width - on a photograph with two faces side by side that eats into
   both. 62svh costs about 19% of the width, which the composition
   survives; a full fill costs 29%, which it does not. */
@media (min-width: 40em) and (max-width: 55.99em) and (max-aspect-ratio: 1 / 1) {
  .hero__inner {
    min-height: 62svh;
  }
}

@media (min-width: 56em) and (min-aspect-ratio: 1 / 1) {
  .hero__inner {
    /* The header is sticky rather than fixed so it occupies flow space
       above this, and subtracting its height lands the bottom edge of
       the hero on the fold with nothing of the next section showing.

       Still min-height rather than height: on a short viewport the
       content must be allowed to push the hero taller rather than
       overflow it. */
    min-height: calc(100svh - var(--header-h));
  }
}

@media (min-width: 64em) and (min-aspect-ratio: 1 / 1) {
  .hero__inner {
    /* The topbar only exists from 64em up, so it is only subtracted here.
       Taking it off below this breakpoint would leave the hero 40px
       short of the fold. */
    min-height: calc(100svh - var(--header-h) - 2.5rem);
  }
}

.hero__content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(2rem, 1.4rem + 2.6vw, 3.5rem);
  padding-inline: var(--gutter);
  /* Pinned at 600px rather than half the container. --wrap is fluid
     now, and letting the hero follow it would widen this column and
     shift a composition that was already settled. The hero is
     deliberately untouched by the container change. */
  max-width: 37.5rem;
  /* Centred within its half rather than pinned to the right of it.
     Aligning the left edge to the site container looked correct on
     paper but left roughly 400px of dead paper at 1920 while the text
     itself sat almost touching the image. The hero is a full-bleed
     band, so it carries its own alignment rather than the container's. */
  margin-inline: auto;
  width: 100%;
}

@media (min-width: 40em) {
  .hero__content {
    padding-right: clamp(1.5rem, 0.3rem + 3.2vw, 3.5rem);
  }
}

/* Phone, stacked. The 600px cap exists so the text can share a row with
   the image; stacked below 40em the viewport is narrower than the cap
   anyway, but releasing it keeps the hero aligned to the same gutter as
   every section below. */
@media (max-width: 39.99em) {
  .hero__content {
    max-width: none;
  }
}

.hero__title {
  margin-bottom: var(--space-s);
}

/* Tablet portrait and phone landscape.
   The text column is around 405px here rather than the 960px it gets on
   desktop, so the display size has to come down with it or the heading
   breaks to three ragged lines. Sized so "Effortless beauty," holds one
   line across the whole range, matching the desktop treatment. */
@media (min-width: 40em) and (max-width: 55.99em) {
  .hero__title {
    font-size: clamp(2rem, 0.119rem + 4.7vw, 2.75rem);
  }
}

.hero__lead {
  margin-bottom: var(--space-m);
}

/* Media column bleeds to the viewport edge from 40em up. Below that it
   sits above the content and needs its own ratio. */
.hero__media {
  position: relative;
  order: -1;
  /* 16:10 on phones. At 3:2 the image alone took 260px of a 390px
     viewport and pushed the primary CTA below the fold. */
  aspect-ratio: 16 / 10;
  overflow: hidden;
}

@media (min-width: 40em) {
  .hero__media {
    order: 0;
    /* No ratio once split. The column stretches to the grid row, which is
       sized by the content column and min-height. */
    aspect-ratio: auto;
  }
}

/* Absolutely positioned so the image contributes nothing to the row
   height. With height:100% and a parent of indeterminate height, the
   percentage resolves to auto and the image renders at its intrinsic
   ratio instead - a 1280x1600 portrait in a 960px column becomes 1200px
   tall and drags the whole hero past the fold. This is the fix for that. */
.hero__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Centred. On the current 1200x1600 hero image this keeps roughly the
     28% to 72% band, which holds both the eyes and the lips. Biasing
     upward crops the mouth off, which is the subject of the photo. */
  object-position: center;
}

/* Trust strip sitting directly under the hero CTA. */
.hero__trust {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-m);
  margin-top: var(--space-m);
  padding-top: var(--space-s);
  border-top: var(--rule-w) solid var(--c-rule);
  font-size: var(--step--1);
  color: var(--c-muted);
}

.hero__trust-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
}

.hero__trust-item svg {
  width: 1rem;
  height: 1rem;
  flex: 0 0 auto;
  color: var(--brass-ink);
}

/* Star cluster reads as a rating. A single star next to a review count
   reads as a one-star rating, which is the opposite of the intent. */
.hero__trust-item .stars svg {
  width: 0.875rem;
  height: 0.875rem;
}

/* Compact page hero for interior pages. */
.page-hero {
  padding-block: clamp(2.5rem, 1.4rem + 4.8vw, 5.5rem);
  background-color: var(--shell);
  border-bottom: var(--rule-w) solid var(--stone);
}

.page-hero__title {
  max-width: 20ch;
}

.page-hero .lead {
  margin-top: var(--space-m);
}

/* Jump index.
   Sits under a page hero on long pages that stack several sections, so
   a visitor can see the shape of the page before scrolling it. Rules
   above and below rather than buttons, to stay out of the way of the
   real calls to action. */
.jump {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-l);
  margin-top: var(--space-l);
  padding-top: var(--space-m);
  border-top: var(--rule-w) solid var(--stone);
}

.jump a {
  position: relative;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--muted-ink);
  padding-block: var(--space-3xs);
  transition: color var(--dur-fast) var(--ease);
}

.jump a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--brass);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease);
}

.jump a:hover,
.jump a:focus-visible {
  color: var(--ink);
}

.jump a:hover::after,
.jump a:focus-visible::after {
  transform: scaleX(1);
}

/* 10  Section furniture
   ------------------------------------------------------------ */

.section-head {
  max-width: 44rem;
  margin-bottom: clamp(2rem, 1.2rem + 3vw, 3.5rem);
}

.section-head--centred {
  margin-inline: auto;
  text-align: center;
}

.section-head p {
  margin-top: var(--space-s);
  color: var(--c-text);
}

.section-head--centred p {
  margin-inline: auto;
  max-width: 52ch;
}


/* 11  Cards and grids
   ------------------------------------------------------------ */

.grid {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: 1fr;
}

@media (min-width: 40em) {
  .grid--2,
  .grid--4 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 40em) {
  .grid--3 { grid-template-columns: repeat(2, 1fr); }
}

/* A three-item grid--3 goes single column between 40em and 64em.

   At two columns the third item sits alone on its own row with an empty
   cell beside it - on the reviews block that is two quotes side by side
   and one stranded underneath, which reads as a layout that ran out
   rather than a deliberate arrangement.

   Scoped by :has() to grids that hold exactly three children, because
   grid--3 also carries the six-item results grids on /treatments/skin
   and /treatments/permanent-makeup, and six stacked squares would be an
   absurd scroll on a tablet. Those have an even count, tile cleanly at
   two columns, and are left alone.

   Without :has() support this rule simply does not apply and the layout
   falls back to two columns, which is where it is today. */
@media (min-width: 40em) and (max-width: 63.99em) {
  .grid--3:has(> :nth-child(3):last-child) {
    grid-template-columns: 1fr;
  }
}

@media (min-width: 64em) {
  .grid--3 { grid-template-columns: repeat(3, 1fr); }
  .grid--4 { grid-template-columns: repeat(4, 1fr); }
  .grid { gap: var(--space-l); }
}

.card {
  display: flex;
  flex-direction: column;
  background-color: var(--paper);
  border: var(--rule-w) solid var(--stone);
  border-radius: var(--radius);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.card__media {
  position: relative;
  overflow: hidden;
  /* Square, matching .frame, .result and the gallery. One ratio for
     every photograph on the site, so a card image and a split image and
     a gallery tile all read as the same object. */
  aspect-ratio: 1 / 1;
  background-color: var(--shell);
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}

.card__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: var(--space-m);
  gap: var(--space-2xs);
}

@media (min-width: 64em) {
  .card__body { padding: var(--space-l); }
}

.card__title {
  font-size: var(--step-2);
}

.card__text {
  color: var(--muted-ink);
  font-size: var(--step-0);
}

.card__list {
  margin-top: var(--space-2xs);
  font-size: var(--step--1);
  color: var(--muted-ink);
}

.card__list li {
  position: relative;
  padding-left: var(--space-s);
}

.card__list li + li {
  margin-top: var(--space-3xs);
}

.card__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 4px;
  height: 4px;
  background-color: var(--brass);
  transform: rotate(45deg);
}

.card__foot {
  margin-top: auto;
  padding-top: var(--space-m);
}

/* Marked list.
   Treatment listings on the hub and the four child pages. Same brass
   diamond as .card__list, but set at body size rather than caption
   size, because here the list is the content rather than a supporting
   detail. A nested <strong> becomes the treatment name with the
   description running underneath it. */
.marked-list {
  list-style: none;
}

.marked-list li {
  position: relative;
  padding-left: var(--space-m);
  color: var(--c-text);
}

.marked-list li + li {
  margin-top: var(--space-s);
}

.marked-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  background-color: var(--c-accent);
  transform: rotate(45deg);
}

.marked-list strong {
  display: block;
  color: var(--c-heading);
  font-weight: 500;
}

/* Compact variant, for a list of names with no descriptions under them.
   The default spacing exists to separate a heading from its paragraph;
   with nothing but names it just looks loose. */
.marked-list--compact li + li {
  margin-top: var(--space-2xs);
}

/* Whole-card link. The anchor sits on the title and stretches over
   the card, so screen readers still announce a single sensible link. */
.card__link {
  text-decoration: none;
  color: inherit;
}

.card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.card--linked {
  position: relative;
}

.card--linked:hover,
.card--linked:focus-within {
  border-color: var(--brass);
  box-shadow: var(--shadow-m);
}

.card--linked:hover .card__media img {
  transform: scale(1.04);
}

.card--linked:focus-within {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .card--linked:hover .card__media img { transform: none; }
}


/* 12  Stats, award, results and timeline
   ------------------------------------------------------------ */

/* Award credential.
   The awarding body's mark is orange, well outside this palette. It is
   contained inside a bordered block so it reads as an external
   credential rather than a palette break. Do not recolour a
   third-party award badge - it misrepresents the mark and most
   awarding bodies prohibit it in their usage terms. */

.award {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  /* Sized to its content, not the column. Left to stretch it ran the
     full width of a text column and left a third of the box empty,
     which reads as an unfinished panel rather than a credential.
     fit-content still shrinks to the available space where the column
     is narrow, as it is in the home page media column. */
  width: fit-content;
  max-width: 100%;
  padding: var(--space-s) var(--space-m);
  border: var(--rule-w) solid var(--c-rule);
  background-color: var(--paper);
}

.section--tint .award {
  background-color: var(--paper);
}

.award__badge {
  flex: 0 0 auto;
  width: 4.5rem;
  height: auto;
}

@media (min-width: 40em) {
  .award__badge { width: 5.5rem; }
}

.award__title {
  display: block;
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.35;
  color: var(--ink);
}

.award__meta {
  display: block;
  margin-top: var(--space-3xs);
  font-size: var(--step--1);
  color: var(--muted-ink);
}

.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-s);
  padding-block: var(--space-l);
  border-top: var(--rule-w) solid var(--c-rule);
  border-bottom: var(--rule-w) solid var(--c-rule);
}

.stat {
  text-align: center;
}

.stat__value {
  display: block;
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  color: var(--c-heading);
}

.stat__label {
  display: block;
  margin-top: var(--space-2xs);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-muted);
}


/* Result figure.
   Before and after imagery with a visible caption. The caption is not
   decoration: CAP rules expect before and after images to be
   representative, and fresh work looks markedly different from healed,
   so every image has to say which it is where a sighted visitor can
   read it. Alt text alone does not discharge that.

   Square. The supplied results are square, so
   in practice this crops nothing and simply guarantees an even grid
   whatever gets added later. It was briefly removed on the theory that
   a side-by-side composite might be around 8:5 and get sliced in half;
   that is not what the actual files are, and dropping it made a tidy
   grid ragged for no gain. */
.result {
  margin: 0;
}

.result img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background-color: var(--shell);
}

.result figcaption {
  margin-top: var(--space-xs);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--c-muted);
}

.result figcaption strong {
  display: block;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-accent-tx);
  margin-bottom: 2px;
}

/* Timeline.
   Numbered because healing genuinely is a sequence.

   Rows up to 75em, then a horizontal run of steps. Full width it has
   the room to read left to right, which is what a sequence wants, and
   it stops a five-step list towering over the photograph beside it. */
.timeline {
  border-top: var(--rule-w) solid var(--c-rule);
}

.timeline__step {
  display: grid;
  gap: var(--space-2xs) var(--space-m);
  padding-block: var(--space-m);
  border-bottom: var(--rule-w) solid var(--c-rule);
}

@media (min-width: 40em) {
  .timeline__step {
    grid-template-columns: 9rem 1fr;
    align-items: baseline;
  }
}

@media (min-width: 75em) {
  .timeline {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--space-m);
    border-top: 0;
  }

  .timeline__step {
    display: block;
    padding-block: var(--space-m) 0;
    border-top: var(--rule-w) solid var(--c-rule);
    border-bottom: 0;
  }

  .timeline__when {
    display: block;
    margin-bottom: var(--space-xs);
  }
}

.timeline__when {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1.2;
  color: var(--c-accent-tx);
}

.timeline__what {
  color: var(--c-text);
}

.timeline__what strong {
  display: block;
  font-weight: 500;
  color: var(--c-heading);
  margin-bottom: var(--space-3xs);
}

/* 13  Reviews
   ------------------------------------------------------------ */

.review {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  padding: var(--space-l);
  background-color: var(--paper);
  border: var(--rule-w) solid var(--stone);
}

.section--tint .review {
  background-color: var(--paper);
}

.stars {
  display: inline-flex;
  gap: 3px;
  color: var(--brass-ink);
}

.stars svg {
  width: 1rem;
  height: 1rem;
}

.review__text {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--ink);
}

.review__meta {
  margin-top: auto;
  padding-top: var(--space-2xs);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted-ink);
}


/* 14  FAQ
   Native details and summary. No JavaScript, fully keyboard operable.
   ------------------------------------------------------------ */

.faq {
  max-width: 48rem;
  border-top: var(--rule-w) solid var(--c-rule);
}

.faq__item {
  border-bottom: var(--rule-w) solid var(--c-rule);
}

.faq__q {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-m);
  padding-block: var(--space-m);
  font-family: var(--font-body);
  font-size: var(--step-1);
  font-weight: 500;
  line-height: 1.4;
  color: var(--c-heading);
  cursor: pointer;
  list-style: none;
}

.faq__q::-webkit-details-marker {
  display: none;
}

.faq__q:hover {
  color: var(--brass-ink);
}

.faq__icon {
  position: relative;
  flex: 0 0 auto;
  width: 1rem;
  height: 1rem;
  margin-top: 0.4em;
}

.faq__icon::before,
.faq__icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 1px;
  background-color: currentColor;
  transition: transform var(--dur) var(--ease);
}

.faq__icon::after {
  transform: rotate(90deg);
}

.faq__item[open] .faq__icon::after {
  transform: rotate(0deg);
}

.faq__a {
  padding-bottom: var(--space-m);
  max-width: 60ch;
  color: var(--c-text);
}

.faq__a > * + * {
  margin-top: var(--space-s);
}


/* 15  Gallery
   ------------------------------------------------------------ */

/* Denser than .grid, because a gallery of nearly forty images at three
   columns runs to a lot of screens on a phone.

   It holds .result figures rather than bare square tiles. Captionless
   tiles were the original plan, but the standing rule is that before
   and after imagery states whether it is fresh or healed somewhere a
   sighted visitor can read it - and a caption that only appears once
   the lightbox is open does not satisfy that. So the captions here are
   short and always visible. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-m) var(--space-s);
}

@media (min-width: 48em) {
  .gallery-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-l) var(--space-m);
  }
}

@media (min-width: 75em) {
  .gallery-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* Zoom trigger.
   Wrapped around result and gallery images by main.js rather than
   written into the markup, so that with JavaScript off there is no
   button sitting there doing nothing. */
.zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}

/* Lightbox.
   Native <dialog>, which brings the focus trap, the Escape key and
   inert-ing the rest of the page with it. Reimplementing those by hand
   is where bespoke lightboxes usually go wrong.

   The enlarged image is the same file the grid already loaded, not a
   larger one. Those files are 1600px and the tiles render them at about
   380px, so the detail is already sitting in the browser cache and the
   dialog opens instantly with no second request. */
.lightbox {
  /* position: fixed, restated deliberately.

     The UA stylesheet gives a modal dialog position: fixed, but author
     CSS beats the UA stylesheet, so setting anything else here drops
     the dialog into normal document flow. showModal() then scrolls it
     into view and the page jumps - which is what "relative" did.

     Fixed also establishes the containing block the close and nav
     buttons position against, so nothing else is needed for those.

     Centred with a transform rather than margin: auto, because the
     global * { margin: 0 } reset would undo that and because auto
     margins do not centre vertically when the height is intrinsic. */
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  width: min(64rem, 92vw);
  max-height: 92svh;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--body-ink);
  overflow: hidden;
}

.lightbox::backdrop {
  background-color: rgba(23, 19, 15, 0.88);
}

.lightbox__figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  max-height: 92svh;
}

.lightbox__img {
  width: 100%;
  min-height: 0;
  max-height: 76svh;
  object-fit: contain;
  background-color: var(--shell);
}

.lightbox__caption {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-m) var(--space-l);
  border-top: var(--rule-w) solid var(--stone);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--muted-ink);
}

.lightbox__caption strong {
  display: block;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brass-ink);
  margin-bottom: 2px;
}

.lightbox__count {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.1em;
  color: var(--muted-ink);
  white-space: nowrap;
}

/* Controls. Same square treatment as everything else on the site, so
   they read as part of it rather than as a plugin. */
.lightbox__close,
.lightbox__nav {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background-color: var(--paper);
  border: var(--rule-w) solid var(--stone);
  color: var(--ink);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}

.lightbox__close svg,
.lightbox__nav svg {
  width: 1.125rem;
  height: 1.125rem;
}

.lightbox__close:hover,
.lightbox__nav:hover {
  background-color: var(--shell);
}

.lightbox__close {
  top: var(--space-s);
  right: var(--space-s);
}

/* Vertically centred on the image rather than the dialog, so they do
   not drift over the caption bar as the image height changes. */
.lightbox__nav {
  top: calc(50% - 2.5rem);
  transform: translateY(-50%);
}

.lightbox__nav--prev { left: var(--space-s); }
.lightbox__nav--next { right: var(--space-s); }

.lightbox__nav[hidden] { display: none; }

/* Filter row for the gallery page. */
/* Filter row.
   Hidden until the .js class lands, so without scripting the full set
   is simply shown rather than offering controls that do nothing. */
.filters {
  display: none;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-bottom: var(--space-l);
}

.js .filters {
  display: flex;
}

.filter {
  padding: var(--space-3xs) var(--space-s);
  min-height: 2.5rem;
  display: inline-flex;
  align-items: center;
  border: var(--rule-w) solid var(--stone);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted-ink);
  transition: all var(--dur-fast) var(--ease);
}

.filter:hover {
  border-color: var(--ink);
  color: var(--ink);
}

.filter[aria-pressed="true"] {
  background-color: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* Announced to screen readers on filter change, since the visual
   change alone is not conveyed. */
.filter-status {
  margin-top: var(--space-l);
  font-size: var(--step--1);
  color: var(--c-muted);
}


/* 16  CTA band
   ------------------------------------------------------------ */

.cta-band {
  position: relative;
  padding-block: var(--section-y-lg);
  text-align: center;
}

.cta-band__inner {
  max-width: 42rem;
  margin-inline: auto;
}

.cta-band__title {
  margin-bottom: var(--space-m);
}

.cta-band p {
  margin-bottom: var(--space-l);
}

.cta-band__trust {
  margin-top: var(--space-l);
  padding-top: var(--space-m);
  border-top: var(--rule-w) solid var(--c-rule);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-muted);
}


/* 17  Forms
   ------------------------------------------------------------ */

.form {
  display: grid;
  gap: var(--space-m);
  max-width: 34rem;
}

.form__row {
  display: grid;
  gap: var(--space-m);
}

@media (min-width: 40em) {
  .form__row--2 { grid-template-columns: 1fr 1fr; }
}

.field {
  display: grid;
  gap: var(--space-3xs);
}

.field__label {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}

.field__hint {
  font-size: var(--step--1);
  color: var(--muted-ink);
}

.field__control {
  width: 100%;
  min-height: 3rem;
  padding: var(--space-xs) var(--space-s);
  background-color: var(--paper);
  border: var(--rule-w) solid var(--stone);
  border-radius: var(--radius);
  color: var(--body-ink);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

textarea.field__control {
  min-height: 9rem;
  resize: vertical;
  line-height: 1.6;
}

.field__control::placeholder {
  color: var(--muted-ink);
  opacity: 1;
}

.field__control:hover {
  border-color: var(--muted-ink);
}

.field__control:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.field__control:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.field__control[aria-invalid="true"] {
  border-color: var(--error);
  box-shadow: inset 0 0 0 1px var(--error);
}

.field__error {
  font-size: var(--step--1);
  color: var(--error);
}

.field__error:empty {
  display: none;
}

/* Honeypot. Off-screen rather than display:none so bots still fill it. */
.field--trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__consent {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2xs);
  align-items: start;
  font-size: var(--step--1);
  color: var(--muted-ink);
}

.form__consent input {
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.28em;
  accent-color: var(--ink);
}

.form__consent a {
  color: var(--ink);
}

/* Contact details list, sits beside the form. */
.contact-list {
  display: grid;
  gap: var(--space-m);
  margin: 0;
}

.contact-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-s);
  align-items: start;
}

.contact-item svg {
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.28em;
  color: var(--brass-ink);
  flex: 0 0 auto;
}

.contact-item__label {
  display: block;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-ink);
  margin-bottom: 2px;
}

.contact-item__value {
  font-size: var(--step-1);
  color: var(--ink);
  text-decoration: none;
  font-style: normal;
}

a.contact-item__value:hover {
  color: var(--brass-ink);
  text-decoration: underline;
}

/* Opening hours */
.hours {
  margin: 0;
  border-top: var(--rule-w) solid var(--c-rule);
}

.hours__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-m);
  padding-block: var(--space-2xs);
  border-bottom: var(--rule-w) solid var(--c-rule);
  font-size: var(--step-0);
}

.hours__day {
  color: var(--c-muted);
}

.hours__time {
  color: var(--c-heading);
  font-variant-numeric: tabular-nums;
}


/* 18  Breadcrumbs
   ------------------------------------------------------------ */

.breadcrumb {
  padding-block: var(--space-s);
  font-size: var(--step--1);
  color: var(--muted-ink);
}

.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.breadcrumb li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

.breadcrumb li + li::before {
  content: "/";
  color: var(--stone);
}

.breadcrumb a {
  color: var(--muted-ink);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--ink);
  text-decoration: underline;
}

.breadcrumb [aria-current="page"] {
  color: var(--ink);
}


/* 19  Footer
   ------------------------------------------------------------ */

.site-footer {
  padding-block: var(--section-y) var(--space-l);
}

.footer__grid {
  display: grid;
  gap: var(--space-xl);
}

@media (min-width: 48em) {
  .footer__grid {
    grid-template-columns: 1.4fr 1fr 1fr;
    gap: var(--space-l);
  }
}

@media (min-width: 64em) {
  .footer__grid { gap: var(--space-2xl); }
}

.footer__brand img {
  width: auto;
  height: 3.25rem;
  margin-bottom: var(--space-m);
}

.footer__blurb {
  max-width: 34ch;
  font-size: var(--step-0);
  color: var(--c-text);
}

.footer__heading {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--c-heading);
  margin-bottom: var(--space-s);
}

.footer__list {
  margin: 0;
  display: grid;
  gap: var(--space-2xs);
}

.footer__list a {
  color: var(--c-text);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}

.footer__list a:hover {
  color: var(--brass);
  text-decoration: underline;
}

.socials {
  display: flex;
  gap: var(--space-2xs);
  margin-top: var(--space-m);
}

.social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: var(--rule-w) solid var(--c-rule);
  color: var(--c-text);
  transition: all var(--dur-fast) var(--ease);
}

.social svg {
  width: 1.125rem;
  height: 1.125rem;
}

.social:hover {
  border-color: var(--brass);
  color: var(--brass);
}

.footer__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  margin-top: var(--space-xl);
  padding-top: var(--space-m);
  border-top: var(--rule-w) solid var(--c-rule);
  font-size: var(--step--1);
  color: var(--c-muted);
}

.footer__bar a {
  color: var(--c-muted);
  text-decoration: none;
}

.footer__bar a:hover {
  color: var(--brass);
  text-decoration: underline;
}


/* 20  Mobile action bar
   Two thumb-sized targets, fixed to the bottom below 64em.

   No safe-area handling here, deliberately. env(safe-area-inset-*)
   only returns a non-zero value when the viewport meta carries
   viewport-fit=cover, which this site does not use. Without it the
   browser already insets the layout viewport above the home indicator,
   so reserving space again just stacks two gaps and leaves a dead strip
   under the labels.

   If an edge-to-edge treatment is ever wanted, viewport-fit=cover goes
   in the viewport meta and the inset comes back here - but then it has
   to be handled in the header and footer too, not only this bar.
   ------------------------------------------------------------ */

.mobile-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 120;
  display: grid;
  grid-template-columns: 1fr 1fr;
  height: var(--mobile-bar-h);
  background-color: var(--paper);
  border-top: var(--rule-w) solid var(--stone);
  box-shadow: 0 -4px 20px rgba(23, 19, 15, 0.08);
  transition: transform var(--dur) var(--ease);
}

@media (min-width: 64em) {
  .mobile-bar { display: none; }
}

/* Starts hidden and slides up once the visitor scrolls past the first
   60% of the viewport, so the landing view is not greeted by a toolbar
   and the hero gets that height back.

   Gated on .js so that without JavaScript the bar is simply always
   visible rather than permanently off-screen. */
.js .mobile-bar {
  transform: translateY(110%);
}

.js .mobile-bar[data-visible="true"] {
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .mobile-bar { transition: none; }
}

.mobile-bar__action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  transition: background-color var(--dur-fast) var(--ease);
}

.mobile-bar__action svg {
  width: 1.0625rem;
  height: 1.0625rem;
  flex: 0 0 auto;
}

.mobile-bar__action + .mobile-bar__action {
  background-color: var(--ink);
  color: var(--paper);
}

.mobile-bar__action:active {
  background-color: var(--shell);
}

.mobile-bar__action + .mobile-bar__action:active {
  background-color: var(--body-ink);
}

.mobile-bar__action:focus-visible {
  outline-offset: -3px;
}


/* 21  Motion and reveals
   Elements are visible by default. The .js class is added by main.js,
   so a JavaScript failure leaves everything readable.
   ------------------------------------------------------------ */

/* Hero entrance.
   Runs on load rather than on scroll, because the hero is above the
   fold and there is nothing to scroll into. Pure CSS with no JS hook,
   so a script failure cannot strand it half-animated.

   The photograph is deliberately NOT faded in. Chrome ignores elements
   sitting at opacity 0 when it records Largest Contentful Paint, so
   fading the hero image would push LCP back by the full duration of the
   animation and cost real Lighthouse points. A transform-only settle
   holds opacity at 1 throughout and is free.

   Nothing here moves layout - translate and scale are composited, so
   there is no CLS either. */

@keyframes hero-rise {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}

@keyframes hero-settle {
  from { transform: scale(1.04); }
  to   { transform: scale(1); }
}

.hero__content > *,
.page-hero .wrap > * {
  animation: hero-rise 0.55s var(--ease) both;
}

/* Stagger. Kept to nth-child rather than a class on every element so
   the markup stays clean, at the cost of the sequence shifting if a
   child is inserted. Total run is under half a second to the last
   item, which is short enough not to hold up reading. */
.hero__content > *:nth-child(1),
.page-hero .wrap > *:nth-child(1) { animation-delay: 0.04s; }

.hero__content > *:nth-child(2),
.page-hero .wrap > *:nth-child(2) { animation-delay: 0.11s; }

.hero__content > *:nth-child(3),
.page-hero .wrap > *:nth-child(3) { animation-delay: 0.18s; }

.hero__content > *:nth-child(4),
.page-hero .wrap > *:nth-child(4) { animation-delay: 0.25s; }

.hero__content > *:nth-child(5),
.page-hero .wrap > *:nth-child(5) { animation-delay: 0.32s; }

.hero__media img {
  animation: hero-settle 1.4s var(--ease) both;
}

.js .reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition:
    opacity var(--dur-slow) var(--ease),
    transform var(--dur-slow) var(--ease);
}

.js .reveal.is-visible {
  opacity: 1;
  transform: none;
}

.js .reveal-delay-1 { transition-delay: 0.08s; }
.js .reveal-delay-2 { transition-delay: 0.16s; }
.js .reveal-delay-3 { transition-delay: 0.24s; }

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

  .js .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* Belt and braces on the hero entrance. The rule above already
     collapses it to 0.01ms, and animation-fill-mode: both means it
     settles on the end state, so nothing is left hidden. This makes
     that explicit rather than relying on the interaction. */
  .hero__content > *,
  .page-hero .wrap > *,
  .hero__media img {
    animation: none;
    opacity: 1;
    transform: none;
  }
}


/* 22  Utilities
   ------------------------------------------------------------ */

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

.text-centre { text-align: center; }
.mx-auto { margin-inline: auto; }
.mt-0 { margin-top: 0; }
.mt-s { margin-top: var(--space-s); }
.mt-m { margin-top: var(--space-m); }
.mt-l { margin-top: var(--space-l); }
.mt-xl { margin-top: var(--space-xl); }
.measure { max-width: 60ch; }
.no-wrap { white-space: nowrap; }