:root, .home {
  /* The Auravox mark, for the waiting state below. Inlined so a placeholder never waits for its
     own download — and filled `black` rather than `#000`, because a literal # inside a data: URI
     starts a fragment and truncates it. It cost three rebuilds to see: every computed style read
     back correctly, mask-image and all, and the wells painted flat colour. */
  --h-mark: url('data:image/svg+xml,<svg fill="black" width="481" height="546" viewBox="0 141 481 546" xmlns="http://www.w3.org/2000/svg"><path d="M390.988 539.573C389.766 537.882 387.882 537.54 385.968 537.977C382.965 538.661 381.801 543.894 378.391 543.939C372.639 544.015 369.435 533.187 369.435 525.192V291.634C369.435 266.172 378.657 259.242 380.595 249.424C381.655 244.05 378.083 239.547 372.737 238.984C369.628 238.657 366.794 239.18 364.381 241.215C356.971 247.466 349.432 253.205 341.178 258.287C335.403 261.842 329.723 264.043 323.332 266.172C290.662 277.058 236.702 263.326 225.133 226.796C206.788 229.199 192.026 245.589 202.33 265.037C212.633 284.485 245.199 293.319 269.634 293.658C295.797 294.021 311.766 288.965 331.131 278.832C330.461 283.395 327.824 286.412 324.767 289.741L251.123 369.963L166.247 463.254L94.5686 542.518L78.7112 560.288L84.8719 567.031C110.338 538.897 158.673 570.884 172.236 597.098C172.425 597.464 172.901 597.551 173.213 597.282L217.527 559.246L272.203 513.513C279.759 507.191 286.636 501.244 293.56 494.126C293.56 494.126 293.92 522.181 292.069 535.956C290.219 549.732 284.464 555.726 279.365 556.528L282.043 563.997C298.801 571.787 310.99 578.634 322.343 597.812H329.641C337.387 582.785 356.097 566.63 372.657 560.485C373.354 560.226 373.166 558.769 372.428 558.682C355.148 556.637 345.045 544.042 345.048 528.663L343.844 291.914C343.847 275.548 352.979 258.861 368.786 251.531C362.566 263.078 360.869 271.571 360.866 283.415L360.794 522.194C360.794 535.821 366.982 553.253 385.116 551.512C388.027 551.108 390.205 549.142 391.122 547.417C392.445 544.927 392.602 541.808 390.988 539.573ZM281.364 490.854C263.268 510.477 242.62 527.976 217.625 538.947C195.208 548.786 171.188 543.548 153.123 528.8C146.229 523.172 144.103 515.383 148.826 507.281C151.873 501.971 156.128 497.478 160.59 492.635L294.367 347.402V450.094C294.367 472.707 288.929 481.32 281.364 490.854Z"/><path d="M142.659 276.914C142.901 276.203 143.906 276.203 144.149 276.914L152.276 300.697C155.624 310.495 163.326 318.191 173.131 321.537L196.931 329.659C197.642 329.901 197.642 330.905 196.931 331.147L173.131 339.269C163.326 342.615 155.624 350.311 152.276 360.109L144.149 383.892C143.906 384.603 142.901 384.603 142.659 383.892L134.531 360.109C131.183 350.311 123.481 342.615 113.677 339.269L89.8766 331.147C89.1657 330.905 89.1657 329.901 89.8766 329.659L113.677 321.537C123.481 318.191 131.183 310.495 134.531 300.697L142.659 276.914Z"/><path d="M390.126 173.916C391.66 174.015 393.105 174.673 394.189 175.763L445.164 227.026C446.246 228.115 446.895 229.562 446.988 231.095C454.313 352.686 455.33 474.699 447.388 596.286C447.289 597.803 446.644 599.233 445.573 600.313C428.709 617.328 411.844 634.343 394.981 651.359C393.909 652.44 392.483 653.098 390.965 653.209C290.07 660.624 188.68 662.169 87.7787 654.06C86.2453 653.937 84.8105 653.255 83.744 652.147C67.0322 634.772 50.3219 617.397 33.6101 600.023C32.5434 598.914 31.9182 597.453 31.8531 595.916C26.7224 474.759 26.777 353.412 31.8324 232.253C31.8974 230.695 32.5379 229.216 33.6291 228.102L84.7827 175.87C85.8728 174.757 87.3359 174.086 88.8906 173.988C189.184 167.684 289.833 167.45 390.126 173.916ZM388.414 185.791C387.872 185.245 387.144 184.912 386.376 184.863C290.174 178.687 192.443 178.711 92.6654 184.94C91.8871 184.988 91.1536 185.324 90.6078 185.881L43.7574 233.718C43.2114 234.276 42.8911 235.017 42.8587 235.797C37.8051 357.356 37.8125 472.426 42.8794 592.41C42.9118 593.179 43.2288 593.916 43.7624 594.47C60.9312 612.321 72.5136 624.363 89.6816 642.212C90.2158 642.768 90.9396 643.113 91.7078 643.174C188.645 650.911 284.813 649.787 387.149 642.294C387.909 642.238 388.629 641.904 389.165 641.363C406.469 623.905 418.226 612.043 435.529 594.585C436.065 594.044 436.392 593.323 436.441 592.563C444.038 475.467 443.538 359.937 436.025 234.793C435.979 234.026 435.65 233.295 435.108 232.75C417.652 215.195 405.869 203.345 388.414 185.791Z"/></svg>');
}
/* ══════════════════════════════════════════════════════════════════════════════════════════
   The homepage draft (/home/).

   Page-local on purpose while it is iterated: nothing outside .home is touched, so a week of
   changes here cannot move a pixel anywhere else in the shop. When the page is approved the
   rules that turn out to be patterns (the rail, the pinned stage) graduate to shop.css.

   Built from the brand tokens throughout. The one addition is the DARK GROUND below: the clips
   are a black velvet bust, and a section that is near-black with the brand's maroon in it lets
   the velvet sit on the page rather than in a box. The Apple-style rhythm is light and dark
   sections alternating, big type, one idea per screen, and motion tied to the scroll.

   The scroll effects are driven by home.js writing `--p` (0..1) on a section; everything here
   turns that into transforms and opacity only, so a frame of scrolling never changes layout.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

.home {
  --h-ink:          var(--maroon-800);   /* the dark ground                       */
  --h-on-ink:       var(--beige-100);    /* headings and body on it               */
  --h-on-ink-muted: var(--beige-700);    /* eyebrows, captions, secondary lines   */
  --h-max:          1200px;              /* .shop-container's width, so edges line up */
  --h-sec-y:        var(--space-20);     /* section rhythm: 80 on a phone …       */
  /* The zoom and drift effects may overhang their sections; the page must never scroll
     sideways because of it. clip, not hidden: hidden would make .home a scroll container and
     every sticky pin inside it would stop pinning. */
  overflow-x: clip;
}
@media (min-width: 1024px) {
  .home { --h-sec-y: var(--space-32); }    /* … 128 on a desktop */
}

.h-wrap {
  max-width: var(--h-max);
  margin-inline: auto;
  padding-inline: var(--page-px);
}
.h-sec  { padding-block: var(--h-sec-y); }
/* The closing section runs into the footer. Site-wide the footer keeps a margin above it; here
   the last thing on the page is a full-bleed picture and a gap of page colour under it reads as
   a mistake. Scoped to this page by the body class, so no other page's footer moves. */
.home-page .shop-footer { margin-top: 0; }
.h-dark { background: var(--h-ink); color: var(--h-on-ink); }

.h-eyebrow {
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-3);
}
.h-dark .h-eyebrow, .h-hero .h-eyebrow, .h-layer .h-eyebrow { color: var(--h-on-ink-muted); }

.h-h2 {
  /* Tied to the viewport under the h3 cap: "Spread the Aura, earn commission." is four long
     words and at a fixed 32px it ran off a 320px screen. */
  font-size: clamp(var(--type-h5), 8.2vw, var(--type-h3));
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--text-heading);
  text-wrap: balance;
}
.h-dark .h-h2, .h-layer .h-h2 { color: var(--h-on-ink); }
@media (min-width: 768px) {
  .h-h2 { font-size: var(--type-h2); }
}

.h-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1h);
  min-height: 44px;
  font-weight: var(--weight-demi);
  color: var(--text-action);
  text-decoration: none;
}
.h-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.h-link--light { color: var(--h-on-ink); }

/* The one solid button on a dark ground. .btn-inverse is the shop's on-dark button, and it is
   glass: right for a secondary action, too quiet to be the first thing a visitor presses. This
   keeps its shape and fills it. */
.h-btn-solid,
.h-btn-solid:hover:not(:disabled) {
  background: var(--h-on-ink);
  border-color: var(--h-on-ink);
  color: var(--maroon-500);
}
.h-btn-solid:hover:not(:disabled) { background: var(--beige-300); border-color: var(--beige-300); }

/* Every clip: inert, filling its box. Nothing to press — see home.js. */
.h-clip {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--h-ink);
  pointer-events: none;
}

