/* ===========================================================================
   Skincare vertical · homepage — the playbook system, on a white ground.

   DESIGN.md, encoded:
     · container 1200 · sections separated by space-16..24 (64–96px)
     · radius sm 8 / md 16 / pill 999 · cards border, NO shadow; shadow on hover
     · buttons: GS 500, lowercase, pill
     · flood card: bg = SKU band, text by flood polarity, price mono
     · one display italic per view (`.wit`) · numbers in Martian Mono
     · motion: opacity/translate only, 150/250ms, ease(.32,.72,0,1);
       never parallax, never letter-by-letter, honour reduced-motion
   =========================================================================== */

:root { --sk-ease: cubic-bezier(.32,.72,0,1); }

/* ---- type primitives ---- */
/* .u-mono is a FACE utility. It used to set font-size too, and because this
   file loads after vertical/pdp.css it silently shrank .ms-actives__pct — the
   giant "1%" on every PDP — to 13px. Size belongs to the component. */
.u-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: var(--p-tracking-6); }
.sk-h2 { margin: 0; font-weight: 600; letter-spacing: var(--p-tracking-n20); line-height: 1.05;
  font-size: clamp(2rem, 3.6vw, 3.052rem); text-wrap: balance; color: var(--foreground); }
.sk-h2__accent { color: var(--brand); }
.sk-lead { margin: var(--p-space-4) 0 0; max-width: 38rem; font-size: var(--p-text-18); line-height: 1.55; color: var(--ink-2); }
.sk-head { margin-bottom: clamp(2rem, 4vw, 3rem); }
.sk-head--split { display: grid; gap: var(--p-space-4) var(--p-space-16); align-items: end; }
@media (min-width: 1024px) { .sk-head--split { grid-template-columns: 1fr 1fr; } .sk-head--split .sk-lead { margin-top: 0; } }

/* ---- buttons: the law's pill ---- */
.btn, .ms-btn { border-radius: var(--p-radius-pill); text-transform: lowercase; font-weight: 500; letter-spacing: var(--p-tracking-0); }
.btn--ghost { background: transparent; color: var(--brand); border-color: transparent; }
.btn--ghost:hover { background: var(--brand-muted); color: var(--brand); }
/* White at rest, not cream. It was cream resting and #FFFFFF on hover, so the
   default state was a dimmed version of its own hover — the button looked
   slightly dirty until you touched it. Same rule as .btn--cta and the view
   toggle: cream for the dark stages, white on saturated cupric. */
.btn--onflood { background: #FFFFFF; color: var(--brand); border-color: transparent; }
.btn--onflood:hover { background: var(--brand-muted); color: var(--brand-hover); }

/* ===========================================================================
   THE BENTO — the home page's layout system.

   WHY THIS EXISTS. Every section below used to be its own full-width band with
   its own vertical padding (clamp(4rem, 8vw, 6.5rem)) and its own inner
   container. Three consequences, all visible:

     1. THREE DIFFERENT LEFT EDGES ON ONE SCREEN. .sk-statement bled its media
        panel to the viewport edge while padding its copy onto the 1200px axis;
        .sk-ramp and .sk-start put their heads in .ms-wrap over a grid that was
        also .ms-wrap; the hero was a third. Nothing lined up with anything.
     2. VERTICAL RHYTHM SET BY PADDING, NOT BY A GAP. Two adjacent sections at
        6.5rem each produced a 13rem trough, and WordPress's own block gap
        (19.2px, measured) sat inside it — so the spacing between chapters was
        the sum of three numbers nobody chose together.
     3. THE PAGE WAS BANDS, NOT OBJECTS. A wall of cupric, then white, then a
        wash, then white. Each band read as a separate template.

   The fix is one axis and one number. `--sk-gap` is BOTH the gutter and the
   gap, so the outer margin and every seam between tiles are the same measure
   at every breakpoint — which is what "even padding and gaps" actually means
   and what the reference board is doing. Tiles are full-bleed on desktop:
   there is no 1200px axis on this page any more, because a bento that stops
   short of the viewport is a card grid with extra steps.

   The grid is 12 columns and the spans are named on the tile
   (.sk-tile--4/5/6/7/8). Below 1024 every tile is full width — asymmetry that
   survives to a phone becomes a touch-target problem, not a layout.
   =========================================================================== */
:root { --sk-gap: var(--p-space-3); }
@media (min-width: 640px) { :root { --sk-gap: var(--p-space-4); } }

.sk-bento { padding-inline: var(--sk-gap); }

/* The gap between chapters IS --sk-gap, and it has to out-specify TWO existing
   resets, not one. WordPress's own layout rule (`.wp-container-… > * + *`) is a
   single class, but engine/assets/sections.css also ships
   `main.wp-block-group > * { margin-block: 0 }` — (0,1,1), which silently beat
   a single-class selector here and collapsed the FAQ and the close flush
   against the tiles above them. Every selector below carries two classes.
   The FAQ is engine markup and cannot be given a .sk-bento class from a
   vertical, so it is named. */
.sk-bento + .sk-bento,
.sk-bento + .ms-faq-section,
.ms-faq-section + .sk-bento { margin-block-start: var(--sk-gap); }
.sk-bento:first-child { margin-block-start: 0; }

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

@media (min-width: 1024px) {
  .sk-bento__row { grid-template-columns: repeat(12, 1fr); }
  .sk-bento__row > * { grid-column: span 12; }
  .sk-bento__row > .sk-tile--4 { grid-column: span 4; }
  .sk-bento__row > .sk-tile--5 { grid-column: span 5; }
  .sk-bento__row > .sk-tile--6 { grid-column: span 6; }
  .sk-bento__row > .sk-tile--7 { grid-column: span 7; }
  .sk-bento__row > .sk-tile--8 { grid-column: span 8; }
}

/* ---- the tile ------------------------------------------------------------
   One radius for every tile, deliberately. --radius-card is brand law's `md`
   (16px, "cards, panels, PDP gallery"); a bespoke larger radius for the big
   tiles would be a fourth radius the law does not have, and mixing two would
   cost the thing a bento is for — the sense that one grid produced all of it.
   GROUND IS --surface-2, NOT --card. The page ground is white and --card is
   white too, so a tile painted --card is a tile you cannot see: the grid would
   exist only where a photograph or a flood happened to fall, and every copy
   tile would read as loose text on the page. --surface-2 is the system's own
   faint blue wash, already defined for exactly this ("section bands") — the
   same cool family as the ground, so it introduces no third hue and nothing
   warms. It is the smallest step that still draws a tile. A tile that is not a
   link carries no border and no shadow (law §7.5): the step IS the edge. */
.sk-tile {
  position: relative;
  /* THREE OF THE PHOTO TILES ARE <figure>, WHICH SHIPS margin: 1em 40px. In the
     old per-section CSS each one zeroed that individually (.sk-proto__media,
     .sk-system__media, .sk-start__media all opened with `margin: 0`). A grid
     item's margin comes OUT of its track, so the protocol photograph measured
     379px inside a 459px column and sat 40px inboard of a left edge every other
     tile shares — the one misalignment the whole layout exists to prevent.
     Owned by the tile now, so a new photo tile cannot reintroduce it. */
  margin: 0;
  min-width: 0;                       /* or a long mono row blows the column out */
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--surface-2);
  color: var(--foreground);
}
.sk-tile--pad { padding: clamp(var(--p-space-6), 3.2vw, var(--p-space-12)); }
/* RETIRED, kept named. The copper-system tile used to take blue-100 here,
   carried over from when that section was a full-width tinted band. Next to a
   --surface-2 tile it reads as two washes of the same blue at slightly
   different values rather than as an accent — and that tile already carries
   four cupric rows, so it was never short of colour. Every copy tile is one
   wash now; colour appears where it means something (the cupric statement, the
   deep manifesto, the flood bands) and nowhere else. */
