/* Atelier 151 — "HUD" direction, rev 5: "Taffy Dreams" recolour (Bright
   Spring palette #14). Same instrument panel, repainted spring-bright.
   Ground: pale sky blue #D1EBFA, with lighter and deeper tints of the same
   blue for panels, the photo well, the header and the hero. One ink: deep
   teal #0B2A33 carries every word — titles, prices, labels. The four brights
   are fills, borders and marks, never text on the light surfaces: teal
   #239FA9 draws the HUD (scan line, brackets, rulers, diamonds, frames,
   hover rims, glows), pink #F4649F is the primary key with an ink label,
   mint #4AE5AB is the In-Stock node and the footer accent, mustard #E1B808
   fills the JP badge and ::selection behind ink text. Deep teal #0D626F is
   the only coloured word (links, active nav) and the focus ring. The footer
   is the one dark band (ink), where the brights may be text.
   Palette measured (WCAG 2.x):
   #0B2A33 on #D1EBFA 12.2 · on #E4F2FC 13.2 · on #F0F8FE 14.0 · on #B7DBF2 10.4
     · on #C3E1F5 11.1 · on #BFE2F7 11.1 · on #C9E5F7 11.5 · on the hero chip ~12.1
   #16414E on #D1EBFA 9.0 · on #E4F2FC 9.7 · on #B7DBF2 7.6 · on #C3E1F5 8.1
   #2E5A66 on #D1EBFA 6.1 · on #E4F2FC 6.7 · on #BFE2F7 5.6 · on the hero chip 5.6
     · on #C3E1F5 5.6 · on #B7DBF2 5.2
   #37616D on #D1EBFA 5.5 · on #E4F2FC 6.0 · on #B7DBF2 4.7 · on the hero chip 5.6
     · on #C3E1F5 5.0
   #0D626F on #D1EBFA 5.7 · on #E4F2FC 6.2 · on #C3E1F5 5.2 · on #C9E5F7 5.4
     · on #BFE2F7 5.2 · on #B7DBF2 4.8 — links, active nav, focus rings
   #0A4E58 on #D1EBFA 7.6 (link hover)
   #0B2A33 on #F4649F 5.15 (btn) · on #F782B3 6.3 (btn hover) · on #EF5796 4.7
     (btn active) · on #E1B808 7.9 (JP badge, ::selection) · on #4AE5AB 9.4
   #F0F8FE on rgba(11,42,51,.88) >=9.8:1 over the well (EN badge)
   On #0B2A33 (footer): #D1EBFA 12.2 · #9CC4D4 8.1 · #7FA6B5 5.8 · #4AE5AB 9.4
   #239FA9 on #D1EBFA 2.57 · on #E4F2FC 2.78 — FILL/BORDER/MARK-ONLY, never
     text: every state near it is duplicated by words, motion or a >=3:1 channel
   #4AE5AB on #D1EBFA 1.30 · #E1B808 1.53 · #F4649F 2.37 — FILL-ONLY behind ink
   #4A7684 on #D1EBFA 4.0 · on #E4F2FC 4.4 · on #C3E1F5 3.7 · on #C9E5F7 3.8
     · on #B7DBF2 3.4 — CONTROL BORDERS >=3:1, never text
   #8FB6C9 on #D1EBFA 1.8 — ORNAMENT ONLY
   #6E5900 on #E4F2FC 6.0 (dormant .stock--low tint)
   Rev 6 (owner request: "more special effects" on hover and on the banner).
   In-stock cards carry a hover foil: a prismatic wash of the four brights at
   7-9% alpha over the whole card, a pointer-tracked white highlight at 0.20
   peak (fallback position 50% 26% without foil.js), a 2deg pointer tilt, a
   1.04 cut-out lean-in, and a brightened In-Stock node. Sold-out cards stay
   dormant. The "one ambient
   effect" rule is retired: ambient loops now live in the hero only, are
   transform/opacity-only, >=3s per cycle, low amplitude, and all stop under
   reduced-motion. Contrast audit, worst cases — the hero wash figures were measured
   in-browser (Chromium, real pixels, sampled backgrounds); the foil,
   title-sheen and category-tile figures were computed from the layer
   alphas, not measured from pixels:
   Foil layers sit at z-index 3 over the whole card; .card-body is raised to
     z-index 4 so the words composite ABOVE the wash and highlight while the
     panel behind them still lights. With the highlight centred on the
     readout body: .card-spec #37616D 5.46:1, .stock #2E5A66 6.10:1, ink
     12.13:1 (before the z-index fix they were 3.37:1 / 3.63:1 — the layers
     were composited over the text).
   Title-frame sheen: white band peaks at 0.24 over the ink h1; computed
     worst case 12.11 -> 5.91:1 at the band's centre for <1s in each 8s
     cycle. Same accepted precedent as the .btn sweep (ink on pink 5.15 ->
     2.68 at peak).
   Category-tile sweep (0.35 white, one 650ms pass on hover), computed at
     peak: h3 ink 4.48:1, p muted 2.97:1, mono dim 2.80:1, ink-2 3.68:1.
     Transient; same precedent as the .btn sweep — accepted.
   Hero prism wash, measured by sampling the real background behind the
     hero text across the full 38s rotation (text hidden, 12 phases,
     2nd-percentile background). It is NOT confined to the slab column at
     900-1100px and it does reach the text on phones:
     lede #2E5A66   5.36-5.39 at rest; 4.81 @390px, 4.81 @900px,
                    5.09 @1100px, 5.27 @1280px
     chip #37616D   5.29-5.33 at rest; 4.91 @390px, 5.09 @>=900px
     h1 ink         11.6-11.7 at rest; 11.0-11.5
     Floor stays >=4.5:1 at every width.
   Rev 7 (owner: effects still too subtle; "very striking" hover, "more
   cyber" banner). Cards: the rev-6 7-9% wash is replaced by a true holo —
   an overlay-blended rainbow band at ~55% alpha that tracks the pointer,
   a 0.55 white highlight plus two screen-blended glitter lattices, a
   +-8deg tilt (foil.js), a 6px lift, a 1.10 photo pop with saturate, and a
   neon rim. Category tiles carry the same holo: the highlight and glitter
   live in the aria-hidden .cat-foil span at z-index 3, with the tile words
   raised to z-index 4 so they composite above the foil. Card text still
   composites above all foil layers (.card-body
   z-index 4), so the readout ratios are hover-invariant; the foil lives
   under the words by design, as on a real holo card. Hero: static CRT
   scanlines, a chromatic-split glitch on the h1 (text-shadow only — no
   text copies, nothing duplicated to screen readers, two <200ms bursts
   per 8s, well under 3 flashes/sec), a perspective grid floor scrolling
   at the base (masked clear of the lede), self-drawing circuit traces
   (inline SVG, right side) and a rotating targeting reticle on the slab.
   Per-frame repaint areas (stated per the budget): the floor's
   background-position repaints a hero-width x ~110px strip; the circuit's
   stroke-dashoffset repaints a ~540x300px region on the hero's right; the
   h1 glitch repaints the h1 box; card foil repaints one hovered card.
   All ambient loops remain hero-only, >=3s cycles, and stop under
   reduced-motion. The rev-6 contrast figures above for the foil wash and
   the hover layers are superseded by these, measured on rev 7 as built
   (Chromium, real pixels):
   Hero, sampled background behind the text, 12 phases of the 38s cycle,
     scanlines at 0.03 (at 0.05 the lede failed at 4.46:1 at 900px):
     lede 4.79 @390px, 4.60 @900px, 5.09 @1100px, 5.27 @1280px
     chip 4.91 @390px, 5.09 @>=900px;  h1 ink >=10.8
   Cards, real pixels behind the readout text while hovered, pointer
     swept over a 5x5 grid (category grid and homepage rail):
     .card-title >=10.50, .card-spec >=5.31, .price >=11.64,
     .stock >=5.37 (its box includes the In-Stock node, so conservative).
     The .card-body z-index 4 fix still holds: foil layers composite
     under the words.
   Tilt +-8deg in the rail: 0.44px clear of the clip edge at the worst
     corner — not clipped, but tight; do not increase the tilt.
   Rev 7 hotfix (lead review): the h1 width is expressed in em, not ch —
     a running animation froze Chromium's computed ch against the
     fallback font when Orbitron arrived late, and the 550px-wide title
     jumped onto the eyebrow chip's line at 1280px (broken 7 of 8 loads).
     em is font-independent; glitch kept, rendered width unchanged.
   Rev 8 (owner: the rev-7 energy on the product page — pictures and message
   buttons). Pictures: the photo is for INSPECTION (a buyer reads the PSA
   label off a slab photo), so nothing composites over the photograph on
   hover — no rainbow band, no glint. The drama is translated to the frame:
   a +-3deg pointer tilt (foil.js, now loaded on product pages), the card's
   neon rim, a 2px prismatic conic edge-light (mask-composite ring, hover
   fade-in, static), a pointer-tracked backlight painted as a well
   BACKGROUND layer behind the transparent cut-out, and a 1.03 cut-out pop.
   Sold-out pages (main.detail--out, hook added by the build) are dormant:
   full-fidelity photo, dimmed brackets, no hover effects; the buttons keep
   theirs. Buttons: scoped to .ctas, which enquiry_ctas() emits on product
   pages, the About page and empty category pages — the same two enquiry
   buttons carry the console everywhere; only "Back to catalogue" is
   outside it. The primary key gains an ambient blurred
   pink halo (opacity-only, 4.5s, reduced-motion stops it), a pointer-
   tracked white highlight UNDER the label (z-index 1, the .card-body fix),
   a pink/teal neon rim on hover, and a 2px icon send-off; the ghost gains
   a teal rim glow. Measured on rev 8 as built (Chromium, real pixels, lead review):
   - Button labels: Telegram ink on pink 5.09:1 at rest (the halo sits
     under the label and costs 0.06), 6.32:1 hovered; Email label
     12.18:1 at rest, 5.25:1 hovered.
   - Slab label (PSA, Umbreon) with the pointer on it: luminance spread
     89% of rest; fully legible (photo 3% larger, tilted ~2deg, nothing
     composites over it).
   - Tilt +-2.98deg at the photo corners; no horizontal overflow at 821,
     1000 or 1280px with the photo hovered at its edges.
   - Sold out: 5/5 sold-out pages carry detail--out, 0 in-stock pages do;
     sold-out photo still, buttons still track.
   - Reduced motion: zero running animations on a product page.
   Prices are IBM Plex Mono 500 only — Orbitron's slashed zero never renders
   a figure. */