/* Arrival: a section's heading rises into place the first time it is seen. Only once home.js
   has run (.h-fx) — without the script nothing is ever hidden. */
.h-fx .h-rise {
  opacity: 0;
  translate: 0 var(--space-6);
  transition: opacity 700ms var(--ease-out), translate 700ms var(--ease-out);
}
.h-fx .h-rise.is-in { opacity: 1; translate: 0 0; }


/* ── 1. Hero ──────────────────────────────────────────────────────────────────────────────
   150svh of section, a 100svh pin inside it: the extra half-screen is the scroll the effect
   spends. The clip fills the screen at every width — it is encoded in three cuts (9:16, 4:3,
   2.35:1) so the browser never has to crop it a second time, and the words sit over it on a
   scrim. The first draft letterboxed a 4:3 cut into a portrait screen and filled the space
   around it with a blurred copy of itself; the owner asked for the picture itself instead. */
/* Black, not the maroon-tinted ink the other dark sections use: the clip's own ground IS black,
   and against #12080d the picture ended on a visible edge where it should dissolve. The hero is
   the one section whose ground has to match a photograph. */
.h-hero {
  --p: 0;
  /* The arrival, 1 → 0, written by home.js once the rewind has landed. It is declared 0 here and
     only ever raised by script, so a page that never runs the intro — no JS, reduced motion, a
     clip that would not load — has its words where they belong and always has. */
  --i: 0;
  /* How far the picture is allowed to lag: half the section, which is what makes it move at half
     the page's speed for the whole of the section's travel. */
  --h-parallax: calc((100vh - var(--nav-h)) * 0.5);
  --h-parallax: calc((100svh - var(--nav-h)) * 0.5);
  position: relative;
  /* Exactly one screen under the nav, and not pinned: the first scroll brings the next section
     up rather than spending a quarter-screen holding this one. 150svh, then 125svh, then this. */
  height: calc(100vh - var(--nav-h));
  height: calc(100svh - var(--nav-h));
  background: #000;
  color: var(--h-on-ink);
}
.h-hero-pin {
  position: relative;
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* The gap OPENS as the section is scrolled: the three parts rise together and draw apart from
     each other at the same time, which is the motion the owner asked for — not the block sliding
     up as one piece, which is what the earlier parallax did. */
  gap: calc(var(--space-6) + var(--p) * 7vh);
  /* The block sits off the floor of the screen rather than on it — nearer the middle, which is
     where it reads from a thumb's distance. Raised again on 2026-09-25 at the owner's ask, by one
     step of the scale rather than a hand-picked number: 96px to 128px, which on a 390×844 phone
     lifts the heading off the bottom eighth of the screen and puts the button clear of the home
     indicator. */
  padding: var(--space-6) var(--page-px) var(--space-32);
}
/* On a phone the master plays WHOLE across the top of the screen and the section's black takes
   the rest. The chain runs the full width of the frame, so a taller crop would have to cut its
   ends off — the frame is as wide as the asset and as short as that makes it. The join is a
   mask on the frame rather than a scrim over it, so the black shows through it exactly. */
/* The phone's cut is portrait and fills the screen, the way the wide one does — so there is no
   gap above it to fill and no reflection any more (the owner, 2026-09-25). What is left here is
   the frame: the box the scroll zooms and locks.

   THE MOVING PART IS THE FRAME, not the picture inside it, so anything added to it later is
   carried by both. position: sticky cannot do the lock — .h-hero-pin has overflow: hidden, which
   makes it the scrollport a sticky child would stick to, and it does not scroll. --p is the
   section's own travel out of view, so p × the section's height is exactly how far the page has
   scrolled: translating down by that much cancels it. */
.h-hero-frame {
  position: absolute;
  /* PARALLAX: the picture leaves at HALF the page's speed (the owner, 2026-09-25). --p is the
     section's own travel out of view, so p × height is exactly how far the page has scrolled, and
     translating down by half of that halves the picture's speed relative to it.
     THE FRAME IS THE SECTION, not half a section taller: an overhang meant the clip had to fill a
     box half again as tall, and object-fit ate the sides of it to do so. The whole frame of the
     asset is what opens the page, so the lag shows the section's own black ground instead — which
     is what is behind the picture anyway. */
  inset: 0;
  translate: 0 calc(var(--p) * var(--h-parallax));
  /* Pushes INTO the screen as the reader scrolls: 100% to 128% by the time the section has gone,
     over the same travel as before (the owner, 2026-09-25 — half of the 0.55 it grew by, which
     with the picture now filling the section's height was pushing too far into it). 0.06 was a
     drift nobody would name as a zoom; 0.275 is the middle ground it took four passes to find. */
  scale: calc(1 + var(--p) * 0.275);
  /* A THIRD OF THE WAY DOWN, at every width (the owner, 2026-09-25). The chain enters the frame
     high and the zoom should push into where it already is; at the centre it pulled the top of
     the picture out of the screen, and at two thirds it pushed the chain off the bottom. */
  transform-origin: 50% 33%;
}
.h-hero-media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #000;
}
/* The phone's two layers: the still the section is before a byte of clip is fetched, and the
   canvas the scroll turns. */
.h-hero-still,
.h-hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* THE SWAP IS INSTANT HERE, where every other section on this page crossfades. The hero's still
   IS the clip's opening frame — the encode cuts the poster at REWIND_FROM for exactly this — so
   there is nothing to dissolve between: measured at the moment it goes live, the canvas and the
   still differ by 0.9 of 255, which is WebP against H.264 and nothing else.
   What the fade could do is hold a STATIC poster half-transparent over a picture that has already
   started rewinding. In Chrome that is worth almost nothing (0.27 of 255 against this, 160ms in,
   because the rewind's smoothstep barely moves at first) — but it is a dissolve between two
   pictures that are the same, which is a cost with no benefit on the connection this matters on. */
.h-hero-canvas { opacity: 0; }
.h-hero-media.is-live .h-hero-canvas { opacity: 1; }
.h-hero-media.is-live .h-hero-still { opacity: 0; }
/* AND THE VIDEO IS NOT RENDERED HERE. It is never given a source below 768 — but an unloaded
   <video> with a poster still paints that poster on some engines, and being last in the box it
   painted over the canvas. That is what "the hero is just a still" looked like on a phone. */
@media (max-width: 767px) {
  .h-hero-media video { display: none; }
  /* FULL HEIGHT, and the sides may go (the owner, 2026-09-25). The cut is 784x1120 against a
     screen taller and narrower than that, so there is no framing that keeps both: fitted, it left
     black above and below; filled, it runs the picture from the top of the section to the foot
     and loses a little at each edge. The chain is drawn across the frame, so the edges are where
     it can afford to lose. */
  .h-hero-still, .h-hero-canvas { object-fit: cover; }
}
@media (min-width: 768px) {
  .h-hero-still, .h-hero-canvas { display: none; }
}

/* No scrim at the section's foot, and no mask on the frame's own bottom edge (the owner,
   2026-09-25). The clip's ground is black and so is the section's, so the picture ends where the
   black begins and the join is invisible; a gradient over it only greyed the bottom third.
   The z-index below outlives the scrim it was written for: positioned siblings at z-index auto
   paint in document order, and the words come after the picture — but so would anything else
   added here later. */
.h-hero-copy, .h-hero-ctas { z-index: 1; }

/* A light scrim under the top of the frame only; the foot of the picture is already dissolving
   into the ground, so there is nothing to darken down there. */
.h-hero-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgb(0 0 0 / 0.45), transparent 34%);
}
/* The words rise as they fade, and the heading lifts further than the buttons under it, so the
   three parts open out rather than leaving as one block. A quarter of the pin's travel at most:
   the fade has them gone by --p 0.42, and beyond that the movement is not seen.
   No transition, and a compositor hint: they answer the scroll on the frame it happens, because
   the reader's thumb is the clock. */
.h-hero-copy,
.h-hero-ctas {
  position: relative;
  /* 1.6, not 2.4: the words hold their ground for the first third of the section's exit rather
     than being gone a fifth of the way in.
     …and --i is the ARRIVAL, the same movement run the other way: 1 while the clip is rewinding,
     0 once the words are home. One expression carries both, so the two can never disagree about
     where "in place" is — and with --i at its initial 0 this is exactly what it was before. */
  opacity: calc((1 - var(--p) * 1.6) * (1 - var(--i)));
  transition: none;
  will-change: transform, opacity;
}
/* Before home.js runs, --i is still 0 and this formula shows the words. h-hold is set from
   theme-boot, the script in the head, so the first paint already has them down. home.js drops
   the class in the same turn it raises --i, and from there the formula above is what moves them. */
html.h-hold .h-hero-copy,
html.h-hold .h-hero-ctas { opacity: 0; }
/* Both go UP into place (the owner, 2026-09-25: "fly upward and appear in place"), so they start
   below it — the heading from further down than the button, the same ratio it leaves by. */