.sk-tile--wash  { background: var(--surface-2); }
.sk-tile--flood { background: var(--brand); color: var(--brand-foreground); }
.sk-tile--deep  { background: var(--surface-ink); color: var(--on-dark-warm); }

/* ---- photo tiles ---------------------------------------------------------
   The image is absolutely positioned so the tile can be sized by its ROW —
   a photo tile beside a copy tile takes the copy's height and crops to it,
   which is what keeps every seam in a row straight. aspect-ratio only governs
   the single-column case, where there is no sibling to take a height from. */
/* Only ever seen while the photograph decodes — matched to the copy tiles so
   a slow image does not flash a second colour into the grid. */
.sk-tile--photo { background: var(--surface-2); aspect-ratio: 4 / 3; }
.sk-tile--photo > .sk-tile__img,
.sk-tile__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
@media (min-width: 1024px) {
  .sk-tile--photo { aspect-ratio: auto; min-height: 30rem; }
}

/* ====================================================== HERO — full-bleed
   "The horizon": the photograph is the section, not a card inside it. The
   left ~55% of the wide frame is flat cream BY ART DIRECTION, so the headline
   sits straight on the picture with no scrim — which is the point, since a
   scrim would have to be a gradient and law §7.2 bans those.

   That only holds while the crop holds. Ink #10131A on the cupric floor is
   2.38:1 and fails AA, so the overlay is scoped to >=1024px; below that the
   section STACKS — cream copy panel, then the art-directed frame under it —
   rather than gambling that a narrow viewport keeps the copy above the
   horizon. The panel is the same cream as the wall in the photograph, so
   the seam does not read.

   Cream is the section's own background too, so a missing attachment
   degrades to a plain cream band with ink copy instead of ink on white
   with a hole where the hero was.
   =========================================================== */
/* The hero's ground is the RAMP'S PALEST STEP, not a neutral. Warm cream
   read as beige next to the cupric — and it also contradicted the token
   layer, where --background became white on 2026-08-19 ("white clinical
   ground, founder decision, replacing cream"); this section had quietly
   dragged cream back in as the dominant surface. blue-100 fixes both and
   earns its keep: pale step as the wall, cupric as the floor and the
   product, so the frame IS the dilution ramp rather than a product on a
   backdrop. Ink on it is 15.37:1 and cupric on it 5.42:1 — both already
   audited pairs in §1, no new colour invented. */
.sk-hero { position: relative; display: grid; background: var(--brand-muted); }
/* Stacked: the FRAME LEADS. Copy-first put a 570px block of type above the
   fold on a 390x844 phone and pushed the bottle to y=950 — the first screen
   was headline on blank cream, which is the one thing this hero exists not to
   be. Image-first shows the whole portrait crop, then the copy, and the
   photograph's cupric floor butts straight into the cream copy panel. */
/* Square band, biased down the frame: the portrait crop's top ~78% is wall,
   so a full 4:5 band spent 380 of its 487px on empty cream before the bottle
   appeared. 1/1 at 78% keeps the wall, the horizon and the whole bottle, and
   hands ~100px back to the copy above the fold. */