:root {
  --bg: #d1ebfa;               /* pale sky blue ground */
  --abyss: #0b2a33;            /* footer: the one dark band, deep teal-ink */
  --panel: #e4f2fc;            /* cards, modules: lifted sky */
  --panel-hi: #f0f8fe;
  --well: #b7dbf2;             /* photo viewport: deeper sky */
  --ink: #0b2a33;              /* every word on the light surfaces */
  --ink-2: #16414e;            /* was --bright in the dark rev; the scale has flipped */
  --muted: #2e5a66;
  --dim: #37616d;
  --signal: #239fa9;           /* teal: HUD line work — fills/borders/marks ONLY, never text */
  --signal-deep: #0d626f;      /* teal dark enough to be text: links, active nav, focus */
  --mint: #4ae5ab;             /* In-Stock node; footer accent (text-safe on the dark band) */
  --mustard: #e1b808;          /* fills behind ink text: JP badge, ::selection */
  --link: #0d626f;
  --link-hover: #0a4e58;
  --line: #8fb6c9;             /* ornament only */
  --line-hi: #4a7684;          /* >=3:1 control borders */
  --btn: #f4649f;
  --btn-hi: #f782b3;
  --btn-lo: #ef5796;
  --on-btn: #0b2a33;
  --on-abyss: #d1ebfa;         /* footer primary text */
  --on-abyss-muted: #9cc4d4;   /* footer links */
  --on-abyss-dim: #7fa6b5;     /* footer fine print */
  --grid: rgba(11, 42, 51, 0.06);
  --tick: rgba(11, 42, 51, 0.45);   /* corner brackets: ornament only, carries no state */
  /* Corner registration brackets: eight background layers on a pseudo-element
     above the viewport. Pure decoration. */
  --ticks:
    linear-gradient(var(--tick), var(--tick)) 0 0/16px 2px,
    linear-gradient(var(--tick), var(--tick)) 0 0/2px 16px,
    linear-gradient(var(--tick), var(--tick)) 100% 0/16px 2px,
    linear-gradient(var(--tick), var(--tick)) 100% 0/2px 16px,
    linear-gradient(var(--tick), var(--tick)) 0 100%/16px 2px,
    linear-gradient(var(--tick), var(--tick)) 0 100%/2px 16px,
    linear-gradient(var(--tick), var(--tick)) 100% 100%/16px 2px,
    linear-gradient(var(--tick), var(--tick)) 100% 100%/2px 16px;
  /* Same brackets, inset 14px, for the hero frame corners. */
  --ticks-hero:
    linear-gradient(var(--tick), var(--tick)) left 14px top 14px/22px 2px,
    linear-gradient(var(--tick), var(--tick)) left 14px top 14px/2px 22px,
    linear-gradient(var(--tick), var(--tick)) right 14px top 14px/22px 2px,
    linear-gradient(var(--tick), var(--tick)) right 14px top 14px/2px 22px,
    linear-gradient(var(--tick), var(--tick)) left 14px bottom 14px/22px 2px,
    linear-gradient(var(--tick), var(--tick)) left 14px bottom 14px/2px 22px,
    linear-gradient(var(--tick), var(--tick)) right 14px bottom 14px/22px 2px,
    linear-gradient(var(--tick), var(--tick)) right 14px bottom 14px/2px 22px;
  --page: 1200px;
  --radius: 6px;
  /* Chamfered modules via asymmetric radii: outlines and overflow clipping
     stay intact — clip-path would clip focus rings, so it is used only on
     non-interactive elements (badges, the hero silhouette). */
  --facet: 4px 14px 4px 14px;
  --facet-s: 3px 10px 3px 10px;
  --facet-l: 6px 22px 6px 22px;
  --glow-card: 0 0 0 1px rgba(35, 159, 169, 0.4), 0 10px 30px rgba(11, 42, 51, 0.3),
    0 0 24px rgba(35, 159, 169, 0.15);
  --shadow-panel: 0 18px 44px rgba(11, 42, 51, 0.32);
  --font-display: "Orbitron", "Segoe UI", system-ui, sans-serif;
  --font-body: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  /* A light wash at the top settling onto the sky ground. It never drifts
     deeper than --bg, so every text token keeps its measured ratio to the
     bottom of the longest page. */
  background: linear-gradient(180deg, #eaf6fe 0, var(--bg) 420px);
  background-color: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: 0.02em;
  margin: 0;
}

h1 { font-size: clamp(1.7rem, 3.6vw, 2.4rem); }
h2 { font-size: clamp(1.3rem, 2.6vw, 1.75rem); }
h3 { font-size: 1.15rem; }

a { color: var(--link); }            /* 5.7:1 on --bg */
a:hover { color: var(--link-hover); } /* 7.6:1 */

img { max-width: 100%; display: block; }

::selection { background: var(--mustard); color: var(--ink); }   /* 7.9:1 */

[hidden] { display: none !important; }

.wrap {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 24px;
}

.mono {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--dim);              /* 5.5:1 on --bg */
}

/* A token that must never split across lines. Code-like fragments in product
   titles — set codes such as "(EB-02)", card numbers — are wrapped in
   <span class="nobr"> by the build; nowrap keeps the token whole and the
   text wraps around it. Measured: the longest token (~7 chars of Orbitron
   600) is ≈63px at card-title size and ≈137px at h1 size, so it fits the
   narrowest card body (159px at 320px) and the h1 measure (272px at 320px)
   with room to spare. Ordinary hyphenated words ("Super-Premium",
   "Ultra-Premium", "non-refundable") are NOT wrapped in .nobr and still
   break at the hyphen, as normal prose should. */
.nobr { white-space: nowrap; }

/* Category-page and About ledes set their colour and measure inline; this
   supplies the type so they are not body-default. */
.lede { font-size: 1.06rem; line-height: 1.6; margin: 16px 0 0; }

/* ---------- header ---------- */

.site-header {
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  /* Solid sky, deliberately not translucent: the deep-teal brand mark
     keeps >=4.5:1 over anything scrolling beneath. */
  background: #c3e1f5;
  z-index: 20;
}

/* The signal scanline across the top of every page. */
.site-header::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--signal), rgba(35, 159, 169, 0.3));
  pointer-events: none;
}

.site-header .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  min-height: 62px;
  flex-wrap: wrap;
}

/* ---------- brand (header + footer) ---------- */

/* Chrome slab emblem + typeset wordmark. At 40px the slab's own ATELIER
   band is illegible; the emblem is the mark, the wordmark is the name. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 6px 0;                 /* 52px tap target inside the 62px bar */
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: 0.04em;
  color: var(--ink);
  text-decoration: none;
}

/* width/height attributes pin the ratio — no shift on load.
   CSS owns the rendered height. Never lazy-loaded: above the fold. */
.brand-mark {
  display: block;
  height: 40px;
  width: auto;
  flex: 0 0 auto;
}

/* The wordmark must be ONE flex item: without this wrapper the flex gap
   splits "Atelier" and "151" into two anonymous items 12px apart. */
.brand-word { white-space: nowrap; }

/* Selector narrowed from ".brand span": 5.2:1 on the header ground. */
.brand-num { color: var(--signal-deep); }

/* Hover: teal glow on the slab (mark-only) and the numeral to link-hover.
   Instant, like the nav — no transition for reduced-motion to unwind. */
@media (hover: hover) {
  .brand:hover .brand-mark {
    filter: drop-shadow(0 0 6px rgba(35, 159, 169, 0.55));
  }
  .brand:hover .brand-num { color: var(--link-hover); }
}

/* Refines the global :focus-visible so the ring hugs the taller row. */
.brand:focus-visible {
  outline: 2px solid var(--signal-deep);
  outline-offset: 3px;
  border-radius: var(--facet-s);
}

/* Footer brand row: same cached emblem file at the same 40px on the dark
   band. Not a link — no hover/focus treatment. */
.footer-brand {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* Phone tap target: a 44px line box makes the brand link >=44px tall with
   zero visible change. The header bar is 56-62px — taller than 44px — and
   .wrap already centres its items, so the glyph sits exactly where it did;
   only the clickable box grows, inside the existing bar. Scoped to the
   header so the footer brand is untouched. */
.site-header .brand { line-height: 44px; }

.header-end {
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
}

/* Pure-CSS menu state for phones. Hidden on desktop; see the <=820px block. */
.nav-check, .nav-btn { display: none; }

.site-nav { display: flex; gap: 22px; flex-wrap: wrap; }

.site-nav a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);            /* 5.6:1 on the header */
  text-decoration: none;
  padding: 6px 0;
  border-bottom: 2px solid transparent;
}

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

/* Active mark: deep-teal text + teal bar, the reference's "active node". */
.site-nav a[aria-current="page"] {
  color: var(--signal-deep);    /* 5.2:1 on the header */
  border-bottom-color: var(--signal);
}

/* ---------- hero / masthead ---------- */

.hero {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--line);
  background:
    radial-gradient(1000px 460px at 10% 0%, rgba(35, 159, 169, 0.1), rgba(35, 159, 169, 0) 62%),
    var(--ticks-hero),
    linear-gradient(var(--grid) 1px, transparent 1px) 0 0/100% 40px,
    linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/40px 100%,
    repeating-linear-gradient(180deg, rgba(11, 42, 51, 0.03) 0 1px, rgba(11, 42, 51, 0) 1px 4px),
    #bfe2f7;
  background-repeat: no-repeat;
}
/* The topmost layer is a static CRT raster: 1px ink lines every 4px at 3%
   alpha. No motion, no repaint. At 5% (as first shipped) it cost the lede
   ~0.35:1 on top of the prism glow — measured 4.46:1 at 900px, a fail;
   at 3% the measured lede floor is 4.60:1 at 900px and 4.79:1 at 390px. */

/* Ambient rule, rev 6: ambient loops live ONLY in the hero (the owner asked
   for a livelier banner). Every loop is transform/opacity-only, >=3s per
   cycle, low amplitude, and may lower a text ratio but never below 4.5:1
   (worst cases recorded in the header comment), and everything stops under
   reduced-motion. Item effects elsewhere are user-triggered (hover/focus)
   or one-shot (entrance rise), never infinite.
   The scan line: transform-only, faint, clipped by the hero. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(35, 159, 169, 0), rgba(35, 159, 169, 0.14), rgba(35, 159, 169, 0)) 0 0/100% 3px no-repeat;
  transform: translateY(-100%);
  animation: hero-scan 9s linear infinite;
}

@keyframes hero-scan { to { transform: translateY(100%); } }

/* Area-chart silhouette at the base: a teal ridge clipped to peaks.
   Without clip-path support it simply does not render. */
.hero::after {
  content: "";
  display: none;
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 118px;
  pointer-events: none;
  opacity: 0.45;
  background: linear-gradient(180deg, rgba(35, 159, 169, 0.15), rgba(35, 159, 169, 0) 82%);
}

@supports (clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)) {
  .hero::after {
    display: block;
    clip-path: polygon(0 100%, 0 58%, 6% 58%, 9% 34%, 13% 34%, 16% 64%, 22% 64%,
      26% 18%, 31% 18%, 35% 52%, 41% 52%, 45% 10%, 50% 10%, 54% 44%, 60% 44%,
      64% 28%, 70% 28%, 74% 60%, 80% 60%, 84% 36%, 90% 36%, 94% 54%, 100% 54%,
      100% 100%);
  }
}

.hero .wrap { position: relative; z-index: 1; padding-block: 64px 52px; }

/* Eyebrow as a label chip: thin outline, spaced uppercase, status node. */
.hero .mono {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--line-hi);
  border-radius: 3px;
  padding: 6px 12px;
  background: rgba(228, 242, 252, 0.6);
  color: var(--dim);                /* 5.6:1 on the chip */
}