.h-hero-copy { translate: 0 calc(var(--p) * -16vh + var(--i) * 10vh); }
.h-hero-ctas { translate: 0 calc(var(--p) * -6vh + var(--i) * 4vh); }
/* …and the sentence draws away from the heading above it as they go, so on the way in it closes
   up to it. */
.h-hero-sub { margin-top: calc(var(--space-4) + var(--p) * 3vh - var(--i) * 1.5vh); }
.h-hero-title {
  /* 9vw: Coconat is a wide face, and at 10vw "through." alone outran a 390px screen. */
  font-size: clamp(var(--type-h3), 9vw, var(--type-display));
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tighter);
  color: var(--h-on-ink);
  text-wrap: balance;
}
.h-hero-sub {
  font-size: var(--type-body-lg);
  line-height: var(--leading-snug);
  color: var(--h-on-ink-muted);
  max-width: 34ch;
}
/* One button, at its own width, on the left. Full-bleed it read as a bar across the foot of
   the picture rather than as something to press. */
.h-hero-ctas { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: var(--space-3); }
.h-hero-ctas .btn { flex: 0 0 auto; }

/* ── The button breathes with the lamp ──────────────────────────────────────────────────
   The clip's light sweeps the top right of the frame and rises and falls; the white button
   takes its hue from that patch and its rhythm from that curve, so the two pulse together.
   The stops below are MEASURED from the file that ships — ./scripts/home/hero-pulse.sh prints
   this block, and its header says what is sampled and why the fill is a tint rather than the
   colour itself (the clip's own blue is a near-black grey-blue; as a button it is a dark slab).

   `alternate` over one leg, not a loop over the whole clip: the clips are ping-ponged, so the
   second half IS the first half backwards and one leg of keyframes covers both. It also makes
   the phase a single number — mountHero() sets a negative delay from video.currentTime, and
   from then on the CSS clock and the decoder's clock are the same clock. */
/* Generated by scripts/home/hero-pulse.sh — hue 200deg, lightness 6.5%..23.1% */
:root {
  --h-pulse-rest: rgb(193 224 239);   /* frame 0, which is also the poster */
  --h-pulse-band: linear-gradient(135deg,
    rgb(193 224 239) 0%,          /* frame 0, glow 0.78 */
    rgb(189 222 238) 10%,         /* frame 14, glow 0.84 */
    rgb(203 229 242) 20%,         /* frame 27, glow 0.62 */
    rgb(199 227 241) 30%,         /* frame 41, glow 0.68 */
    rgb(210 233 244) 40%,         /* frame 54, glow 0.52 */
    rgb(218 236 246) 50%,         /* frame 68, glow 0.41 */
    rgb(222 238 247) 60%,         /* frame 82, glow 0.35 */
    rgb(234 244 250) 70%,         /* frame 95, glow 0.18 */
    rgb(239 247 251) 80%,         /* frame 109, glow 0.09 */
    rgb(240 248 251) 90%,         /* frame 122, glow 0.08 */
    rgb(246 250 253) 100%);       /* frame 136, glow 0.00 */
}

/* …and the same curve in beige-500, for a button with no clip behind it (the owner, 2026-09-25).
   Its own depth, because beige-500 is nearly white: at the blue's mix amounts the band came out
   246 to 254 and was invisible. */
:root {
  --h-pulse-rest-beige: rgb(237 232 225);
  --h-pulse-band-beige: linear-gradient(135deg,
    rgb(237 232 225) 0%,          /* frame 0, glow 0.78 */
    rgb(236 230 223) 10%,         /* frame 14, glow 0.84 */
    rgb(240 235 230) 20%,         /* frame 27, glow 0.62 */
    rgb(239 234 228) 30%,         /* frame 41, glow 0.68 */
    rgb(242 238 233) 40%,         /* frame 54, glow 0.52 */
    rgb(244 240 236) 50%,         /* frame 68, glow 0.41 */
    rgb(245 242 238) 60%,         /* frame 82, glow 0.35 */
    rgb(248 246 244) 70%,         /* frame 95, glow 0.18 */
    rgb(250 248 246) 80%,         /* frame 109, glow 0.09 */
    rgb(250 249 247) 90%,         /* frame 122, glow 0.08 */
    rgb(252 251 249) 100%);       /* frame 136, glow 0.00 */
}
/* one leg of the ping-pong: 5.7083s */
/* The band is half again the button, so nearly the whole curve — deepest blue to near-white —
   is on the face at once, and the animation slides it from the top-left corner to the
   bottom-right one. At 300% only a third of the curve showed and it read as a tint that
   changed, not as light crossing a surface. Background position is a paint-only property: no
   layout, no repaint of anything around it. */
@keyframes h-hero-pulse {
  from { background-position: 0% 0%; }
  to   { background-position: 100% 100%; }
}
/* The Timeless button wears the same sweep in the page's own beige. It keeps the hero's rhythm
   and its keyframes — the light crossing a face is the same gesture either side of the page —
   but nothing here syncs it to a clip: the chain in this section is seeked by the scroll, not
   played, so there is no clock to share. */
.h-motion-cta,
.h-motion-cta:hover:not(:disabled) {
  background-color: var(--h-pulse-rest-beige);
  background-image: var(--h-pulse-band-beige);
  background-size: 150% 150%;
  background-position: 0% 0%;
  animation: h-hero-pulse 0.9514s ease-in-out infinite alternate;
}

/* The hover is in the selector on purpose: .h-btn-solid:hover sets `background`, the shorthand,
   which would drop the band and leave a beige button in the middle of a pulse. The face does not
   change under a cursor — the light IS the state. */
.h-hero-pulse,
.h-hero-pulse:hover:not(:disabled) {
  /* Frame 0 is the poster, so a button that never animates still shows the colour of the frame
     the reader is looking at. */
  background-color: var(--h-pulse-rest);
  background-image: var(--h-pulse-band);
  background-size: 150% 150%;
  background-position: 0% 0%;
  /* A SIXTH of a leg: out and back three times while the clip's light goes out once (the owner
     asked for three times the speed, 2026-09-22). Three cycles to one leg is still a harmonic of
     the clip's clock rather than a departure from it — they meet on every leg boundary, and
     syncPulse() still sets the phase from video.currentTime. */
  animation: h-hero-pulse 0.9514s ease-in-out infinite alternate;
}
@media (max-height: 700px) and (max-width: 1023px) {
  .h-hero-title { font-size: var(--type-h4); }
  .h-hero-sub   { font-size: var(--type-body); }
}
@media (min-width: 768px) {
  .h-hero-pin {
    /* One step off the floor, 96px to 128px, and 32px further right than the page gutter.
       The phone keeps its own inset (the owner, 2026-09-26). */
    padding-bottom: var(--space-32);
    padding-left: calc(var(--page-px) + var(--space-8));
  }
  .h-hero-copy, .h-hero-ctas { max-width: 40rem; }
}
@media (min-width: 1024px) {
  /* Wide, the master fills the screen again: there is room for the whole 2.35:1 frame and the
     words sit over it. */
  .h-hero-media::after {
    /* Only the left-hand wash, which is what the words are read against. The bottom one is gone
       with the phone's (the owner, 2026-09-25). */
    background: linear-gradient(to right, color-mix(in srgb, var(--h-ink) 45%, transparent), transparent 55%);
  }
  .h-hero-pin {
    /* The centred column's own gutter, plus two steps. The extra was 40px; 64px shifts the
       block right, and the floor goes from 96px to 128px so the line sits a little higher
       (the owner, 2026-09-26). */
    padding-inline: max(var(--space-16), calc((100% - var(--h-max)) / 2 + var(--space-16)));
    padding-bottom: var(--space-32);
  }
  .h-hero-title { font-size: clamp(var(--type-display), 5.5vw, 5.5rem); }
  .h-hero-sub { font-size: var(--type-h6); }
}

/* The shop's tooltip opens LEFTWARD (right: 0), which is right for a badge near the end of a
   line and wrong for one at the start of it. Here the badge lands on the page gutter, and the
   bubble hung 115px off the left edge of a 390px screen — visible only as a sentence that
   explains nothing when you tap it. shop.css already makes this exact swap for .page-header-row
   under 480px; this is the same fix at a different anchor. */
[data-reveal] .tip-wrap .tip-bubble { right: auto; left: 0; }

/* ── 2. The claim ────────────────────────────────────────────────────────────────────────
   Each word's opacity is a function of the paragraph's progress and its own index. The +6 lets
   the last words finish lighting before progress reaches 1, rather than on the final pixel.
   The 925 badge is one of those words, and lights with them. */
.h-statement { background: var(--surface-page); }
/* One column: the mark and the button are gone, and the sentence is the section. It sits
   directly above the tiles, which carry the marks and the links.
   THE GALLERY'S WIDTH, from 1024 (the owner, 2026-09-25): 56rem, centred, which is the measure
   the two columns of photographs are set in. The sentence ran the full 1200 and read as a
   different page from the pictures above it. */