/* `position: relative` is load-bearing, not tidiness. The layers are
   absolutely positioned; without it they resolve against .sk-hero and
   fill the whole 960px section instead of this 390px band, which blew
   the bottle up and cropped it off the right edge on a phone. */
.sk-hero__stage { order: 1; position: relative; overflow: hidden; aspect-ratio: 1 / 1; }
.sk-hero__stage picture { display: contents; }
.sk-hero__img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 100%; display: block; }
.sk-hero__inner { order: 2; width: 100%; padding-block: var(--p-space-12) var(--p-space-16); }
.sk-hero__copy { max-width: 34rem; }

/* Mono, deadpan, caps at ONE tracking rung — the face is wide already (§2). */
.sk-hero__eyebrow { margin: 0 0 var(--p-space-5); font-size: var(--p-text-13);
  text-transform: uppercase; color: var(--ink-2); }

/* Display tier. `--font-display` — Raleway since PR #17 replaced Fraunces;
   this rebased onto that rename, so do not reintroduce `--font-serif`,
   which no longer resolves and the phantom gate rejects. */
/* 61px, not the 76px this carried when the hero was a bottle on a floor.
   That size was chosen for a frame that was mostly empty — the headline WAS
   the hero. A model carries the frame now and the copy sits in a five-column
   field beside her, where 76px wrapped to five lines and read as cramped
   rather than confident. 61px lands it in four. */
.sk-hero__h1 { margin: 0; font-family: var(--font-display); font-weight: 600;
  letter-spacing: var(--p-tracking-n20); line-height: 1.05;
  font-size: clamp(var(--p-text-30), 4.4vw, var(--p-text-61));
  color: var(--foreground); text-wrap: balance; }
.sk-hero__h1 span { color: var(--brand); }
.sk-hero__sub { margin: var(--p-space-6) 0 0; max-width: 32rem; font-size: var(--p-text-18); line-height: 1.55; color: var(--ink-2); }
.sk-hero__cta { display: flex; flex-wrap: wrap; gap: var(--p-space-3); margin-top: var(--p-space-8); }

/* The dilution ramp — the only decorative device the brand has (§8). Hard
   bands, no gradient. It sits on the section's bottom edge in both layouts,
   so the cupric floor in the photograph resolves into the five doses. */
.sk-hero__ramp { position: absolute; inset-inline: 0; bottom: 0; z-index: 2;
  display: grid; grid-template-columns: repeat(5, 1fr); height: 0.5rem; }
.sk-hero__ramp span:nth-child(1) { background: var(--flood-100); }
.sk-hero__ramp span:nth-child(2) { background: var(--flood-200); }
.sk-hero__ramp span:nth-child(3) { background: var(--flood-300); }
.sk-hero__ramp span:nth-child(4) { background: var(--flood-400); }
.sk-hero__ramp span:nth-child(5) { background: var(--flood-500); }

@media (max-width: 767px) { .sk-hero__cta .btn { width: 100%; } }
/* 768–1279 still stacks, but on the 16:9 frame — the portrait <source> is
   scoped to <=767px, so this band gets the wide crop. */
@media (min-width: 768px) and (max-width: 1279px) {
  .sk-hero__stage { aspect-ratio: 16 / 9; }
  .sk-hero__img { object-position: 62% 50%; }
}
/* Overlay. There is no horizon in this frame any more, and that DELETES a
   class of problem rather than solving it.

   The bottle-on-a-floor hero had a hard cupric band across its lower third,
   and ink on cupric is 2.38:1 — fails AA. Keeping copy off that band took a
   grid whose rows mirrored the frame's measured horizon (71.34%) plus an
   argument about how `cover` maps object-position onto it. All of that existed
   to protect text from ONE region of ONE picture.

   The model frame is a single flat pale-blue sweep. Ink is 15.37:1 everywhere
   on it, so the rows, the ratio and the invariant are all gone and the copy is
   simply centred.

   The HORIZONTAL constraint is the real one now: the copy must clear the
   model. Measured on the frame itself, her leftmost edge sits at 43.2% of the
   frame width, so the crop anchors LEFT to spend that whole clear field on
   type, and the column is five bento columns — six runs into her hand. */
@media (min-width: 1280px) {
  .sk-hero { align-items: center; isolation: isolate;
    min-height: max(42rem, min(88svh, 60rem)); }
  .sk-hero__stage { position: absolute; inset: 0; z-index: 0; aspect-ratio: auto; }
  /* Anchor left: the type field is the left of the frame, so any horizontal
     crop has to come off the right, where she is already edge-cropped. */
  .sk-hero__img { object-position: 0% 50%; }
  .sk-hero__inner { order: 0; align-self: center; position: relative; z-index: 1;
    padding-block: var(--p-space-12); }
  .sk-hero__copy { max-width: none; }
}
/* ============================================ HERO — depth (parallax)
   §7.13 used to ban parallax outright. Amended 2026-08-21 by founder
   decision, hero only, under four conditions the implementation below is
   built to satisfy — see the skill file for the full text.

   THE TWO PLANES ARE ONE PHOTOGRAPH PULLED APART. `cf-hero-fg` is
   `cf-hero-horizon` with the cream wall keyed out (border-connected flood
   fill, so the cream LABEL TYPE inside the bottle's silhouette survives —
   a plain colour key ate it), leaving floor, bottle and the bottle's own
   cast shadow. `cf-hero-wall` is the same set shot empty. So the depth is
   real: same sun, same grade, same grain, one shadow direction (§5).

   NO JAVASCRIPT. The motion is a CSS scroll timeline, which the compositor
   runs off the main thread. GSAP + Lenis — the reference implementation —
   would have added ~70KB of runtime to a theme that ships no bundler, and
   Lenis takes over the whole page's scrolling to do it. It also would not
   degrade: here, a browser without `animation-timeline` never matches the
   @supports block and simply renders the flat frame, which is the finished
   hero. Same for `prefers-reduced-motion: reduce`.

   `linear` is correct here and is not the banned easing. A scrubbed layer
   has to track scroll position exactly; easing a scrubbed transform makes
   it lag behind the pointer and read as jank rather than depth.

   DIRECTION IS A CONTRAST DECISION, NOT A TASTE ONE. Both text layers
   drift UP, away from the cupric floor. Ink on cupric is 2.38:1 and fails
   AA, and the grid invariant above only guarantees the copy's STATIC
   position — anything that moved it downward would spend the 48px of
   clearance that invariant buys. Moving it up only ever adds clearance.
   The foreground plate is planted at 0 so the horizon never moves relative
   to the grid rows that were keyed to it. */