.hero .mono::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--signal);
  box-shadow: 0 0 6px rgba(35, 159, 169, 0.8);
  flex: 0 0 auto;
  /* "Open for business" LED: a slow breathe, stopped under reduced-motion. */
  animation: node-pulse 3.2s ease-in-out infinite;
}

@keyframes node-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.35); }
}

/* Title in the glowing rectangular frame, straight from the reference. */
.hero h1 {
  display: inline-block;
  /* overflow:hidden moves an inline-block's baseline to its bottom margin
     edge (the lede drops 6.8px); vertical-align:top keeps the line box. */
  vertical-align: top;
  /* Hosts the sheen ::after. overflow:hidden clips the sweep to the frame;
     the frame's own box-shadow glow is OUTSIDE the box, so it is untouched. */
  position: relative;
  overflow: hidden;
  font-weight: 700;
  font-size: clamp(2rem, 5vw, 3.4rem);
  line-height: 1.1;
  letter-spacing: 0.02em;
  /* em, not ch: ch resolves against whatever font is current, and Chromium
     does not recompute it on an ANIMATED element once Orbitron finishes
     loading — a late font left the width at the fallback resolution
     (550px not 680px) and the title jumped onto the eyebrow chip's line
     (measured broken 7 of 8 loads). Orbitron 700's ch is 0.8339em, so
     15ch = 12.51em; same rendered width (680.5px vs 680.4px), but
     font-independent, so the glitch animation is safe to keep. */
  max-width: 12.51em;
  margin-top: 22px;
  padding: 20px 24px;
  border: 1px solid rgba(35, 159, 169, 0.6);
  background: rgba(228, 242, 252, 0.5);
  box-shadow: 0 0 24px rgba(35, 159, 169, 0.16), inset 0 0 24px rgba(35, 159, 169, 0.05);
}

/* Chromatic-split glitch on the title: text-shadow only, so no text is
   duplicated (nothing for screen readers to read twice) and no markup is
   required. Two bursts per 8s cycle; each burst is two 90ms offsets =
   well under 3 flashes/sec and a tiny painted area (the h1 box). Pink and
   teal ghosts are decoration; the ink glyph itself never moves more than
   1px and never leaves its 12:1 ground. */
.hero h1 {
  animation: hero-glitch 8s steps(1) infinite;
}

@keyframes hero-glitch {
  0%, 100% { text-shadow: none; transform: translateX(0); }
  2%  { text-shadow: -3px 0 rgba(244, 100, 159, 0.85), 3px 0 rgba(35, 159, 169, 0.85); transform: translateX(1px); }
  3%  { text-shadow: 2px 0 rgba(244, 100, 159, 0.85), -2px 0 rgba(35, 159, 169, 0.85); transform: translateX(-1px); }
  4%, 46% { text-shadow: none; transform: translateX(0); }
  47% { text-shadow: -2px 0 rgba(244, 100, 159, 0.8), 2px 0 rgba(35, 159, 169, 0.8); transform: translateX(1px); }
  48% { text-shadow: none; transform: translateX(0); }
}

.hero p.lede {
  color: var(--muted);          /* 5.6:1 on the hero ground */
  max-width: 58ch;
  margin: 20px 0 0;
  font-size: 1.06rem;
  line-height: 1.6;
}

/* Title-frame sheen: one light pass every 8s; 60% of the cycle is a pause.
   Peaks at 0.24 white over the ink title — the .btn sweep precedent. */
.hero h1::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg, rgba(255, 255, 255, 0) 44%, rgba(255, 255, 255, 0.24) 50%, rgba(255, 255, 255, 0) 56%);
  transform: translateX(-102%);
  animation: hero-title-sheen 8s ease-in-out infinite;
}

@keyframes hero-title-sheen {
  0% { transform: translateX(-102%); }
  40% { transform: translateX(102%); }
  100% { transform: translateX(102%); }
}

/* Prism wash: a 560px conic blend of the four brights, blurred 56px,
   rotating once per 38s behind the slab column. z-index -1 keeps it behind
   the wrap's text (the wrap's z-index 1 makes a stacking context, so it
   cannot escape the hero). Pinned right, but at 900-1100px it reaches the
   lede and the eyebrow chip, and below 900px nothing reduces it — it sits
   behind all the hero text on phones. Measured floor >=4.5:1 at every
   width (figures in the header comment). */
.hero .wrap::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%;
  right: -40px;
  width: 560px;
  height: 560px;
  margin-top: -280px;
  pointer-events: none;
  background: conic-gradient(from 0deg,
    rgba(35, 159, 169, 0.16), rgba(244, 100, 159, 0.1),
    rgba(74, 229, 171, 0.12), rgba(225, 184, 8, 0.08),
    rgba(35, 159, 169, 0.16));
  filter: blur(56px);
  animation: hero-prism 38s linear infinite;
}

@keyframes hero-prism {
  to { transform: rotate(1turn); }
}

/* Cyberspace floor: a wireframe plane in perspective scrolling toward the
   viewer. Sits at the hero's base, z-index -1 with the prism (the wrap's
   stacking context keeps both behind the text), and is masked to fade out
   well before the lede's baseline so it never sits behind body text —
   only the h1 frame's lower edge and empty ground overlap it, and the
   h1's own background chip keeps its ratio. The scroll animates
   background-position: per-frame repaint is the masked strip, roughly
   hero-width x 110px. 4s linear loop; stopped under reduced-motion. */
.hero .wrap::after {
  content: "";
  position: absolute;
  z-index: -1;
  left: -24px;
  right: -24px;
  bottom: 0;
  height: 150px;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, rgba(35, 159, 169, 0.5) 0 1px, rgba(35, 159, 169, 0) 1px 56px),
    repeating-linear-gradient(180deg, rgba(35, 159, 169, 0.5) 0 1px, rgba(35, 159, 169, 0) 1px 28px);
  transform-origin: 50% 100%;
  transform: perspective(340px) rotateX(58deg);
  animation: hero-floor 4s linear infinite;
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 55%);
  mask-image: linear-gradient(180deg, transparent 0, #000 55%);
  opacity: 0.55;
}

@keyframes hero-floor {
  to { background-position: 0 0, 0 28px; }
}

/* Circuit traces: inline SVG layer on the hero's right half, behind the
   slab (z-index -1, inside the wrap's stacking context). Paths draw
   themselves on a 7s loop: draw 0-55%, hold to 100%, then snap back to
   blank at the loop point. No fade: a keyframed opacity would override
   the per-path opacities 0.5/0.4/0.35 set by nth-child. The stroke is
   teal at <=0.5 opacity over empty ground; the layer is masked to fade
   before it reaches the lede's left edge, so no text ever sits on it.
   stroke-dashoffset repaints the SVG box (~540x300, right side) each
   frame while drawing. Static fully-drawn at 0.28 opacity under
   reduced-motion. Decorative: aria-hidden in markup. */
.hero-circuit {
  position: absolute;
  z-index: -1;
  top: 0;
  right: 0;
  height: 100%;
  width: min(46%, 540px);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 45%);
  mask-image: linear-gradient(90deg, transparent 0, #000 45%);
}

.hero-circuit path {
  fill: none;
  stroke: var(--signal);
  stroke-width: 1.5;
  opacity: 0.5;
  stroke-dasharray: 620;
  stroke-dashoffset: 620;
  animation: hero-trace 7s linear infinite;
}

.hero-circuit path:nth-child(2) { animation-delay: 0.6s; opacity: 0.4; }
.hero-circuit path:nth-child(3) { animation-delay: 1.2s; opacity: 0.35; }

.hero-circuit circle {
  fill: var(--signal);
  opacity: 0.5;
  animation: hero-node 7s ease-in-out infinite;
}

@keyframes hero-trace {
  0% { stroke-dashoffset: 620; }
  55%, 85% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

@keyframes hero-node {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 0.9; }
}

/* Below the showcase threshold the right half is crowded; the circuit
   hides with the slab. The floor, glitch and scanlines remain. */
@media (max-width: 899px) {
  .hero-circuit { display: none; }
}

/* ---------- hero showcase (desktop only, >=900px) ----------

   The Chrome Slab registered on the right of the masthead. Absolutely
   positioned inside .hero .wrap (already position:relative, z-index 1),
   so nothing reflows and the ambient scan line (z-index 2) still crosses
   it. Decorative: alt="", pointer-events off, no hover/focus states. The
   levitation, roll and bracket pulse below are ambient loops under the rev-6
   hero rule (transform/opacity-only, >=3s cycles, stopped under
   reduced-motion). At <=899px the <source media> prevents the download
   entirely; display:none is the belt-and-braces for the 1x1 fallback. */
.hero-showcase {
  position: absolute;
  right: 24px;                    /* flush with the content's right edge */
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}

.hero-showcase img {
  display: block;
  height: clamp(180px, 22vw, 240px);
  width: auto;
  /* Floor shadow follows the alpha silhouette — the same idea as the
     radial floor shadow in the card wells, one cheap filter. Static: a
     drop-shadow loop would repaint the slab every frame for no real gain. */
  filter: drop-shadow(0 20px 24px rgba(11, 42, 51, 0.28));
  /* Levitation: +-6px float and +-0.5deg roll on a 7s alternate cycle, in
     phase with the bracket pulse so slab and registration read as one
     instrument. Transform-only; stopped under reduced-motion. */
  animation: slab-float 7s ease-in-out infinite alternate;
}

@keyframes slab-float {
  from { transform: translateY(-6px) rotate(-0.5deg); }
  to { transform: translateY(6px) rotate(0.5deg); }
}

/* Specimen brackets floating 14px off the slab — the existing --ticks
   ornament token, constant opacity, carrying no state. */
.hero-showcase::before {
  content: "";
  position: absolute;
  inset: -14px;
  background: var(--ticks);
  background-repeat: no-repeat;
  opacity: 0.5;
  /* Registration pulse, same 7s period as the slab float. Opacity-only. */
  animation: slab-ticks 7s ease-in-out infinite alternate;
}

@keyframes slab-ticks {
  from { opacity: 0.3; }
  to { opacity: 0.75; }
}

/* Targeting reticle: a dashed instrument ring rotating slowly around the
   slab, broken by two gaps so it reads as a scope, not a circle.
   Conic-gradient ring via mask; transform-only 14s rotation. */
