/* ============================================================
   ducioo × ool — styles
   Tunable values live in tokens.css, which loads first. Nothing
   here should hardcode a colour, duration, easing or backdrop —
   if you need a new one, add it there.
   ============================================================ */

/* ============================================================
   Type — two families, three roles (ducioo_revision_2.md §4)
   Playfair Display is self-hosted rather than pulled from Google:
   the planned CSP in the security audit is font-src 'self', which
   would block the CDN. Variable weight axis, latin + latin-ext only.
   ============================================================ */
@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/assets/fonts/playfair-display-latin-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/assets/fonts/playfair-display-latin-ext-var.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0;
}
/* Section headers only. Cinzel Decorative Regular (Natanael Gama),
   SIL Open Font License 1.1 — the licence ships beside the font as
   OFL-cinzel-decorative.txt. Converted to WOFF2 from google/fonts,
   60KB to 21KB, glyphs unchanged. Not preloaded: every header sits
   below the fold. */
@font-face {
  font-family: "Cinzel Decorative";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/cinzel-decorative-regular.woff2") format("woff2");
}

/* Display serif for statements, sans for information. The split is
   statement vs information, not section by section. */
.display,
.about__lede,
.buy__name,
.buy__price {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: -0.01em;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Lenis (vendored, v1.3.26) — inlined so the page keeps one stylesheet */
html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* Dark in both colour schemes, deliberately. Black here is the art
   direction, not a theme: every asset has a black background baked in,
   the strap tiles bottom out on it and the scroll frames are cut
   for it. A light mode would be a second asset set, not a variable
   swap — see ducioo_revision_2.md §3. Declaring the scheme (rather than
   leaving it unset) stops the UA rendering form controls, scrollbars
   and the overscroll area light on a light-preference machine.
   There is deliberately no prefers-color-scheme block anywhere. */
html {
  color-scheme: dark;
  scroll-behavior: auto;
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--void);
  color: var(--ink);
  font-family: var(--font);
  font-weight: 400;
  line-height: 1.7;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* height:auto so the width/height attributes give the intrinsic ratio
   without pinning a pixel height — rules that set an explicit height
   (hero layers, strap tiles) are more specific and still win. */
img, video { display: block; max-width: 100%; height: auto; }

:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 3px;
}

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

.label {
  font-size: var(--fs-label);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* ============================================================
   Layout primitives — 12-col, 1440 max, 80/20 gutters
   ============================================================ */
/* Content sections. min() rather than a flat cap: at 1440 this is
   ~1325px, at 2560 it reaches the 1600 ceiling instead of either
   staying 1325 (lost on the screen) or running to 92% of 2560 (too
   wide to read). Full-bleed sections take .wrap--bleed instead. */
.wrap {
  width: min(92vw, var(--maxw-content));
  margin-inline: auto;
}

/* edge to edge, only a gutter */
.wrap--bleed {
  width: 100%;
  max-width: none;
  margin-inline: 0;
  padding-inline: 16px;
}

/* multi-column rows can carry more width than prose */
.wrap--grid { width: min(94vw, var(--maxw-grid)); }

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

/* Overrides .label's face, size, case and tracking (same specificity,
   later in the file); keeps its --ink-dim colour. */
.section-label {
  display: block;
  text-align: center;
  margin-bottom: clamp(48px, 8vh, 96px);
  font-family: var(--font-section);
  font-size: var(--fs-section);
  line-height: 1.2;
  text-transform: none;
  letter-spacing: var(--track-section);
}

@media (max-width: 767px) {
  /* exact 20px gutters rather than 4vw either side */
  .wrap,
  .wrap--grid { width: calc(100vw - 2 * var(--gutter-mobile)); }
  .wrap--bleed { padding-inline: var(--gutter-mobile); }
  .section { padding-block: var(--section-pad-mobile); }
  .section-label { margin-bottom: 48px; }
}

/* ============================================================
   Reveal — the one entrance on the page
   opacity + transform only, fires once, never reverses.
   Scoped to .js so the page is readable with no JavaScript.
   ============================================================ */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(var(--reveal-y));
  transition:
    opacity   var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms);
}
.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* elements start visible — no entrance at all */
  .js [data-reveal],
  .js [data-reveal].is-revealed {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================
   Preloader — the assembly frame set has to load regardless, so
   the wait is designed rather than hidden. Number comes from real
   progress; see assembly-scroll.js.
   ============================================================ */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-content: center;
  gap: 14px;
  justify-items: center;
  background: var(--void);
  transition: opacity var(--dur-slow) var(--ease-out),
              visibility 0s linear var(--dur-slow);
}
.preloader[data-done="true"] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.preloader__pct {
  font-family: var(--font-display);
  font-size: clamp(40px, 7vw, 92px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.preloader__sign { font-size: 0.45em; margin-left: 2px; color: var(--ink-dim); }

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

/* §6's crown caption, top right. It used to sit under the component
   counter; with the counter removed (copy_revision_4.md) it takes the
   counter's top edge rather than leaving the gap standing. */
.seq__techline {
  position: absolute;
  top: calc(var(--nav-h) + clamp(16px, 2vh, 28px));
  right: clamp(20px, 5vw, 80px);
  z-index: 2;
  margin: 0;
  text-align: right;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.seq.is-pinned .seq__techline { opacity: 1; }

.buy__techline { display: block; margin-top: 18px; }

@media (max-width: 767px) {
  .seq__techline { display: none; }   /* label and progress stack under the band; no slot */
}

/* ============================================================
   Tonal zones and their seams
   Each section paints its own tone; the seam between two tones is
   a gradient, never an edge. The negative margin pulls the seam
   over the start of the next section so no gap can open.
   ============================================================ */
.section-seam {
  /* Drawn 2px above its layout slot, solid from-tone for those 2px,
     with the extra height taken back from the overlap so nothing below
     moves. Where a section ends on a fractional pixel, its last row
     antialiases against the page black: invisible on black, a dark
     hairline on a toned wall (measured 34 on 42). Starting inside the
     section above puts the seam's own partial row over matching tone. */
  top: -2px;
  height: calc(var(--seam-height) + 2px);
  margin-bottom: calc(-1 * var(--seam-height) - 2px);
  position: relative;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--from-tone) 0, var(--from-tone) 2px, var(--to-tone));
}

#hero     { background: var(--surface-hero); }
#straps {
  background:
    radial-gradient(ellipse at 50% -10%, #14110d 0%, var(--surface-straps) 60%);
}
#buy      { background: var(--surface-buy); }

/* §4 stage glow. buy-viewer.js places it: centred on the model, with
   radii reaching black at the section's nearest edges, so it never meets
   a neighbouring section as a line. It fades with the canvas, and only
   while the 3D view is live. */
#buy { position: relative; }
#buy::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    var(--stage-3d-rx, 0px) var(--stage-3d-ry, 0px)
      at var(--stage-3d-x, 50%) var(--stage-3d-y, 50%),
    var(--stage-3d-centre), var(--stage-3d-edge));
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
html[data-buy-view="3d"] #buy::before { opacity: 1; }