/* Both planes share .sk-hero__img, so every object-position rule above
   already applies to them and the keyed floor lands exactly where the flat
   frame's floor did — which is what keeps the horizon on the grid line. */
.sk-hero__layer { position: absolute; inset: 0; }
.sk-hero__layer--wall, .sk-hero__layer--flat { z-index: 0; }
.sk-hero__layer--fg { z-index: 1; }
.sk-hero__stage picture { display: contents; }

@media (min-width: 1280px) {
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      /* Headroom: the wall hangs 14% above the stage, so drifting down 10%
         of its own box can never expose the top edge. Applied only here —
         a static wall stays flush. */
      .sk-hero--deep .sk-hero__layer--wall { top: -14%; }

      .sk-hero--deep { view-timeline-name: --sk-hero; view-timeline-axis: block; }
      .sk-hero--deep [data-parallax-layer] {
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: --sk-hero;
        animation-range: exit 0% exit 100%;
      }
      .sk-hero--deep [data-parallax-layer="1"] { animation-name: sk-drift-wall; }
      .sk-hero--deep [data-parallax-layer="2"] { animation-name: sk-drift-eyebrow; }
      .sk-hero--deep [data-parallax-layer="3"] { animation-name: sk-drift-say; }
      .sk-hero--deep [data-parallax-layer="4"] { animation-name: sk-drift-fg; }
    }
  }
}
/* Transform and opacity only (§8). The wall lags downward as the section
   leaves, the two text layers lift at different rates, the product stays
   planted — back moves most, front moves least. */
@keyframes sk-drift-wall    { to { transform: translate3d(0, 10%, 0); } }
@keyframes sk-drift-eyebrow { to { transform: translate3d(0, calc(-1 * var(--p-space-6)), 0); } }
@keyframes sk-drift-say     { to { transform: translate3d(0, calc(-1 * var(--p-space-12)), 0); } }
@keyframes sk-drift-fg      { to { transform: translate3d(0, 0, 0); } }

/* No photograph: drop the min-height, or the fallback renders a short copy
   block adrift in a 42rem band of empty blue. */
.sk-hero--nopic { min-height: 0; }
.sk-hero--nopic .sk-hero__inner { padding-block: var(--p-space-24); }

/* ============================================================ THE RAMP */
/* No section background: .sk-tile owns the ground now, and the head and the
   strip share one tile rather than sitting on a bare band. */
/* ---- HEAD: the title and the section's own CTA share one line; the lead sits
   under both. The button is centred against the TITLE ROW, not the whole head
   — hung off the full block it floats opposite the gap between heading and
   lead and reads as unanchored. Two rows, three children, no wrapper div:
   the lead spans so its measure is set by .sk-lead's max-width and not by
   whatever width the button leaves over.

   On phones the grid collapses to a column and the button re-orders BELOW the
   lead: a CTA between a heading and its own explanation reads as an interrupt,
   and at that width it would be a full-width bar cutting the head in half. */
.sk-ramp__head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--p-space-4); }
.sk-ramp__lead { order: 2; margin-top: 0; }
.sk-ramp__all { order: 3; white-space: nowrap; }
@media (min-width: 768px) {
  .sk-ramp__head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title action" "lead lead";
    align-items: center;
    column-gap: var(--p-space-10);
    row-gap: var(--p-space-4);
  }
  .sk-ramp__title { grid-area: title; }
  .sk-ramp__all { grid-area: action; justify-self: end; }
  .sk-ramp__lead { grid-area: lead; }
}

/* On the bare ground, so it pads itself. The tile that used to do this is gone
   — see ms_ramp_sc(). */
.sk-ramp__head { padding-block: clamp(var(--p-space-6), 3vw, var(--p-space-10)); }

/* --sk-gap, not a second spacing decision: the seam between two cards and the
   seam between two tiles are the same measure. That is the whole bento rule,
   and it is why these five cards can sit in the grid as tiles in their own
   right rather than inside one. */