.hero-showcase::after {
  content: "";
  position: absolute;
  inset: -30px;
  pointer-events: none;
  background: conic-gradient(from 0deg,
    rgba(35, 159, 169, 0.55) 0deg 70deg, rgba(35, 159, 169, 0) 70deg 100deg,
    rgba(35, 159, 169, 0.55) 100deg 190deg, rgba(35, 159, 169, 0) 190deg 220deg,
    rgba(35, 159, 169, 0.55) 220deg 290deg, rgba(35, 159, 169, 0) 290deg 320deg,
    rgba(35, 159, 169, 0.55) 320deg 360deg);
  -webkit-mask-image: radial-gradient(circle, transparent 62%, #000 63%, #000 65%, transparent 66%);
  mask-image: radial-gradient(circle, transparent 62%, #000 63%, #000 65%, transparent 66%);
  animation: reticle-spin 14s linear infinite;
}

@keyframes reticle-spin {
  to { transform: rotate(1turn); }
}

/* The showcase has its own threshold, wider than the hero's 820px
   reflow: measured in-browser, the gap between hero text and the
   bracket ornament is negative at 821px and only reads as clear from
   ~900px up. Pairs with <source media="(min-width: 900px)"> in the
   masthead markup, so sub-900px viewports never download the slab. */
@media (max-width: 899px) {
  .hero-showcase { display: none; }
}

/* ---------- catalogue tiles (homepage index) ---------- */

.cats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 18px;
  margin: 30px auto 0;
  padding: 0 24px 46px;
  max-width: var(--page);
}

.cat {
  position: relative;
  display: block;
  padding: 22px 22px 20px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--facet);
  overflow: hidden;               /* clips the sweep to the chamfer */
  text-decoration: none;
  color: inherit;
  transition: transform 160ms ease-out, border-color 160ms ease-out,
    box-shadow 160ms ease-out;
}

/* Tilt vars --rx/--ry are set by foil.js from the pointer, exactly as on
   .card; without the script they are 0deg and the hover is a 6px lift with
   the static foil at its fallback position. */
.cat:hover, .cat:focus-visible {
  transform: translateY(-6px) perspective(800px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  border-color: var(--signal);
  box-shadow:
    0 0 0 1px rgba(35, 159, 169, 0.85),
    0 14px 34px rgba(11, 42, 51, 0.35),
    0 0 34px rgba(35, 159, 169, 0.5),
    0 0 60px rgba(244, 100, 159, 0.35);
}

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

/* The same light-pass the card wells have, across the whole tile.
   ::before is already the corner brackets; the sweep takes ::after. */
/* Rev 7 foil on the tiles, same as the cards. The rainbow band takes
   ::after (it supersedes the rev-6 sweep — one light effect, stronger);
   the highlight + glitter live in the .cat-foil span (aria-hidden) because
   ::before is the corner brackets. Both layers sit at z-index 3, clipped
   to the chamfer by the tile's overflow:hidden; the tile's words are
   raised to z-index 4 below so they composite ABOVE the foil, the same
   fix .card-body carries (under the foil the card readout measured
   3.37:1). */
.cat::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(115deg,
    rgba(35, 159, 169, 0) 20%,
    rgba(35, 159, 169, 0.55) 32%,
    rgba(74, 229, 171, 0.5) 41%,
    rgba(225, 184, 8, 0.5) 50%,
    rgba(244, 100, 159, 0.55) 59%,
    rgba(35, 159, 169, 0) 72%);
  background-size: 300% 300%;
  background-position: calc(100% - var(--mx, 50%)) 50%;
  mix-blend-mode: overlay;
  opacity: 0;
  transition: opacity 180ms ease-out;
}

.cat-foil {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    radial-gradient(240px 240px at var(--mx, 50%) var(--my, 26%),
      rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 62%),
    radial-gradient(rgba(255, 255, 255, 0.85) 1px, rgba(255, 255, 255, 0) 1.6px)
      calc(var(--mx, 50%) * 0.08) calc(var(--my, 26%) * 0.08) / 24px 24px,
    radial-gradient(rgba(255, 255, 255, 0.6) 1px, rgba(255, 255, 255, 0) 1.6px)
      calc(11px + var(--mx, 50%) * -0.06) calc(13px + var(--my, 26%) * -0.06) / 34px 34px;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 180ms ease-out;
}

.cat:hover::after, .cat:focus-visible::after,
.cat:hover .cat-foil, .cat:focus-visible .cat-foil {
  opacity: 1;
}

/* Words above the foil (z-index 4 vs 3), so hover ratios are invariant.
   The corner brackets (::before, z auto) stay under the foil, as on the
   card wells. */
.cat > .mono, .cat > h3, .cat > p {
  position: relative;
  z-index: 4;
}

.cat h3 { margin: 12px 0 8px; transition: color 140ms ease-out; }
.cat p { margin: 0 0 14px; color: var(--muted); font-size: 0.92rem; }

/* Index number as an outlined chip. */
.cat > .mono:first-child {
  display: inline-block;
  border: 1px solid var(--line-hi);
  border-radius: 3px;
  padding: 2px 8px;
  color: var(--dim);
}

/* "Browse" readout strip: ruled division, strong-ink label. */
.cat .mono:last-child {
  display: block;
  border-top: 1px solid var(--line);
  padding-top: 10px;
  color: var(--ink-2);
}

@media (hover: hover) {
  .cat:hover h3 { color: var(--signal-deep); }   /* 6.2:1 on the panel */
}

/* ---------- catalogue grid ---------- */

.section { padding-block: 56px; }

/* Category and About page titles get a ruled instrument header. */
.section > h1 {
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}

.section > h1::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 12px;
  background: var(--signal);
  transform: rotate(45deg);
  box-shadow: 0 0 8px rgba(35, 159, 169, 0.55);
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 26px;
  flex-wrap: wrap;
}

/* Diamond node before major section titles. Decorative only. */
.section-head h2::before,
.gameband-head h2::before,
.faq h2::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: 12px;
  background: var(--signal);
  transform: rotate(45deg);
  box-shadow: 0 0 7px rgba(35, 159, 169, 0.5);
}

/* Tick scale stretched between the title and the count. Ornament only. */
.section-head::after, .gameband-head::after {
  content: "";
  flex: 1 1 48px;
  align-self: flex-end;
  height: 6px;
  margin: 0 2px 5px;
  background: repeating-linear-gradient(90deg, rgba(55, 97, 109, 0.4) 0 1px, transparent 1px 9px);
  border-bottom: 1px solid var(--line);
}

.gameband-head::after { border-bottom: 0; margin-bottom: 3px; }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 26px;
}

/* ---------- product card ---------- */

.card {
  position: relative;             /* anchors the foil wash + highlight layers */
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--facet);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  transition: transform 160ms ease-out, border-color 160ms ease-out,
    box-shadow 160ms ease-out;
}

/* Tilt vars --rx/--ry are set by foil.js from the pointer; without the
   script they are 0deg and the hover is exactly the old 2px lift. */
.card:hover, .card:focus-visible {
  transform: translateY(-6px) perspective(800px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  border-color: var(--signal);
  box-shadow:
    0 0 0 1px rgba(35, 159, 169, 0.85),
    0 14px 34px rgba(11, 42, 51, 0.35),
    0 0 34px rgba(35, 159, 169, 0.5),
    0 0 60px rgba(244, 100, 159, 0.35);
}

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

/* ---------- hover foil (rev 6) ----------

   Two layers at z-index 3 above the whole card, clipped to the chamfer by
   the card's overflow:hidden, both pointer-events:none. The badge (z-index
   2 inside .card-media) sits under them, which is fine: ink on mustard at
   7.9:1 loses nothing under a 9% tint. The readout text does NOT sit under
   them: .card-body carries position:relative; z-index:4 so the words
   composite above the foil while the panel behind them still lights
   (without it, measured ratios fell to 3.37:1/3.63:1 on hover; with it,
   5.46:1/6.10:1 — numbers in the header comment). Sold-out cards get
   neither layer. Without foil.js the highlight sits at a fixed top-centre
   sheen (the var fallbacks) and the tilt is 0deg — the effect is complete
   with JavaScript off; the script only makes the light follow the hand. */

/* Light source + glitter: a 0.55 white highlight tracking the pointer,
   screen-blended, over two offset pin-prick lattices that drift a few
   pixels with the hand -- the "starlight" speckle of real foil. Screen
   blend only ever lightens; the card-body text sits at z-index 4 ABOVE
   this layer, so readout ratios are unchanged on hover (re-measure). */
.card:not(.card--out)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    radial-gradient(240px 240px at var(--mx, 50%) var(--my, 26%),
      rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 62%),
    radial-gradient(rgba(255, 255, 255, 0.85) 1px, rgba(255, 255, 255, 0) 1.6px)
      calc(var(--mx, 50%) * 0.08) calc(var(--my, 26%) * 0.08) / 24px 24px,
    radial-gradient(rgba(255, 255, 255, 0.6) 1px, rgba(255, 255, 255, 0) 1.6px)
      calc(11px + var(--mx, 50%) * -0.06) calc(13px + var(--my, 26%) * -0.06) / 34px 34px;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 180ms ease-out;
}

/* The holo band: a diagonal rainbow of the four brights at ~50-55% alpha,
   overlay-blended across the whole card. The band is 300% wide and its
   position tracks --mx, so tilting the pointer sweeps the spectrum the
   way tilting a real holo card does. Overlay on the pale panel and over
   the photograph is what makes it READ as foil rather than a tint. Under
   the words by design (z-index 3 vs .card-body's 4). */
.card:not(.card--out)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(115deg,
    rgba(35, 159, 169, 0) 20%,
    rgba(35, 159, 169, 0.55) 32%,
    rgba(74, 229, 171, 0.5) 41%,
    rgba(225, 184, 8, 0.5) 50%,
    rgba(244, 100, 159, 0.55) 59%,
    rgba(35, 159, 169, 0) 72%);
  background-size: 300% 300%;
  background-position: calc(100% - var(--mx, 50%)) 50%;
  mix-blend-mode: overlay;
  opacity: 0;
  transition: opacity 180ms ease-out;
}

.card:not(.card--out):hover::before, .card:not(.card--out):focus-visible::before,
.card:not(.card--out):hover::after, .card:not(.card--out):focus-visible::after {
  opacity: 1;
}

/* Cut-out lean-in: the object itself answers the hover — a 1.04 rise plus a
   soft teal halo that follows the photograph's alpha silhouette. */
.card-media img { transition: transform 180ms ease-out, filter 180ms ease-out; }

/* Cut-out pop: 1.10 rise, richer colour, and a stronger teal halo that
   follows the alpha silhouette. The filter repaint is confined to the
   hovered card's media area. */
@media (hover: hover) {
  .card:not(.card--out):hover .card-media img,
  .card:not(.card--out):focus-visible .card-media img {
    transform: scale(1.1);
    filter: saturate(1.35) brightness(1.05)
      drop-shadow(0 10px 20px rgba(35, 159, 169, 0.55));
  }
}

/* The In-Stock node brightens like an instrument LED answering the hover.
   Graphic only; the word beside it is unchanged. */
@media (hover: hover) {
  .card:not(.card--out):hover .stock::before,
  .card:not(.card--out):focus-visible .stock::before {
    transform: scale(1.35);
  }
}