/* Moss tone (revision 5 §6). The surfaces themselves come from
   tokens.css; this is what tokens cannot do.

   Straps: the wall only — a flat moss wall in place of black's
   warm top light. Frames, mats and photographs are untouched; the
   moulding is their edge.

   Buy in photo mode (the poster while 3D loads, and the fallback): the
   photos have black baked in, which on moss is a hard-edged black
   rectangle. A soft black halo around the stage turns that edge into
   a vignette. It fades out as the 3D view — transparent, over the
   glow — takes over. */
html[data-tone="moss"] #straps { background: var(--surface-straps); }
html[data-tone="moss"] .buy__stage {
  transition: box-shadow var(--dur-base) var(--ease-out);
}
html[data-tone="moss"]:not([data-buy-view="3d"]) .buy__stage {
  box-shadow: 0 0 90px 50px var(--void);
}
/* the halo is painted by a positioned element, so the panel is lifted
   above it rather than dimmed by it */
html[data-tone="moss"] .buy__panel { position: relative; z-index: 1; }
html[data-tone="moss"] #about,
html[data-tone="moss"] .buy__panel {
  --ink: var(--moss-ink);
  --ink-dim: var(--moss-ink-dim);
}
/* The "Every part" line between About and its exit seam has to be
   moss too. Not a background on the line itself: it carries
   data-reveal, and while it animates in its background moves with it
   and opens a black gap under About. About's own shadow fills the band
   instead — the seam and the assembly section both paint over the
   excess. 400px clears the line's tallest padding. */
html[data-tone="moss"] #about { box-shadow: 0 400px 0 var(--surface-about); }
#buy > .wrap { position: relative; }
#about    { background: var(--surface-about); }
#seq      { background: var(--surface-assembly); }
#facts    { background: var(--surface-facts); }
#carousel { background: var(--surface-carousel); }
#arrives  { background: var(--surface-arrives); }
.footer   { background: var(--surface-footer); }

/* A seam overlaps the top of the next section by design and paints
   above it (z-index 1), so the tone change covers that section's
   background. But 12vh outgrows the top padding on tall screens and
   phones, and the seam was painting over the first line of content —
   measured: the footer headings at every size, and the About,
   Carousel, Specification and What arrives labels at 2560x1440 and
   390. Content sits one layer higher; backgrounds stay underneath, so
   the gradient still reads. */
.section > .wrap,
.footer > .wrap {
  position: relative;
  z-index: 2;
}

/* ============================================================
   Fact strip — numbers as a statement, not a table
   ============================================================ */
.fact-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(24px, 4vw, 64px);
  padding-block: clamp(60px, 10vh, 120px);
}
.fact b {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(32px, 4vw, 56px);
  font-weight: 400;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.fact i {
  display: block;
  margin-top: 12px;
  font-style: normal;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}

@media (max-width: 900px) {
  .fact-strip { grid-template-columns: repeat(2, 1fr); row-gap: 40px; }
}

/* ============================================================
   Technical captions — small, factual, doubling as texture
   Only ever where a real value exists. Never invented to fill
   the pattern; an instance with no true value is omitted.
   ============================================================ */
.techline {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ============================================================
   §1 — Nav
   Fixed 64px bar, revealed by nav.js once the hero is fully past.
   ============================================================ */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--nav-h);

  /* Glass, not blur. The saturate is what stops it reading as grey
     haze, and the top-lit hairline is what reads as an edge catching
     light. It has the hero video and the drifting carousel passing
     underneath, so there is something to refract. */
  background: var(--backdrop-ambient);
  backdrop-filter: blur(var(--backdrop-blur)) saturate(var(--backdrop-saturate));
  -webkit-backdrop-filter: blur(var(--backdrop-blur)) saturate(var(--backdrop-saturate));
  border-top: 1px solid var(--backdrop-ambient-edge);
  border-bottom: 1px solid var(--edge);

  /* visibility, not just opacity, so the two links leave the tab order
     while the bar is hidden */
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out),
              visibility 0s linear var(--dur-base);
}
.nav.is-visible {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s;
}

/* Without backdrop-filter, a 4% white wash over black is invisible and
   the bar would float unreadably over the page. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav { background: var(--backdrop-ambient-solid); }
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: 100%;
}
.nav__end {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Hover anywhere in the bar is colour or underline only — never scale,
   lift or shadow (revision 5 §2). */
.nav__mark {
  display: block;
  flex: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.nav__mark:hover { opacity: 0.72; }
/* 48px exactly: the three logo files are pre-sized to it */
.nav__logo { display: block; width: auto; height: 48px; }

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(20px, 2.4vw, 36px);
  margin-left: auto;
  margin-right: clamp(20px, 2.4vw, 36px);
}
/* The underline is always there, just transparent, so showing it
   moves nothing. */
.nav__link,
.nav__menu {
  font: inherit;
  font-size: var(--fs-label);
  color: var(--ink-dim);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 5px;
  transition: color var(--dur-fast) var(--ease-out),
              text-decoration-color var(--dur-fast) var(--ease-out);
}
.nav__link:hover,
.nav__menu:hover { color: var(--ink); }
.nav__menu { display: none; }

/* Tone switch — a light/dark toggle in form. The knob carries the
   current tone's icon (moon for black, sun for moss) and slides;
   that slide is a state change, not a hover effect. Hover changes the
   track's edge colour only. */