.h-statement-grid { display: grid; }
@media (min-width: 1024px) {
  /* 56rem of CONTENT, so the gutter this element also carries has to be added on: .h-wrap and
     .h-statement-grid are the same element, and capping it at 56rem left the sentence 64px
     narrower than the photographs it is meant to line up with. */
  .h-statement-grid { max-width: calc(56rem + 2 * var(--page-px)); margin-inline: auto; }
}
.h-reveal {
  /* The page's width, not a measure of its own: at 30ch it left a third of the column empty on
     the right and read as a mistake rather than as a setting (the owner, 2026-09-24). */
  max-width: none;
  font-size: var(--type-h4);
  font-weight: var(--weight-demi);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--text-heading);
  text-wrap: pretty;
}
@media (min-width: 768px) {
  .h-reveal { font-size: var(--type-h2); }
}
/* The one link in the sentence. It takes the action colour and keeps the underline a link needs
   to be a link — but inherits the word's own opacity, so it lights with the line it sits in. */
.h-reveal-link {
  color: var(--text-action);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: from-font;
}
.h-reveal-link:hover { text-decoration-thickness: 2px; }

.h-fx .h-reveal .w {
  opacity: clamp(0.14, calc(var(--p, 0) * (var(--n, 30) + 6) - var(--i, 0)), 1);
  transition: opacity 120ms linear;
}
/* The badge, set into a line of body text: it keeps the shop's frame and blue, and takes the
   line's own size so the sentence's rhythm is not broken by a box. */
/* Equal air top and bottom. Zero vertical padding left the box hugging the digits' caps and
   sitting low against the line it is set in. */
.h-925.badge-925 {
  display: inline-flex;
  align-items: center;
  vertical-align: baseline;
  padding: 0.12em 0.34em;
  font-size: 0.82em;
  line-height: 1;
}
.h-925 .badge-925-num { font-size: inherit; }
/* The rigged marks: one square, the drawing filling it. The keyframes are shop.css's, shared
   with /learn/values/; this only sizes the box and leaves it paused for the scroll to seek. */
.h-rig { --vi-run: paused; display: block; color: var(--text-action); }
.h-rig svg { display: block; width: 100%; height: 100%; }

@media (min-width: 768px) {
}

/* ── 3. The anchor in three dimensions ───────────────────────────────────────────────────
   A stage and a selector. The stage holds a still until anchor3d.js has WebGL running, and the
   still stays underneath it afterwards — the canvas is transparent (clearAlpha 0), so what sits
   behind the chain is this stylesheet's ground rather than anything three.js paints.

   THE GROUND IS THEME-AWARE, because the model is not: it is lit for a mid ground and paints on
   a transparent canvas, so this stylesheet is the only thing deciding what the silver sits on.
   Both ends are the BEIGE scale, chosen by the owner in /home/anchor-lab.html on 2026-09-24 with
   the two frames side by side: a warm mid-grey either way, one pair from the light half of the
   scale and one from its dark half. The maroon scale lost — at the dark end it stacked three
   darks on each other, and at the light end it went rose. */
.h-anchor {
  --h-3d-ground: var(--beige-300);
  --h-3d-edge: var(--beige-200);
}
[data-theme="dark"] .h-anchor {
  --h-3d-ground: var(--beige-700);
  --h-3d-edge: var(--beige-600);
}
/* FULL WIDTH ON A PHONE (the owner, 2026-09-25). It was held to the panel's 22rem so that the
   heading, the model and the menus lined up as one block — but on a 390px screen that is a
   column of text with an inch of dark either side of it, while every other heading on the page
   runs the width of the page. Desktop puts it back in the column beside the model, below. */
.h-anchor-head { width: 100%; }
.h-anchor-line {
  margin-top: var(--space-4);
  font-size: var(--type-body-lg);
  line-height: var(--leading-relaxed);
  color: var(--h-on-ink-muted);
}
.h-anchor-grid {
  display: grid;
  gap: var(--space-6);
  margin-top: var(--space-8);
}
/* 4:5, and NOT the whole width it is given (the owner, 2026-09-24: the frame was too large on
   both). One number sets it at each breakpoint, and the review page reads the same number, so a
   framing chosen there is chosen at the size it will ship at. */
.h-anchor { --h-3d-w: 19rem; }      /* 17rem, until the owner asked for a little more (2026-09-25) */
@media (min-width: 768px)  { .h-anchor { --h-3d-w: 20rem; } }
@media (min-width: 1024px) { .h-anchor { --h-3d-w: 24rem; } }
.h-anchor-stage {
  position: relative;
  margin: 0 auto;
  width: 100%;
  max-width: var(--h-3d-w);
  /* The stills' own shape, so the still and the model frame the bust identically. */
  aspect-ratio: 4 / 5;
  border-radius: calc(var(--radius-card) * 2);
  overflow: hidden;
  background: radial-gradient(120% 90% at 50% 18%, var(--h-3d-edge), var(--h-3d-ground) 68%);
  /* The canvas answers the drag itself; without this the browser scrolls the page instead on a
     touch screen, and the model turns a few degrees and stops. */
  touch-action: none;
}
.h-anchor-still,
.h-anchor-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* THE STILL NO LONGER MATCHES THE MODEL, and cannot be made to with a crop. The stills were
   rendered straight on; the section now opens with the bust turned 80° (the owner, 2026-09-24),
   and no scale or offset turns a photograph of the front into a photograph of the side. So the
   crop is off: the still shows the whole bust, honestly, for the second or two before the model
   arrives — and for a reader with no WebGL or reduced motion, who gets a straight-on product
   picture rather than a wrong one.
   The fix is to re-render the nineteen stills from the live viewer at the opening view, which is
   a job for scripts/home/, not for this rule. */
.h-anchor-still {
  object-fit: contain;
}
/* The canvas fades in over the still rather than replacing it: both are the same render from
   the same camera, so there is nothing to see in the swap except the light coming up. */
.h-anchor-canvas { opacity: 0; transition: opacity var(--transition-slow); }
.h-anchor.is-live .h-anchor-canvas { opacity: 1; }
.h-anchor.is-live .h-anchor-still { opacity: 0; transition: opacity var(--transition-slow); }
/* Centred on a phone, and no wider than it needs to be: the selects ran the full width of the
   screen while the model sat in a 17rem frame above them, so the section read as a small picture
   on top of a big form (the owner, 2026-09-24). */
.h-anchor-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  width: 100%;
  max-width: 22rem;
  margin-inline: auto;
  text-align: center;
}
.h-anchor-panel .h-anchor-label { text-align: left; }
.h-anchor-cta { align-self: center; }
/* The two menus share the line, half the width each, on a phone as much as anywhere: they are
   one choice in two parts and stacking them put a scroll between the width and its lengths. */
.h-anchor-menus {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
}
.h-anchor-axis { display: flex; flex-direction: column; gap: var(--space-2); }
.h-anchor-label {
  font-size: var(--type-caption);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--h-on-ink-muted);
}
/* The shop's own select, on this section's dark ground: same box, same chevron, same focus ring
   as every other field in the shop, so the one on a homepage is not a special case. */
.h-anchor-select {
  width: 100%;
  background-color: color-mix(in srgb, var(--h-on-ink) 8%, transparent);
  border-color: color-mix(in srgb, var(--h-on-ink) 30%, transparent);
  color: var(--h-on-ink);
}
.h-anchor-select:hover { border-color: var(--h-on-ink); }
/* The list itself is drawn by the operating system, which does not inherit any of the above —
   so the options are set on a light ground or they come out invisible on some platforms. */
.h-anchor-select option { color: var(--text-default); background: var(--surface-default); }
.h-anchor-select option:disabled { color: var(--text-subtle); }
/* The name, then the style's icon RIGHT AFTER IT — the style cards' arrangement, where the mark
   follows the words it belongs to (the owner, 2026-09-25). It was justified to the two ends of
   the panel, which at this width left the icon stranded an inch from anything it names. */
.h-anchor-read {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2);
  text-align: left;
  font-size: var(--type-body-lg);
  color: var(--h-on-ink);
}
.h-anchor-read .chain-icon { flex: 0 0 auto; }
.h-anchor-cta { align-self: flex-start; }
@media (min-width: 1024px) {
  /* TWO HALVES (the owner, 2026-09-25): the model in one, everything you read or press in the
     other. The columns used to hug their contents and centre the pair, which tied the whole
     section's position to the frame's width — change --h-3d-w and the words moved. Halves hold
     still, and each side is arranged within its own: the viewer centred in its half, the words
     and the menus on the left edge of theirs. */
  .h-anchor-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* The heading starts at the top of its half and the button's foot lands on the viewer's foot:
       one auto row for the words, one that takes the rest for the menus.
       The block is as tall as the viewer — its width by its 4:5 — so that second row has exactly
       that height to spread its parts over. Without it the column set the height, the viewer
       centred itself inside it, and the button finished 14px below the picture. */
    grid-template-rows: auto minmax(0, 1fr);
    align-items: stretch;
    min-height: calc(var(--h-3d-w) * 5 / 4);
    column-gap: var(--space-12);
  }
  .h-anchor-stage {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: start;
    justify-self: center;
  }
  .h-anchor-head {
    grid-column: 2;
    grid-row: 1;
    /* The half it is given, not 24rem of it: that cap was set when this column hugged the menus
       under it, and in a half-width column it broke a five-word heading over four lines. */
    max-width: none;
    align-self: start;
  }
  .h-anchor-panel {
    grid-column: 2;
    grid-row: 2;
    align-self: stretch;
    justify-content: space-between;
    margin-inline: 0;
    text-align: left;
  }
  .h-anchor-cta { align-self: flex-start; }
}