/* Entrance rise: grids and tiles float up 18px and fade in as they enter the
   viewport. Pure-CSS scroll-driven animation, gated twice — @supports for
   animation-timeline, no-preference for motion — so an unsupported browser
   or a reduced-motion user simply sees the cards, and no card can be left
   at opacity 0. Rails are excluded (the auto-advance would retrigger the
   rise every 4s). Animating the individual `translate` property and
   `opacity` — never `transform` — so the rise can never fight the hover
   transform/tilt. Range entry 0% entry 40%: the rise finishes early in the
   entry phase; a row that can only partially enter (the last row of a page
   too short to scroll) parks mid-animation, so the opacity ramp is front-
   loaded (fully opaque by 60%) and such a row renders at >=55% opacity.
   A <details class="setblock"> opened after scrolling re-evaluates the
   timeline against the new layout: cards deep inside the viewport jump
   straight to the finished state, cards near the fold rise once. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .grid > .card, .cats > .cat {
      animation: card-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 40%;
    }
  }
}

@keyframes card-rise {
  0% { opacity: 0; translate: 0 18px; }
  60% { opacity: 1; }
  100% { opacity: 1; translate: 0 0; }
}

/* The viewport: transparent cut-outs float over a deeper sky well with a
   soft floor shadow under the product and a top light — the deeper mid-blue
   flatters dark packaging and white packaging alike. (.card-media--photo is
   intentionally neutral — cut-out is the default; .is-plate below is the
   white-sweep opt-out.) */
.card-media {
  position: relative;
  aspect-ratio: 1 / 1;
  background:
    radial-gradient(58% 42% at 50% 87%, rgba(11, 42, 51, 0.14), rgba(11, 42, 51, 0) 72%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 42%),
    linear-gradient(rgba(11, 42, 51, 0.05) 1px, transparent 1px) 0 0/100% 26px,
    linear-gradient(90deg, rgba(11, 42, 51, 0.05) 1px, transparent 1px) 0 0/26px 100%,
    var(--well);
}

.card-media img { width: 100%; height: 100%; object-fit: contain; }

/* Corner registration brackets, drawn ABOVE the plate. Ornament only. */
.card-media::before, .detail-media::before, .cat::before {
  content: "";
  position: absolute;
  z-index: 1;
  pointer-events: none;
  background: var(--ticks);
  background-repeat: no-repeat;
  opacity: 0.55;
  transition: opacity 160ms ease-out;
}

.card-media::before, .cat::before { inset: 8px; }
.detail-media::before { inset: 12px; }

.card:hover .card-media::before, .card:focus-visible .card-media::before,
.cat:hover::before, .cat:focus-visible::before { opacity: 1; }

/* Scan sweep on hover: user-triggered, transform-only, removed under
   reduced-motion. Sold-out units are dormant — no sweep. */
.card:not(.card--out) .card-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(105deg, rgba(255, 255, 255, 0) 42%, rgba(255, 255, 255, 0.35) 50%, rgba(255, 255, 255, 0) 58%);
  transform: translateX(-102%);
}

@media (hover: hover) {
  .card:not(.card--out):hover .card-media::after,
  .card:not(.card--out):focus-visible .card-media::after {
    transform: translateX(102%);
    transition: transform 650ms ease-out;
  }
}

/* Sold out: the cut-out recedes and a deactivation hatch overlays the well.
   Decorative — the "Sold out" stamp carries the state. */
.card--out .card-media img {
  filter: grayscale(0.7);
  opacity: 0.5;
  transition: filter 160ms ease-out, opacity 160ms ease-out;
}

.card--out:hover .card-media img { filter: grayscale(0.45); opacity: 0.65; }

.card--out .card-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: repeating-linear-gradient(45deg, rgba(11, 42, 51, 0.05) 0 1px, transparent 1px 10px);
}

/* The white-sweep opt-out: a lit specimen plate inside the frame. The
   brackets darken via --tick so they stay visible on white. */
.card-media.is-plate, .detail-media.is-plate {
  background: #fff;
  box-shadow: inset 0 0 0 1px rgba(11, 42, 51, 0.22), inset 0 0 30px rgba(35, 159, 169, 0.1);
  --tick: rgba(11, 42, 51, 0.6);
}

/* Placeholder for products with no photograph. Typographic, on grid. */
.ph {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: 12px; padding: 22px; text-align: center;
  background:
    linear-gradient(var(--grid) 1px, transparent 1px) 0 0 / 100% 32px,
    linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0 / 32px 100%,
    var(--well);
}

/* No bright reaches 4.5:1 on the well, so the tiny format label is ink. */
.ph-fmt {
  font-family: var(--font-mono);
  font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 500;
  color: var(--ink);            /* 10.4:1 on the well */
}

.ph-title {
  font-family: var(--font-display); font-weight: 600;
  font-size: 1rem; line-height: 1.3; color: var(--ink-2);   /* 7.6:1 on the well */
  max-width: 16ch;
}

/* Language chips: EN a dark frosted tag, JP a filled mustard tag. The
   text is the signal; the fill only reinforces it. */
.badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: 3px;   /* fallback shape when clip-path is unsupported */
}

@supports (clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)) {
  .badge {
    border-radius: 0;
    clip-path: polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%);
  }
}

.badge--en {
  background: rgba(11, 42, 51, 0.88);
  border: 1px solid rgba(240, 248, 254, 0.45);
  color: var(--panel-hi);        /* >=9.8:1 over the well */
}
.badge--jp { background: var(--mustard); color: var(--ink); }  /* 7.9:1 */

/* Panel seam between the viewport and the readout body. */
.card-body {
  position: relative;
  z-index: 4;                     /* text stays above the foil layers */
  padding: 14px 16px 16px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

.card-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.01em;
  line-height: 1.3;
  color: var(--ink);
}

.card-spec {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--dim);            /* 6.0:1 on the panel */
}

/* Price/stock readout strip: ruled division above the instruments. */
.card-foot {
  margin-top: auto;
  border-top: 1px solid var(--line);
  padding-top: 9px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

/* Prices are ink readouts: mono, tabular — no bright on the palette is
   readable as a figure on these surfaces. */
.price {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.98rem;
  letter-spacing: 0.02em;
  color: var(--ink);             /* 13.2:1 on the panel */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.stock {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* In-Stock marker: a glowing mint node with a thin ink ring so it reads on
   the pale panel. Graphic only (1.3:1); the word beside it carries the state. */
.stock::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--mint);
  box-shadow: 0 0 0 1px rgba(11, 42, 51, 0.35), 0 0 6px rgba(74, 229, 171, 0.9),
    0 0 2px rgba(74, 229, 171, 0.9);
  flex: 0 0 auto;
  transition: transform 140ms ease-out;
}

/* Sold out: the node is gone and the words become an outlined stamp. */
.stock--out {
  color: var(--muted);
  padding: 3px 8px;
  border: 1px solid var(--line-hi);
  border-radius: 2px 8px 2px 8px;
  align-self: flex-start;
}

.stock--out::before { display: none; }

/* Both stock states the same height, so the price above stays level across
   a grid row whether the neighbouring card is in stock or sold out. The
   Sold-out stamp is a 15px line + 3px padding + 1px border, top and bottom,
   = 23px; the plain In-Stock line is pinned to the same 23px (fixed
   line-height + min-height, content centred by the existing align-items).
   Scoped to the card footer — the detail page's price-row is untouched. */
.card-foot .stock {
  line-height: 15px;
  min-height: 23px;
}

/* Legacy quantity tint from the old build; kept harmlessly. Dark mustard,
   6.0:1 on the panel — the only mustard dark enough to be text. */
.stock--low { color: #6e5900; }

/* ---------- product detail ---------- */

.detail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 44px;
  padding-block: 48px;
  align-items: start;
}

.gallery { display: block; }

/* The main viewport: positive border, chamfer, brackets, grid, floor shadow. */
.detail-media {
  position: relative;
  background:
    radial-gradient(60% 44% at 50% 88%, rgba(11, 42, 51, 0.16), rgba(11, 42, 51, 0) 72%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 40%),
    linear-gradient(rgba(11, 42, 51, 0.06) 1px, transparent 1px) 0 0/100% 34px,
    linear-gradient(90deg, rgba(11, 42, 51, 0.06) 1px, transparent 1px) 0 0/34px 100%,
    var(--well);
  border-radius: var(--facet-l);
  border: 1px solid var(--line-hi);
  aspect-ratio: 1 / 1;
  overflow: hidden;
}

/* .detail-media--photo is neutral (cut-out default); .is-plate opts back to
   the white plate, styled with .card-media.is-plate above. */
.detail-media img { width: 100%; height: 100%; object-fit: contain; }
.detail-media .ph-title { font-size: 1.3rem; max-width: 20ch; }

/* ---------- rev 8: the inspection rig ----------

   Hover drama lives on the FRAME — tilt, neon rim, prismatic edge-light,
   backlight behind the cut-out — never over the photograph: a buyer reads
   grade, set and card number off a slab label at this size, so nothing
   composites above the img. Sold-out pages (main.detail--out) are dormant:
   the photo keeps full fidelity, the effects simply do not arm. Tilt vars
   --mx/--my/--rx/--ry come from foil.js (+-3deg here, not the cards' +-8);
   with JS off the fallbacks give a plain 4px lift and a centred backlight. */
.detail-media {
  transition: transform 200ms ease-out, border-color 160ms ease-out,
    box-shadow 200ms ease-out, background 200ms ease-out;
}

/* Hover only: :focus-within never matches here — .detail-media contains
   no focusable element (the thumbnails are siblings, inside .gallery). */
.detail:not(.detail--out) .detail-media:hover {
  transform: translateY(-4px) perspective(1000px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  border-color: var(--signal);
  box-shadow:
    0 0 0 1px rgba(35, 159, 169, 0.85),
    0 18px 40px rgba(11, 42, 51, 0.35),
    0 0 36px rgba(35, 159, 169, 0.5),
    0 0 64px rgba(244, 100, 159, 0.3);
  /* Backlight: first layer, BEHIND the transparent cut-out — it blooms
     around the silhouette and can never wash out a label. The rest of the
     stack restates the well unchanged (drift risk noted for review). */
  background:
    radial-gradient(280px 280px at var(--mx, 50%) var(--my, 42%), rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 62%),
    radial-gradient(60% 44% at 50% 88%, rgba(11, 42, 51, 0.16), rgba(11, 42, 51, 0) 72%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 40%),
    linear-gradient(rgba(11, 42, 51, 0.06) 1px, transparent 1px) 0 0/100% 34px,
    linear-gradient(90deg, rgba(11, 42, 51, 0.06) 1px, transparent 1px) 0 0/34px 100%,
    var(--well);
}

/* Prismatic edge-light: a 2px conic ring of the four brights on the frame's
   edge, mask-composited to a ring so 0% of it covers the photo. Static (no
   rotation), hover-only fade. Gated on mask-composite support — without the
   mask the conic would cover the photo, so the ring simply never appears. */
.detail-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(from 210deg,
    rgba(35, 159, 169, 0.9), rgba(74, 229, 171, 0.75),
    rgba(225, 184, 8, 0.7), rgba(244, 100, 159, 0.85),
    rgba(35, 159, 169, 0.9));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 200ms ease-out;
}

@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
  .detail:not(.detail--out) .detail-media:hover::after { opacity: 1; }
}

/* Cut-out pop: a restrained 1.03 rise (the cards get 1.10; this viewport is
   ~555px in a 24px gutter, and 1.03 grows ~8px per side — no overflow) with
   a soft teal halo on the alpha silhouette. */