.tone-switch {
  flex: none;
  padding: 6px 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--ink);
}
.tone-switch__track {
  position: relative;
  display: block;
  width: 44px;
  height: 24px;
  border: 1px solid var(--ink-faint);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.tone-switch:hover .tone-switch__track { border-color: var(--ink-dim); }
.tone-switch:focus-visible { outline: none; }
.tone-switch:focus-visible .tone-switch__track {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}
.tone-switch__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--void);
  transition: transform var(--dur-fast) var(--ease-in-out);
}
.tone-switch__icon { grid-area: 1 / 1; display: block; }
.tone-switch__icon--light { display: none; }
.tone-switch[aria-checked="true"] .tone-switch__track { background: rgba(255, 255, 255, 0.14); }
.tone-switch[aria-checked="true"] .tone-switch__knob { transform: translateX(20px); }
.tone-switch[aria-checked="true"] .tone-switch__icon--dark { display: none; }
.tone-switch[aria-checked="true"] .tone-switch__icon--light { display: block; }
@media (prefers-reduced-motion: reduce) {
  .tone-switch__knob,
  .tone-switch__track { transition: none; }
}

/* Below 1024px the links move into a panel under the bar, opened by
   Menu. Fully opaque: at the 88% backdrop fallback the frames and
   their captions read through behind the links. */
@media (max-width: 1023px) {
  .nav__menu { display: block; }
  .nav__links {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 8px 4vw 16px;
    background: var(--void);
    border-bottom: 1px solid var(--edge);
    display: none;
  }
  .nav.is-open .nav__links { display: flex; }
  .nav__link { padding-block: 14px; }
}

/* text only, hairline border, no fill */
.nav__buy {
  padding: 7px 16px;
  border: 1px solid var(--ink-faint);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.nav__buy:hover { border-color: var(--ink); }

/* ============================================================
   §2 — Hero
   ============================================================ */
.hero {
  position: relative;
  height: 100svh;
  min-height: 460px;
  overflow: hidden;
  background: var(--void);
}

/* The poster is a real <img> underneath rather than only the video's
   poster attribute: it paints on first frame and the video crossfades
   over it once it can play, so there is never a black flash. */
.hero__poster,
.hero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero__poster { z-index: 0; }
.hero__media {
  z-index: 1;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.hero__media.is-playing { opacity: 1; }

/* Stops are set from measurement, not by eye, and have to be re-taken
   whenever the hero type moves. Now that the mark sits top-right and
   the tagline bottom-left, nothing overlaps the pavé dial in the
   centre of frame, so the heavy mid-frame darkening the centred
   layout needed is gone — it was costing the watch contrast for no
   legibility gain. What is left is a top vignette behind the mark and
   a bottom one behind the tagline and cue. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(to bottom,
    rgba(0, 0, 0, calc(0.45 * var(--hero-scrim))) 0%,
    rgba(0, 0, 0, calc(0.12 * var(--hero-scrim))) 22%,
    rgba(0, 0, 0, 0)                              48%,
    rgba(0, 0, 0, calc(0.18 * var(--hero-scrim))) 78%,
    rgba(0, 0, 0, calc(0.75 * var(--hero-scrim))) 100%);
}

/* everything that leaves on exit, in one layer */
.hero__ui {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.hero__cue {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  text-align: center;
}

/* Default: lower left, as a diagonal counterweight to the mark. This
   opens the frame corner-to-corner and leaves the video's centre
   clear. */
.hero__tagline {
  position: absolute;
  left: 40px;
  bottom: 40px;
  z-index: 3;
}

/* Alternative: right-aligned beneath the mark. Add
   .hero--tagline-under to the section to switch. 180px of lockup at
   1216×773 is 114px tall, so 40 + 114 + 16 clears it. */
.hero--tagline-under .hero__tagline {
  left: auto;
  bottom: auto;
  right: 40px;
  top: 170px;
  text-align: right;
}

/* Mark in the top-right corner, clear of the video's subject. At the
   corner it reads as a mark rather than a headline, so it comes down
   from 280px to 180. */
.hero__title {
  position: absolute;
  top: 40px;
  right: 40px;
  z-index: 3;
  margin: 0;
  line-height: 0;
}
.hero__lockup {
  width: 180px;   /* shipped at 1216px, downscaled */
  height: auto;
}

/* Label typography per the motion spec (11px / 0.14em / uppercase),
   but --ink rather than the spec's --ink-dim. Measured: dim needs
   ~0.86 scrim alpha over this footage to clear 4.5:1, which would
   blank the watch out; --ink clears it at 0.56. Revisit if the clip
   is recut with a darker lower third. */
.hero__tagline {
  color: var(--ink);
}

/* scroll cue: a 1px rule with a highlight travelling down it */
.hero__cue {
  top: 92%;
  width: 1px;
  height: 40px;
  background: var(--ink-faint);
  overflow: hidden;
  transition: opacity var(--dur-base) var(--ease-out);
}
.hero__cue-line {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    transparent 0%, var(--ink) 50%, transparent 100%);
  transform: translateY(-100%);
  animation: cue-travel 2s var(--ease-in-out) infinite;
}
@keyframes cue-travel {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(100%); }
}
/* fades out permanently on the first scroll input */
.hero.is-scrolled .hero__cue { opacity: 0; }
.hero.is-scrolled .hero__cue-line { animation: none; }

@media (max-width: 767px) {
  .hero__title { top: 20px; right: 20px; }
  /* 180px would be half the width of a 390px screen and stop reading
     as a corner mark, so it steps down with the gutter */
  .hero__lockup { width: 150px; }
  .hero__tagline { left: 20px; bottom: 20px; }
  .hero--tagline-under .hero__tagline { right: 20px; top: 135px; }
}

@media (prefers-reduced-motion: reduce) {
  .nav,
  .nav.is-visible,
  .hero__media,
  .hero__cue { transition: none; }
  /* the one autonomous loop on the page stops entirely */
  .hero__cue-line { animation: none; }
}

/* ============================================================
   §6 — Assembly scroll
   ============================================================ */
.seq {
  position: relative;
  background: var(--void);
}

.seq__inner {
  position: relative;
  height: 100svh;
  overflow: hidden;
  display: grid;
  place-items: center;
}

/* Capped, not stretched. The desktop source is 1100x619; filling a
   2560 or 3440 viewport is a 2x+ upscale of already-compressed
   JPEGs and will never be sharp. Beyond the cap the stage stays
   full width and the canvas letterboxes inside it, so the watch
   does not shrink relative to the screen — it just stops being
   enlarged past its own clarity. */
.seq__canvas {
  display: block;
  width: 100%;
  max-width: var(--maxw-canvas);
  height: 100%;
  margin-inline: auto;
}

.seq__label {
  position: absolute;
  /* the nav is always shown by the time §3 is pinned, so clear it */
  top: calc(var(--nav-h) + clamp(16px, 2vh, 28px));
  left: clamp(20px, 5vw, 80px);
  z-index: 2;
  transition: opacity var(--dur-base) var(--ease-out);
}

/* label and progress hairline fade in when the pin engages and out
   when it releases — assembly-scroll.js toggles .is-pinned */
.seq__label,
.seq__progress { opacity: 0; }
.seq.is-pinned .seq__label,
.seq.is-pinned .seq__progress { opacity: 1; }

/* right-edge progress hairline (desktop) */
.seq__progress {
  position: absolute;
  top: 0;
  right: 0;
  width: 1px;
  height: 100%;
  background: var(--edge);
  z-index: 2;
  transition: opacity var(--dur-base) var(--ease-out);
}
.seq__progress-fill {
  position: absolute;
  inset: 0;
  background: var(--ink-dim);
  /* scale, not height — height forces layout every scrub tick */
  transform: scaleY(var(--seq-progress, 0));
  transform-origin: top;
}

/* Mobile: the 16:9 frames can't fill a portrait viewport without
   either cropping the explosion or shrinking the watch to a sliver.
   Keep `contain` (every part stays visible) and compose the band,
   label and progress as one centred stack so the dead space above
   and below reads as deliberate negative space. */
@media (max-width: 767px) {
  .seq__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 22px;
  }
  .seq__canvas {
    width: 100vw;
    height: auto;
    aspect-ratio: 1920 / 1080;
  }
  .seq__label {
    position: static;
    text-align: center;
  }
  .seq__progress {
    position: relative;
    width: 120px;
    height: 1px;
    flex: none;
  }
  .seq__progress-fill {
    transform: scaleX(var(--seq-progress, 0));
    transform-origin: left;
  }
}