/* ── 3b. Timeless selection ──────────────────────────────────────────────────────────────
   The canvas is what the reader sees once the clip is decoded; the still under it is what they
   see until then, and if the fetch fails. */
.h-motion-grid {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}
.h-motion-line {
  max-width: 42ch;
  margin-top: var(--space-4);
  font-size: var(--type-body-lg);
  line-height: var(--leading-relaxed);
  color: var(--h-on-ink-muted);
}
.h-motion-cta { justify-self: start; }
.h-motion-frame {
  position: relative;
  margin: 0;
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius-card) * 2);
  overflow: hidden;
  background: var(--h-ink);
}
.h-motion-still,
.h-motion-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.h-motion-canvas { opacity: 0; transition: opacity var(--transition-slow); }
.h-motion-frame.is-live .h-motion-canvas { opacity: 1; }
.h-motion-frame.is-live .h-motion-still { opacity: 0; transition: opacity var(--transition-slow); }
@media (min-width: 1024px) {
  .h-motion-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    grid-template-rows: auto auto;
    column-gap: var(--space-16);
  }
  /* Wide, the words and the button are one column again and the picture spans both rows; on a
     phone the source order does it — words, picture, button. */
  .h-motion-copy  { grid-column: 1; grid-row: 1; align-self: end; }
  .h-motion-cta   { grid-column: 1; grid-row: 2; align-self: start; margin-top: var(--space-6); }
  .h-motion-frame { grid-column: 2; grid-row: 1 / span 2; }
}

/* ── 4. The six styles: a rail ───────────────────────────────────────────────────────────
   The rail runs to the screen's edges but its first card lines up with the headings above it:
   the side padding is the page gutter plus whatever the centred 1200px column leaves. Padded
   top and bottom too, because a scrolling box clips BOTH axes (CLAUDE.md) and a lifted card's
   shadow would be cut off. */
.h-styles { padding-bottom: calc(var(--h-sec-y) - var(--space-6)); }
/* The arrows sit on the heading's line, hard right, at every width: nowrap, and the heading
   takes whatever is left. Wrapping put them under the words on a phone, where they read as two
   stray circles. */
.h-head-row {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
}
.h-head-row .h-h2 { min-width: 0; }
.h-rail-side { display: flex; flex: 0 0 auto; flex-direction: column; align-items: center; gap: var(--space-2); }
.h-rail-nav { display: flex; flex: 0 0 auto; gap: var(--space-2); }
/* The home cards' dots, in this page's colours: a hint that the row runs on, not a control. */
.h-rail-dots { display: flex; align-items: center; gap: var(--space-2); pointer-events: none; }
.h-dot {
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--border-strong);
  opacity: 0.4;
  transition: opacity var(--transition-fast), transform var(--transition-fast), background var(--transition-fast);
}
.h-dot.is-on { background: var(--text-action); opacity: 1; transform: scale(1.25); }
/* The window's edge, with cards still beyond it. See wireRail() — this is the half of the
   sliding window a reader can actually see. */
.h-dot.is-edge { transform: scale(0.55); opacity: 0.3; }
.h-rail-btn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: var(--border-w-small) solid var(--border-default);
  background: var(--surface-default);
  color: var(--text-heading);
  cursor: pointer;
  transition: opacity var(--transition-slow), background var(--transition-slow);
}
.h-rail-btn:hover:not(:disabled) { background: var(--surface-raised); }
.h-rail-btn:disabled { opacity: 0.35; cursor: default; }
.h-rail-btn:focus-visible { outline: var(--border-w-medium) solid var(--border-focus); outline-offset: 2px; }

.h-rail {
  --h-rail-pad: calc((100% - min(100%, var(--h-max))) / 2 + var(--page-px));
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--h-rail-pad);
  padding: var(--space-6) var(--h-rail-pad);
  scrollbar-width: none;
}
.h-rail::-webkit-scrollbar { display: none; }

.h-style-card {
  flex: 0 0 min(78vw, 21rem);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: calc(var(--radius-card) * 2);
  background: var(--surface-default);
  color: inherit;
  text-decoration: none;
  transition: translate var(--transition-slow), box-shadow var(--transition-slow);
}
.h-style-card:not(.is-pending):hover { translate: 0 -2px; box-shadow: var(--shadow-md); }
.h-style-img {
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--maroon-700);
}
.h-style-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: scale 600ms var(--ease-out);
}
.h-style-card:hover .h-style-img img { scale: 1.04; }
/* A flex column with the meta pushed to the floor: the names run to one, two or three lines
   and the cards are all the same height, so without this the mm/cm row sat at a different
   height on every card and the bottom padding looked uneven. */