.sk-ramp__strip { display: grid; gap: var(--sk-gap); grid-template-columns: repeat(5, 1fr); align-items: stretch; }
@media (max-width: 1023px) { .sk-ramp__strip { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 639px)  { .sk-ramp__strip { grid-template-columns: repeat(2, 1fr); } }

/* ---- RAMP CARD (the best-seller card) --------------------------------------
   Rationale for the split from .flood lives on ms_skin_ramp_card() in
   verticals/skincare/inc/sections.php. The short version: the flood card had
   nowhere to put a CTA that was not cupric-on-cupric, so the five best sellers
   could not be bought from. Here the band is a BAR — five solid steps still
   march down the strip, printed on the same line as the dose they encode —
   and the commerce layer sits on the page ground at full ink contrast.

   The card's own border is what bounds the palest step. F-100 (#DEEBFA) is
   1.15:1 against a white page, which is why the flood card had to grow a
   bespoke inset ring in its own label colour; a bar inside a bordered card
   inherits that edge for free and the ring is not repeated here. */
.rampcard {
  container-type: inline-size;
  display: flex; flex-direction: column;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
  transition: transform 250ms var(--sk-ease), border-color 250ms var(--sk-ease), box-shadow 250ms var(--sk-ease);
}
/* Law §7.5: a card is a border at rest, elevation only on interaction. */
@media (hover: hover) and (pointer: fine) {
  .rampcard:hover, .rampcard.is-hover {
    transform: translateY(-2px);
    border-color: color-mix(in oklab, var(--foreground) 15%, transparent);
    box-shadow: var(--elevation-lift);
  }
  .rampcard:hover .rampcard__img, .rampcard.is-hover .rampcard__img { transform: scale(1.03); }
}
@media (prefers-reduced-motion: reduce) {
  .rampcard, .rampcard__img { transition: none; }
  .rampcard:hover, .rampcard.is-hover { transform: none; }
  .rampcard:hover .rampcard__img, .rampcard.is-hover .rampcard__img { transform: none; }
}

/* The packshots carry their own cream studio ground, so the stage is a window
   onto the photograph and not a coloured panel — nothing is inset, nothing is
   blended. Same conclusion the shop card reached; see the mix-blend note at
   the foot of this file for what happens when a flood is forced behind one. */
.rampcard__stage { position: relative; display: block; aspect-ratio: 1; overflow: hidden; background: var(--surface); outline: none; }
.rampcard__stage:focus-visible { box-shadow: inset 0 0 0 3px color-mix(in oklab, var(--ring) 40%, transparent); }
.rampcard__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 250ms var(--sk-ease); }

/* ---- the dose bar: this is the ramp ----
   Polarity is mechanical — ink through F-400, cream on F-500 — and it arrives
   with the band from ms_flood_band(), so nothing here decides a colour. Full
   strength, never dimmed: `opacity` on a band mixes toward the band and that
   is exactly how `F-400 50 ml AM` shipped at 3.87:1. Hierarchy comes from
   size, case and tracking instead. 12px is the law's floor for any text. */
.rampcard__dose {
  margin: 0;
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--p-space-1) var(--p-space-2-5);
  padding: var(--p-space-2-5) var(--p-space-4);
  background: var(--band, var(--muted));
  color: var(--band-on, var(--foreground));
  font-size: var(--p-text-12);
  line-height: 1.35;
  letter-spacing: var(--p-tracking-6);
  text-transform: uppercase;
}
.rampcard__band { font-weight: 600; }
/* No band, no bar colour — a SKU with no assignment gets the neutral surface
   rather than a defaulted step. Inventing a band is a §4 cull. */
.rampcard--plain .rampcard__dose { background: var(--muted); color: var(--ink-2); }

.rampcard__body { display: flex; flex: 1; flex-direction: column; gap: var(--p-space-2); padding: var(--p-space-4); }
@container (min-width: 15rem) { .rampcard__body { gap: var(--p-space-2-5); padding: var(--p-space-5); } }

/* The name is an h3, so it takes the display face from engine/style.css along
   with every other heading — the same register as .shopcard__name, which is
   also an h3. Deliberately NOT pinned back to the sans here: that is the exact
   defect the 2026-08-21 typography fix removed from this vertical. Only the
   tracking is overridden, because the global h1–h3 rule is tuned for headline
   scale and over-tightens a 17px line. */
.rampcard__name {
  margin: 0; font-size: var(--p-text-17); line-height: 1.3;
  font-weight: 600; letter-spacing: var(--p-tracking-n10); text-wrap: balance;
}
.rampcard__name a { color: var(--foreground); text-decoration: none; outline: none; transition: color 150ms var(--sk-ease); }
.rampcard__name a:hover { color: var(--brand); }
.rampcard__name a:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; border-radius: var(--radius-control); }

/* Two lines, clamped AND reserved. Clamping alone stops a third line; it does
   not stop a one-line tagline from lifting that card's rating row 21px above
   its neighbours', which is what made the five star rows read as a ragged
   column rather than a system. min-height holds the block at two lines so the
   ratings align; price and CTA are already pinned by margin-top:auto below. */