/* ---- annotations: chapter lines ----
   DOM rather than canvas, so they stay selectable and legible to
   assistive tech. Opacity is driven from assembly-scroll.js. */
.seq__chapter {
  position: absolute;
  left: 50%;
  bottom: clamp(56px, 10vh, 96px);
  transform: translateX(-50%);
  z-index: 2;
  margin: 0;
  text-align: center;
  white-space: nowrap;
  color: var(--ink);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

@media (max-width: 767px) {
  .seq__chapter { bottom: 40px; font-size: 10px; }
}

/* local loading state — only visible if the visitor reaches the
   section before the frame set has finished decoding */
.seq__loading {
  position: absolute;
  left: clamp(20px, 5vw, 80px);
  bottom: clamp(20px, 4vh, 40px);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 12px;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.seq__loading[data-active="true"] { opacity: 1; }
.seq__loading-rail {
  width: 120px;
  height: 1px;
  background: var(--edge);
  position: relative;
}
.seq__loading-fill {
  position: absolute;
  inset: 0;
  background: var(--ink-dim);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.18s linear;
}

@media (prefers-reduced-motion: reduce) {
  .seq__progress, .seq__loading { display: none; }
}

/* ============================================================
   Link lines — one line between sections, setting up the next
   ============================================================ */
.linkline {
  display: block;
  text-align: center;
  padding-block: clamp(72px, 12vh, 140px);
  margin: 0;
}

/* ============================================================
   §7 — Model carousel
   ============================================================ */
.carousel__viewport {
  overflow: hidden;
  /* the peek is what signals it scrolls, so nothing clips it away */
  cursor: grab;
}
.carousel__viewport:active { cursor: grabbing; }

.carousel__track {
  display: flex;
  gap: var(--car-gap);
  list-style: none;
}

/* Slide width is a real size with a floor and a ceiling, so how
   many fit is a consequence of viewport width rather than a fixed
   fraction that shows the same count on every screen. */
.carousel__slide {
  flex: 0 0 clamp(280px, 22vw, 300px);
}
.carousel__slide img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;  /* let the drag land on the track */
}

/* Slides are sized from height so three portrait frames fit across with
   the next one peeking. The spec's "8 columns" assumed 3:2 landscape. */
.carousel { --car-gap: 16px; }

/* A drifting track has no "slide 3 of 10" and no start or end, so the
   counter, hairline and arrows are all meaningless while it moves.
   They come back only in the reduced-motion fallback, where the track
   is static and stepping is the only way through it. */
.carousel__controls {
  display: none;
  align-items: center;
  gap: 20px;
  margin-top: 28px;
}
.carousel.is-stepped .carousel__controls { display: flex; }

.carousel.is-drifting .carousel__viewport { cursor: grab; }
.carousel.is-drifting .carousel__track { will-change: transform; }

.carousel__progress {
  position: relative;
  width: 120px;
  height: 1px;
  background: var(--edge);
  flex: 0 0 auto;
}
.carousel__progress-fill {
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
}

.carousel__counter {
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

.carousel__arrows {
  margin-left: auto;
  display: flex;
  gap: 8px;
}
.carousel__arrow {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--ink-faint);
  color: var(--ink);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.carousel__arrow:hover:not(:disabled) { border-color: var(--ink); }
.carousel__arrow:disabled {
  color: var(--ink-faint);
  border-color: var(--edge);
  cursor: default;
}

@media (max-width: 767px) {
  /* full width less a 20px peek */
  .carousel { --car-gap: 12px; }
  /* full width less a 20px peek, the treatment validated on device */
  .carousel__slide {
    flex: 0 0 calc(100vw - (2 * var(--gutter-mobile)) - 20px);
  }
  .carousel__controls { gap: 14px; margin-top: 20px; }
  .carousel__progress { width: 80px; }
}

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

/* ============================================================
   §3 → §4 — pinned reveal
   Straps slides up and off; Buy is inert underneath and is
   uncovered rather than navigated to. Driven from scroll position
   every frame, so it reverses naturally.
   ============================================================ */
.reveal-pin {
  position: relative;
  height: var(--reveal-pin-height);
}
.reveal-pin__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}
.reveal-layer {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  overflow: hidden;
}
/* Both layers are opaque, each in its own section's tone. The sections
   inside are shorter than the viewport-tall layers, and a transparent
   Straps layer let Buy show through above and below the tiles before
   the reveal — the logo, Reset view and the terms line, on any screen
   where the tile grid is short against the height (1024×768,
   1280×1024, 2560×1300 all measured). */
.reveal-layer--under { z-index: 1; background: var(--surface-buy); }
/* no will-change here — reveal-pin.js sets it only while the layer
   is moving; left on, it softens the §3 hover crossfade */
.reveal-layer--over  { z-index: 2; background: var(--surface-straps); }

/* The layers are a viewport tall and clipped, so the sections inside
   them have to fit. Section padding comes down, and the product shot
   is bounded by viewport height rather than column width — at 3:4 a
   six-column shot is ~880px tall, which overflows any laptop-height
   stage. Bounding the height instead keeps Buy whole from 700svh
   upward. */
.reveal-layer > .section {
  padding-block: clamp(24px, 4vh, 72px);
}
/* The stage carries the explicit width and the shot fills it. Explicit,
   not auto: with srcset an img's intrinsic size is its sizes value, so
   width:auto would let that estimate move the layout. And the 3D canvas
   lies over the stage, so the stage has to be the box. min(column, 68svh
   at 3:4) is the same box auto produced. */
.reveal-layer .buy__stage {
  width: min(100%, calc(68svh * 3 / 4));
  margin-inline: auto;
}
.reveal-layer .buy__shot {
  width: 100%;
  height: auto;
  max-height: 68svh;
}
.reveal-layer .buy__figure {
  display: grid;
  justify-items: center;
}

/* Two cases unpin entirely and render both sections in normal flow.

   Reduced motion, because the reveal is scroll-driven movement:
   Buy is simply there rather than being uncovered, and Straps is
   never shown as a transitional state.

   And below 900px, where the Buy grid stacks. Stacked, Buy is
   taller than a phone viewport, so a clipped 100svh stage cuts off
   the Add to bag button — and a 240vh pinned slide is the same
   shape as the mobile pin trouble in §6. The reveal is a desktop
   flourish; on a phone the two sections simply follow one another. */
@media (prefers-reduced-motion: reduce), (max-width: 900px) {
  .reveal-pin { height: auto; }
  /* Flow, not stacking — and Straps first. In the markup Buy is the
     under layer and comes first, because the pinned reveal slides
     Straps up off it; there, z-index decides what the visitor sees, so
     source order never shows. Unpinned it does, and it put Buy above
     the tiles that are supposed to lead to it — right under the "Worn"
     line introducing them. Ordering here keeps the markup as the
     reveal needs it. */
  .reveal-pin__stage {
    position: static;
    height: auto;
    overflow: visible;
    display: flex;
    flex-direction: column;
  }
  .reveal-layer { position: static; display: block; }
  .reveal-layer--over { transform: none !important; order: -1; }
  .reveal-layer > .section { padding-block: var(--section-pad-desktop); }
  .reveal-layer .buy__stage { width: 100%; }
  .reveal-layer .buy__shot {
    width: 100%;
    max-height: none;
    aspect-ratio: 3 / 4;
  }
}
@media (max-width: 767px) {
  .reveal-layer > .section { padding-block: var(--section-pad-mobile); }
}

/* ============================================================
   §3 — Four straps
   ============================================================ */
.straps__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(12px, 1.6vw, 24px);
}