.detail-media img {
  transition: transform 200ms ease-out, filter 200ms ease-out, opacity 180ms ease-out;
}

@media (hover: hover) {
  .detail:not(.detail--out) .detail-media:hover img {
    transform: scale(1.03);
    filter: saturate(1.12) drop-shadow(0 12px 22px rgba(35, 159, 169, 0.45));
  }
}

/* Gallery swap: gallery.js adds .is-swapping, swaps src on transitionend,
   removes it — a 180ms fade/scale between photographs. JS-off and
   reduced-motion never see the class. */
.detail-media img.is-swapping { opacity: 0; transform: scale(0.985); }

/* Sold out: dormant frame, full-fidelity photo — the words "Sold out" in
   the price row carry the state; the picture stays inspectable. */
.detail--out .detail-media::before { opacity: 0.35; }

/* ---------- gallery ----------

   No-JS fallback is unchanged: thumbnails are plain links that open the
   photograph; gallery.js upgrades them to in-place swapping. */
.thumbs {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin-top: 10px;
}

.thumb {
  width: 72px; height: 72px;
  border-radius: var(--facet-s);
  overflow: hidden;
  border: 1px solid var(--line-hi);
  background: var(--well);
  flex: 0 0 auto;
  opacity: 0.55;
  transition: opacity 140ms, border-color 140ms, box-shadow 140ms, transform 140ms ease-out;
}

.thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }

.thumb.is-plate { background: #fff; }

/* Selected thumbnail: teal rim plus full opacity — two channels. */
.thumb.is-on {
  opacity: 1;
  border-color: var(--signal);
  box-shadow: 0 0 0 1px var(--signal), 0 0 10px rgba(35, 159, 169, 0.35);
}

@media (hover: hover) {
  .thumb:hover { opacity: 1; }
}

/* The focused thumb comes fully opaque so the ring stays >=3:1. */
.thumb:focus-visible {
  outline: 2px solid var(--signal-deep);
  outline-offset: 2px;
  opacity: 1;
}

/* Rev 8: thumbnails answer the hover like miniature viewports — a 1.06
   rise (a transform scales the thumb box itself, so it is not clipped;
   it grows ~2px into the 10px gap, which is fine), teal rim and glow. The
   selected thumb's rim+opacity state is unchanged; links and the no-JS
   path are untouched. */
@media (hover: hover) {
  .thumb:hover {
    transform: scale(1.06);
    border-color: var(--signal);
    box-shadow: 0 0 0 1px var(--signal), 0 0 12px rgba(35, 159, 169, 0.45);
  }
}

/* Spec list as a readout panel: one hairline per row, indexed labels. */
.detail dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0 22px;
  margin: 24px 0;
  padding: 4px 16px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--facet-s);
}

.detail dl dt, .detail dl dd {
  padding: 9px 0;
  border-top: 1px solid var(--line);
}

.detail dl dt:first-of-type, .detail dl dd:first-of-type { border-top: 0; }

.detail dt {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--dim);            /* 6.0:1 on the panel */
}

.detail dt::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 9px;
  background: var(--signal);
  transform: rotate(45deg);
}

.detail dd { margin: 0; color: var(--ink); }

.detail ul { padding-left: 18px; color: var(--ink); }
.detail ul li { margin-bottom: 5px; }

/* ---------- Seller's note (product page only) ---------- */
/* The owner's free-text note about this specific unit: condition, provenance,
   handling. Follows the spec list and its price-note footnote (they stay glued
   together) and precedes "About the set" and the enquiry buttons. Same panel
   chrome as the spec <dl>, but a heading plus paragraphs inside and NO hairline
   dividers — hairlines are the spec row's signature, and this must not read as
   one. Rendered only when the product has a note; there is no empty state. */
.product-note {
  margin: 24px 0;                 /* matches .detail dl; adjacent margins collapse to 24px */
  padding: 13px 16px;             /* 13px = the dl's 4px pad + 9px first-row pad, so the
                                     label sits exactly where a first spec row would */
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--facet-s);
}

/* The heading wears the spec-label uniform (mono, uppercase, dim, teal diamond)
   so the panel reads as one long entry in the readout family. It is an h3 in the
   outline, parallel to "About the set". */
.product-note-title {
  margin: 0 0 9px;                /* 9px echoes the spec rows' vertical padding */
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--dim);              /* 6.0:1 on the panel */
}

.product-note-title::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 9px;              /* identical to dt::before — the diamond column runs
                                     unbroken down the spec list and this heading */
  background: var(--signal);
  transform: rotate(45deg);
}

.product-note p {
  margin: 0;
  color: var(--ink);              /* 13.2:1 on the panel */
  font-size: 0.95rem;             /* prose: above the 0.9rem footnotes, below spec values */
  line-height: 1.55;
  /* anywhere, not break-word: only anywhere counts toward min-content, so a
     long unbroken SKU or URL wraps instead of widening the grid column and
     scrolling the page sideways. */
  overflow-wrap: anywhere;
}

.product-note p + p { margin-top: 10px; }   /* gap only; never a hairline */

.price-row {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin: 6px 0 0;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}

/* The dominant readout on the page. IBM Plex Mono 500 — never Orbitron:
   Orbitron's zero is slashed, and a buyer must not misread the figure.
   Ink, 12.2:1 on --bg, and at 2.4rem it is large text besides. */
.price-lg {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 2.4rem;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  white-space: nowrap;
}

/* Case and box priced at once: both figures named, hairline between rows. */
.price-stack { display: flex; flex-direction: column; gap: 8px; }
.price-stack .price-unit + .price-unit {
  border-top: 1px solid var(--line);
  padding-top: 8px;
}
.price-unit { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.price-stack .price-unit .price-lg { font-size: 1.5rem; white-space: normal; }

.per, .price-from {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}

.price-from { margin-right: 2px; }

.chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 18px 0 0; }
.chip {
  border: 1px solid var(--line-hi);
  border-radius: 3px;
  padding: 5px 11px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}

/* Primary action: the pink console key. Ink label 5.15:1 on the fill. */
.btn {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  background: var(--btn);
  color: var(--on-btn);
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.88rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 13px 22px;
  border: 1px solid transparent;
  vertical-align: middle;
  border-radius: var(--facet-s);
  min-height: 48px;
  line-height: 22px;
  margin-top: 22px;
  box-shadow: 0 0 18px rgba(244, 100, 159, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.4);
  transition: background 140ms ease-out, border-color 140ms ease-out,
    color 140ms ease-out, transform 140ms ease-out, box-shadow 140ms ease-out;
}

.ico { flex: 0 0 auto; display: block; overflow: visible; }

/* Scan sweep, user-triggered only: a light sheen across the pink fill. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg, rgba(255, 255, 255, 0) 42%, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0) 58%);
  transform: translateX(-102%);
}

@media (hover: hover) {
  .btn:hover::after, .btn:focus-visible::after {
    transform: translateX(102%);
    transition: transform 550ms ease-out;
  }
}

.btn:hover { background: var(--btn-hi); transform: translateY(-1px); }  /* ink label 6.3:1 */
.btn:active { background: var(--btn-lo); transform: translateY(0); }    /* 4.7:1 */

/* Focus ring clears the saturated fill: offset puts it on the page, with a
   ground-coloured gap ring so it never touches teal-on-pink. */
.btn:focus-visible {
  outline: 2px solid var(--signal-deep);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px var(--bg), 0 0 18px rgba(244, 100, 159, 0.35);
}

.btn--ghost {
  background: transparent;
  border-color: var(--line-hi);    /* 4.0:1 control border */
  color: var(--ink);               /* 12.2:1 */
  box-shadow: none;
}

.btn--ghost:hover {
  background: rgba(35, 159, 169, 0.08);
  border-color: var(--signal);
  color: var(--signal-deep);       /* 5.4:1 on the tint */
}
.btn--ghost:active { background: rgba(35, 159, 169, 0.14); }

/* The button group owns the vertical spacing, not the buttons: wrapped
   buttons then sit exactly `gap` apart instead of gap + margin. Standalone
   .btn keeps its own 22px margin-top. */
.ctas { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.ctas .btn { flex: 0 1 auto; margin-top: 0; }

/* ---------- rev 8: the enquiry console ----------

   Scoped to .ctas, which enquiry_ctas() emits on product pages, the
   About page and empty category pages — the console (halo, rims, icon
   nudge) appears on all of them; only "Back to catalogue" sits outside
   .ctas and is untouched. overflow becomes visible so the halo can
   breathe past the key's edge, so the sweep ::after must stay INSIDE the
   box: it moves its gradient (background-size 300%, position 100% -> 0)
   instead of translating the layer. A translated layer waits at -102%,
   just off the key's left edge, and overflow:visible draws it as a stray
   streak over the page (the rev 8 blocking bug); border-radius:inherit
   cannot clip a translated element. Label and icon rise to z-index 1
   above every light layer (the .card-body fix): measured 5.09:1 rest /
   6.32:1 hover on pink — the highlight only lightens the pink. */
.ctas .btn { overflow: visible; }
.ctas .btn > * { position: relative; z-index: 1; }
.ctas .btn::after {
  border-radius: inherit;
  transform: none;
  background-size: 300% 100%;
  background-position: 100% 0;
}

/* This also beats the base .btn hover sweep (transform translateX(102%)
   + transform transition), which would otherwise still run on .ctas
   buttons: higher specificity, both properties restated. One
   button-sized repaint for 550ms per hover; nothing is drawn outside a
   button except the halo. */
@media (hover: hover) {
  .ctas .btn:hover::after, .ctas .btn:focus-visible::after {
    transform: none;
    background-position: 0 0;
    transition: background-position 550ms ease-out;
  }
}

/* Ambient halo, primary only: a blurred pink bloom breathing 0.30 -> 0.70
   on a 4.5s cycle. Opacity-only, the one ambient effect outside the hero,
   stopped under reduced-motion. The ghost stays quiet. */
.ctas .btn::before {
  content: "";
  position: absolute;
  inset: -7px;
  z-index: 0;
  pointer-events: none;
  border-radius: 12px;
  background: radial-gradient(65% 130% at 50% 50%, rgba(244, 100, 159, 0.55), rgba(244, 100, 159, 0) 70%);
  filter: blur(7px);
  opacity: 0.35;
  animation: cta-halo 4.5s ease-in-out infinite;
}

@keyframes cta-halo {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.7; }
}

.ctas .btn--ghost::before { display: none; }

/* Hover: the ambient halo yields to a pointer-tracked white highlight at
   --mx/--my (foil.js; fixed centre sheen without it) UNDER the label, and
   the key gains the card's neon rim in its own colour. The one-shot sweep
   ::after is unchanged. */
.ctas .btn:not(.btn--ghost):hover::before,
.ctas .btn:not(.btn--ghost):focus-visible::before {
  animation: none;
  filter: none;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(150px 80px at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 70%);
  opacity: 1;
}