.rampcard__tagline {
  margin: 0; font-size: var(--p-text-14); line-height: 1.45; color: var(--ink-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: 2.9em;  /* 2 x line-height, in the element's own type size */
}

/* margin-top:auto pins price + CTA to the card's foot, so five cards with
   different-length names still align their buttons on one line. */
.rampcard__price {
  margin: auto 0 0; padding-top: var(--p-space-1);
  font-size: var(--p-text-20); line-height: 1.2;
  font-variant-numeric: tabular-nums; color: var(--foreground);
}
.rampcard__price .woocommerce-Price-amount { color: inherit; font: inherit; }

.rampcard__actions { display: grid; }
/* The rating is optional markup (silent until the SKU has real reviews), so it
   must not open a gap of its own when absent — the body's flex gap covers it. */
.rampcard .ratingmini { min-width: 0; }

/* ---- FLOOD CARD (DESIGN.md · Card · flood) ----------------------------------
   The card's ground IS the SKU's band and the label obeys flood polarity. That
   is brand law and it is unchanged here. What changed is everything the law
   does not say, because three of those things were defects.

   1. THE DOSE ROW WAS DIMMED WITH `opacity: .8`, AND THAT FAILED WCAG AA.
      Opacity does not "soften" a colour, it mixes it toward whatever is
      BEHIND it — here, the band. Measured against the shipped tokens:

          band            pair          full     at .8
          F-100 #DEEBFA   ink           15.37    8.89
          F-200 #A9CDF2   ink           11.24    7.17
          F-300 #7AABE9   ink            7.81    5.52
          F-400 #4483E0   ink            4.93    3.87   <- AA fail, 12px text
          F-500 #0B55CE   white          6.55    4.77   <- 0.27 of margin

      `F-400 50 ml AM` under the daily spf was the failing case, live. The
      row is now drawn at full strength and its hierarchy comes from what
      hierarchy is supposed to come from — size, weight and tracking. A dose
      line is also the most factual thing on the card; it was the one element
      being asked to whisper.

   2. THE META WAS JAMMED AGAINST THE PHOTOGRAPH. `padding: 0 20px 20px` put
      zero space between the bottom of the image and the top of the name, so
      the type read as spill rather than as a caption.

   3. THE HOVER SHADOW WAS A ONE-OFF. `0 4px 12px color-mix(… --foreground 8%)`
      was hand-rolled here while the theme already ships `--elevation-lift` for
      exactly this. A bespoke shadow is a value that cannot reskin and that no
      other card matches. It uses the token now, and only on hover — brand law
      §7.5 gives a card a border at rest and elevation only on interaction. */
.flood {
  position: relative;
  display: flex; flex-direction: column;
  text-decoration: none;
  color: var(--band-on, var(--foreground));
  background: var(--band, var(--surface));
  border-radius: var(--radius-card);
  overflow: hidden;
  /* THE PALE END OF THE RAMP NEEDED AN EDGE. F-100 is #DEEBFA on a #FFFFFF
     page — 1.15:1 against its own ground, so the jelly cleanser's card had no
     discernible boundary and read as loose type floating on the section. Drawn
     as an inset ring in the card's OWN label colour rather than as a border:
     it scales with the band (barely there on cupric, definite on the palest
     step), and it costs no layout, so the card does not resize on hover. */
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--band-on, var(--foreground)) 12%, transparent);
  transition: transform 250ms var(--sk-ease), box-shadow 250ms var(--sk-ease);
}
.flood--plain { border: 1px solid var(--border); }
.flood:hover, .flood.is-hover {
  transform: translateY(-2px);
  /* box-shadow does not cascade per-layer: the inset ring has to be restated
     here or hovering a card silently removes its edge. */
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--band-on, var(--foreground)) 12%, transparent),
              var(--elevation-lift);
}
.flood:active { transform: translateY(0); }
/* A flood card is an anchor on a saturated ground, so the ring is drawn in the
   card's own label colour: a cupric ring on a cupric card is invisible. */
.flood:focus-visible, .flood.is-focus {
  outline: 2px solid var(--band-on, var(--ring));
  outline-offset: 3px;
}
.flood__media { aspect-ratio: 1; display: block; overflow: hidden; }
.flood__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 250ms var(--sk-ease); }
.flood:hover .flood__img, .flood.is-hover .flood__img { transform: scale(1.03); }
.flood__meta {
  display: grid; gap: var(--p-space-1-5);
  padding: var(--p-space-4) var(--p-space-5) var(--p-space-5);
  min-width: 0;
}
.flood__name {
  font-weight: 600; letter-spacing: var(--p-tracking-n10); font-size: var(--p-text-17);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Full contrast. Tracking and case do the work `opacity` was doing badly. */
.flood__row {
  display: flex; flex-wrap: wrap; gap: var(--p-space-1) var(--p-space-3);
  font-size: var(--p-text-12); letter-spacing: var(--p-tracking-6);
  text-transform: uppercase; font-variant-numeric: tabular-nums;
}
.flood__price { font-size: var(--p-text-15); font-variant-numeric: tabular-nums; }
/* Every other price container in the theme resets the WooCommerce span to
   `color: inherit`. This one never did — it happens to inherit correctly
   today, which means the omission is invisible until a plugin or a WC update
   colours `.woocommerce-Price-amount`, and then one card in five turns
   unreadable on cupric with nothing in this file to explain why. */
.flood__price .woocommerce-Price-amount { color: inherit; font: inherit; }

/* ---- FLOOD CARD, ROW (the `start here` protocol) ----------------------------
   The packshots carry their own cream stage. Butted straight against the band
   the two grounds met at a hard vertical seam — the single loudest thing in
   the section, and it read as a broken image rather than as a design. The
   photograph is now INSET as a tile with its own radius, so the band frames it
   deliberately. Same fix Zellerfeld and Maxima use on colour-blocked rows. */
.flood--row { flex-direction: row; align-items: center; gap: var(--p-space-1); padding: var(--p-space-2-5); }
.flood--row .flood__media {
  aspect-ratio: 1; width: 6.5rem; flex: 0 0 6.5rem;
  border-radius: calc(var(--radius-card) - var(--p-space-2));
  overflow: hidden;
}
.flood--row .flood__meta {
  padding: var(--p-space-2) var(--p-space-4);
  flex: 1 1 auto; min-width: 0;
  align-content: center;
}
@media (max-width: 419px) {
  .flood--row .flood__media { width: 5rem; flex-basis: 5rem; }
  .flood--row .flood__name { font-size: var(--p-text-16); }
}

/* ============================================================ STATEMENT */
/* Colour comes from .sk-tile--flood; everything here is type. The headline
   inherits rather than naming --on-dark-warm, so the tile's polarity is the
   single place the light-on-dark decision is made. */
.sk-statement__copy { display: flex; flex-direction: column; justify-content: center; }
.sk-statement__h { margin: 0; font-weight: 600; letter-spacing: var(--p-tracking-n30); line-height: 0.98;
  font-size: clamp(2.6rem, 4.4vw, 4.2rem); color: inherit; text-wrap: balance; }
.sk-statement__row { margin: var(--p-space-6) 0 0; color: inherit; opacity: .85; }
.sk-statement__p { margin: var(--p-space-5) 0 var(--p-space-8); max-width: 32rem; font-size: var(--p-text-17); line-height: 1.55;
  color: color-mix(in oklab, currentColor 88%, transparent); }
.sk-statement__copy .btn { align-self: start; }

/* ============================================================ PROTOCOL */
/* The photograph used to be `position: sticky; top: 6rem`. Inside a bento row
   that is actively wrong: the tile detaches from the row it belongs to and
   travels over the next one, so the two seams it is supposed to hold straight
   are the first things to break. */
.sk-proto__cols { display: grid; gap: var(--p-space-8) var(--p-space-10); grid-template-columns: 1fr 1fr; margin-top: clamp(var(--p-space-6), 2.5vw, var(--p-space-10)); }
@media (max-width: 639px) { .sk-proto__cols { grid-template-columns: 1fr; } }
.sk-proto__h { margin: 0 0 var(--p-space-4); font-size: var(--p-text-24); font-weight: 400; color: var(--brand); letter-spacing: 0.1em; }
.sk-proto__list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); }
.sk-proto__item { display: grid; grid-template-columns: 5.5rem 1fr; gap: var(--p-space-4); align-items: baseline;
  padding: var(--p-space-3) 0; border-bottom: 1px solid var(--border); }