.strap-cell {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Picture frames on a wall. The frame, mat and hook are container
   only — the crossfade inside is unchanged. */
.strap-tile {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  margin: 0;
  padding: var(--frame-moulding);
  border: 0;
  border-radius: 2px;
  background: var(--frame-wood);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.06) inset,
    0 -1px 0 rgba(0, 0, 0, 0.6) inset,
    0 18px 40px -12px rgba(0, 0, 0, 0.7),
    0 2px 8px rgba(0, 0, 0, 0.5);
}

/* inner bevel — moulding catching light */
.strap-tile::before {
  content: "";
  position: absolute;
  inset: 6px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 1px;
  pointer-events: none;
  z-index: 2;
}

.strap-tile__hook {
  position: absolute;
  top: -9px;
  left: 50%;
  transform: translateX(-50%);
  width: 18px;
  height: 9px;
  background: linear-gradient(180deg, #3a3025, #1a140e);
  border-radius: 0 0 9px 9px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.5);
}

/* the pale mat the art is recessed into */
.strap-tile__mat {
  position: relative;
  display: block;
  height: 100%;
  width: 100%;
  padding: 5%;
  background: var(--frame-mat);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4) inset;
}

.strap-tile__art {
  position: relative;
  display: block;
  height: 100%;
  width: 100%;
  overflow: hidden;
  background: var(--void);
}

.strap-tile__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
  transition: opacity var(--dur-base) var(--ease-out);
}
/* The product shot stays put underneath; only the (opaque) worn shot
   fades in over it. Same end state as fading both, without the
   mid-transition luminance dip where the near-black surface would
   show through two half-opacity layers. */
.strap-tile__layer--product { opacity: 1; transition: none; }
.strap-tile__layer--model   { opacity: 0; }

/* reveal — hover anywhere in the cell, keyboard focus, or held after
   a touch tap. Opacity only: no scale, no slide. */
.strap-cell:hover         .strap-tile__layer--model,
.strap-tile:focus-visible .strap-tile__layer--model,
.strap-tile.is-shown      .strap-tile__layer--model { opacity: 1; }

/* selected — a hairline frame, no motion */
.strap-tile.is-selected::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--accent-faint);
  pointer-events: none;
}

.strap-tile:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 3px;
}

.strap-tile__caption {
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
  transition: color var(--dur-fast) var(--ease-out);
}
.strap-cell:hover .strap-tile__caption,
.strap-tile:focus-visible ~ .strap-tile__caption,
.strap-tile.is-selected ~ .strap-tile__caption { color: var(--ink); }

@media (max-width: 900px) {
  .straps__grid { grid-template-columns: repeat(2, 1fr); gap: 16px 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .strap-tile__layer,
  .strap-tile__caption { transition: none; }
}

/* ============================================================
   §5 — About
   The one editorial moment: image and copy side by side. The
   photograph is deliberately brighter than the rest of the page,
   so it takes no scrim and no grading.
   ============================================================ */
.about__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 0;
  align-items: center;
}
.about__figure { grid-column: 1 / span 6; }
.about__text   { grid-column: 8 / span 5; }

.about__figure img { width: 100%; height: auto; }

.about__lede {
  font-size: var(--fs-head);
  line-height: 1.12;
  margin-bottom: 24px;
}
.about__body {
  font-size: var(--fs-body);
  line-height: 1.7;
  color: var(--ink-dim);
}
.about__body + .about__body { margin-top: 20px; }