.h-style-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-1h);
  padding: var(--space-4) var(--space-4);
}
.h-style-meta { margin-top: auto; }
.h-style-name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--type-h5);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h5);
  color: var(--text-heading);
}
.h-style-name .chain-icon { flex: 0 0 auto; }
.h-style-line {
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.h-style-meta {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: var(--space-2h);
  font-size: var(--type-body-sm);
  color: var(--text-body);
}
/* The last card is one of the seven, not a panel: same picture edge to edge, same body, and
   its link where the others keep their measurements.
   Which means its line has to SIT where theirs do: .h-link carries a 44px tap target, written for
   links that are themselves the thing you press, and here the whole card is the link. Left in, it
   made this one card's last line twice the height of the other six and dropped its text below
   their baseline (the owner, 2026-09-25). */
.h-style-meta .h-link { min-height: 0; }


/* ── 5. Find by fit ──────────────────────────────────────────────────────────────────────
   380svh of track, one screen of pin: five lengths get about two-thirds of a screen each. */
.h-fit { background: var(--surface-default); }
/* 190svh, not 380: five lengths over about a screen and a half of scroll rather than three.
   At the old rate the reader had to work to get through one section. */
.h-fit-track { position: relative; height: 190vh; height: 190svh; }
.h-fit-stage {
  position: sticky;
  top: var(--nav-h);
  display: flex;
  height: calc(100vh - var(--nav-h));
  height: calc(100svh - var(--nav-h));
}
/* Two columns on a phone: everything, and the progress rail. The rail spans both rows, so it
   runs from the top of the heading to the foot of the last length button with nothing measured.
   The gap is what keeps it off the buttons. */
.h-fit-grid {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* Rows at their content height, the block centred in the screen. With the second row taking
     the leftover space, the rail — which spans both rows — ran 111px past the foot of the last
     length button, because the figure sits centred inside that row rather than filling it. */
  grid-template-rows: auto auto;
  align-content: center;
  column-gap: var(--space-5);
  row-gap: var(--space-3);
  width: 100%;
  padding-block: var(--space-6) var(--space-4);
}
.h-fit-copy   { grid-column: 1; grid-row: 1; }
.h-fit-figure { grid-column: 1; grid-row: 2; }
.h-fit-rail   { grid-column: 2; grid-row: 1 / span 2; }
.h-fit-readout {
  display: flex;
  align-items: baseline;
  gap: var(--space-1h);
  margin-top: var(--space-4);
  color: var(--text-heading);
}
/* The wheel. One line tall, clipped, with the five numbers stacked inside it and the strip
   translated by --o (0..4) — continuous, so the number climbs with the scroll instead of
   snapping between lengths. Each cell is exactly 1em so --o is a count of numbers. */
/* --odo-cell is the window AND the step, so the two cannot disagree. 1.18em rather than 1em:
   Coconat's numerals stand taller than their line box, and at exactly 1em the neighbouring
   length showed through above and below the one being read.
   The window's edges fade. Mid-turn the wheel is showing the foot of one length and the head of
   the next, which is what a wheel does — but cut off square at 90px it reads as broken type,
   and faded it reads as a wheel. */
.h-odo {
  --odo-cell: 1.18em;
  display: block;
  height: var(--odo-cell);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
  font-size: var(--type-display);
  font-weight: var(--weight-bold);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.h-odo-strip {
  display: block;
  translate: 0 calc(var(--o, 0) * var(--odo-cell) * -1);
}
.h-odo-strip > span {
  display: flex;
  align-items: center;
  height: var(--odo-cell);
}
.h-fit-unit { font-size: var(--type-h5); font-weight: var(--weight-demi); }
.h-fit-name {
  margin-left: var(--space-3);
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* Held at three lines so the bust below does not jump as the sentence changes length. */
.h-fit-sit {
  max-width: 44ch;
  min-height: calc(3 * var(--leading-relaxed) * 1em);
  margin-top: var(--space-2);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}
/* The bust and its lengths, side by side, as the configurator on /chains/ arranges them: the
   drawing takes the room that is left and the buttons stack in a column beside it. */
/* The figure is only as tall as the bust — `align-self: center`, not stretched to the row. The
   lengths beside it stretch to the FIGURE, so with the figure stretched to the whole stage the
   column came out 238px taller than the drawing it is meant to measure. */
.h-fit-figure {
  align-self: center;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  min-height: 0;
}
/* How far the section has been scrolled through its five lengths. */
.h-fit-rail {
  width: 4px;
  border-radius: 4px;
  background: var(--border-default);
  overflow: hidden;
}
.h-fit-rail-run {
  display: block;
  width: 100%;
  height: calc(var(--run, 0) * 100%);
  background: var(--text-action);
}
/* The column runs the bust's full height: the unit sits level with the top of the drawing and
   the last length lands level with its foot, which is how the configurator's own length column
   is set out beside the same art. */
/* As tall as the drawing and no taller: the unit level with the top of the bust, 60 level with
   its foot. Stretching against the bust's own height — spread over the whole row, the column
   came out taller than the art. */
.h-fit-lens {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-1);
}
.h-fit-unit-note {
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  text-align: center;
}
.h-fit-lens .chip-option { display: flex; border: 0; padding: 0; background: none; }
.h-fit-lens .chip-option-label {
  width: 100%;
  justify-content: center;
  /* Narrower than the shop's chip, which is sized for a row of words; these are two digits, and
     every millimetre here is one the drawing beside them gets to keep. */
  padding-inline: var(--space-2h);
}
/* Verbatim from `.chip-option input:checked + .chip-option-label` in tokens.css: these are
   buttons rather than inputs, so the checked rule cannot reach them. Same as the home Chain
   Guide card, which makes the same swap for the same reason. */
.h-fit-lens .chip-option.is-on .chip-option-label {
  background: var(--surface-action);
  color: var(--text-on-action);
  border-color: var(--border-action);
}
.h-fit-lens .chip-option:focus-visible .chip-option-label {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
.h-fit-cta { display: flex; justify-content: flex-start; margin-top: var(--space-4); }

/* One chain on the bust, not five. The configurator draws every length as a ghost because the
   reader is choosing between them there; here the section is showing one length at a time, and
   the four it is not showing were read as part of the drawing. */
.h-fit-bust .chain:not(.selected) { display: none; }

/* The bust: no panel, the whole figure, the torso dissolving at the foot. Its own viewBox is
   set in home.js — wider than the guide card's, so the shoulders are inside the frame.
   grid-column/row reset: shop.css pins every .chain-stage to row 1, column 1 of the
   configurator's grid, which here put the bust in the copy's cell at zero height. */
/* The element takes the viewBox's own shape (home.js sets aspect-ratio when it frames the
   drawing), so the lengths beside it line up against the ART, not a taller box with the drawing
   letterboxed inside it. */
.h-fit-bust.chain-stage {
  grid-column: auto;
  grid-row: auto;
  align-self: center;
  width: 100%;
  min-height: 0;
  border: none;
  border-radius: 0;
  background: transparent;
}
.h-fit-bust.chain-stage::before { content: none; }
.h-fit-bust.chain-stage svg {
  top: 0; left: 0; width: 100%; height: 100%;
  -webkit-mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
}
@media (min-width: 1024px) {
  .h-fit-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr) auto;
    /* Content height, centred in the screen — not a 1fr row filling it. The rail stretches to
       this row, and against a full-screen row it would have run half a screen above the heading
       and half below the last length button. */
    grid-template-rows: auto;
    align-content: center;
    align-items: center;
    column-gap: var(--space-16);
    padding-block: var(--space-10);
  }
  .h-fit-copy { grid-column: 1; grid-row: 1; }
  .h-fit-figure { grid-column: 2; grid-row: 1; align-items: center; align-self: center; }
  /* The same progress rail the phone gets, in its own column at the far right (the owner,
     2026-09-22 — it was declared here and had no height, because `align-items: center` sizes an
     empty box to its content, which is nothing). */
  .h-fit-rail { grid-column: 3; grid-row: 1; align-self: stretch; }
  /* Shorter than its cell, and centred in it: the box is what crops the figure, so this is the
     setting that decides how much torso is shown. See frame() in home.js. */
  .h-fit-bust.chain-stage { max-height: 56vh; }
  .h-odo { font-size: calc(var(--type-display) * 1.5); }
}

/* ── 6. Worn ─────────────────────────────────────────────────────────────────────────────
   Two columns, the second set lower and drifting the other way, so the pair shears slightly
   as it passes. --p is 0.5 when the grid is centred on the screen: that is the rest position.
   The ground is the page beige Choose your style inherits. Headings and captions stay on the
   light-section ink, so this section does not take .h-dark. */
.h-worn { background: var(--surface-page); }
.h-worn-grid {
  --p: 0.5;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  align-items: start;
  margin-top: var(--space-8);
}
/* Both columns begin level. The left one holds four photographs and the right three, and the
   left travels up by exactly the difference (--lag, measured in home.js) across the section's
   pass, so the last photograph in each column finishes level with the other. */
.h-worn-col { display: grid; gap: var(--space-3); translate: none; }
/* The shorter column is the one that moves: down by the height difference across the pass, so
   the first photographs are level as the section arrives and the last are level as it leaves. */
.h-worn-col--lag { translate: 0 calc(var(--p) * var(--lag, 0px)); }
.h-worn-fig a { display: block; border-radius: var(--radius-card); overflow: hidden; }
.h-worn-fig img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius-card);
  background: var(--maroon-700);
}
.h-worn-fig figcaption {
  margin-top: var(--space-2);
  font-size: var(--type-caption);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}

@media (min-width: 1024px) {
  .h-worn-grid { max-width: 56rem; margin-inline: auto; gap: var(--space-8); margin-top: var(--space-12); }
  .h-worn-col  { gap: var(--space-8); }
}


/* The way on from a section: one button, centred, clear of whatever drifted above it. */
.h-cta-row {
  display: flex;
  justify-content: center;
  margin-top: var(--space-12);
}
.h-dark .h-cta-row { margin-top: var(--space-16); }

/* ── 7. Layering ─────────────────────────────────────────────────────────────────────────
   The photograph settles from 118% to 100% as it crosses the screen. */
.h-layer {
  --p: 0.5;
  position: relative;
  overflow: hidden;
  background: var(--h-ink);
  color: var(--h-on-ink);
}
.h-layer-media { display: block; height: 85vh; height: 85svh; overflow: hidden; }
.h-layer-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* From 145% down to 100% across the pass. 18% was a drift; this reads as a move. */
  scale: calc(1.45 - var(--p) * 0.45);
}
/* BOTH SECTIONS MAKE THE SAME MOVE (the owner, 2026-09-25). The answering one had a smaller one
   — 115% to 100%, on the argument that its photograph is a person rather than a crop of a neck —
   and side by side that read as the second picture failing to do what the first one had just
   done. They end in the same place; now they start there too. */
.h-layer-copy {
  position: absolute;
  inset: 0 0 auto 0;
  padding: var(--space-10) var(--page-px) var(--space-24);
  background: linear-gradient(to bottom, color-mix(in srgb, var(--h-ink) 88%, transparent), transparent);
}
/* Gone on a phone, and gone from the layout with it: the photograph is the link there, and the
   wash that used to seat this button on the picture went with the button. */
.h-layer-cta { display: none; }
.h-layer-text {
  max-width: 34ch;
  margin-top: var(--space-3);
  font-size: var(--type-body-lg);
  line-height: var(--leading-relaxed);
  color: var(--h-on-ink);
}
/* The picture on the other side, for the section that answers the one above it. On a phone both
   are a photograph with the words over its top, so there is nothing to flip there. */