/* 12px, not 11. Brand law §2 puts the floor at 0.75rem / 12px — "nothing ships
   smaller, anywhere" — and Martian Mono is a WIDE face, so caps micro-labels
   are the last place to go under it. Measured: these two rules were the only
   sub-12px text on the page, 17 nodes of it. */
.sk-proto__step { font-size: var(--p-text-12); color: var(--ink-3); }
.sk-proto__item a { color: var(--foreground); text-decoration: none; font-weight: 500; }
.sk-proto__item a:hover { color: var(--brand); }

/* ============================================================ THE SYSTEM */
.sk-system__body { display: flex; flex-direction: column; justify-content: center; }
.sk-rows { list-style: none; margin: var(--p-space-8) 0 0; padding: 0; display: grid; gap: var(--p-space-2); }
.sk-row { display: grid; grid-template-columns: 1fr auto auto; gap: var(--p-space-4); align-items: center;
  background: var(--band); color: var(--band-on); border-radius: var(--p-radius-pill); padding: var(--p-space-3) var(--p-space-5); font-size: var(--p-text-15); }
.sk-row a { color: inherit; text-decoration: none; font-weight: 600; }
.sk-row .u-mono { font-size: var(--p-text-12); opacity: .9; }
@media (max-width: 639px) { .sk-row { grid-template-columns: 1fr auto; } .sk-row .u-mono:nth-of-type(1) { display: none; } }
.sk-system__shield { margin: var(--p-space-7) 0 0; color: var(--ink-2); font-size: var(--p-text-15); line-height: 1.55; }
.sk-system__shield a { color: var(--brand); }

/* ============================================================ START HERE */
.sk-start__body { display: flex; flex-direction: column; justify-content: center; }
.sk-start__cards { display: grid; gap: var(--p-space-3); align-content: start; margin-top: clamp(var(--p-space-6), 2.5vw, var(--p-space-10)); }
.sk-set { display: grid; grid-template-columns: 1fr auto; grid-template-areas: "k k" "n p" "b b"; gap: 0.35rem var(--p-space-4); align-items: center;
  text-decoration: none; color: var(--foreground); border: 1px solid var(--border); border-radius: var(--radius-card); padding: var(--p-space-5);
  transition: border-color 150ms var(--sk-ease), box-shadow 250ms var(--sk-ease); }
.sk-set:hover { border-color: var(--border-strong); box-shadow: var(--elevation-lift); }
.sk-set__k { grid-area: k; color: var(--ink-3); font-size: var(--p-text-12); }
.sk-set__n { grid-area: n; font-weight: 600; font-size: var(--p-text-18); letter-spacing: var(--p-tracking-n10); }
.sk-set__p { grid-area: p; font-size: var(--p-text-16); }
.sk-set .btn { grid-area: b; margin-top: var(--p-space-3); justify-self: start; }

/* ============================================================ MANIFESTO */
/* The page's one deep step, and the page's one Fraunces italic. Cream on
   blue-800 is brand law's own pairing for a deep web section (13.67:1). */
.sk-manifesto__tile { text-align: left; }
.sk-manifesto__serif { margin: 0; max-width: 24ch; font-size: clamp(2.2rem, 5vw, 3.8rem); line-height: 1.1; color: inherit; }
.sk-manifesto__body { margin: var(--p-space-6) 0 0; font-size: var(--p-text-18); line-height: 1.6; max-width: 44rem;
  color: color-mix(in oklab, currentColor 82%, transparent); }