@media (hover: hover) {
  .ctas .btn:not(.btn--ghost):hover {
    box-shadow:
      0 0 0 1px rgba(244, 100, 159, 0.9),
      0 10px 24px rgba(11, 42, 51, 0.3),
      0 0 32px rgba(244, 100, 159, 0.6),
      0 0 60px rgba(35, 159, 169, 0.35);
  }

  .ctas .btn--ghost:hover {
    box-shadow:
      0 0 0 1px var(--signal),
      0 0 22px rgba(35, 159, 169, 0.4),
      0 6px 16px rgba(11, 42, 51, 0.18);
  }
}

/* The send-off: the plane/envelope nudge up-right on hover. Icon/label
   order and wording untouched. */
.ctas .btn .ico { transition: transform 160ms ease-out; }

@media (hover: hover) {
  .ctas .btn:hover .ico, .ctas .btn:focus-visible .ico {
    transform: translate(2px, -2px) rotate(6deg);
  }
}

.footer-contact { display: flex; flex-wrap: wrap; gap: 8px 18px; }

.notice {
  border-left: 2px solid var(--signal);
  padding: 10px 0 10px 14px;
  color: var(--muted);
  font-size: 0.9rem;
  margin: 20px 0 0;
}

.price-note {
  font-style: italic;
  color: var(--dim);              /* 5.5:1 on --bg */
  font-size: 0.9rem;
  margin: 18px 0 0;
  max-width: 46ch;
}

/* ---------- footer ---------- */

.site-footer {
  position: relative;
  border-top: 1px solid var(--line);
  background:
    linear-gradient(rgba(209, 235, 250, 0.05) 1px, transparent 1px) 0 0 / 100% 40px,
    linear-gradient(90deg, rgba(209, 235, 250, 0.05) 1px, transparent 1px) 0 0 / 40px 100%,
    var(--abyss);
  color: var(--on-abyss);              /* 12.2:1 on --abyss */
  padding-block: 38px 44px;
  font-size: 0.85rem;
}

/* Mirrors the header scanline, closing the page — in mint, the footer's
   accent, the one bright that is also text on the dark band. */
.site-footer::before {
  content: "";
  position: absolute;
  top: -1px; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, rgba(74, 229, 171, 0.4), var(--mint));
  pointer-events: none;
}

.site-footer .wrap { display: flex; gap: 22px; justify-content: space-between; flex-wrap: wrap; }

.site-footer .brand { color: var(--on-abyss); }
.site-footer .brand span, .site-footer .brand span:hover { color: var(--mint); }  /* 9.4:1 */

/* outline-color here wins over the global :focus-visible rule by specificity,
   so the focus ring stays >=3:1 on the dark band without a new rule. */
.site-footer a { color: var(--on-abyss-muted); outline-color: var(--mint); }  /* 8.1:1 */
.site-footer a:hover { color: var(--mint); }   /* 9.4:1 */

.contact-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.contact-link .ico { color: var(--mint); }

.disclaimer { max-width: 62ch; margin-top: 18px; font-size: 0.78rem; line-height: 1.5; color: var(--on-abyss-dim); }  /* 5.8:1 */

/* ---------- homepage rails ----------

   Mechanics unchanged: the row scrolls with CSS scroll-snap alone; the
   script only adds arrows and auto-advance on top. */
.rail-section + .rail-section { margin-top: 46px; }

/* 30px: rev 7's 6px lift plus neon glow needs headroom below, and the
   rail-track's grown top padding must not overlap .rail-more's 14px
   tap-target overhang above. 30px clears the link's tap box by 0.5px at
   1280px (27px at 390px); 26px overlapped it by 3.5px. */
.rail-section .section-head { margin-bottom: 30px; }

.rail-more {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-2);            /* 9.0:1 on --bg */
  white-space: nowrap;
  /* 44px tap target that leaves the heading row untouched: padding grows the
     clickable (border) box to ~46px, the matching negative margins pull the
     layout box back to its original size — row height and the baseline
     alignment with the h2 do not move. The overhang lands in the head's own
     margins, never on a neighbouring control. */
  padding-block: 14px;
  margin-block: -14px;
}

.rail-more:hover { color: var(--signal-deep); text-decoration: underline; }

.rail { position: relative; }

.rail-track {
  display: flex;
  gap: 26px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-left: 2px;
  /* 16px above / 24px below so the rev-7 lift, tilt and 34px glow are not
     clipped by the rail's overflow; negative margins keep layout offset. */
  padding: 16px 2px 24px;
  margin: -16px -2px -24px;
  scrollbar-width: thin;
  scrollbar-color: var(--line-hi) transparent;
  overscroll-behavior-x: contain;
}

.rail-track::-webkit-scrollbar { height: 8px; }
.rail-track::-webkit-scrollbar-track { background: transparent; }
.rail-track::-webkit-scrollbar-thumb {
  background: var(--line-hi);
  border-radius: 4px;
}

/* Edge fade — pure decoration; if mask-image is unsupported the rail simply
   renders without it. */
@supports (mask-image: linear-gradient(to right, black, black)) {
  .rail-track {
    mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
  }
}

.rail-track > .card {
  flex: 0 0 clamp(160px, 40vw, 280px);
  scroll-snap-align: start;
}

.rail-nav {
  position: absolute;
  top: calc(50% - 40px);
  transform: translateY(-50%);
  z-index: 2;
  width: 44px;
  height: 44px;
  border-radius: var(--facet-s);
  border: 1px solid var(--line-hi);
  background: var(--panel);
  color: var(--ink);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  box-shadow: var(--shadow-panel);
  transition: opacity 220ms ease-out, background 140ms ease-out, border-color 140ms ease-out,
    color 140ms ease-out;
}

.rail:hover .rail-nav,
.rail-nav:focus-visible { opacity: 1; }

.rail-nav:hover { border-color: var(--signal); color: var(--signal-deep); }
.rail-nav:active { background: var(--panel-hi); }
.rail-nav:disabled { opacity: 0 !important; pointer-events: none; }
.rail-nav--prev { left: -6px; }
.rail-nav--next { right: -6px; }

@media (hover: none) {
  .rail-nav { display: none; }
}

@media (max-width: 560px) {
  .rail-track { gap: 12px; }
  .rail-section + .rail-section { margin-top: 34px; }
}

/* ---------- set blocks ---------- */

.setblock { margin: 0; padding-top: 48px; scroll-margin-top: 80px; }
.setblock:first-of-type { padding-top: 8px; }

.setblock > summary {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  padding-bottom: 12px;
  margin-bottom: 22px;
  cursor: pointer;
  list-style: none;
  min-height: 44px;
  transition: border-color 140ms ease-out;
}

.setblock > summary::-webkit-details-marker { display: none; }

@media (hover: hover) {
  .setblock > summary:hover { border-bottom-color: var(--line-hi); }
  .setblock > summary:hover h3 { color: var(--signal-deep); }
}

/* An open set block switches its rule to the signal channel; the plus marker
   rotates to a cross beside it, so the state never rides on the rim alone. */
.setblock[open] > summary { border-bottom-color: var(--signal); }

.setblock > summary h3 { transition: color 140ms ease-out; }
.setblock > summary > .mono { margin-left: auto; color: var(--dim); }

/* Plus marker that rotates to a cross when the channel opens. The rotation
   and the border carry the state together. */
.setblock > summary::after {
  content: "";
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  background:
    linear-gradient(var(--signal), var(--signal)) center/12px 2px no-repeat,
    linear-gradient(var(--signal), var(--signal)) center/2px 12px no-repeat;
  transition: transform 180ms ease-out;
}

.setblock[open] > summary::after { transform: rotate(45deg); }

.setblock:not([open]) > summary { margin-bottom: 0; }
.setblock:not([open]) { padding-top: 26px; }

/* The set name and its code chip lay out as one wrapping row. A chip that
   does not fit beside the name drops whole to the next line, flush left
   under the name, with a tidy gap — it never splits at its hyphen. (The
   chip's bordered box is ~4px taller than the name's line box, so a summary
   line carrying both gains up to ~4px of height at every width — flagged
   for review, visually negligible.) */
.setblock > summary h3 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
}

/* Set codes render as data tags. One label, never broken internally. */
.setblock h3 .mono {
  margin-left: 0;
  margin-right: 0;
  white-space: nowrap;
  color: var(--ink-2);
  background: var(--panel);
  border: 1px solid var(--line-hi);
  border-radius: 3px;
  padding: 2px 7px;
}

.setblock .grid { row-gap: 20px; }

/* ---------- FAQ (native <details>, no JS) ---------- */

.faq { margin-top: 64px; }
.faq h2 { margin-bottom: 22px; }

.faq-list {
  border-top: 1px solid var(--line);
  max-width: 78ch;
}

.faq-item { border-bottom: 1px solid var(--line); }

.faq-item summary {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 18px 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: 0.01em;
  line-height: 1.4;
  list-style: none;
  transition: color 140ms ease-out;
}

.faq-item summary::-webkit-details-marker { display: none; }

@media (hover: hover) {
  .faq-item summary:hover { color: var(--signal-deep); }
}

.faq-item[open] summary { color: var(--signal-deep); }   /* 5.7:1 on --bg */

.faq-item summary::after {
  content: "";
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  margin-left: auto;
  margin-top: 4px;
  background:
    linear-gradient(var(--signal), var(--signal)) center/12px 2px no-repeat,
    linear-gradient(var(--signal), var(--signal)) center/2px 12px no-repeat;
  transition: transform 180ms ease-out;
}

.faq-item[open] summary::after { transform: rotate(45deg); }

.faq-answer {
  padding: 0 34px 20px 0;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.6;
}

.faq-answer p { margin: 0; }

/* ---------- game bands ---------- */

.gameband { padding-top: 18px; scroll-margin-top: 80px; }
.gameband + .gameband { padding-top: 56px; }

.gameband-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line-hi);
}

.gameband-head h2 { font-size: clamp(1.35rem, 2.8vw, 1.85rem); font-weight: 700; }

.gameband-head .mono { color: var(--dim); }

.gameband .setblock:first-of-type { padding-top: 30px; }
.gameband .setblock h3 { font-family: var(--font-display); font-size: 1.15rem; margin: 0; }

/* ---------- search ---------- */

.search { position: relative; margin: 0; }

.search input {
  width: 210px;
  max-width: 100%;
  min-height: 40px;
  padding: 8px 12px;
  border-radius: var(--facet-s);
  border: 1px solid var(--line-hi);
  background: var(--panel);
  color: var(--ink);
  caret-color: var(--signal-deep);
  font: inherit;
  font-size: 0.88rem;
  -webkit-appearance: none;
  appearance: none;
}

.search input::placeholder { color: var(--dim); }   /* 6.0:1 on the field */

.search input:focus {
  border-color: var(--signal-deep);
  outline: none;
  box-shadow: 0 0 0 3px rgba(35, 159, 169, 0.22);
}

/* The panel ships with the `hidden` attribute set in markup; the script
   removes it once there is something to show. */
.search-panel[hidden] { display: none; }