@media (min-width: 1024px) {
  .h-layer { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; }
  .h-layer--flip .h-layer-media { grid-column: 2; grid-row: 1; }
  .h-layer--flip .h-layer-copy  { grid-column: 1; grid-row: 1; }
  .h-layer-media { height: auto; aspect-ratio: 4 / 5; max-height: calc(100vh - var(--nav-h)); }
  /* THE WORDS SIT ON THE PAGE'S OWN COLUMN, not on the grid cell's edge. The photograph runs to
     the screen's edge in both sections; the heading beside it has to line up with every other
     heading on the page, or the section reads as a separate page pasted in. The max() is the
     gutter plus whatever the centred 1200px column leaves — the same expression .h-hero-pin and
     .h-rail use — and the flipped section takes it on the other side. */
  .h-layer-copy {
    position: static;
    padding: var(--space-16);
    /* …and its outer edge is the PAGE's edge, not the grid cell's: these words have to line up
       with every other heading on the page or the section reads as a separate page pasted in.
       100vw, not 100%: a percentage here resolves against the column, which is half the screen,
       and the max() then quietly picked the fallback. */
    padding-right: max(var(--space-16), calc((100vw - var(--h-max)) / 2 + var(--page-px)));
    background: none;
  }
  /* Mirrored for the section that answers it: picture hard against the right edge, words on the
     page's own column on the left. */
  /* Hard against the right edge, at ITS TWIN'S WIDTH (the owner, 2026-09-25) — not half the
     screen. The width is stated rather than left to the ratio, because an item whose height
     comes from max-height and whose width comes from the ratio has no width to move: both
     justify-self: end and an auto margin collapsed it to nothing. Stating it is also what the
     other section arrives at — max-height times four fifths — so the two match by construction.
     The min() keeps it inside its column on a short, wide screen. */
  .h-layer--flip .h-layer-media {
    width: min(100%, calc((100vh - var(--nav-h)) * 4 / 5));
    margin-left: auto;
  }
  .h-layer--flip .h-layer-copy {
    padding-right: var(--space-16);
    padding-left: max(var(--space-16), calc((100vw - var(--h-max)) / 2 + var(--page-px)));
  }
  /* Under the words, in their column. It used to be a row of its own, which the two-column grid
     placed BELOW THE PHOTOGRAPH in the left column — a button under the picture rather than with
     the sentence it belongs to. */
  .h-layer-cta { display: inline-flex; margin-top: var(--space-6); }
}


/* ── 8. What you can count on ────────────────────────────────────────────────────────────
   Four tiles, each led by its mark: three rigged drawings from /learn/values/, which settle as
   they cross the screen, and the 925 badge itself for the one fact that already has a mark in
   this shop. Two up on a phone, four across from 1024. */
.h-why { background: var(--surface-page); }
.h-bento {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-8);
}
/* Centre-stacked: the mark over the figure over the line, all centred, so the four read as one
   set. The 925 tile is the exception — it has no mark, because the badge is its mark. */
.h-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  min-height: 13rem;
  padding: var(--space-5);
  text-align: center;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: calc(var(--radius-card) * 2);
  background: var(--surface-default);
  color: inherit;
  text-decoration: none;
  transition: translate var(--transition-slow), box-shadow var(--transition-slow);
}
.h-tile:hover { translate: 0 -2px; box-shadow: var(--shadow-md); }
/* Every tile is the same three rows: a mark of one fixed height, the figure, the line. The mark
   row keeps its height whether it holds a drawing or the badge, so the figures sit on the same
   line across all four cards. */
.h-tile-rig,
.h-tile-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--space-16);
  margin-bottom: var(--space-2);
}
.h-tile-rig { width: var(--space-16); }

/* The badge stands where the other tiles' marks stand, so all four read as one set — and at the
   size of the line it introduces, not as a caption above it.
   Both rules are needed: .badge-925-num carries a fixed --type-caption in shop.css, so sizing
   the badge alone grows its box and leaves the number 12px inside it. */
.h-tile-badge .badge-925 {
  font-size: clamp(var(--type-body-lg), 5.5vw, var(--type-h5));
  padding: 0.12em 0.42em;
}
.h-tile-badge .badge-925-num { font-size: inherit; }
@media (min-width: 768px) {
  .h-tile-badge .badge-925 { font-size: var(--type-h4); }
}
/* No margin-top: auto. Pushing the figure to the floor made each card's gap the size of
   whatever text sat under it — the shipping tile had a hole between its van and its words while
   the returns tile did not. Top-aligned, the four marks line up across the row and the slack
   falls at the bottom of every card equally. */
.h-tile-big {
  /* "Free shipping" is two words that must not be cut in a half-width tile on a 320px screen.
     The figure shrinks with the viewport before it overflows. */
  font-size: clamp(var(--type-body-lg), 5.5vw, var(--type-h5));
  text-wrap: balance;
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  color: var(--text-heading);
}
.h-tile-unit { font-size: 0.62em; font-weight: var(--weight-demi); color: var(--text-muted); }
/* The phrase a tile's link is really about, marked as a link inside a card that is one. */
.h-tile-link { color: var(--text-action); text-decoration: underline; text-underline-offset: 2px; }
.h-tile-text { font-size: var(--type-body-sm); line-height: var(--leading-normal); color: var(--text-muted); }
@media (min-width: 768px) {
  .h-tile-big { font-size: var(--type-h4); }
}
@media (min-width: 1024px) {
  .h-bento { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }
  .h-tile  { min-height: 17rem; padding: var(--space-6); }
  .h-tile-rig { width: var(--space-20); }
  .h-tile-rig, .h-tile-badge { height: var(--space-20); }
}

/* ── 9. The Referral Keychain ────────────────────────────────────────────────────────────
   The tag turns beside the words. model-viewer draws into its own canvas and sizes to the box
   it is given, so the box is what this rule is for. */
.h-aff-grid {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}
/* min-width: 0 on both cells. A grid item floors at its own min-content width, and the widest
   thing in the copy column is a button whose label does not wrap: at 320px that made the column
   311px wide inside a 288px container and pushed the paragraph off the screen. Same trap as the
   flex note in CLAUDE.md, same one-line fix. */
.h-aff-grid > * { min-width: 0; }
.h-aff-line {
  max-width: 42ch;
  margin-top: var(--space-4);
  font-size: var(--type-body-lg);
  line-height: var(--leading-relaxed);
  color: var(--h-on-ink-muted);
}
.h-aff-cta { display: flex; justify-content: flex-start; margin-top: var(--space-5); }
.h-aff-tag {
  aspect-ratio: 1;
  width: min(80%, 17.5rem);
  margin-inline: auto;
}
.h-aff-tag model-viewer { width: 100%; height: 100%; background: transparent; }
@media (min-width: 1024px) {
  .h-aff-grid { grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: var(--space-16); }
  /* Source order is tag-then-words, because that is the order a phone reads them in. Wide, the
     words lead and the tag turns beside them, so the columns are named rather than left to the
     source. */
  .h-aff-copy { grid-column: 1; grid-row: 1; }
  .h-aff-tag { grid-column: 2; grid-row: 1; width: 80%; max-width: 24rem; }
}

/* ── 10. Instagram ───────────────────────────────────────────────────────────────────────
   The six-styles card, with a square photograph and the caption under it. */
.h-ig { padding-bottom: var(--h-sec-y); }
.h-ig-card {
  flex: 0 0 min(78vw, 21rem);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: calc(var(--radius-card) * 2);
  background: var(--surface-default);
  color: inherit;
  text-decoration: none;
  transition: translate var(--transition-slow), box-shadow var(--transition-slow);
}
.h-ig-card:not(.is-pending):hover { translate: 0 -2px; box-shadow: var(--shadow-md); }
.h-ig-img { aspect-ratio: 1; overflow: hidden; background: var(--surface-raised); }
.h-ig-img img { width: 100%; height: 100%; object-fit: cover; }
/* The body fills whatever the picture leaves, and the link sits on its floor: as a plain last
   line it floated wherever the caption happened to end, and the cards are all one height. */
.h-ig-card { justify-content: stretch; }
.h-ig-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-4) var(--space-5);
}
.h-ig-body .h-link { margin-top: auto; }
.h-ig-kind {
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
}
.h-ig-caption {
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-body);
}
.h-follow { display: inline-flex; align-items: center; gap: var(--space-2); }
.h-follow-mark { display: inline-flex; }
.h-follow-mark svg { width: 20px; height: 20px; }

/* ── 11. Close ─────────────────────────────────────────────────────────────────────────── */
.h-close { position: relative; overflow: hidden; padding-block: var(--space-32); margin-bottom: 0; text-align: center; }
.h-close-media { position: absolute; inset: 0; opacity: 0.45; }
/* display:contents so the <picture> around the two cuts is not a box in the middle of this one —
   the img still sizes against .h-close-media, as it did when it was the only child. */
.h-close-media picture { display: contents; }
.h-close-media img { width: 100%; height: 100%; object-fit: cover; }
.h-close-copy {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
}
/* THE PHONE'S FRAME IS BLURRED RATHER THAN DARKENED (the owner, 2026-09-25). It carries the same
   legibility: the lamp in that frame sits where the sentence does, and softening it takes the
   detail out from under the words without taking the picture down with it — a wash behind the
   copy did the job by the numbers and read as a black box over a photograph.
   SCALED UP WITH IT, because a blur samples past the element's own edges and fades out into
   nothing there. 1.06 pushes that soft rim outside the box, which overflow:hidden on the section
   then clips.
   6px, halved from 12 at the owner's ask (2026-09-25) once they had seen it. The number trades
   against the sentence over it: blur spreads the lamp in this frame and drops its peak, so 5px
   leaves the line at 3.6:1, 8px at 4.3 and 12px at 4.6, where 20px body text wants 4.5. At 6 it
   sits near 3.8 — under that bar on the brightest tenth of the ground, and the owner's call with
   the measurement in front of them. */