/* =================================================== FAQ — a bento row too */
/* Tier 3 override of the engine's centred full-width FAQ. It is the only
   chapter between the manifesto and the close, and left as a centred band it
   put a 28rem centred column between two full-bleed grids — the page visibly
   changed layout system for one section and changed back. Same 12 columns,
   same gap, head and list as two tiles. */
.ms-faq-section {
  padding-inline: var(--sk-gap);
  padding-block: 0;
  margin-block-start: var(--sk-gap);
  display: grid; gap: var(--sk-gap);
  max-width: none;
}
@media (min-width: 1024px) { .ms-faq-section { grid-template-columns: repeat(12, 1fr); } }
.ms-faq-section .ms-faq__head,
.ms-faq-section .ms-faq__list {
  background: var(--surface-2); border-radius: var(--radius-card);
  padding: clamp(var(--p-space-6), 3.2vw, var(--p-space-12));
  margin: 0; min-width: 0;
}
@media (min-width: 1024px) {
  .ms-faq-section .ms-faq__head { grid-column: span 4; align-content: center; }
  .ms-faq-section .ms-faq__list { grid-column: span 8; }
}
/* The engine centres this head; in a left tile that reads as a mistake. */
.ms-faq-section .ms-faq__head { text-align: start; }
.ms-faq-section .ms-faq__lede { margin-inline: 0; }
.ms-faq-section .ms-faq__list { border-top: 0; }
.ms-faq-section .ms-faqitem:last-child { border-bottom: 0; }

/* ============================================================ CLOSE */
/* A full-width photo tile, so the page ends inside the same grid it spent its
   whole length building. The copy panel is an INSET card on the photograph —
   two levels, not three. */
.sk-close__tile {
  position: relative; overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--surface-2);
  min-height: min(80dvh, 44rem);
  display: flex; align-items: flex-end;
}
.sk-close__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 60% 0%; }
.sk-close__copy { position: relative; width: 100%; padding: clamp(var(--p-space-5), 3vw, var(--p-space-10)); }
.sk-close__panel { display: inline-block; background: var(--background); border-radius: var(--radius-card); padding: clamp(var(--p-space-6), 3vw, var(--p-space-10)); max-width: 30rem; }
.sk-close__h { margin: 0 0 var(--p-space-5); font-weight: 600; letter-spacing: var(--p-tracking-n30); line-height: 1; font-size: clamp(2.4rem, 5.5vw, 4.5rem); color: var(--foreground); max-width: 14ch; }
.sk-close__row { display: flex; flex-wrap: wrap; gap: var(--p-space-3); }
.sk-close .btn--onink { background: var(--foreground); color: var(--brand-foreground); }

/* The page's last tile needs the same measure BELOW it that every seam above
   it uses, or the footer butts straight against a rounded corner. */
.sk-bento.sk-close { padding-block-end: var(--sk-gap); }

/* ---- reveal (opacity/translate only; honours reduced motion) ---- */
@media (prefers-reduced-motion: no-preference) {
  .flood, .rampcard, .sk-row { will-change: transform; }
}

/* ---- shop grid: NOT flooded ----------------------------------------------
   DESIGN.md puts the flood on the PDP gallery panel and on "flood cards of
   the 3-step routine, ramp-ordered" — small curated sets. Applied to a
   23-item grid it inverts the kit's palette ratio (cream ~40% / cupric ~25%)
   to ~80% saturated blue, and doubles a flood the packshot already carries.
   The grid uses the default card (components.css); the band survives as a
   swatch on the spec line, so "the colour is the dose" is still legible. */
.shopcard[data-flood] .shopcard__spec::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 0.625rem; height: 0.625rem; border-radius: var(--radius-pill);
  background: var(--band); margin-inline-end: var(--p-space-2);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--foreground) 10%, transparent);
}

/* ---- ramp on phones: a snap strip, not an orphaned 2-up grid ---- */
@media (max-width: 639px) {
  .sk-ramp__strip { display: flex; gap: var(--sk-gap); overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: var(--p-space-2); scrollbar-width: none; }
  .sk-ramp__strip::-webkit-scrollbar { display: none; }
  .sk-ramp__strip .rampcard { flex: 0 0 72vw; scroll-snap-align: start; }
}

/* F-500: multiply has nowhere to go on cupric (cupric x cupric = invisible
   bottle). On the darkest flood the photograph sits as a cream WINDOW inside
   the card — the kit sheet's own tile logic — instead of being blended. */
/* The packshots carry their own stage, so a flood card is a coloured card with
   a PHOTO PANEL in it — the kit sheet's tile logic. They fill the media box
   edge to edge; nothing is inset and nothing is blended. multiply used to tint
   the stage toward the flood, which worked on the pale bands and sank the
   bottle into the ground on F-500. Removed rather than special-cased. */
.shopcard[data-flood] .shopcard__stage img { mix-blend-mode: normal; }

/* WordPress applies --wp--style--block-gap (1.2rem = 19.2px, measured) as
   margin-block-start on every sibling block at the root, so the footer
   template-part sat 19px below </main> as a white strip, and the hero sat 19px
   below the nav. The root's children are exactly header / main / footer —
   full-bleed bands that must touch — so the gap is wrong there specifically.
   Scoped to the root: block gap inside content is untouched. */
.wp-site-blocks > * + * { margin-block-start: 0; }