@media (max-width: 900px) {
  .about__grid { display: block; }
  .about__text { margin-top: 32px; }
}

/* ============================================================
   §8 — What arrives
   ============================================================ */
.arrives__figure img {
  width: 100%;
  aspect-ratio: 21 / 9;
  object-fit: cover;
}
.arrives__contents {
  display: block;
  margin-top: 28px;
  text-align: center;
}

@media (max-width: 767px) {
  /* 21:9 leaves the box a sliver on a phone — give it more height */
  .arrives__figure img { aspect-ratio: 16 / 9; }
  .arrives__contents { margin-top: 20px; }
}

/* ============================================================
   §4 — Buy
   ============================================================ */
.buy__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 0;
  align-items: center;
}
.buy__figure { grid-column: 1 / span 6; }
.buy__panel  { grid-column: 8 / span 5; }

.buy__shot {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  transition: opacity var(--dur-base) var(--ease-out);
}

.buy__name {
  font-size: var(--fs-head);
  line-height: 1.12;
}
/* the lockup is three lines tall and centred on itself */
.buy__logo {
  display: block;
  width: clamp(132px, 12vw, 176px);
  height: auto;
}
/* the price is a statement, so it takes the display face */
.buy__price {
  margin-top: 12px;
  font-size: 28px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.buy__straps { margin-top: 40px; }
.buy__straps-label { display: block; margin-bottom: 12px; }

/* material, then colour — two axes resolved without leaving the panel */
.buy__materials {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}
.buy__material {
  flex: 1 1 0;
  padding: 13px 18px;
  background: none;
  border: 1px solid var(--edge);
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.buy__material:hover:not(:disabled) { border-color: var(--ink-faint); color: var(--ink); }
.buy__material.is-active { border-color: var(--accent); color: var(--ink); }
.buy__material:disabled { cursor: default; color: var(--ink-faint); }

/* an unavailable material says so rather than looking broken */
.buy__material-note {
  margin-left: 8px;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The doc only sanctions glass here if the row overlaps the product
   shot — over bare black it is the grey rectangle both documents warn
   about. So on desktop the row is pulled left over the image edge and
   given the same treatment as the nav. Stacked on mobile there is
   nothing to overlap, so the glass comes off and it sits flat. */
.buy__swatches {
  display: flex;
  gap: 12px;
  padding: 12px;
  /* percentage resolves against the panel width, so the overlap holds
     proportionally at every width instead of only at 1440. The panel
     is 5 of 12 columns and the gutter between it and the shot is one
     column, so -30% clears the gutter and lands ~half a column over
     the image. */
  margin-left: -30%;
  width: fit-content;
  border-radius: 2px;
  background: var(--backdrop-accent);
  backdrop-filter: blur(var(--backdrop-blur)) saturate(var(--backdrop-saturate));
  -webkit-backdrop-filter: blur(var(--backdrop-blur)) saturate(var(--backdrop-saturate));
  border-top: 1px solid var(--backdrop-accent-edge);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .buy__swatches { background: var(--backdrop-accent-solid); }
}

.buy__swatch {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  padding: 0;
  background: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.buy__swatch-chip {
  display: block;
  width: 26px;
  height: 26px;
  border: 1px solid var(--edge);
}
.buy__swatch:hover { border-color: var(--ink-faint); }
.buy__swatch.is-selected { border-color: var(--accent); }

.buy__chosen {
  display: block;
  margin-top: 12px;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
}

/* the only inverted element on the page */
.buy__button {
  width: 100%;
  height: 56px;
  margin-top: 32px;
  background: var(--accent);
  color: var(--accent-ink);
  border: 0;
  font-family: inherit;
  font-size: 14px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.buy__button:hover { background: #FFFFFF; }
.buy__button:focus-visible { outline: 1px solid var(--ink); outline-offset: 3px; }

.buy__reassurance { display: block; margin-top: 16px; }

/* Custom finish request — a quiet line under the swatches, well below
   Add to bag in weight. Shown only once custom-finish.js has confirmed
   <dialog> support, so it can never be a dead link. */
.buy__custom {
  display: none;
  margin-top: 10px;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
}
.has-custom-finish .buy__custom { display: block; }
.buy__custom-link {
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  letter-spacing: inherit;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--ink-faint);
  text-underline-offset: 3px;
  cursor: pointer;
  transition: text-decoration-color var(--dur-fast) var(--ease-out);
}
.buy__custom-link:hover { text-decoration-color: var(--ink); }

/* §4 live 3D viewer (buy-viewer.js). The photo stays in the stage as
   the poster and the fallback; the canvas lies over it and fades in
   only once the model has rendered — so the panel is never blank, and
   falling back is just this fade in reverse. The canvas is transparent
   so the stage glow shows through, which means the photo has to fade
   out as it fades in. !important because buy.js sets the shot's
   opacity inline while it swaps colours. */
.buy__stage { position: relative; }
.buy__stage.is-3d .buy__shot { opacity: 0 !important; }
.buy__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  cursor: grab;
  transition: opacity var(--dur-base) var(--ease-out);
}
.buy__canvas:active { cursor: grabbing; }
.buy__stage.is-3d .buy__canvas { opacity: 1; }

/* loading line, as §6's own */
.buy__load {
  position: absolute;
  left: 20%;
  right: 20%;
  bottom: 18px;
  height: 1px;
  background: var(--edge);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}
.buy__load[data-active="true"] { opacity: 1; }
.buy__load-fill {
  position: absolute;
  inset: 0;
  background: var(--ink-dim);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.18s linear;
}

.buy__reset {
  position: absolute;
  right: 12px;
  bottom: 12px;
  padding: 6px 10px;
  background: none;
  border: 1px solid var(--edge);
  color: var(--ink-dim);
  font-family: inherit;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.buy__stage.is-3d .buy__reset { opacity: 1; pointer-events: auto; }
.buy__reset:hover { color: var(--ink); border-color: var(--ink-faint); }

/* ?fps test readout only */
.buy__fps {
  position: absolute;
  left: 8px;
  top: 8px;
  z-index: 1;
  padding: 3px 6px;
  font-size: var(--fs-label);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--backdrop-ambient-solid);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .buy__canvas,
  .buy__load,
  .buy__reset { transition: none; }
}

@media (max-width: 900px) {
  .buy__grid { display: block; }
  .buy__panel { margin-top: 40px; }
  /* nothing behind it once stacked, so no glass */
  .buy__swatches {
    margin-left: 0;
    padding: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: 0;
    /* Eight 44px swatches need 436px and a phone has ~350, which put
       the last two off-screen and unreachable. One row of tracks that
       shrink to fit instead: ~40px each at 390, and a shorter range
       (rubber's three or four) still gets full-size 44px. */
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 44px);
    gap: 4px;
    width: 100%;
  }
  .buy__swatch {
    width: 100%;
    height: auto;
    aspect-ratio: 1;
  }
}

/* ============================================================
   Custom finish request dialog
   Native <dialog>: focus trap, Esc and the top layer come free.
   No entrance animation — it shows no change worth animating.
   ============================================================ */
.custom {
  width: min(480px, calc(100vw - 2 * var(--gutter-mobile)));
  max-height: calc(100svh - 2 * var(--gutter-mobile));
  margin: auto;
  padding: 0;          /* the form carries the padding, so a click on
                          the dialog element itself is the backdrop */
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--edge);
  border-radius: 2px;
}
.custom::backdrop { background: var(--scrim-modal); }

.custom__form {
  display: grid;
  gap: 20px;
  padding: 28px;
}
.custom__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}
.custom__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 24px;
  line-height: 1.2;
  letter-spacing: -0.01em;
}
.custom__close {
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.custom__close:hover { color: var(--ink); }
.custom__note {
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-dim);
}
.custom__field { display: grid; gap: 8px; }
.custom__field textarea,
.custom__field input {
  width: 100%;
  padding: 12px 14px;
  background: var(--void);
  color: var(--ink);
  border: 1px solid var(--edge);
  border-radius: 0;
  font: inherit;
  font-size: var(--fs-body);   /* 16px: below this iOS zooms on focus */
  line-height: 1.5;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.custom__field textarea { resize: vertical; min-height: 88px; }
.custom__field textarea:focus-visible,
.custom__field input:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 0;
  border-color: var(--accent);
}
.custom__field ::placeholder { color: var(--ink-dim); opacity: 1; }