@media (max-width: 767px) {
  .h-close-media img {
    filter: blur(6px);
    scale: 1.06;
  }
}
.h-close-title {
  font-size: clamp(var(--type-h2), 11vw, 5.5rem);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tighter);
  color: var(--h-on-ink);
}
.h-close-sub {
  margin-bottom: var(--space-4);
  font-size: var(--type-body-lg);
  line-height: var(--leading-snug);
  color: var(--h-on-ink-muted);
}


/* ── Reduced motion: every continuous effect held at rest ─────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .h-hero-media, .h-hero-copy, .h-hero-ctas,
  .h-worn-col, .h-worn-col--lag, .h-layer-media img { scale: none; translate: none; opacity: 1; }
  /* The lamp is not sweeping for this reader — the poster is the hero — so the button holds
     the colour of the frame they are looking at rather than breathing on its own. */
  .h-hero-pulse, .h-motion-cta { animation: none; }
  .h-fx .h-rise { opacity: 1; translate: none; transition: none; }
  .h-odo-strip { translate: none; }
  .h-style-card, .h-style-img img, .h-tile, .h-reviews a.rev-card { transition: none; }
}

/* ── While a photograph is on its way ────────────────────────────────────────────────────────
 *
 * Every well on this page reserves its space before the picture arrives — the layout has never
 * shifted (CLS 0) — but until 2026-09-26 what it reserved was an empty coloured box, and the
 * photograph simply appeared in it. On a phone, on a real connection, that is most of the first
 * few seconds of the page: a grid of blanks.
 *
 * So the blank becomes the brand's own mark, breathing. It is a MASK rather than a picture, which
 * is what lets one definition serve every ground on the page: the mark takes `currentColor` from
 * whatever section it sits in, so it reads light on the dark sections and dark on the light ones
 * without a second copy or a theme rule. Inlined rather than fetched, because a placeholder that
 * waits for its own download is not a placeholder.
 *
 * It sits UNDER the photograph, not instead of it: `.media-fade` (tokens.css) settles the picture
 * in on top, and since every one of these is `object-fit: cover` the mark is simply covered when
 * it arrives. Nothing has to hide it, so nothing can fail to. A photograph that never loads keeps
 * the mark, which is the right empty state rather than a hole.
 */
.h-well {
  position: relative;
  isolation: isolate;
  /* The mark has to contrast with the WELL's own ground, which is not always the ground the text
     around it is on: a style card is dark-plated inside a light card, so `currentColor` — the
     card's ink — drew a near-black mark on near-black and showed nothing at all. Each well says
     which way it goes; this is the light-ground default. */
  --h-mark-ink: var(--text-subtle);
}
/* The plated wells: a dark photograph well, wherever it sits. */
.h-style-img.h-well,
.h-layer-media.h-well,
.h-worn-fig .h-well { --h-mark-ink: var(--beige-200); }
.h-well::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-color: var(--h-mark-ink);
  opacity: 0.16;
  -webkit-mask: var(--h-mark) center / 2.75rem no-repeat;
  mask: var(--h-mark) center / 2.75rem no-repeat;
  animation: h-waiting 1.8s var(--ease-in-out, ease-in-out) infinite;
}
/* Above the mark, so the picture covers it as it arrives. */
.h-well > img,
.h-well > picture,
.h-well > video,
.h-well > canvas { position: relative; z-index: 1; }

@keyframes h-waiting {
  0%, 100% { opacity: 0.10; }
  50%      { opacity: 0.26; }
}
/* Still a mark, just not a moving one. */
@media (prefers-reduced-motion: reduce) {
  .h-well::before { animation: none; opacity: 0.16; }
}

/* ── 7a. What customers say ──────────────────────────────────────────────────────────────
   A light band between the two dark sections around it. The same quote the product page
   uses, with no card around it: a portrait, the stars, then the words.

   The words are held to a short measure, a little wider than the first pass, so a
   comment still wraps without becoming a single long line.

   Two layouts, switched at the page's own wide breakpoint (1024), not the shop's 768.
   Below that, two comments stack. The first fades out after six seconds, the second
   after nine, and from then on each one changes every six seconds, so they never
   switch together. From 1024, each row is a track of quotes, up to three across,
   copied once so a translate of -50% lands on the same frame. The two tracks run
   opposite ways, at the same speed, and they do not stop while the section is in view.
   The section stays hidden until that script has something to put in it. */
.h-reviews { background: var(--surface-page); }
.h-reviews .h-h2 { margin: 0; }
.h-reviews-stack {
  display: grid;
  gap: var(--space-10);
  margin-top: var(--space-16);
  max-width: 40rem;
}
.h-reviews-slot {
  transition: opacity 600ms var(--ease-out, ease);
}
.h-reviews-slot.is-out { opacity: 0; }
.h-reviews-marquee {
  display: none;
  margin-top: var(--space-16);
  overflow: hidden;
  container-type: inline-size;
  /* A hair of fade at the cut, shorter than the pad around each quote, so a
     profile that has entered is not dissolved while the line beside it is solid. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 0.25rem, #000 calc(100% - 0.25rem), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 0.25rem, #000 calc(100% - 0.25rem), transparent);
}
.h-reviews-marquee:focus { outline: none; }
.h-reviews-marquee:focus-visible {
  outline: var(--border-w-medium, 2px) solid var(--border-focus);
  outline-offset: 2px;
}
.h-reviews-row {
  display: flex;
  /* The pad is the gap. A flex gap on top of it left the zoom crossing into
     the next quote. */
  gap: 0;
  width: max-content;
  align-items: flex-start;
}
.h-reviews-row.is-left  { animation: h-rev-left  linear infinite; }
.h-reviews-row.is-right { animation: h-rev-right linear infinite; }
.h-reviews-row + .h-reviews-row { margin-top: var(--space-10); }
/* Off screen only. A hover pause made the rows hitch every time the pointer crossed them. */
.h-reviews-marquee.is-paused .h-reviews-row { animation-play-state: paused; }

@keyframes h-rev-left {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
@keyframes h-rev-right {
  from { transform: translate3d(-50%, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* The card is shop.css, shared with the product page. The band only drops the
   product page's rule and lets each quote keep its own width. */
.h-reviews .rev-card { align-items: flex-start; }
/* The comment is the link. The zoom is the same one the style-card photographs use:
   a small scale, over the same 600ms, and nothing else — no lift, no shadow, because
   these are not cards. It grows from the centre, inside .h-reviews-pad. A top-left
   origin walked the quote sideways as it grew, and the growth crossed into the next
   quote, so the pointer lost the hover and the zoom started over. */
.h-reviews a.rev-card {
  color: inherit;
  text-decoration: none;
  transition: scale 600ms var(--ease-out);
}
.h-reviews a.rev-card:hover,
.h-reviews a.rev-card:focus-visible { scale: 1.04; }
.h-reviews a.rev-card:focus-visible {
  outline: var(--border-w-medium, 2px) solid var(--border-focus);
  outline-offset: 4px;
}
.h-reviews-row .h-reviews-pad {
  /* Up to three in view. --rev-across is how many unique cards the row actually has,
     capped at 3, so two quotes fill the band instead of leaving a third slot that is
     the same person again. 100cqi is the band, not the track: a percentage of the row
     would be circular, because the row is sized by its cards. The 22rem stands in
     where a browser has no container query.
     The padding is the 1.04 growth (2% of the quote, each side) with room past it,
     so the circle stays inside this box and inside the marquee clip. */
  flex: 0 0 auto;
  box-sizing: border-box;
  width: min(22rem, 80vw);
  width: calc(100cqi / var(--rev-across, 3));
  padding: var(--space-4) var(--space-8);
}
.h-reviews-row .rev-card { width: 100%; }
.h-reviews-stack .h-reviews-pad {
  /* One column, so the zoom's only wall is the screen. 2.5% is the 1.04
     growth (2% of the quote, each side) with a little past it, and it tracks
     the column: a fixed gutter is enough on a phone and short on a wide one. */
  padding: var(--space-2) 2.5%;
}
/* A short measure: the band is three across, and a quote that runs the full third
   reads as a paragraph rather than something someone said. */
.h-reviews .rev-body { max-width: 20rem; }

@media (min-width: 1024px) {
  .h-reviews-stack { display: none; }
  .h-reviews-marquee { display: block; }
}
@media (prefers-reduced-motion: reduce) {
  .h-reviews-slot { transition: none; }
  /* `.is-left` / `.is-right` name the animation at two classes. A one-class
     `animation: none` loses to that, media query or not, and the rows keep
     drifting for someone who asked them to stop. Name the directions. */
  .h-reviews-row.is-left,
  .h-reviews-row.is-right { animation: none; }
  .h-reviews-row { flex-wrap: wrap; width: auto; }
  .h-reviews-marquee {
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
    padding-inline: var(--page-px);
  }
  .h-reviews-row .h-reviews-pad { width: min(22rem, 100%); }
}