.search-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: min(380px, calc(100vw - 48px));
  /* 3px scanline stripe pinned to the panel top. */
  background:
    linear-gradient(90deg, var(--signal), rgba(35, 159, 169, 0.35)) top / 100% 3px no-repeat,
    var(--panel);
  border: 1px solid var(--line-hi);
  border-radius: var(--facet);
  box-shadow: var(--shadow-panel);
  max-height: min(420px, 70vh);
  overflow-y: auto;
  z-index: 30;
}

.search-results { list-style: none; margin: 0; padding: 8px 6px 6px; }

.search-results a {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px;
  border-radius: 6px;
  text-decoration: none;
  color: var(--ink);
}

.search-results a:hover,
.search-results [aria-selected="true"] a {
  background: rgba(35, 159, 169, 0.1);
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--signal);
}

.search-thumb {
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--well);
  border: 1px solid var(--line);
}

.search-thumb img { width: 100%; height: 100%; object-fit: contain; }
.search-thumb-blank { display: block; width: 100%; height: 100%; background: var(--panel-hi); }

.search-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }

.search-title {
  font-size: 0.88rem;
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-meta {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-price {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--ink);              /* 13.2:1 on the panel */
}

.search-empty { padding: 14px 12px; color: var(--muted); font-size: 0.88rem; }

/* ---------- responsive: <=820px — phones and small tablets ----------

   The phone layout is designed, not squeezed: the sticky header is one 56px
   bar (brand + a pure-CSS menu button); navigation and the script-revealed
   search field fold into a drop-down panel driven by .nav-check. With
   JavaScript off the panel shows the four nav destinations and nothing dead.
   The breakpoint sits at 820px, not 760px: between 761 and ~800px the desktop
   header wraps to a cramped two-row bar, so the collapsed header covers that
   band; from 821px up the desktop header fits on one line. */

@media (max-width: 820px) {
  .detail { grid-template-columns: 1fr; gap: 28px; padding-block: 32px; }
  .section { padding-block: 40px; }
  .setblock, .gameband { scroll-margin-top: 68px; }

  .site-header .wrap { min-height: 56px; padding-block: 6px; flex-wrap: nowrap; gap: 14px; }
  .brand { font-size: 1.05rem; gap: 10px; }
  .site-header .brand-mark { height: 32px; }

  /* Visually hidden but focusable: keyboard users toggle it with space and
     see the ring on the label. clip-path hides it visually; the indicator
     survives on .nav-btn via the sibling rule below. */
  .nav-check {
    display: block;
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    clip-path: inset(50%);
    overflow: hidden;
  }

  .nav-btn {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    height: 44px;
    padding: 0 14px;
    border: 1px solid var(--line-hi);
    border-radius: var(--facet-s);
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
    flex: 0 0 auto;
  }

  .nav-btn .mono { color: var(--dim); font-size: 0.7rem; }

  .nav-btn-bars {
    width: 16px; height: 12px;
    background: linear-gradient(currentColor, currentColor) center/100% 2px no-repeat;
    position: relative;
    color: var(--ink-2);
  }

  .nav-btn-bars::before, .nav-btn-bars::after {
    content: "";
    position: absolute;
    left: 0; right: 0;
    height: 2px;
    background: currentColor;
    transition: transform 180ms ease-out;
  }

  .nav-btn-bars::before { top: 0; }
  .nav-btn-bars::after { bottom: 0; }

  .nav-btn:hover { border-color: var(--signal); }

  .nav-check:focus-visible ~ .nav-btn {
    outline: 2px solid var(--signal-deep);
    outline-offset: 2px;
  }

  /* Open state: the bars become a cross and the chip goes active. */
  .nav-check:checked ~ .nav-btn { border-color: var(--signal); }
  .nav-check:checked ~ .nav-btn .mono { color: var(--signal-deep); }
  .nav-check:checked ~ .nav-btn .nav-btn-bars { background: none; color: var(--signal-deep); }
  .nav-check:checked ~ .nav-btn .nav-btn-bars::before { transform: translateY(5px) rotate(45deg); }
  .nav-check:checked ~ .nav-btn .nav-btn-bars::after { transform: translateY(-5px) rotate(-45deg); }

  /* The drop-down console: search first (when the script reveals it), then
     the four destinations as full-width rows. A step lighter than the header
     so deep-teal active text keeps >=4.5:1 on it. */
  .header-end {
    position: absolute;
    top: 100%; left: 0; right: 0;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: #c9e5f7;
    border-bottom: 1px solid var(--line);
    padding: 12px 24px 16px;
    box-shadow: var(--shadow-panel);
  }

  .nav-check:checked ~ .header-end { display: flex; }

  .search { order: -1; width: 100%; margin: 0 0 8px; }
  .search input { width: 100%; min-height: 46px; }
  .search-panel { right: auto; left: 0; width: 100%; top: calc(100% + 6px); }

  .site-nav { flex-direction: column; gap: 0; width: 100%; }

  .site-nav a {
    display: flex;
    align-items: center;
    min-height: 48px;
    padding: 0 2px 0 10px;
    font-size: 0.78rem;
    border-bottom: 1px solid rgba(74, 118, 132, 0.5);
  }

  .site-nav a:last-child { border-bottom: 0; }

  /* Current page: deep-teal text plus an inset bar — two channels. */
  .site-nav a[aria-current="page"] {
    border-bottom-color: rgba(74, 118, 132, 0.5);
    box-shadow: inset 2px 0 0 var(--signal);
  }

  /* Phone hero: the frame and brackets stay, tightened. */
  .hero .wrap { padding-block: 44px 38px; }
  .hero h1 {
    font-size: clamp(1.55rem, 7.4vw, 2.1rem);
    max-width: 14.18em;   /* 17ch in Orbitron 700 (ch = 0.8339em) — see .hero h1 */
    margin-top: 18px;
    padding: 14px 16px;
  }
  .hero .mono { font-size: 0.62rem; padding: 5px 10px; }
  .hero p.lede { font-size: 0.98rem; margin-top: 16px; }
  .hero::after { height: 72px; }

  .price-lg { font-size: 2rem; }
  .ctas .btn { flex: 1 1 100%; }
}

/* ---------- responsive: <=560px — small phones ---------- */

@media (max-width: 560px) {
  /* Set-block summary: marker pinned right across two rows; title above,
     count below. */
  .setblock > summary {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "title marker"
      "meta  marker";
    align-items: center;
    column-gap: 14px;
    row-gap: 4px;
  }

  .setblock > summary h3 { grid-area: title; }
  .setblock > summary > .mono { grid-area: meta; margin-left: 0; }
  .setblock > summary::after { grid-area: marker; align-self: center; }

  /* Category grid: 2-up from 360px (the 320-359px row layout is below). */
  .grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
  }

  .setblock .grid { row-gap: 12px; }

  .card-body { padding: 10px 10px 12px; gap: 5px; }
  .card-title { font-size: 0.82rem; line-height: 1.25; }
  .card-spec { font-size: 0.6rem; letter-spacing: 0.05em; }
  .card-foot { gap: 2px; padding-top: 6px; }
  .price { font-size: 0.92rem; }
  .stock { font-size: 0.6rem; gap: 5px; }
  .stock--out { padding: 2px 6px; }

  .badge { font-size: 0.55rem; padding: 3px 7px; top: 6px; left: 6px; }

  .ph { padding: 12px; gap: 8px; }
  .ph-fmt { font-size: 0.55rem; letter-spacing: 0.1em; }
  .ph-title { font-size: 0.8rem; max-width: 14ch; }

  .thumb { width: 56px; height: 56px; }
  .thumbs { gap: 8px; }

  .rail-track > .card { flex-basis: clamp(150px, 42vw, 240px); }

  .cats { grid-template-columns: 1fr; gap: 12px; padding-bottom: 36px; }
  .cat { padding: 18px 18px 16px; }

  .section-head::after, .gameband-head::after { flex-basis: 24px; }

  .footer-contact { flex-direction: column; align-items: flex-start; gap: 4px; }
}

/* ---------- responsive: 320-359px — the narrowest phones ----------

   Below the 2-up threshold the card becomes a horizontal scan-row: viewport
   left, readout right, so the catalogue stays dense instead of stacking
   full-width portraits. */

@media (max-width: 359px) {
  .grid { grid-template-columns: 1fr; }

  /* The horizontal scan-row is a category-GRID layout only — never the
     homepage rails. A rail card is ~150px wide, so a 92px viewport plus its
     body cannot sit side by side; rail cards keep the normal vertical
     layout at every width. */
  .grid > .card { flex-direction: row; align-items: stretch; }

  .grid > .card .card-media {
    flex: 0 0 92px;
    width: 92px;
    aspect-ratio: auto;
    align-self: stretch;
  }

  .grid > .card .card-body { border-top: 0; border-left: 1px solid var(--line); padding: 8px 10px; }
  .grid > .card .card-title { font-size: 0.8rem; }
  .grid > .card .badge { font-size: 0.5rem; padding: 2px 6px; top: 4px; left: 4px; }
}

/* Deep teal: 5.7:1 on --bg, 6.2:1 on --panel, 5.2:1 on the header. On the
   dark footer the links set their own outline-color (see .site-footer a). */
:focus-visible { outline: 2px solid var(--signal-deep); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  /* Kill the ambient scan line and every sweep layer; the sold-out hatch
     stays (it does not move). */
  .hero::before, .hero .wrap::before, .hero h1::after,
  .hero .mono::before, .hero-showcase img, .hero-showcase::before { animation: none; }
  /* Rev 7: glitch, floor, reticle stop; the circuit parks fully drawn at
     low opacity (static decoration, allowed) — the glitch's text-shadow
     and transform are flat at the keyframes' 0% state, so freezing the
     animation is sufficient. */
  .hero h1, .hero .wrap::after, .hero-showcase::after { animation: none; }
  .hero-circuit path { animation: none; stroke-dashoffset: 0; opacity: 0.28; }
  .hero-circuit circle { animation: none; opacity: 0.28; }
  /* The entrance rise never applies (its media query requires no-preference),
     so there is nothing to unwind here. */
  .card:not(.card--out) .card-media::after, .btn::after { display: none; }
  /* Rev 8: the halo freezes at a faint static glow (static decoration,
     allowed); the edge-light and backlight never arm (hover rules still
     apply but nothing moves — the edge-light is opacity-faded in, so it is
     pinned off here to keep the frame quiet); foil.js does nothing under
     reduced-motion, so no tilt; gallery.js never adds .is-swapping. */
  .ctas .btn::before { animation: none; opacity: 0.3; }
  .detail-media::after { opacity: 0 !important; }
  /* The foil wash and highlight are static; the tilt never applies because
     foil.js does nothing under reduced-motion. */
  .card, .cat, .rail-nav, .thumb, .btn,
  .card--out .card-media img, .card-media img,
  .card:not(.card--out)::before, .card:not(.card--out)::after,
  .cat::after, .cat-foil,
  .stock::before,
  .card-media::before, .detail-media::before, .cat::before,
  .setblock > summary, .setblock > summary h3, .setblock > summary::after,
  .faq-item summary, .faq-item summary::after,
  .nav-btn-bars::before, .nav-btn-bars::after,
  .detail-media, .detail-media img, .thumb, .ctas .btn, .ctas .btn .ico { transition: none; }
}