.custom__submit {
  height: 52px;
  background: var(--accent);
  color: var(--accent-ink);
  border: 0;
  font-family: inherit;
  font-size: 14px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.custom__submit:hover { background: #FFFFFF; }
.custom__status {
  font-size: 13px;
  color: var(--ink-dim);
}
.custom__status:empty { display: none; }

/* ============================================================
   §9 — Footer
   ============================================================ */
.footer {
  padding-block: 80px;
  border-top: 1px solid var(--edge);
}
.footer__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}
.footer__head { display: block; margin-bottom: 16px; }

.footer__list { list-style: none; }
.footer__list li + li { margin-top: 8px; }
.footer__list a,
.footer__placeholder,
.footer__parent a {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.footer__list a:hover,
.footer__parent a:hover { color: var(--ink-dim); }

.footer__base {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 64px;
  padding-top: 32px;
  border-top: 1px solid var(--edge);
}
.footer__logo { width: 108px; height: auto; opacity: 0.6; }
.footer__parent { color: var(--ink-faint); }
.footer__statement { line-height: 1.9; }

@media (max-width: 767px) {
  .footer { padding-block: 56px; }
  .footer__grid { grid-template-columns: 1fr; gap: 28px; }
  .footer__base {
    flex-direction: column;
    align-items: flex-start;
    gap: 16px;
    margin-top: 40px;
  }
}

/* ============================================================
   Reduced motion — crossfades stay (they carry state) but shorten
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .strap-tile__layer,
  .buy__shot { transition-duration: 120ms; }
}

/* ============================================================
   Checkout dialog (checkout.js)
   Built on the custom finish dialog: same surface, hairlines, 16px
   inputs and the one solid button. Wider, because delivery needs two
   columns on a laptop.
   ============================================================ */
.checkout { width: min(560px, calc(100vw - 2 * var(--gutter-mobile))); }
.checkout__body { gap: 22px; }
.checkout__summary {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding-block: 14px;
  border-block: 1px solid var(--edge);
  font-size: 14px;
}
.checkout__price {
  font-family: var(--font-display);
  font-size: 22px;
  font-variant-numeric: tabular-nums;
}
.checkout__step,
.checkout__form { display: grid; gap: 18px; }
.checkout__step[hidden],
.checkout__form[hidden] { display: none; }

.checkout__tabs { display: flex; gap: 24px; border-bottom: 1px solid var(--edge); }
.checkout__tab {
  padding: 0 0 10px;
  margin-bottom: -1px;
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.checkout__tab:hover { color: var(--ink); }
.checkout__tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }

.checkout__hint { text-transform: none; letter-spacing: 0.02em; color: var(--ink-faint); }
.checkout__signed { font-size: 13px; color: var(--ink-dim); }
.checkout__signed span { color: var(--ink); }

.checkout__group { display: grid; gap: 14px; margin: 0; padding: 0; border: 0; min-width: 0; }
.checkout__group > legend { padding: 0; margin-bottom: 14px; }
.checkout__row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }

.checkout__method {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px;
  border: 1px solid var(--edge);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.checkout__method:hover { border-color: var(--ink-faint); }
.checkout__method:has(input:checked) { border-color: var(--ink); }
.checkout__method input { margin-top: 3px; accent-color: var(--ink); }
.checkout__method b { display: block; font-weight: 500; font-size: 15px; }
.checkout__method i { display: block; font-style: normal; font-size: 13px; line-height: 1.5; color: var(--ink-dim); }

.custom__submit:disabled { opacity: 0.6; cursor: progress; }
.checkout__link {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
.checkout__secondary {
  justify-self: center;
  font-size: 13px;
  color: var(--ink-dim);
  text-underline-offset: 3px;
}
.checkout__secondary:hover { color: var(--ink); }
.checkout__ref { font-weight: 500; color: var(--ink); letter-spacing: 0.04em; }
.checkout .custom__status { color: var(--ink); }

@media (max-width: 520px) {
  .checkout__row { grid-template-columns: 1fr; }
  .custom__form.checkout__body { padding: 22px 18px; }
}

/* ============================================================
   Portal pages — /account/ and /admin/
   Working pages: plain bar, no motion, the same type, surfaces and
   form controls as the checkout dialog.
   ============================================================ */
.portal { min-height: 100svh; }
.portal__bar {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--nav-h);
  background: var(--backdrop-ambient-solid);
  border-bottom: 1px solid var(--edge);
}
.portal__bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: 100%;
}
.portal__mark img { display: block; width: auto; height: 48px; }
.portal__signout,
.portal__link {
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-dim);
  cursor: pointer;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.portal__signout:hover,
.portal__link:hover { color: var(--ink); }

.portal__main { padding-block: clamp(40px, 8vh, 96px) 96px; }
.portal__loading { text-align: center; }
.portal__panel { display: grid; gap: 20px; }
.portal__panel[hidden] { display: none; }
.portal__auth { max-width: 440px; margin-inline: auto; }
.portal__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(28px, 3vw, 40px);
  line-height: 1.15;
  letter-spacing: -0.01em;
}
.portal__lede { color: var(--ink-dim); font-size: 15px; }
.portal__subhead { display: block; margin: 8px 0 12px; }
.portal__status { color: var(--ink); }
.portal__notice {
  padding: 12px 14px;
  border: 1px solid var(--edge);
  font-size: 14px;
}
.portal__notice[hidden] { display: none; }

.portal__orders {
  display: grid;
  grid-template-columns: minmax(260px, 1fr) 2fr;
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
  margin-top: 12px;
}
.portal__list { list-style: none; display: grid; gap: 8px; }
.portal__row {
  display: grid;
  gap: 4px;
  width: 100%;
  padding: 14px;
  text-align: left;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--edge);
  font: inherit;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.portal__row:hover { border-color: var(--ink-faint); }
.portal__row[aria-current="true"] { border-color: var(--ink); }
.portal__row-id { font-size: 14px; letter-spacing: 0.04em; }
.portal__row-meta { font-size: 13px; color: var(--ink-dim); }
.portal__empty { color: var(--ink-dim); font-size: 14px; }
.portal__empty a { color: var(--ink); }

.portal__badge {
  justify-self: start;
  padding: 3px 8px;
  font-size: 11px;
  letter-spacing: 0.06em;
  border: 1px solid var(--ink-faint);
  color: var(--ink-dim);
  white-space: nowrap;
}
.portal__badge[data-status="paid"],
.portal__badge[data-status="shipped"],
.portal__badge[data-status="delivered"] { border-color: var(--ink); color: var(--ink); }
.portal__badge[data-status="awaiting_transfer"],
.portal__badge[data-status="pending_payment"],
.portal__badge[data-status="payment_confirming"],
.portal__badge[data-status="partially_paid"] { border-style: dashed; color: var(--ink); }

.portal__detail {
  display: grid;
  gap: 16px;
  padding: clamp(20px, 3vw, 32px);
  background: var(--surface);
  border: 1px solid var(--edge);
}
.portal__detail[hidden] { display: none; }
.portal__detail-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.portal__detail-title { font-family: var(--font-display); font-weight: 400; font-size: 24px; }
.portal__explain { color: var(--ink-dim); font-size: 14px; line-height: 1.6; }
.portal__dev {
  padding: 10px 12px;
  border: 1px dashed var(--ink-faint);
  font-size: 12px;
  color: var(--ink-dim);
}
.portal__lines { display: grid; gap: 0; }
.portal__line {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 16px;
  padding-block: 10px;
  border-top: 1px solid var(--edge);
  font-size: 14px;
}
.portal__line dd { overflow-wrap: anywhere; }
.portal__address { font-style: normal; font-size: 14px; line-height: 1.7; }
.portal__history { list-style: none; display: grid; gap: 8px; font-size: 14px; }
.portal__history li { display: flex; justify-content: space-between; gap: 16px; }
.portal__history time { color: var(--ink-dim); font-size: 13px; white-space: nowrap; }

@media (max-width: 767px) {
  .portal__orders { grid-template-columns: 1fr; }
  .portal__line { grid-template-columns: 96px 1fr; }
}

/* ---- admin dashboard ---- */
.admin__summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--edge);
  border: 1px solid var(--edge);
  margin-bottom: 28px;
}
.admin__stat { display: grid; gap: 6px; padding: 16px; background: var(--surface); }
.admin__stat b { font-family: var(--font-display); font-weight: 400; font-size: 26px; font-variant-numeric: tabular-nums; }
.admin__filters {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: 14px;
  margin-bottom: 20px;
}
.admin select,
.admin__filters input {
  width: 100%;
  padding: 12px 14px;
  background: var(--void);
  color: var(--ink);
  border: 1px solid var(--edge);
  border-radius: 0;
  font: inherit;
  font-size: 15px;
}
.admin__actions input { background: var(--void); }
.admin__layout {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr);
  gap: 24px;
  align-items: start;
}
.admin__tablewrap { overflow-x: auto; border: 1px solid var(--edge); }
.admin__table { width: 100%; border-collapse: collapse; font-size: 14px; }
.admin__table th {
  padding: 10px 12px;
  text-align: left;
  font-weight: 400;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
  border-bottom: 1px solid var(--edge);
  white-space: nowrap;
}
.admin__table td { padding: 12px; border-bottom: 1px solid var(--edge); vertical-align: top; }
.admin__table td small { display: block; color: var(--ink-dim); font-size: 12px; }
.admin__table tbody tr { cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out); }
.admin__table tbody tr:hover { background: var(--surface); }
.admin__table tbody tr[aria-selected="true"] { background: var(--surface); box-shadow: inset 2px 0 0 var(--ink); }
.admin__id { white-space: nowrap; letter-spacing: 0.04em; }
.admin__detail { position: sticky; top: calc(var(--nav-h) + 16px); max-height: calc(100svh - var(--nav-h) - 32px); overflow-y: auto; }
.admin__actions { display: grid; gap: 14px; }
.admin__buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.admin__btn {
  padding: 11px 16px;
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.admin__btn:hover { background: #FFFFFF; }
.admin__btn--quiet { background: none; color: var(--ink); border-color: var(--ink-faint); }
.admin__btn--quiet:hover { background: none; border-color: var(--ink); }
.admin__btn:disabled { opacity: 0.5; cursor: progress; }
.admin__history li > span { display: grid; gap: 2px; }
.admin__history small { color: var(--ink-faint); font-size: 12px; }

@media (max-width: 1100px) {
  .admin__layout { grid-template-columns: 1fr; }
  .admin__detail { position: static; max-height: none; }
}
@media (max-width: 767px) {
  .admin__filters { grid-template-columns: 1fr; }
}
.admin__test {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px dashed var(--ink-dim);
  color: var(--ink-dim);
  vertical-align: 1px;
}
