/* ============================================================================
   SchoenerBeats — single-track store page
   Premium, modern, confident. A dimensional dark room (lifted, not a void),
   one clean cold light from above, a present pool below. Modern grotesque type
   with real presence. Restraint, but no timidity.
   ========================================================================== */

/* --- Self-hosted fonts (Blueprint §10.4) — latin-subset variable WOFF2, served
   from this origin, so no fonts.googleapis.com/gstatic request. --------------- */
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/space-grotesk-var.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/inter-var.woff2") format("woff2");
}

/* --- Metric-matched fallbacks (2026-07-21) --------------------------------------
   `font-display: swap` paints the page in a system font first and re-paints it once
   the WOFF2 lands. With plain fallbacks the two fonts occupy different widths, so the
   brand line visibly resized and everything under it jumped. These faces stretch Arial
   to occupy the SAME space as the real font: the swap then changes letterforms and
   moves nothing.

   THREE TRAPS, all hit while getting these numbers — do not "simplify" this back:
   1. A variable font's metrics must be taken AT THE WEIGHT IN USE. Reading Space
      Grotesk's default instance (wght 300) via fontTools gave 101.0%; the truth at
      the weights this site renders is ~106.1%. The first attempt left the brand line
      12px too narrow — WORSE than plain unadjusted Arial.
   2. `local("Arial Bold")` is NOT reliable and made it 17px too wide. Every face here
      therefore uses plain Arial. A face declaring `font-weight: 400 700` renders its
      data WITHOUT synthetic bolding (verified: identical width at 400/500/600/700),
      so one Arial Regular face serves the whole range at a stable width.
   3. Consequently the required ratio barely moves with weight for Space Grotesk
      (105.9-106.4% across 400-700), so it gets ONE face. Inter drifts more
      (105.5% -> 107.8%) and gets two buckets.

   All percentages are measured in this page against a control face, over a corpus of
   the site's own strings. Residual is ~5% worst case for Space Grotesk and ~2% for
   Inter, driven by which LETTERS a string contains — that is the physical floor of
   matching Arial to a different typeface, not a tuning error. Arial is the base
   because it ships on Windows and macOS; anything falling past it lands on system-ui
   unadjusted, which is the old behaviour, not a regression.
   RE-MEASURE if a font file is re-subset or a new weight enters the design. ------ */
@font-face {
  font-family: "Space Grotesk Fallback";
  font-weight: 400 700;
  src: local("Arial");
  size-adjust: 106.100%;
  ascent-override: 92.743%;
  descent-override: 27.521%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback";
  font-weight: 400 500;
  src: local("Arial");
  size-adjust: 105.494%;
  ascent-override: 91.830%;
  descent-override: 22.865%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback";
  font-weight: 600 700;
  src: local("Arial");
  size-adjust: 107.750%;
  ascent-override: 89.907%;
  descent-override: 22.386%;
  line-gap-override: 0%;
}

:root {
  --bg:      #0b1014;
  --bg2:     #141b21;
  --lift:    #202a32;
  --ink:     #edf1f4;
  --ink-dim: #adb7bf;
  --light:   #dfe5ea;   --light-rgb: 223, 229, 234;
  --hair:    rgba(237, 241, 244, 0.13);
  --cta:     #edf1f4;
  --cta-ink: #2e3439;

  /* Premium accents — derived 2026-07-05 from the user's OWN gold implementations
     (10-Key-Points line art, AWeber ONE-OF-US pages, Mythbuster card). His gold is
     ANTIQUE and GRADIENT: olive-brown shadow -> antique mid -> champagne highlight.
     Luminance movement = metal = premium; flat saturated yellow = paint = cheap.
     Rose + cyan are the same family: muted, dusty, luminous on near-black.
     Role coding: gold = paid/buy · rosé = share ("heart") · cyan = download. */
  --gold-deep: #7b6545;
  --gold:      #ae9560;  --gold-rgb: 174, 149, 96;
  --gold-hi:   #e9d9ab;
  --gold-ink:  #000000;
  --rose:      #d6829f;  --rose-rgb: 214, 130, 159;
  --rose-hi:   #eab3c4;
  /* CYAN — the CALM brand cyan. Both values below are brand colours (canonical list:
     Execution/Branding/Brand_Assets/README.md); they were adopted INTO the palette on
     2026-08-12 as the calm variation of the logo cyan #8ee3f8. Same hue, same
     lightness, ~31% less intensity than the logo value.

     DO NOT "correct" these to #8ee3f8. That was done on 2026-08-12, in three variants,
     and rejected by the client on sight: the vivid logo cyan reads LESS grown up and
     less premium, which is the opposite of what this store is. The vivid value is the
     poppy one and belongs on poppy surfaces, not here. Both are brand-legal; this is
     the deliberate choice between them, not an oversight. */
  --cyan:      #7fc3d3;  --cyan-rgb: 127, 195, 211;
  --cyan-hi:   #a5ddeb;

  --dock-h: 23vh;

  /* THE TOP STRIP — the ONE place its two heights are written (2026-08-07). */
  --strip-h: 112px;
  --strip-h-folded: 56px;

  /* The metric-matched face sits FIRST after the real font — it is what the page is
     painted in before the WOFF2 arrives, and it occupies identical space. */
  --disp: "Space Grotesk", "Space Grotesk Fallback", "Segoe UI", system-ui, sans-serif;
  --sans: "Inter", "Inter Fallback", "Segoe UI", system-ui, sans-serif;
}

* { box-sizing: border-box; }
html, body {
  margin: 0; min-height: 100%;
  background: var(--bg); color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
/* The root font size is the beat page's ONE scaling unit (see the beat-page block
   at the end of this file); every other page is px-typed, so pin the inherited
   body size back to the browser default and nothing else moves. */
body { min-height: 100vh; overflow-x: hidden; font-size: 16px; }

/* --- THE TOP STRIP (2026-08-07) ---------------------------------------------
   A see-through band across the top that darkens what is under it. One version,
   no blur. Its two heights are --strip-h / --strip-h-folded, written in exactly
   one place (:root).

   THE FOLD — scroll-linked, not timed (client's call 2026-08-08, replacing the
   250ms/ease-in-out version). The band gives up height from its BOTTOM edge, one
   pixel per pixel scrolled, until it has lost 56 and then holds. There is no
   duration and no easing: its size IS the scroll position, so it is glued to the
   finger. That is the whole point — the client's objection to every timed fold he
   has seen is the disconnect between scroll speed and fold speed.

   Why `transform: scaleY()` and NOT `height`: animating height re-runs page layout
   every frame, which is the textbook cause of stutter and defeats the purpose
   (Chrome's own scroll-driven-animation guidance says animate transform, not
   dimensions). Only transform and opacity run on the compositor. `transform-origin:
   top` pins the top edge, so the visual result is identical to shrinking: top edge
   fixed, bottom edge riding up. This holds only while the band is EMPTY — any
   contents must sit on top of it as their own layer, never inside it, or they
   would be squashed with it.

   `animation-fill-mode: both` is what makes iOS behave. Safari reports a NEGATIVE
   scroll position during the rubber-band bounce at the top, and the spec
   deliberately does not clamp it — it leaves the out-of-range result to the
   animation's fill. `both` holds the start state, so the band sits still through
   the bounce instead of growing. `linear` keeps it 1:1 with the scroll; any easing
   would re-introduce exactly the disconnect this replaced.

   `animation-timeline` MUST come after the `animation` shorthand or it is reset.

   Reduced motion is deliberately NOT special-cased: WCAG 2.3.3 exempts movement
   the user directly controls by scrolling, and what it targets is decoration
   moving at a DIFFERENT rate than the page (parallax). One band at the visitor's
   own speed is the exempt case, and giving these visitors the snap instead would
   hand them a jump — more jarring than a movement they are driving themselves. */
body::before {
  content: "";
  position: fixed; top: 0; left: 0; right: 0;
  height: var(--strip-h);
  z-index: 5;
  /* SOLID colour at 55% ELEMENT opacity — visually identical to the same colour
     written as 55% alpha, because this box is empty. That equivalence is NOT a
     general rule: fading an element fades a flattened picture of it and everything
     inside, while an alpha background fades only the colour. It holds here, and it
     is the reason the logo must later sit ON the band, never inside it. */
  background: rgb(5, 7, 9);
  opacity: 0.55;
  transform-origin: top;
}

/* SOLIDIFYING rides the SAME 0-56px scroll range as the fold, so the band is fully
   solid at the exact moment it stops shrinking.

   `opacity`, NOT `background-color` — this is the 2026-08-08 stutter, found by the
   client on a real machine and confirmed by comparing two deploys. Colour is a
   PAINT operation: putting it in this keyframe dropped the whole animation off the
   compositor and made the fold visibly judder, on a page that had been smooth. Only
   transform and opacity are compositor-only, and both together on one element are
   safe — the layer is promoted once and both ride it.

   LINEAR on purpose. The eye compresses brightness to roughly a cube root of the
   real light, so an evenly-paced fade is ALREADY perceived as slow-then-rushing —
   the hand-moving-across-a-lamp effect. The acceleration comes from the visitor's
   eye, not from a curve; an ease would put its gentle settle exactly where the eye
   is most sensitive and flatten the effect. Browsers blend in the perceptual space
   rather than physical light, so this reads the same in every browser. Strongest
   for a dark-adapted eye, which is exactly who is looking at this site. */
@keyframes sb-strip-fold {
  to {
    transform: scaleY(calc(var(--strip-h-folded) / var(--strip-h)));
    opacity: 1;
  }
}

/* THE HAIRLINE — the strip's bottom edge (2026-08-08, always visible; a fade-in
   version is a possible later step).

   Its OWN element, not a border on the band: the band is squashed, and a border
   would be squashed with it — a 1px line would arrive at 0.5px by the time the
   fold finishes. This one is never scaled, only slid, so its thickness is constant.

   It travels 112px -> 56px, the same distance over the same 0-56px of scroll, so
   it and the band's bottom edge are the same line at every moment. Moved with
   `translateY`, which rides the compositor — the same reason the fold uses scale
   and the solidify uses opacity.

   APPLE FIRST (Store.md standing rule): the half-pixel below is for Apple's sake.
   A CSS pixel is not a screen dot — on a dense screen one CSS pixel covers a 2x2
   block, so a 1px line renders twice as thick as intended. 0.5px maps to exactly
   one real dot and is reliable in Safari on both macOS and iOS, which is the
   majority of this store's traffic. 1px stays as the default for the screens that
   would round 0.5px away to nothing.

   KNOWN RISK, not yet judged: a thin line slid by the compositor can land between
   screen dots and soften while moving. It is a real, documented cost of this
   approach and it cannot be seen on the client's 61-dpi ultrawide — only on a
   dense screen (his phone). If it reads soft while scrolling, the fix is snapping
   to whole dots; not done pre-emptively. */
body::after {
  content: "";
  position: fixed; top: 0; left: 0; right: 0;
  /* Thickness lives in ONE variable and the position is derived from it. Writing
     the offset as a literal `- 1px` was the 2026-08-08 bug the client found: the
     dense-screen rule below halved the thickness but the offset kept subtracting a
     whole pixel, so the line rode up INTO the band and left a sliver of band
     showing beneath it. Visible from 150% browser zoom upward, which is how he
     reproduced it on a 1x monitor. Thickness and offset must never be two numbers. */
  --hairline-w: 1px;
  height: var(--hairline-w);
  z-index: 6;
  /* The shared --hair token is rgba(237,241,244,0.13) and is used elsewhere (the
     brand-line divider). The client judged that a touch bright HERE, against the
     near-black top of the page, so this line runs at 60% of it — its own value, so
     the shared token and everything using it stay untouched. (Tried in order: 90%,
     barely registered; 70%, "definitely getting there"; 60%. Bright-on-dark reads
     brighter than the number suggests — the same brightness compression that shapes
     the fade — so the steps have to be coarse to be felt at all.) */
  background-color: transparent;
  /* THE LINE'S FADE (2026-08-08): half strength at the top of the page, full
     strength once the strip is folded and solid — so both edges of the strip gain
     presence together instead of a line switching on.
     Done with ELEMENT opacity, never by animating the colour: colour is a paint
     operation and putting one in a scroll-linked keyframe is exactly what made the
     fold judder earlier today. 0.5 x 0.078 = 0.039, which is the 30% he asked for,
     so the two numbers stay tied and only one of them is ever edited. */
  opacity: 0.5;
  /* THE LIGHT WRAP (2026-08-10) — the line is dimmed where the cave is dark and
     left alone where the cave is lit, so it reads as an edge the room is lighting
     rather than a border drawn on top of a photograph. Compositing calls this a
     light wrap: matching a foreground element to the light of the plate behind it.
     The field's own rule is the client's taste stated back to him — you want to
     FEEL it, not see it — so nothing here is exaggerated past what the photo does.

     WHY IT WAS NEEDED, and it is not taste: a light line has MORE contrast against
     a dark backdrop than against a lit one, so one uniform line is automatically
     loudest exactly where the room is blackest. That is why the ends read hot while
     the middle read right, and it is why the whole line had to be held at 60%.

     THE CURVE IS MEASURED, NOT INVENTED. Sampled from cave10.jpg along this line's
     own row (viewport 1920x1080, the client's screen): brightness runs ~12 at the
     far left, peaks at ~36 at 52% across, and falls to ~15-17 at the right. Ends sit
     at about a THIRD of the peak — the room out there is dark but not black. The
     asymmetry is real and deliberate: the right shoulder holds longer than the left.
     Rock detail in the plate (small bumps at 8% and 24%) is smoothed OUT —
     reproducing texture in the line would read as a defect. 22 stops, well past the
     ~13 below which a gradient visibly bands on near-black.

     STRENGTH — the one dial, and the only number to edit (client, 2026-08-10).
     The measured curve at full depth was too strong: he could barely find the line
     at the ends, "almost having to hallucinate it". It is now pulled back to 33% of
     its full depth. The stops below are `1 - (1 - measured) * 0.33`, so the ends
     went 0.34 -> 0.78 and the peak stays 1.00. To change it, re-derive the stops
     from the measured values with a new factor — the measured curve is recorded in
     Manuals/Store/Top_Strip.md so it never has to be sampled again.

     The line's own alpha is UNTOUCHED at the peak: 0.078 x 1.00 is exactly the 60%
     he approved, and 0.078 x 0.78 at the ends is 0.061. Nothing gets brighter than
     what he signed off, and nothing now drops near invisibility either.

     THE CROP IS THE WHOLE TRICK, and it is his own solution. The mask is sized and
     centred by the SAME rules as the photograph (cover, centred), so a narrow screen
     cuts the fade off at exactly the point it cuts the photo off. The bright part of
     the fade therefore lands on the bright part of the cave at EVERY width, with no
     phone-specific version and no second curve to maintain. `--bg-aspect` is
     cave10.jpg's own 1376/752; `max(100vw, 100vh * aspect)` reproduces what
     `background-size: cover` computes for it, and `mask-position: 50%` matches its
     `background-position: 50% 12%` horizontally (the 12% is vertical and cannot
     reach a 1px-tall line).

     APPLE FIRST (Store.md standing rule) — naming the two places Apple decided this:
     the `-webkit-` pair is written FIRST because Safari before 15.4 only understands
     the prefixed form, and `mask-position` is used rather than the `mask` shorthand
     because the shorthand resets mask-size in older WebKit. A browser that
     understands neither simply gets the even line already approved.

     Known limit, stated rather than hidden: `100vh` matches this fixed element's
     containing block on desktop; on a phone whose toolbars change height it can drift
     from the photo's actual height by a few percent, which moves the fade's peak
     slightly. It is a soft effect, so a few percent is not visible — but it is the
     one place this is an approximation rather than an exact match. */
  background-image: linear-gradient(90deg,
    rgba(237,241,244,0.0572) 0%,
    rgba(237,241,244,0.0576) 4.76%,
    rgba(237,241,244,0.0582) 9.52%,
    rgba(237,241,244,0.0590) 14.29%,
    rgba(237,241,244,0.0607) 19.05%,
    rgba(237,241,244,0.0628) 23.81%,
    rgba(237,241,244,0.0659) 28.57%,
    rgba(237,241,244,0.0691) 33.33%,
    rgba(237,241,244,0.0726) 38.1%,
    rgba(237,241,244,0.0754) 42.86%,
    rgba(237,241,244,0.0774) 47.62%,
    rgba(237,241,244,0.0780) 52.38%,
    rgba(237,241,244,0.0769) 57.14%,
    rgba(237,241,244,0.0747) 61.9%,
    rgba(237,241,244,0.0714) 66.67%,
    rgba(237,241,244,0.0682) 71.43%,
    rgba(237,241,244,0.0649) 76.19%,
    rgba(237,241,244,0.0624) 80.95%,
    rgba(237,241,244,0.0603) 85.71%,
    rgba(237,241,244,0.0593) 90.48%,
    rgba(237,241,244,0.0586) 95.24%,
    rgba(237,241,244,0.0581) 100%);
  background-repeat: no-repeat;
  background-position: 50% 50%;
  background-size: 100vw 100%;
  will-change: transform, opacity;
  transform: translateY(calc(var(--strip-h) - var(--hairline-w)));
  animation: sb-hairline-fold linear both;
  animation-timeline: scroll(root block);
  animation-range: 0px var(--strip-h-folded);
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  body::after { --hairline-w: 0.5px; }
}
/* The photo is `cover`, so on any window narrower than its 1376/752 shape the photo
   is wider than the window. Written as two plain values in two media queries rather
   than one max()/calc() so nothing on this moving element has to be recomputed. */
@media (max-aspect-ratio: 1376/752) {
  body::after { background-size: 182.98vh 100%; }
}

@keyframes sb-hairline-fold {
  to {
    transform: translateY(calc(var(--strip-h-folded) - var(--hairline-w)));
    opacity: 1;
  }
}

/* THE TOP-UP LAYER (2026-08-10) — why the light wrap must not exist at the top of
   the page, in the client's own reasoning: at rest the band is see-through, so the
   line needs more light to hold its contrast; once solid the backdrop is darker and
   it needs less. So the dimming has to ARRIVE with the fold, not be there on load.

   Fading one shading into another is a paint operation and is what stuttered twice
   today. This avoids it entirely: the graded line below is permanent, and this layer
   carries only the LIGHT IT IS MISSING — nothing at the bright spot, most at the
   ends. Both showing = the even line he approved. This one fades out across the same
   0-56px as the fold and the solidify, so all three finish together and what remains
   is the graded line at full strength. The only thing animating is transparency.

   The values are SOLVED, not subtracted: two faint light layers compose as
   1-(1-a)(1-b), so b = 1 - 0.922/(1-g). That lands the top of the page on exactly
   0.078 rather than near it.

   It lives on the ROOT element's own ::after, which nothing else in this stylesheet
   uses — so the strip stays a stylesheet-only component and no template is touched.
   Being after <body> in tree order, it paints above the line at the same z-index. */
html::after {
  content: "";
  position: fixed; top: 0; left: 0; right: 0;
  --hairline-w: 1px;
  height: var(--hairline-w);
  z-index: 6;
  pointer-events: none;
  background-image: linear-gradient(90deg,
    rgba(237,241,244,0.0245) 0%,      rgba(237,241,244,0.0204) 5%,
    rgba(237,241,244,0.0166) 10%,     rgba(237,241,244,0.0133) 15%,
    rgba(237,241,244,0.0098) 20%,     rgba(237,241,244,0.0076) 23.04%,
    rgba(237,241,244,0.0069) 25%,     rgba(237,241,244,0.0053) 30%,
    rgba(237,241,244,0.0036) 35%,     rgba(237,241,244,0.0029) 37.11%,
    rgba(237,241,244,0.0021) 40%,     rgba(237,241,244,0.0008) 45%,
    rgba(237,241,244,0.0004) 47.07%,  rgba(237,241,244,0.0001) 50%,
    rgba(237,241,244,0.0000) 52%,     rgba(237,241,244,0.0001) 55%,
    rgba(237,241,244,0.0004) 58.76%,  rgba(237,241,244,0.0008) 60%,
    rgba(237,241,244,0.0018) 62.10%,  rgba(237,241,244,0.0030) 65%,
    rgba(237,241,244,0.0046) 70%,     rgba(237,241,244,0.0063) 75%,
    rgba(237,241,244,0.0064) 75.44%,  rgba(237,241,244,0.0087) 80%,
    rgba(237,241,244,0.0131) 85%,     rgba(237,241,244,0.0167) 90%,
    rgba(237,241,244,0.0190) 95%,     rgba(237,241,244,0.0205) 100%);
  background-repeat: no-repeat;
  background-position: 50% 50%;
  background-size: 100vw 100%;
  will-change: transform, opacity;
  opacity: 1;
  transform: translateY(calc(var(--strip-h) - var(--hairline-w)));
  animation: sb-hairline-topup linear both;
  animation-timeline: scroll(root block);
  animation-range: 0px var(--strip-h-folded);
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  html::after { --hairline-w: 0.5px; }
}
@media (max-aspect-ratio: 1376/752) {
  html::after { background-size: 182.98vh 100%; }
}

@keyframes sb-hairline-topup {
  to {
    transform: translateY(calc(var(--strip-h-folded) - var(--hairline-w)));
    opacity: 0;
  }
}

@supports (animation-timeline: scroll()) {
  body::before {
    animation: sb-strip-fold linear both;
    animation-timeline: scroll(root block);
    animation-range: 0px var(--strip-h-folded);
  }
}

/* Browsers without scroll-linked animation (~16%, and shrinking — the test above
   is run by the browser itself, so they upgrade themselves): a hard snap to 56 at
   56px of scroll, the client's call. No transition — a snap, not a slow fold. */
@supports not (animation-timeline: scroll()) {
  body.strip-folded::before { height: var(--strip-h-folded); }
}

/* --- The room: a dimensional dark space, light pooling from above ----------- */
.room { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.room::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(76% 50% at 50% 1%, rgba(var(--light-rgb), 0.16) 0%, transparent 60%),
    radial-gradient(128% 86% at 50% 30%, var(--lift) 0%, var(--bg2) 46%, var(--bg) 100%);
}
.glow {                   /* luminous pool of light on the title */
  position: absolute; top: 6vh; left: 50%; transform: translateX(-50%);
  width: 74vw; max-width: 820px; height: 58vh;
  background: radial-gradient(ellipse 54% 48% at 50% 30%,
    rgba(var(--light-rgb), 0.14) 0%, rgba(var(--light-rgb), 0.04) 42%, transparent 72%);
  filter: blur(26px);
}
.vignette { position: absolute; inset: 0;
  background: radial-gradient(122% 94% at 50% 24%, transparent 46%, rgba(0,0,0,0.58) 100%); }

/* Focus dim — on play the room light lowers slightly, in step with the tongue
   (same durations/curves). Only the backdrop dims; content (.stage: panel, text,
   CTA) keeps full brightness, so the buy button gains pop against the darker room.
   Tune brightness(): 0.92 = 8% dim; try 0.88–0.95. */
.room { transition: filter 450ms cubic-bezier(.4,0,1,1); }
body.sb-playing .room { filter: brightness(0.92); transition: filter 2350ms cubic-bezier(.22,.68,.28,1); }
@media (prefers-reduced-motion: reduce) {
  .room, body.sb-playing .room { transition: none; }
}

#lightfx { position: fixed; inset: 0; z-index: 1; pointer-events: none; }

/* subtle film grain — keeps it filmic, not flat */
.grain { position: fixed; inset: 0; z-index: 6; pointer-events: none; opacity: 0.04; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }

/* --- Stage ------------------------------------------------------------------ */
/* This is column flex + align-items:center, so children are centred by
   SHRINK-TO-FIT: any child given a fixed width stops being centred the way you
   expect. Measure before assuming a centring bug is in the child.
   Visual_Design 2026-07-21. */
.stage {
  position: relative; z-index: 2; min-height: 100vh;
  display: flex; flex-direction: column; align-items: center;
  /* The dock is `height: var(--dock-h)` but ALSO `min-height: 140px`, so on a short
     viewport it is TALLER than --dock-h and the reserved space was too small — the
     bottom of the page then sat under the fixed dock canvas, which swallowed the
     click (the tape pill "did nothing" until you scrolled). Reserve the real height. */
  padding: 34px 26px calc(max(var(--dock-h), 140px) + 36px);
}

/* CENTRE, and do not "fix" this to baseline — that was tried on 2026-07-24 and it is
   a regression. A wordmark with a small-caps tagline beside it is a LOCKUP, and a
   lockup is aligned on CAP HEIGHT, not on the baseline (baseline alignment is for
   text that reads as one continuous line, e.g. a wordmark next to nav links).
   Measured on the real fonts: plain box-centring puts the two cap-centres within
   0.18px on beat pages and 0.04px on the homepage — already correct. Switching to
   `align-items: baseline` moved them 2.82px and 3.96px APART. Verified equally that
   `text-box-trim: trim-both; text-box-edge: cap alphabetic` scores 0.16px — no better
   than centring here, so it is deliberately NOT used: it would add a property most of
   this store's real traffic (71% Safari, which only got it in 26.2) cannot yet run. */
/* --- THE CART CONTROL (2026-08-10) -----------------------------------------
   Step 1 of the cart build. Research, decisions and what is still open:
   Manuals/Store/Cart.md. Markup lives in templates/_topbar.html.

   COLOUR — both values are STARTING POINTS and each is ONE word to change,
   which is the whole point of naming them here. No authority has a position on
   badge colour; Material says error-red, Fluent says brand, Atlassian says
   neutral, and none of the three gives a reason. Red is an inherited habit from
   Apple's app-icon badge, and the one real body of research says red reads as
   HAZARD — wrong for a neutral tally. Saturated colour is specifically advised
   against on near-black. So: rosé for the count (already on the page, already
   means "a thing you do", far enough from gold to never compete) and the
   wordmark's own ink for the trolley (it is furniture, like everything else in
   the strip). Gold is unavailable — it is reserved for paid ACTIONS, and a count
   is information. Orange (#ff8400, real, in his Tertiary palette) was rejected:
   it is the most saturated colour he owns and appears nowhere on this site.

   PLACEMENT — 48px of finger area inside the 56px folded strip, at the very
   right, which is where all ten comparable stores put it. The drawn trolley is
   24-32px by the standards; the box around it is what must reach 44-48. */
/* ONE line weight for everything drawn in the strip, in real screen pixels.
   `vector-effect: non-scaling-stroke` below is what makes that possible: without it
   a stroke is measured in the drawing's own coordinates and changes whenever the
   drawing is resized, which is how the trolley ended up at 1.4px while the count's
   stems sat near 4 (2026-08-10 — the client caught it, the reference has the trolley
   HEAVIER than the number). Change this one value and every strip drawing follows;
   override it on a single element to make just that one heavier. */
:root {
  --cart-ink: var(--ink); --cart-count-ink: var(--rose);
  /* 4.3px = the reference's own 5-unit stroke at this scale. It is deliberately
     heavier than the count's stems, which is the relationship the reference has and
     the one I had backwards before. */
  --strip-gutter: 24px; --strip-stroke: 4.3px;
  /* THE BUTTON BOX — one height for both ends of the strip, and the top offset is
     DERIVED from it so the pair stays centred in the surviving 56px when either
     number changes. The client expects to shrink the strip's contents once they all
     exist (2026-08-14), so resizing has to stay a one-number edit. */
  /* THE PANEL SURFACE — one token per panel, so a whole look is one value each.
     Default = the colour both panels have carried since 2026-08-13. The audit
     variants below (data-panel) only ever set these two. */
  /* PITCH BLACK (client, 2026-08-18). Opaque, not translucent: the panel is a
     surface the room does not reach, which is what Lamborghini and Universal both do
     on a dark page. The hairline border is what gives it its edge. */
  --panel-bg: #000000;
  --menu-bg:  #000000;
  --strip-btn-h: 48px;
  --strip-btn-top: calc((var(--strip-h-folded) - var(--strip-btn-h)) / 2);
  --menu-icon-w: 32px;
  /* THE TROLLEY'S SIZE, ONE NUMBER (client, 2026-08-14 — the only authorised shrink).
     Every measurement inside the glyph is written as ITS OWN fraction of this, so the
     drawing, the count and their offsets keep the proportions measured off his reference
     whatever this is set to. NEVER change it without being told to. */
  --cart-glyph: 48px;
}

/* THE TWO STRIP BUTTONS — the burger at the left gutter, the cart at the right —
   share ONE rule, because the strip has to teach one rule and not two.

   Both sit in the top 48px of the band's surviving 56px, so neither moves when the
   strip folds.

   THE BOX IS THE AFFORDANCE (client, 2026-08-14). A bare symbol does not tell a
   visitor it can be pressed. The only measured test on record put a box around a bare
   icon at +22.4% taps — more than three times the gain from adding a word beside it,
   which is why neither button carries a word. The border is the same --hair the cart
   panel is drawn with, so the boxes read as the panel's own family rather than as new
   furniture. */
.cart-link, .menu-btn {
  box-sizing: border-box;
  position: fixed; top: var(--strip-btn-top); z-index: 7;
  height: var(--strip-btn-h);
  display: flex; align-items: center; justify-content: center;
  /* NO BOX (client, 2026-08-14). Both drawn boxes were removed: they created a stream
     of sizing problems and he called the next ones before they arrived. Amazon carries
     no box either — its box appears on hover, which he checked himself. The clickable
     signal will be something of our own on hover, decided after the research; until it
     exists, these controls are bare.

     `border: 0` is NOT redundant: a <button> with no border declared falls back to the
     BROWSER'S own button border, which is what put a strange box back around the burger
     the moment ours was deleted. */
  border: 0;
  background: transparent;
  color: var(--cart-ink); text-decoration: none;
}
/* THE CART'S BOX IS SQUARE, WITH THE SAME AIR ON ALL FOUR SIDES (client, 2026-08-14).
   The first version gave it 10px at the sides and NOTHING top or bottom, so the trolley
   was jammed against two edges and floating between the other two — that asymmetry was
   invented, not required.

   THE BOX IS 48 AND THE TROLLEY SHRANK TO FIT IT — the client's explicit instruction,
   and the ONLY shrink authorised: the burger is untouched. 48 outer minus the 1px
   border on each side leaves 46 inside; the drawing sits at 44, which keeps exactly the
   1px gap to the border it had when the box was 52. That is the smallest reduction that
   fits — 4px off a 48px drawing, nothing taken for tidiness. */
.cart-link {
  right: var(--strip-gutter);
  width: var(--strip-btn-h); padding: 0;
}
.menu-btn  {
  left: var(--strip-gutter); padding: 0 10px;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.menu-glyph { display: block; width: var(--menu-icon-w); height: auto; }
/* 48px is the FINGER area and stays fixed; the trolley is drawn smaller inside it,
   because the standards size the target at 44-48 and the drawing itself at 24-32. */
.cart-glyph { position: relative; display: block; width: var(--cart-glyph); height: var(--cart-glyph); }
/* The viewBox is cropped to the drawing's own edges, so the trolley FILLS its box
   instead of hanging in empty air — otherwise it reads as sagging and any word
   placed beside it would align to the air rather than to the trolley. */
/* Every number below is the reference's own measurement scaled by 48/56 = 0.857,
   which is what makes the trolley fill the box's width. Trolley 56x39 -> 48x33.4,
   sitting at y 8.6 so the whole drawing (number included) is centred in the box. */
.cart-glyph svg {
  position: absolute; left: 0; display: block;
  top: calc(var(--cart-glyph) * 8.6 / 48);
  width: var(--cart-glyph);
  height: calc(var(--cart-glyph) * 33.4 / 48);
  /* No stroke width here on purpose: the traced path is FILLED, so the reference's
     own line weight is part of the shape and scales with it. --strip-stroke stays
     defined for anything drawn in the strip later, but the cart does not use it. */
}
/* THE COUNT — matched to the client's chosen reference (Amazon), not to a rule I
   invented. An earlier attempt sized the digit to exactly fill the basket's interior;
   he rejected it on sight as far too small, and he was right.

   How the reference actually behaves, and what these three numbers reproduce:
   the number is TALLER THAN THE WHOLE TROLLEY, its top clears the handle, and its
   bottom stops well short of the basket's floor rather than resting on it.

   In this 48px box the trolley spans y 8.25 to 39.75; its handle tops out at y 9.3
   and the basket's floor sits at y 31.2. So the digit's bottom at y 28 leaves a
   clear 3px above the floor, and at 31px its cap reaches y 6 — above the handle.
   The basket is open at the top precisely so this can happen with no line through it.

   All three numbers are MEASURED off the reference, scaled by the same 0.857:
     cap height   17 units -> 14.6px, so the font size is 14.6 / 0.70 = 21px
     horizontal   the digit's centre sits at 59.8% across the trolley, never at 50%
     vertical     its cap top is 3 units ABOVE the trolley's top, and its bottom
                  stops 13 units clear of the basket floor — it is not resting in
                  the basket, it straddles the open top. */
.cart-count {
  position: absolute; left: 59.8%; transform: translateX(-50%);
  bottom: calc(var(--cart-glyph) * 25 / 48);
  font-family: var(--disp); font-weight: 400; line-height: 1;
  font-size: calc(var(--cart-glyph) * 21 / 48);
  font-variant-numeric: tabular-nums;
  color: var(--cart-count-ink);
}

/* --- THE CART PANEL (2026-08-13) --------------------------------------------
   Drawn from the research and this brand, deliberately NOT from the checkout page
   — that page has never been audited and copying it would have baked an unreviewed
   design into the one control every visitor touches (client, 2026-08-13).

   IT IS A LIGHTER SURFACE, NOT A SHADOWED ONE. On dark interfaces a shadow barely
   registers; "this is above that" is said by making the raised surface lighter
   (Light_Motion_Perception.md §2). So --lift does the work and the shadow is only
   support. TRANSLUCENT on purpose, and that is a mechanism rather than an effect:
   a see-through layer separates itself from the page while keeping a visible
   reminder of what is behind, so the visitor does not lose their place — which is
   the entire argument for a panel instead of navigating away (§3).

   RIGHT EDGE TO RIGHT EDGE, opening leftward. The convention in every positioning
   engine is to align the panel's edge with the trigger's and shift only to stay on
   screen; the cart sits hard against the right gutter, so centring it under the
   cart would push half of it off, the shift would shove it back, and it would line
   up with nothing.

   `popover="auto"` gives click-outside and Escape for free, drawn by the browser.
   The UA stylesheet's own popover defaults (centred, inset 0, canvas background,
   solid border) MUST be overridden explicitly — that is the classic trap here. */
.cart-panel {
  position: fixed;
  inset: auto var(--strip-gutter) auto auto;   /* right-aligned; top set by script */
  z-index: 8;
  width: 380px; max-width: calc(100vw - 32px);
  margin: 0; padding: 0;
  /* FLUSH against the strip's bottom edge, no gap (client, 2026-08-13 — air above it
     read as detached). SQUARE TOP CORNERS so it grows out of the strip rather than
     sitting against it as a separate card, but a COMPLETE border on all four sides:
     the first attempt dropped the top border and let the strip's hairline serve as it,
     and the client saw a box missing one edge, not a joined object. */
  border: 1px solid var(--hair);
  border-radius: 0 0 4px 4px;
  background: var(--panel-bg);
  -webkit-backdrop-filter: blur(14px) saturate(1.08);
  backdrop-filter: blur(14px) saturate(1.08);
  box-shadow: 0 26px 64px rgba(0, 0, 0, 0.6), 0 2px 10px rgba(0, 0, 0, 0.42);
  color: var(--ink);
  overflow: visible;
}
.cart-panel::backdrop { background: transparent; }   /* non-modal: the page stays lit */

/* THE POINTER — a small square turned 45 degrees, poking up out of the panel's top
   edge at the trolley. Client asked for it from Amazon's own account flyout, and the
   design systems back it: a caret tip "closely associates the container with the
   related trigger", and they single out exactly this case — several controls sitting
   near each other, where without it the panel merely appears somewhere rather than
   visibly belonging to the thing that was clicked.

   IT POINTS AT THE BASKET, NOT AT THE MIDDLE OF THE BOX. First built at the geometric
   centre of the 48px control — 24px in from the panel's right edge — and the client saw
   it sitting off to the left. He is right, and the reason is already in our own
   measurements: the handle hangs off the trolley's left, so its visual mass is not
   centred in its box. That is the identical reason the COUNT sits at 59.8% across and
   never at 50%. The pointer takes the same 59.8%: 0.598 x 48 = 28.7px from the glyph's
   left, i.e. 19.3px in from the panel's right edge. Half the pointer is 5px, hence
   right: 14.3px.

   Only its top and left borders are drawn — those are the two edges facing out once
   it is rotated; the other two sit inside the panel and would print a line across it. */
.cart-panel::before {
  content: "";
  position: absolute; top: -5px; right: 14.3px;
  width: 10px; height: 10px;
  transform: rotate(45deg);
  background: var(--panel-bg);
  border-top: 1px solid var(--hair);
  border-left: 1px solid var(--hair);
}
@media (prefers-reduced-transparency: reduce) {
  .cart-panel::before { background: var(--lift); }
}

/* ARRIVING TAKES LONGER THAN LEAVING — 200ms in, 150ms out. A substantial change
   sits in the 200-300ms band, and things arriving want slightly longer than things
   leaving (Light_Motion_Perception.md §4). Never linear: linear motion reads as
   wrong. Only opacity and transform, so the whole thing rides the compositor.
   `allow-discrete` is what lets a popover animate at all — without it the browser
   flips display and the transition never runs. */
.cart-panel {
  opacity: 0; transform: translateY(-6px);
  transition: opacity 150ms ease-in, transform 150ms ease-in,
              overlay 150ms allow-discrete, display 150ms allow-discrete;
}
.cart-panel:popover-open {
  opacity: 1; transform: translateY(0);
  transition: opacity 200ms cubic-bezier(0.16, 0.84, 0.44, 1),
              transform 200ms cubic-bezier(0.16, 0.84, 0.44, 1),
              overlay 200ms allow-discrete, display 200ms allow-discrete;
}
@starting-style {
  .cart-panel:popover-open { opacity: 0; transform: translateY(-6px); }
}

/* The counterweight the research names: non-opaque surfaces genuinely hurt some
   people with vision problems, and the operating systems ship a switch for it. */
@media (prefers-reduced-transparency: reduce) {
  .cart-panel { background: var(--lift); -webkit-backdrop-filter: none; backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cart-panel, .cart-panel:popover-open { transform: none; }
}

/* THE LIST SCROLLS, THE MONEY AND THE WAYS OUT DO NOT. With two or three items a
   panel already runs past the bottom of a screen, so an unpinned checkout button
   has to be hunted for. The height is set so the FIFTH row is half visible — a
   peeking row is the honest signal that there is more, and it costs no chrome. */
.cart-list {
  list-style: none; margin: 0; padding: 0 18px;
  max-height: min(246px, 46vh); overflow-y: auto; overscroll-behavior: contain;
}
/* ONE LINE PER BEAT: name · licence · price · remove. The licence moved up out of a
   second line into space that was already empty, so the panel can now change a licence
   without being any taller than when it could not. */
.cart-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center; gap: 10px;
  padding: 13px 0; border-bottom: 1px solid var(--hair);
}
.cart-row:last-child { border-bottom: 0; }
.cart-row-title {
  font-family: var(--disp); font-size: 15px; font-weight: 600;
  letter-spacing: -0.01em; line-height: 1.25; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-decoration: none; transition: color 0.16s ease;
}
/* No underline at rest — several underlined titles would clutter a panel whose job is
   a glance — but it is a link and hovering says so. */
.cart-row-title:hover {
  color: var(--light); text-decoration: underline;
  text-underline-offset: 3px; text-decoration-color: var(--ink-dim);
}
/* Tapes: one licence, nothing to pick, so it stays a plain link. */
.cart-row-lic {
  font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none;
  border-bottom: 1px solid var(--hair); padding-bottom: 1px;
  transition: color 0.16s ease, border-color 0.16s ease;
}
.cart-row-lic:hover { color: var(--ink); border-bottom-color: var(--ink-dim); }

/* THE LICENCE PICKER. At rest it is one word and a caret — no box, no button chrome,
   nothing that competes with the beat's name. It has to read as a value you may change,
   not as a control demanding to be used, because this panel is an overview first. */
.cart-lic { display: inline-flex; min-width: 0; }
/* THE TYPE LIVES ON THE LABEL, not on the button, so a lone span carrying this class
   renders identically — that is what makes the off-screen measurement of "EXCLUSIVE"
   exact rather than approximate. --lic-w is set from that measurement, so the label is
   always exactly as wide as the longest word it can hold, and never wider. */
.cart-lic-now {
  display: inline-block;
  width: var(--lic-w, auto);
  font-family: var(--sans); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.12em; text-transform: uppercase;
  white-space: nowrap; text-align: left;
}
.cart-lic-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 5px 3px 6px; margin: 0;
  border: 1px solid transparent; border-radius: 3px; background: transparent;
  font-family: var(--sans); color: var(--ink-dim);
  cursor: pointer; white-space: nowrap;
  transition: color 0.16s ease, border-color 0.16s ease, background-color 0.16s ease;
}
.cart-lic-btn:hover { color: var(--ink); border-color: var(--hair);
  background: rgba(var(--light-rgb), 0.05); }
.cart-lic-btn[aria-expanded="true"] { color: var(--ink); border-color: var(--hair);
  background: rgba(var(--light-rgb), 0.07); }
.cart-lic-btn:focus-visible { outline: 2px solid rgba(var(--light-rgb), 0.6);
  outline-offset: 1px; }
.cart-lic-caret { width: 11px; height: 11px; flex: 0 0 auto;
  transition: transform 0.16s ease; }
.cart-lic-btn[aria-expanded="true"] .cart-lic-caret { transform: rotate(180deg); }

/* The list is a popover, so it sits in the TOP LAYER — that is what stops the
   scrolling row list from clipping it, and it is placed against the viewport by script
   at the moment it opens. Same surface language as the panel it belongs to. */
.cart-lic-list {
  position: fixed; inset: auto; margin: 0; padding: 4px;
  min-width: 190px; max-width: 260px;
  border: 1px solid var(--hair); border-radius: 4px;
  background: rgba(46, 46, 46, 0.94);   /* neutral lift: the panel is black, so the layer above it is grey with no hue (2026-08-18) */
  -webkit-backdrop-filter: blur(14px) saturate(1.08);
  backdrop-filter: blur(14px) saturate(1.08);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.6);
  color: var(--ink); overflow: visible;
}
.cart-lic-list:popover-open { display: block; }
@media (prefers-reduced-transparency: reduce) {
  .cart-lic-list { background: #2e2e2e; -webkit-backdrop-filter: none; backdrop-filter: none; }
}
.cart-lic-opt {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 7px 9px; border-radius: 3px; cursor: pointer;
}
.cart-lic-opt-name {
  font-size: 10.5px; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-dim);
}
.cart-lic-opt-price {
  font-family: var(--disp); font-size: 13px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink-dim);
}
/* `is-at` is where the keyboard cursor is; `aria-selected` is what is actually in the
   cart. The pattern keeps them separate, so they are shown differently: a lit row for
   the cursor, a brighter type for the one you own. */
.cart-lic-opt.is-at { background: rgba(var(--light-rgb), 0.09); }
.cart-lic-opt:hover { background: rgba(var(--light-rgb), 0.09); }
.cart-lic-opt[aria-selected="true"] .cart-lic-opt-name,
.cart-lic-opt[aria-selected="true"] .cart-lic-opt-price { color: var(--ink); }
.cart-lic-read {
  display: block; margin: 3px 0 1px; padding: 6px 9px 4px;
  border-top: 1px solid var(--hair);
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none;
  transition: color 0.16s ease;
}
.cart-lic-read:hover { color: var(--ink); }
/* Pinned to the measured width of "$10,000". The selector's POSITION is decided by what
   sits to its right, so if this cell shrank on an $80 row the selector would slide right
   of where the $10,000 row puts it. The price itself is unchanged — still right-aligned
   against the remove control. */
.cart-row-price {
  display: inline-block; min-width: var(--price-w, auto); text-align: right;
  font-family: var(--disp); font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink); white-space: nowrap;
}
/* The only x in the panel, so it cannot be misread as "close" — there is no close
   x, deliberately (client: on a cart an x reads as discard). */
.cart-row-kill {
  width: 26px; height: 26px; display: grid; place-items: center;
  padding: 0; border: 0; border-radius: 4px; background: transparent;
  color: var(--ink-dim); cursor: pointer;
  transition: color 0.16s ease, background-color 0.16s ease;
}
.cart-row-kill svg { width: 13px; height: 13px; }
.cart-row-kill:hover { color: var(--ink); background: rgba(var(--light-rgb), 0.07); }

/* UNDO, NOT "ARE YOU SURE?" — and it replaces the row in place, where the eye
   already is. No timer: a message that disappears before it is read is the
   documented failure of this whole family of patterns. */
.cart-row--undone { grid-template-columns: 1fr auto; }
.cart-undo-msg { font-size: 13px; color: var(--ink-dim); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The beat's name is the way back to hearing it. Underlined so it is visibly a link
   without becoming a third loud thing in a row that already ranks two controls. */
.cart-undo-name {
  color: var(--ink-dim); text-decoration: none;
  border-bottom: 1px solid var(--hair);
  transition: color 0.16s ease, border-color 0.16s ease;
}
.cart-undo-name:hover { color: var(--ink); border-bottom-color: var(--ink-dim); }
/* UNDO LEFT, DISMISS RIGHT — client's ruling 2026-08-13, and it OVERRULES the design
   systems, which say a notice like this carries one action and that action may never
   be "Dismiss". His reasoning is about the customer rather than the component: they
   clicked the x, the reasonable assumption is that they meant it, and a notice you
   cannot send away — where the only offered move is the opposite of what you just
   chose — is disrespectful of a competent person. So Dismiss keeps the primary
   right-hand slot and Undo is the exception on the left.

   Both stay words rather than an x: the only x in this panel means "remove this beat",
   and one glyph must not carry two meanings in one list. */
.cart-undo-acts { display: inline-flex; align-items: center; gap: 14px; }
.cart-undo-btn, .cart-undo-dismiss {
  padding: 0; border: 0; background: transparent; cursor: pointer;
  font-family: var(--sans); font-size: 12px; font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  transition: color 0.16s ease;
}
.cart-undo-btn { color: var(--cyan); }
.cart-undo-btn:hover { color: var(--cyan-hi); }
.cart-undo-dismiss { color: var(--ink-dim); font-weight: 400; }
.cart-undo-dismiss:hover { color: var(--ink); }

.cart-blank { padding: 26px 18px 22px; text-align: center; }
.cart-blank-head { margin: 0; font-family: var(--disp); font-size: 15px;
  font-weight: 600; color: var(--ink); }
.cart-blank-sub { margin: 7px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-dim); }

.cart-foot { padding: 14px 18px 18px; border-top: 1px solid var(--hair); }
.cart-total {
  display: flex; justify-content: space-between; align-items: baseline;
  margin: 0 0 14px;
}
/* "Total", not "Subtotal": there is no shipping and no VAT to add (§ 19 UStG), so
   this figure IS what will be charged. Saying subtotal would imply a surprise. */
.cart-total-label {
  font-size: 10.5px; font-weight: 500; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink-dim);
}
.cart-total-sum {
  font-family: var(--disp); font-size: 20px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink);
}

/* THREE WAYS OUT, one obviously louder. Never two equals side by side: the loud one
   carries the gold reserved site-wide for paid actions, the other two are quiet text.
   Vague or missing paths out of a cart panel is the specific thing usability testing
   recorded as stranding people. */
.cart-exits { display: flex; flex-direction: column; gap: 12px; }
.cart-checkout { width: 100%; justify-content: center; padding: 13px 20px; }
.cart-exits-quiet { display: flex; justify-content: space-between; align-items: center; }
.cart-quiet {
  padding: 0; border: 0; background: transparent; cursor: pointer;
  font-family: var(--sans); font-size: 12px; letter-spacing: 0.02em;
  color: var(--ink-dim); text-decoration: none;
  transition: color 0.16s ease;
}
.cart-quiet:hover { color: var(--ink); }
/* Built, not wired: /cart/ does not exist yet (client 2026-08-13 — build every exit
   now, connect this one when its page arrives). It reads as a label, not as a button
   that ignores you. GIVE IT AN href AND DELETE THIS RULE when the page ships. */
.cart-quiet--pending { opacity: 0.42; cursor: default; }
.cart-quiet--pending:hover { color: var(--ink-dim); }

/* An empty cart shows the total resting at zero — that is the point of letting the
   panel open empty — but never a checkout button that cannot work. Nothing is greyed
   out: a disabled control loses its contrast and drops out of keyboard reach, so the
   dead action is simply absent. */
.cart-exits[data-empty="1"] .cart-checkout,
.cart-exits[data-empty="1"] .cart-quiet--pending { display: none; }
.cart-exits[data-empty="1"] .cart-exits-quiet { justify-content: flex-end; }

.topbar { display: flex; align-items: center; gap: 12px; }

#logo { width: clamp(120px, 16vw, 176px); height: auto; aspect-ratio: 1 / 1; border-radius: 50%; object-fit: contain; display: block; }

/* Tongue "peek" — rests ~3/4 out; the last quarter slides out on play and
   retracts a touch quicker on pause. Enabled per page via body[data-tongue].
   NOTE (2026-07-02): the retracted 3/4 rest state + clip are now BAKED INTO
   logo-round.svg itself (attributes on #tongue-window / #tongue-group), so the
   first-paint <img> already shows the correct rest state — no load flash.
   Without a data-tongue attribute the tongue therefore rests 3/4 out (a page
   wanting it fully out must set #tongue-group { transform: translate(0,0) }). */
#tongue-group { transform-box: fill-box; transform-origin: 50% 0%; }
body[data-tongue="peek"] #tongue-group { transform: scaleY(0.75); transition: transform 200ms cubic-bezier(.4,0,1,1); }
body[data-tongue="peek"].sb-playing #tongue-group { transform: scaleY(1); transition: transform 500ms cubic-bezier(.22,.68,.28,1); }
@media (prefers-reduced-motion: reduce) {
  body[data-tongue="peek"] #tongue-group,
  body[data-tongue="peek"].sb-playing #tongue-group { transition: none; }
}

/* Tongue "slide" — the rigid, premium version: the tongue translates behind a
   fixed clip at the mouth line (y=820), so it genuinely slides with no stretch.
   The 3/4-out REST state is the SVG's own default (baked attributes: clip on
   #tongue-window, translate(8.4 -55.3) on #tongue-group) — CSS only declares the
   return transition. Play overrides to translate(0,0) = fully out (2.35s); pause
   falls back to the baked attribute (0.45s). */
body[data-tongue="slide"] #tongue-group { transition: transform 450ms cubic-bezier(.4,0,1,1); }
body[data-tongue="slide"].sb-playing #tongue-group { transform: translate(0px, 0px); transition: transform 2350ms cubic-bezier(.22,.68,.28,1); }
@media (prefers-reduced-motion: reduce) {
  body[data-tongue="slide"] #tongue-group { transition: none; }
}
.topbar .wordmark { font-family: var(--disp); font-size: 19px; font-weight: 600; letter-spacing: -0.01em; }
/* The divider can no longer be this element's left border: with baseline alignment
   the tagline's BOX sits lower than the wordmark's, so a border on it would hang
   ~5px low. It is a pseudo-element instead, anchored to the shared baseline and
   sized to the wordmark's cap height. Both numbers are em of the TAGLINE, and the
   wordmark:tagline size ratio is a constant 1.81 in all three variants (inner page
   19/10.5, homepage 26.6/14.7, beat page 1.19rem/0.66rem) — so one pair of values
   holds everywhere and nothing needs a per-variant override. */
.topbar .tagline {
  position: relative;
  /* Optical nudge, the user's call by eye (2026-07-24): mathematically centred, the
     tagline reads too HIGH against a mixed-case wordmark, because "SchoenerBeats" is
     mostly lowercase so its visual mass sits below its cap-centre. The divider is a
     child of this element, so it inherits the nudge automatically — the two must
     never be shifted separately. */
  top: 2px;
  margin-left: 3px; padding-left: 14px;
  font-size: 10.5px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-dim);
}
.topbar .tagline::before {
  content: ""; position: absolute; left: 0;
  /* Centred on the cap-centre, NOT anchored to the baseline — anchoring it to the
     baseline is what left it floating 4.4px high on beat pages and 6.1px high on the
     homepage (measured, and the user spotted it by eye before it was measured).
     Because the row is box-centred and the cap-centres coincide, the tagline's own
     box centre IS the shared cap-centre, so 50% is the right anchor. */
  top: 50%; transform: translateY(-50%);
  /* the WORDMARK's cap height, in tagline em: 13.31/10.56 on beat pages and
     18.61/14.70 on the homepage both give 1.26 — one value covers every variant. */
  height: 1.26em;
  border-left: 1px solid var(--hair);
}
/* Homepage only: the brand line is the first thing a new visitor reads, so it runs
   1.4x the size it takes on the inner pages (user 2026-07-19). */
.topbar--home .wordmark { font-size: 26.6px; }
.topbar--home .tagline { font-size: 14.7px; padding-left: 18px; }

/* The store description, directly under the brand line and centred with it. It sits
   OUTSIDE .topbar (see the note in catalog.html): .stage centres its children by
   shrink-to-fit, so a full-width child inside the brand row would stretch that row
   and left-align the wordmark. The collapsed tail is display:none — a normal
   disclosure, which search engines read in full from the source; what they penalise
   is text hidden to stuff keywords, not a control the visitor can open. */
.subtitle {
  margin: 8px 0 0; max-width: 620px; text-align: center;
  font-size: 13.5px; line-height: 1.5; color: var(--ink-dim);
}
.subtitle-rest { display: none; }
.subtitle.is-open .subtitle-rest { display: inline; }
/* The dots belong to the word they follow ("beats…", never "beats …"), so the button
   carries NO left margin while collapsed. Open, its label is a collapse caret sitting
   after a full stop, where a gap is correct — hence the margin only in that state. */
.subtitle-toggle {
  margin-left: 0; padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; line-height: 1; color: var(--ink-dim);
  transition: color 0.18s ease;
}
.subtitle-toggle:hover { color: var(--ink); }
.subtitle.is-open .subtitle-toggle { margin-left: 6px; font-size: 11px; vertical-align: 1px; }

.middle {
  flex: 1 1 auto; width: 100%; max-width: 720px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 22px;
}
.title {
  margin: 0; font-family: var(--disp);
  /* R19 — smaller so the licenses stay above the fold on a laptop. */
  /* FIXED BOX, not a fixed font: the title block is always the same height on every
     beat page, so every first screen ends on the same line no matter how long the
     name is. The text is centred inside it, and --title-scale (build.py) shrinks a
     long name until it fits that box instead of pushing the page down. */
  height: var(--title-box, 22vh); min-height: var(--title-box-min, 132px);
  display: flex; align-items: center; justify-content: center;
  font-size: clamp(calc(44px * var(--title-scale, 1)), calc(8vw * var(--title-scale, 1)),
                   calc(96px * var(--title-scale, 1)));
  font-weight: 600; line-height: 0.92; letter-spacing: -0.03em;
}
.specs {
  margin: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 22px;
  font-size: 12.5px; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-dim);
}
.specs .spec { position: relative; }
.specs .spec + .spec::before { content: ""; position: absolute; left: -12px; top: 50%; width: 4px; height: 4px; border-radius: 50%; background: rgba(var(--light-rgb), 0.5); transform: translateY(-50%); }

/* acquire — confident and clear */
.acquire { margin-top: 14px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.price-line { display: flex; align-items: baseline; gap: 12px; }
.price { font-family: var(--disp); font-size: 28px; font-weight: 600; letter-spacing: -0.01em; }
.price-meta { font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-dim); }
.cta {
  display: inline-flex; align-items: center; gap: 12px;
  padding: 15px 30px; border-radius: 4px;
  background: var(--cta); color: var(--cta-ink);
  font-size: 13px; font-weight: 600; letter-spacing: 0.04em;
  text-decoration: none; cursor: pointer;
  transition: transform 0.22s cubic-bezier(.2,.7,.2,1), box-shadow 0.22s ease;
}
.cta:hover { transform: translateY(-2px); box-shadow: 0 16px 44px rgba(0,0,0,0.45); }
.cta:active { transform: translateY(0); }
.cta .arrow { width: 15px; height: 15px; }

/* Gold buy CTA — the one warm element in a cold room; reserved for purchases.
   DEPTH RULE (user, 2026-07-05): gold is NEVER one color — uneven multi-stop
   gradients, darker and quieter than instinct says, an off-center sheen layered
   on top, dark edges. Even/symmetric stops or bright saturation = plastic. */
/* Buy CTA — NOT a bright surface (reads fake). A dark, translucent panel that
   sits in the room, gold-lettered, held by a thin gold edge and a RESTRAINED,
   uneven, DARK gold glow (offset shadows in the deep-gold tone, never symmetric,
   never bright). Subtle wins. */
.cta--gold {
  background: rgba(23, 31, 38, 0.66); color: var(--gold-hi);
  border: 1px solid rgba(var(--gold-rgb), 0.42); backdrop-filter: blur(3px);
  box-shadow: -6px 7px 26px rgba(123, 101, 69, 0.14),
    7px -2px 20px rgba(123, 101, 69, 0.17), inset 0 1px 0 rgba(var(--gold-rgb), 0.12);
}
.cta--gold:hover { transform: translateY(-2px); border-color: rgba(var(--gold-rgb), 0.7);
  box-shadow: -6px 7px 26px rgba(123, 101, 69, 0.20),
    7px -2px 20px rgba(123, 101, 69, 0.24), 0 14px 40px rgba(0, 0, 0, 0.45); }

/* Metallic gold TEXT — uneven stops + off-center sheen, but NARROW range: no stop
   is far brighter than its neighbours (no blown highlight). Darkest #c6af7e ->
   softest #d4c28e. Depth from movement, not from a bright spot. */
.gold-metal {
  background: linear-gradient(103deg, #c6af7e 0%, #a18854 30%, #d4c28e 50%, #a18854 70%, #c6af7e 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* --- THE MENU PANEL (2026-08-14) --------------------------------------------
   The cart panel's twin, mirrored to the left: same material, same manners, same
   pointer. Everything load-bearing about that design is argued where it was decided
   (see the cart panel above and Light_Motion_Perception.md) — it is not re-argued
   here, it is deliberately reused, because one strip must teach one rule.

   LEFT EDGE TO LEFT EDGE, opening rightward — the mirror of the cart's right-to-right.
   The convention in every positioning engine is to align the panel's edge with its
   trigger's and shift only to stay on screen.

   `top` is the strip's RESTING bottom edge, which is what a visitor with JavaScript
   switched off gets and is correct at the top of every page; the script then keeps it
   on the strip's live edge as the strip folds. */
.menu-panel {
  box-sizing: border-box;
  position: fixed;
  top: var(--strip-h); left: var(--strip-gutter); right: auto; bottom: auto;
  z-index: 8;
  width: 232px; max-width: calc(100vw - 32px);
  margin: 0; padding: 8px 0;
  border: 1px solid var(--hair);
  border-radius: 0 0 4px 4px;
  background: var(--menu-bg);
  -webkit-backdrop-filter: blur(14px) saturate(1.08);
  backdrop-filter: blur(14px) saturate(1.08);
  box-shadow: 0 26px 64px rgba(0, 0, 0, 0.6), 0 2px 10px rgba(0, 0, 0, 0.42);
  color: var(--ink);
  overflow: visible;
}
.menu-panel::backdrop { background: transparent; }

/* THE POINTER — the cart's, mirrored. --tip-x is MEASURED off the button by the
   script and is the button's centre relative to the panel's left edge, so the tip
   still lands on the button after any resize. The value here is only the no-script
   resting one: half of the button's current 54px box. */
.menu-panel {
  --tip-x: 27px;
}
.menu-panel::before {
  content: "";
  position: absolute; top: -5px; left: calc(var(--tip-x) - 5px);
  width: 10px; height: 10px;
  transform: rotate(45deg);
  background: var(--menu-bg);
  border-top: 1px solid var(--hair);
  border-left: 1px solid var(--hair);
}
@media (prefers-reduced-transparency: reduce) {
  .menu-panel::before { background: var(--lift); }
  .menu-panel { background: var(--lift); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* Arriving takes longer than leaving — the cart panel's timings, unchanged. */
.menu-panel {
  opacity: 0; transform: translateY(-6px);
  transition: opacity 150ms ease-in, transform 150ms ease-in,
              overlay 150ms allow-discrete, display 150ms allow-discrete;
}
.menu-panel:popover-open {
  opacity: 1; transform: translateY(0);
  transition: opacity 200ms cubic-bezier(0.16, 0.84, 0.44, 1),
              transform 200ms cubic-bezier(0.16, 0.84, 0.44, 1),
              overlay 200ms allow-discrete, display 200ms allow-discrete;
}
@starting-style {
  .menu-panel:popover-open { opacity: 0; transform: translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
  .menu-panel, .menu-panel:popover-open { transform: none; }
}

/* THE FALLBACK for a browser with no popover support — the panel is a plain element
   the script shows and hides. Written as "is-shut" rather than "is-open" so that a
   browser which never runs the script leaves the menu OPEN and reachable rather than
   sealed shut with no way in. */
.menu-panel.is-shut { opacity: 0; transform: translateY(-6px); visibility: hidden; }
.menu-panel:not([popover]) { display: block; }

/* THE TWO GROUPS, separated by SPACE and no divider line. Spacing is the strongest
   grouping signal there is and overpowers colour and shape; a border between them is
   the cautious move that usually only adds clutter. The personal group is one step
   quieter in size and shade — never in colour alone, which reads as decoration and
   fails anyone who cannot distinguish it. */
.menu-list { list-style: none; margin: 0; padding: 0; }
.menu-list--quiet { margin-top: 14px; }
.menu-item {
  display: flex; align-items: center;
  min-height: 44px; padding: 0 18px;
  font-family: var(--disp); font-size: 15px; font-weight: 600;
  color: var(--ink); text-decoration: none;
}
.menu-list--quiet .menu-item { font-size: 14px; font-weight: 500; color: var(--ink-dim); }
.menu-item:hover, .menu-item:focus-visible { background: rgba(var(--light-rgb), 0.06); }
.menu-list--quiet .menu-item:hover, .menu-list--quiet .menu-item:focus-visible { color: var(--ink); }

/* WHERE AM I — the question the field treats as a menu's job, not a nicety. Marked
   with a rule down the leading edge plus full-strength ink, so it survives being
   read by someone who cannot separate the two shades. */
.menu-item[aria-current="page"] {
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--cyan);
}

/* --- The pool (player) ------------------------------------------------------ */
/* Paint order and click order are SPLIT here on purpose. The dock's box is full
   height, but the water only ever reaches ~40% down it (site.js: base = H * 0.40),
   so the top of the box paints nothing. A canvas hit-tests its whole box whatever
   it painted, so that transparent strip used to swallow every click — 74px of it at
   1280x800, which killed buttons that were plainly visible above the water. The
   dock and the canvas therefore take NO clicks; only the play button and the seek
   strip below do. Anything given pointer events in here re-creates that dead band —
   see the 2026-07-21 lesson in Manuals/Store/Section_Sizing.md. */
.dock { position: fixed; left: 0; right: 0; bottom: 0; z-index: 3; height: var(--dock-h); min-height: 140px; pointer-events: none;
  /* The always-wet band: everything below the deepest trough the waves can reach.
     Both the seek strip and the controls are bound to it, so the player LIVES in the
     water instead of poking out of it, and the click area matches what you can see. */
  --wet-h: calc(60% - 1.8832 * min(10px, 5%)); }
/* A canvas hit-tests its WHOLE box regardless of what it painted: #water fills
   .dock but paints only the lower part, so the transparent top was silently
   eating clicks on anything above the waterline (a tier button read as clear by
   eye was dead). Never reason about this player's click behaviour from what is
   on screen. Clicks are handled by .dock-seek below, not here.
   Section_Sizing 2026-07-21. */
#water { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* The seek strip covers exactly the water that is ALWAYS wet: everything below the
   deepest trough the waves can reach. Trough = 40% + 1.8832 x the wave amplitude,
   the amplitude being min(10px, 5% of the dock) and 1.8832 being the three waves at
   full energy in phase (amps 1.76 x the 1.07 energy multiplier). Above that line the
   water is only sometimes there, so it stays click-through and the page keeps it. */
.dock-seek { position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--wet-h); pointer-events: auto; cursor: pointer; }
/* ONE ROW, not a stack: the track name sits BESIDE the button, so it costs zero
   vertical space. Three columns (1fr auto 1fr) hold the button dead centre whatever
   the name's length; column 3 stays empty until the tape name lands there.
   This is also what kills the crush — in the old fixed-height COLUMN the name
   inherited flex-shrink and was squeezed to a 3.8px sliver on short windows
   (2026-07-22 lesson, Manuals/Store/Section_Sizing.md).

   Still bound to the always-wet band, NOT to the whole dock: centred in the full box
   the play button floated above the waterline (7.6px above it at 1280x720, 23px above
   the trough). The 48px button is the one element that must not shrink — 44px is the
   accessible-touch-target floor. */
.dock-ui { position: absolute; inset: auto 0 0 0; height: var(--wet-h);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  column-gap: 16px; padding: 0 32px 14px; pointer-events: none; }
.play-btn {
  /* Column 2 explicitly: the button comes FIRST in the DOM (it is the control that
     matters), so without this it would take column 1 and sit hard left. */
  grid-column: 2; grid-row: 1;
  pointer-events: auto; flex: 0 0 auto; width: 48px; height: 48px; border-radius: 50%;
  border: 1px solid rgba(var(--light-rgb), 0.5); background: rgba(11, 16, 20, 0.4);
  color: var(--ink); cursor: pointer; display: grid; place-items: center;
  backdrop-filter: blur(4px); transition: border-color 0.22s ease, background-color 0.22s ease, transform 0.12s ease;
}
.play-btn:hover { border-color: rgba(var(--light-rgb), 0.95); background: rgba(var(--light-rgb), 0.1); }
.play-btn:active { transform: scale(0.96); }
.play-btn svg { width: 19px; height: 19px; }
.play-btn .icon-pause { display: none; }
.dock.is-playing .play-btn .icon-play { display: none; }
.dock.is-playing .play-btn .icon-pause { display: block; }
.times { pointer-events: none; position: absolute; right: 32px; top: 50%;
  transform: translateY(-50%); font-size: 11px; letter-spacing: 0.12em; font-variant-numeric: tabular-nums; color: var(--ink-dim); }
.times .sep { opacity: 0.45; margin: 0 7px; }

/* --- Step-4 additions: nav, blurb, tier selector, listings, footer ---------- */
.home-link { color: inherit; text-decoration: none; }

.blurb { margin: 0; max-width: 520px; font-size: 14px; line-height: 1.55; color: var(--ink-dim); }

.tiers { display: flex; flex-direction: column; gap: 8px; width: min(440px, 88vw); }
.tier {
  display: flex; align-items: baseline; gap: 14px; text-align: left;
  padding: 12px 16px; border-radius: 4px; cursor: pointer;
  border: 1px solid var(--hair); background: rgba(11, 16, 20, 0.35);
  color: var(--ink); font-family: var(--sans);
  transition: border-color 0.18s ease, background-color 0.18s ease;
}
.tier:hover { border-color: rgba(var(--light-rgb), 0.45); }
.tier.is-selected { border-color: rgba(var(--light-rgb), 0.9); background: rgba(var(--light-rgb), 0.07); }
.tier--unavailable { opacity: 0.35; cursor: default; }
.tier .tier-label { font-size: 13px; font-weight: 600; letter-spacing: 0.02em; }
.tier .tier-delivers { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-dim); }
.tier .tier-price { margin-left: auto; font-family: var(--disp); font-size: 16px; font-weight: 600; }
/* No gold price text in the selector — instead the SELECTION frame itself turns
   metallic when Exclusive is the picked tier (frame varies by license). */
.tier.is-selected[data-tier="exclusive"] {
  border-color: transparent;
  background:
    linear-gradient(rgba(20, 27, 33, 0.92), rgba(20, 27, 33, 0.92)) padding-box,
    linear-gradient(155deg, rgba(158, 134, 83, 0.52) 0%, rgba(109, 88, 58, 0.34) 30%,
      rgba(187, 169, 120, 0.42) 50%, rgba(109, 88, 58, 0.32) 70%,
      rgba(158, 134, 83, 0.46) 100%) border-box;
}

.microcopy { margin: 0; font-size: 11px; letter-spacing: 0.06em; color: var(--ink-dim); }
.tape-cta {
  font-size: 12px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none; border-bottom: 1px solid var(--hair);
  padding-bottom: 2px; transition: color 0.18s ease, border-color 0.18s ease;
}
.tape-cta:hover { color: var(--ink); border-color: rgba(var(--light-rgb), 0.6); }

/* listing pages (catalog + tape) */
.stage--list { justify-content: flex-start; }
.listing { width: 100%; max-width: 640px; margin: 0 auto; padding-top: 34px;
  display: flex; flex-direction: column; align-items: center; gap: 26px; }
.logo--list { width: clamp(96px, 12vw, 132px); }
/* 2026-07-20: px, not vw. Browser zoom scales px and leaves vw alone, so a vw
   title drifted against the px tracklist and blurb every time the user zoomed.
   78px is the size in the approved Afro Dance capture. Narrow screens step at
   the site's existing 720px breakpoint, below. */
/* The title occupies a RESERVED TWO-LINE BOX on every tape, so all four pages open
   with the cover, blurb, pill and tracklist at identical heights. The TYPE SIZE is
   constant across tapes and the name is centred in the box - never shrink a long
   name to force one line, which would make each page's title a different size.
   Box = 2 lines x 0.95 line-height x the font size. Sized in rem: see the
   TAPE-PAGE GEOMETRY block at the end of this file. */
.tape-title { margin: 0; font-family: var(--disp); font-size: 4.875rem;
  font-weight: 600; line-height: 0.95; letter-spacing: -0.03em; text-align: center;
  min-height: calc(4.875rem * 0.95 * 2);
  display: flex; align-items: center; justify-content: center; }
.tracks { list-style: none; margin: 0; padding: 0; width: 100%;
  display: flex; flex-direction: column; }
.track-row { display: flex; align-items: center; gap: 13px; padding: 14px 6px;
  border-bottom: 1px solid var(--hair); }
/* R60.5 — the flex rule above overrides the UA's [hidden] default; the search
   filter sets [hidden] per row (site.js setupFilter), so it must win here.
   2026-08-06: this used to be scoped to .track-row alone, which meant EVERY other
   component with its own `display` quietly ignored being hidden — on /checkout/
   the details form stayed on screen underneath the card fields. Any element with a
   `display` rule and a hidden state has the same hole, so the guard is universal now. */
[hidden] { display: none !important; }
/* Direct child only: on the catalog the rows sit inside per-collection groups, where
   only the first row of the whole list is capped (rule in the catalog block below). */
.tracks > .track-row:first-child { border-top: 1px solid var(--hair); }
.track-row.is-current .row-title { color: var(--ink); }
.row-play { flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid rgba(var(--light-rgb), 0.4); background: transparent;
  color: var(--ink); cursor: pointer; display: grid; place-items: center;
  transition: border-color 0.18s ease, background-color 0.18s ease; }
.row-play:hover { border-color: rgba(var(--light-rgb), 0.95); background: rgba(var(--light-rgb), 0.08); }
.row-play svg { width: 15px; height: 15px; }
.row-play .icon-pause { display: none; }
body.sb-playing .track-row.is-current .row-play .icon-play { display: none; }
body.sb-playing .track-row.is-current .row-play .icon-pause { display: block; }
.row-title { margin-right: auto; min-width: 0; font-family: var(--disp); font-size: 19px; font-weight: 600;
  letter-spacing: -0.01em; color: var(--ink); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row-title:hover { text-decoration: underline; text-underline-offset: 4px; }
.row-specs { font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-dim); white-space: nowrap; }
/* The collection portion of the row label is a real link to its tape page. */
.row-specs .row-collection { color: inherit; text-decoration: none; }
.row-specs .row-collection:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }

.quiet { margin: 4px 0 0; }
/* Trailing nav links (Store terms · Privacy / Back to the catalog) centered on
   content pages — they're nav, not prose; long-form prose stays left (R54). */
.content .quiet { text-align: center; }
.quiet-link { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none; }
.quiet-link:hover { color: var(--ink); }

/* Column 1 of the dock grid, pushed right so it sits against the button. min-width:0
   is what lets a grid item shrink at all — without it the name would push the button
   off centre instead of ellipsing. */
.dock-title { pointer-events: none; font-family: var(--disp); font-size: 12px;
  font-weight: 600; letter-spacing: 0.02em; color: var(--ink); text-align: left;
  grid-column: 1; grid-row: 1; justify-self: end; min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.footer { width: 100%; margin-top: 40px; padding: 18px 0 8px; }
.footer-nav { display: flex; justify-content: center; flex-wrap: wrap; gap: 22px; }
.footer-nav a { font-size: 10.5px; font-weight: 500; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-dim); text-decoration: none; }
.footer-nav a:hover { color: var(--ink); }

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

/* --- Step-5: static content pages (licenses / about / services) ------------- */
.content { max-width: 620px; align-items: stretch; }
.content .tape-title { text-align: left; font-size: clamp(38px, 7vw, 64px); }
.content .prose { margin: 0; max-width: none; font-size: 15px; line-height: 1.6; color: var(--ink); }
.content .prose a { color: var(--ink); }
.content .section-label { margin: 6px 0 -12px; font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-dim); }
.content .tiers { width: 100%; }
.content .tier-terms { margin: -4px 2px 4px; max-width: none; line-height: 1.5; }
.content ul.plain { list-style: none; margin: 0; padding: 0; width: 100%;
  display: flex; flex-direction: column; gap: 12px; }
.content ul.plain li { position: relative; padding-left: 18px; font-size: 15px;
  line-height: 1.55; color: var(--ink); }
.content ul.plain li::before { content: ""; position: absolute; left: 0; top: 9px;
  width: 5px; height: 5px; border-radius: 50%; background: rgba(var(--light-rgb), 0.6); }
.content .newsletter { width: 100%; padding: 18px 20px; border: 1px solid var(--hair);
  border-radius: 4px; background: rgba(11, 16, 20, 0.35); }
.content .socials { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 14px; }
.content .socials a { font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none; border-bottom: 1px solid var(--hair); padding-bottom: 2px; }
.content .socials a:hover { color: var(--ink); }
.content .cta { align-self: flex-start; }

/* §9.3 — verbatim license text (Read Contract pages) + buy-surface acceptance */
.content .contract { white-space: pre-wrap; overflow-wrap: break-word; margin: 4px 0 0;
  font-family: var(--sans); font-size: 12.5px; line-height: 1.6; color: var(--ink); }
.sec--license .accept-copy { margin-top: -8px; }
.read-contract { margin-top: -8px; }
/* Never browser default link-blue on the dark room — visible, on-palette. */
.accept-copy .read-contract { color: var(--ink); text-decoration: underline;
  text-underline-offset: 3px; text-decoration-color: rgba(var(--light-rgb), 0.5); }
.accept-copy .read-contract:hover { text-decoration-color: var(--ink); }

@media (max-width: 520px) {
  :root { --dock-h: 18vh; }
  .title { font-size: clamp(calc(40px * var(--title-scale, 1)), calc(13vw * var(--title-scale, 1)),
                            calc(64px * var(--title-scale, 1))); }
  /* Narrower side padding on phones — but the 14px BOTTOM reserve must survive, or
     the track name lands flush against the bottom of the screen again (it read 2.5px
     here before this line carried the bottom value). */
  .dock-ui { padding: 0 18px 14px; }
  .topbar .tagline { display: none; }
  .row-specs { font-size: 10px; letter-spacing: 0.06em; }
  .dock-title { max-width: 100%; }
  /* Phones: drop the tape name (last spec) to its own centred line and hide its
     leading separator dot, so the mechanical line reads BPM · KEY · GENRE over
     COLLECTION instead of wrapping with a stray dot (2026-07-23). */
  .middle--beat .specs .spec--collection { flex-basis: 100%; }
  .middle--beat .specs .spec--collection::before { display: none; }
  /* .tier-cards phone rule MOVED — it sat here, ABOVE the .tier-cards base rule,
     so at equal specificity the base always won and phones silently kept 4
     columns (72px wide). It now lives directly after the base block. */
  .collections { grid-template-columns: 1fr 1fr; }
}
@media (prefers-reduced-motion: reduce) { #lightfx { display: none; } }

/* --- Consent banner (Blueprint §10.4) — injected by the _head.html loader for
   EEA/UK/CH visitors or on geo-fetch failure. Never blocks playback or buying. -- */
/* KNOWN + DEFERRED — do NOT "fix" this in passing (found in four separate
   sessions before it was written down). At bottom:16px this banner overlaps the
   player and covers the play button until dismissed. The user considers a
   banner that blocks until clicked away to be CORRECT behaviour; only its
   PLACEMENT is open, to be decided together with whether the banner is legally
   required at all — after the player work. Niche_Knowledge 2026-07-22. */
.sb-consent {
  position: fixed; z-index: 60; left: 50%; bottom: 16px; transform: translateX(-50%);
  width: min(680px, calc(100vw - 24px));
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 14px 18px; border: 1px solid var(--hair); border-radius: 6px;
  background: rgba(11, 16, 20, 0.94); backdrop-filter: blur(8px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.sb-consent-text { margin: 0; flex: 1 1 260px; font-family: var(--sans);
  font-size: 12.5px; line-height: 1.5; color: var(--ink-dim); }
.sb-consent-text a.sb-consent-link { color: var(--ink); text-decoration: underline; }
.sb-consent-btns { display: flex; gap: 10px; margin-left: auto; }
.sb-consent-btn { font-family: var(--disp); font-size: 13px; font-weight: 600;
  padding: 8px 16px; border-radius: 4px; cursor: pointer; border: 1px solid var(--hair);
  background: transparent; color: var(--ink); }
.sb-consent-btn:hover { border-color: var(--ink-dim); }
.sb-consent-yes { background: var(--cta); color: var(--cta-ink); border-color: var(--cta); }
.sb-consent-yes:hover { border-color: var(--cta); filter: brightness(0.94); }
@media (max-width: 520px) {
  .sb-consent { bottom: 0; border-radius: 6px 6px 0 0; width: 100vw; }
  .sb-consent-btns { width: 100%; }
  .sb-consent-btn { flex: 1; }
}

/* --- Newsletter (steps 5c/5b) — the shared form partial + one-of-us success --- */
.nl-form { width: 100%; max-width: 440px; display: flex; flex-direction: column; gap: 12px; margin: 0; }
.nl-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.nl-field { display: flex; gap: 10px; flex-wrap: wrap; }
.nl-email {
  flex: 1 1 200px; min-width: 0; padding: 13px 16px; border-radius: 4px;
  border: 1px solid var(--hair); background: rgba(11, 16, 20, 0.5); color: var(--ink);
  font-family: var(--sans); font-size: 14px;
}
.nl-email::placeholder { color: var(--ink-dim); }
.nl-email:focus { outline: none; border-color: rgba(var(--light-rgb), 0.7); }
.nl-submit { flex: 0 0 auto; padding: 13px 22px; }
.nl-trade { margin: 0; }
/* Turnstile renders nothing until a site key is wired — don't leave a gap. */
.cf-turnstile:empty { display: none; }

/* Beat-page free-download gate — a no-JS <details> next to the buy CTA (§9.10). */
.nl-gate { width: min(440px, 88vw); border: 1px solid var(--hair); border-radius: 4px;
  background: rgba(11, 16, 20, 0.35); }
.nl-summary { list-style: none; cursor: pointer; padding: 12px 16px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink); }
.nl-summary::-webkit-details-marker { display: none; }
.nl-summary::after { content: "+"; float: right; color: var(--ink-dim); font-weight: 400; }
.nl-gate[open] .nl-summary::after { content: "\2013"; }
.nl-reveal { padding: 0 16px 16px; }
.nl-reveal .nl-form { max-width: none; }

.nl-block { width: 100%; max-width: 440px; display: flex; flex-direction: column; gap: 12px; }

/* one-of-us success page */
.promo { display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; width: 100%; max-width: 440px;
  padding: 18px 20px; border: 1px solid transparent; border-radius: 4px;
  background:
    linear-gradient(rgba(20, 27, 33, 0.82), rgba(20, 27, 33, 0.82)) padding-box,
    linear-gradient(155deg, rgba(158, 134, 83, 0.5) 0%, rgba(109, 88, 58, 0.32) 30%,
      rgba(187, 169, 120, 0.4) 50%, rgba(109, 88, 58, 0.3) 70%,
      rgba(158, 134, 83, 0.44) 100%) border-box; }
.promo-label { font-size: 10.5px; font-weight: 600; letter-spacing: 0.18em;
  text-transform: uppercase; color: rgba(var(--gold-rgb), 0.8); }
.promo-code code.gold-metal { color: transparent; }
.promo-code { display: flex; align-items: center; gap: 14px; }
/* F1.1 — the coupon code carries the reward: bigger + wider-tracked, gold clip kept. */
.promo-code code { font-family: var(--disp); font-size: 34px; font-weight: 600;
  letter-spacing: 0.18em; color: var(--ink); }
.copy-btn { font-family: var(--disp); font-size: 12px; font-weight: 600; cursor: pointer;
  padding: 7px 14px; border-radius: 4px; border: 1px solid var(--hair);
  background: transparent; color: var(--ink); }
.copy-btn:hover { border-color: var(--ink-dim); }
.nl-download { margin-top: 4px; }

/* Thank-you license block + "Before you go" — centered like the rest (R53/R54). */
.license-dl { width: 100%; max-width: 440px; display: flex; flex-direction: column;
  align-items: center; text-align: center; gap: 10px; }
.license-dl .license-form { width: 100%; }
.license-dl .nl-field { justify-content: center; }
.utility .nl-block { align-items: center; text-align: center; }
/* Utility-page message/blurb centered like the rest of these pages (thank-you). */
.utility .blurb { text-align: center; }

/* F1.1 — post-conversion pages -------------------------------------------- */
/* Signature line, styled from .wordmark (thank-you + one-of-us). */
.note-sign { margin: 2px 0 0; font-family: var(--disp); font-size: 17px; font-weight: 600;
  letter-spacing: -0.01em; color: var(--ink); text-align: center; }
/* The license block, promoted to the centerpiece: the SAME metallic frame as
   .promo (verbatim double-background) — gold marks the PAID moment. */
.license-framed { padding: 18px 20px; border: 1px solid transparent; border-radius: 4px;
  background:
    linear-gradient(rgba(20, 27, 33, 0.82), rgba(20, 27, 33, 0.82)) padding-box,
    linear-gradient(155deg, rgba(158, 134, 83, 0.5) 0%, rgba(109, 88, 58, 0.32) 30%,
      rgba(187, 169, 120, 0.4) 50%, rgba(109, 88, 58, 0.3) 70%,
      rgba(158, 134, 83, 0.44) 100%) border-box; }
.license-framed .nl-gate { width: 100%; }

/* --- Review-fix pass (2026-07-05) ------------------------------------------ */

/* R21 — the tape CTA above the tiers, a visible secondary CTA (directed bounce) */
.tape-cta--prominent {
  align-self: center; border: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
  border-radius: 4px; padding: 8.8px 16px; color: var(--ink); background: rgba(11, 16, 20, 0.35);
  text-transform: none; letter-spacing: 0.02em; font-size: 10.4px; font-weight: 600;
}
/* R58 — heart-action tape CTA: TURQUOISE frame over an uneven ROSE glow behind it. */
.tape-cta--prominent { border-color: rgba(var(--cyan-rgb), 0.45);
  box-shadow: -6px 6px 22px rgba(var(--rose-rgb), 0.10), 7px -2px 18px rgba(var(--rose-rgb), 0.14); }
.tape-cta--prominent:hover { border-color: rgba(var(--cyan-rgb), 0.75);
  box-shadow: -6px 6px 22px rgba(var(--rose-rgb), 0.16), 7px -2px 18px rgba(var(--rose-rgb), 0.20); }

/* R22 — /licenses/ four upright cards, side by side.
   BREAKOUT (2026-07-23, measured): `main.listing.content` caps at 620px, which is
   the right prose measure for /about/ + /terms/ but left each card 144px wide with
   ~111px of text = ~14 characters per line, a QUARTER of the 45–75 CPL the
   readability research requires — so every attribute wrapped to 2–3 ragged lines.
   The card row alone breaks out to 1000px (cards ≈ 240px, ≈ 30 chars) while the
   heading and the links stay in the 620px column, centred on the same axis.
   Do NOT widen `.content` itself — that would wreck the prose pages.
   Spacing below is on the 8-point grid, line-heights on the 4px baseline. */
/* minmax(0,1fr), not 1fr: a bare 1fr has an auto MINIMUM, so a long word can
   force one column wider than its siblings — columns must stay strictly equal. */
/* WIDTH IS SET BY THE CONTENT, NOT BY THE PAGE. The row used to be a flat
   1000px, so the columns stretched to 323px while the longest line inside them
   was ~166px — nearly half of every card was empty and the three columns sat so
   far apart they read as three separate objects instead of one comparison.
   Proximity is the strongest grouping cue there is (NN/G) and table guidance is
   explicit that columns should fit their content rather than stretch to 100%.
   The arithmetic, all on the 8-point grid:
     longest content line "Unlimited sales and streams" = 166px  → 168
     + 16px padding each side (Material/Carbon card inset)       → 200 per card
     3 cards + 2 × 24px gaps                                     → 648
   The gap is 24px, NOT 16px: inner spacing must stay smaller than outer or the
   cards merge into one block. The contract link is deliberately EXCLUDED from
   this measurement (user, 2026-07-31) — it wraps until its wording is settled. */
.tier-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  /* 12 rows: audience, label, price, the 8 comparison rows, the contract link.
     The COUNT is the contract between this rule and build.py licenses_content() —
     change one and you change the other. */
  grid-template-rows: repeat(12, auto); column-gap: 24px; row-gap: 8px;
  width: min(648px, calc(100vw - 52px)); margin: 8px 0; margin-left: 50%; translate: -50% 0; }
/* Each card is a SUBGRID of the row track above, so the same attribute sits at
   the same height in every card and the eye can scan a row left→right. Without
   this the cards are independent lists and a two-line cell in one card shunts
   everything below it out of line (the fault the user kept hitting). Padding and
   border are identical on all three, so the shared inset preserves alignment. */
.tier-card { display: grid; grid-row: 1 / -1; grid-template-rows: subgrid;
  padding: 16px; border: 1px solid var(--hair); border-radius: 6px;
  background: rgba(11, 16, 20, 0.35); }
@supports not (grid-template-rows: subgrid) {
  .tier-card { display: flex; flex-direction: column; gap: 8px; }
}
/* The three tiers are SIBLINGS — identical surface, identical border. They grant
   different rights, not different grades of the same thing, so there is nothing
   legitimate to steer and no card earns extra weight (research 2026-07-23,
   Manuals/Store/Visual_Design.md). */
/* Exclusive — SUBTLE WINS: a metallic FRAME only (uneven gradient border via
   double background), body identical to its siblings; the frame is the ONLY
   mark, because Exclusive alone differs in KIND. No gold wash, no gold labels,
   no gold bullets — a frame, not a costume. The padding-box fill is the OPAQUE
   page colour (body = rgb(11,16,20), and the siblings' rgba(11,16,20,.35)
   composites to exactly that over it) — opaque so the border gradient cannot
   bleed into the body as a gold SURFACE, which the depth rule forbids. */
.tier-card:nth-child(3) {
  border-color: transparent;
  background:
    linear-gradient(rgb(11, 16, 20), rgb(11, 16, 20)) padding-box,
    linear-gradient(155deg, rgba(158, 134, 83, 0.30) 0%, rgba(109, 88, 58, 0.18) 30%,
      rgba(187, 169, 120, 0.25) 50%, rgba(109, 88, 58, 0.17) 70%,
      rgba(158, 134, 83, 0.27) 100%) border-box;
}
/* Exclusive card price is NOT gold — the metallic FRAME is the premium signal;
   gold text on top read as too much (user, 2026-07-05). Plain ink like the others. */
.tier-card-audience { font-size: 10px; font-weight: 600; letter-spacing: 0.16em;
  line-height: 16px; text-transform: uppercase; color: var(--ink-dim); }
.tier-card-label { font-family: var(--disp); font-size: 15px; font-weight: 600;
  letter-spacing: 0.01em; line-height: 20px; }
.tier-card-price { font-family: var(--disp); font-size: 30px; font-weight: 600; letter-spacing: -0.02em; line-height: 32px; }
/* One comparison row. No bullet: bullets belong to a LIST of features, and this
   is a grid of values — a dot in front of an em dash reads as a broken item. */
.tier-row { font-size: 12.5px; line-height: 20px; color: var(--ink); }
/* Absence. A quiet dash, never a red X — an X reads as a penalty for looking at
   the cheaper tier. The dash itself is aria-hidden and the row's meaning is
   spoken from a .visually-hidden span beside it: a graphical symbol carrying
   information on its own is a WCAG failure (W3C technique F26). */
.tier-row.is-absent { color: var(--ink-dim); }
/* Start of its row, NOT margin-top:auto: the subgrid already gives all three
   cards the same last row, and pushing to the bottom made the ONE link that
   wraps to two lines start 14px above its siblings. */
.tier-card .quiet-link { align-self: start; }
/* The strip: what is true of ALL tiers. These are deliberately NOT rows — every
   card would show the same mark, which is noise, not comparison (NN/G: drop
   attributes that are identical across the options). */
.tier-strip { width: min(648px, calc(100vw - 52px)); margin: 8px 0 0; margin-left: 50%;
  translate: -50% 0; font-size: 12px; line-height: 20px; color: var(--ink-dim); text-align: center; }
/* 3-up only while the whole row fits: 648 + 52 → vw ≥ 700. Below that one card
   per row, and the subgrid MUST be released: three cards all spanning 1/-1 in a
   single column would stack on top of each other. */
@media (max-width: 700px) {
  .tier-cards { display: flex; flex-direction: column; grid-template-rows: none; }
  .tier-card { display: flex; flex-direction: column; gap: 8px; grid-row: auto; }
}

/* Catalog home — the flat tracklist (identical rows, identical row metrics), with
   ONE addition: the rows of a collection sit beside a fixed SQUARE cover of that
   collection. No gap between groups and no extra borders, so the rows read as one
   continuous list. The cover is a fixed square: it never resizes when a group is
   expanded (the rows column grows, the cover does not). */
#track-list { gap: 0; }
.tape-group { display: grid; grid-template-columns: 134px minmax(0, 1fr); column-gap: 16px;
  align-items: start; width: 100%; --acc: var(--light); --acc-rgb: 223, 229, 234; }
.tape-group--nocover { grid-template-columns: minmax(0, 1fr); }
.tape-group.group-empty { display: none; }
/* The cover is a fixed SQUARE spanning exactly two rows (2 x 67px): it never
   resizes when the group is expanded — the rows column grows, the cover does not. */
/* Wrap: cover button + the "Open collection" link stacked in ONE flex column,
   confined to grid column 1 — the link never touches the rows column (2). */
.group-cover-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.group-cover { position: relative; width: 134px; height: 134px; flex: none; margin-top: 2px; padding: 0;
  border: 1px solid var(--hair); border-radius: 8px; overflow: hidden; cursor: pointer;
  background: none; display: block; }
.group-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.group-cover:hover { border-color: rgba(var(--acc-rgb), 0.6); }
/* The cover had no visible label, so it read as artwork rather than as the control
   that reveals the collection's hidden beats. This bar says so permanently — on every
   device, before any hover. It is absolutely positioned INSIDE the button, so the
   block's locked height (two rows, 2 x 67px = 134px) does not move by a pixel. */
.group-cover-more { position: absolute; left: 0; right: 0; bottom: 0; height: 20px;
  display: flex; align-items: center; justify-content: center; gap: 4px;
  /* The scrim must be near-OPAQUE everywhere the text actually sits. An earlier version
     faded to 0.5 at the top, which is invisible over dark artwork but leaves a mid-grey
     backdrop over a bright cover (Artifacts of Affection is near-white at its bottom
     edge) — the label there measured ~3.3:1, not the ~10:1 the bottom stop suggested.
     The feather now lives entirely in the top ~28%, above the text. */
  background: linear-gradient(to top, rgba(5, 7, 9, 0.97) 0%, rgba(5, 7, 9, 0.94) 72%, rgba(5, 7, 9, 0.55) 100%);
  /* The LIFTED accent, not the raw one: #8b0000 and #008000 are dark-on-dark here and
     the label has to stay readable on every collection, over bright artwork too. */
  color: rgba(var(--acc-lift-rgb, var(--light-rgb)), 0.95); font-family: var(--disp); font-size: 9.5px;
  font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap;
  transition: color 0.18s ease; }
.group-cover:hover .group-cover-more { color: var(--ink); }
.gcm-caret { width: 11px; height: 11px; flex: none; transition: transform 0.18s ease; }
/* One state pair, driven by the button's own aria-expanded — site.js already sets it
   on BOTH toggle paths (the per-cover click and the global "Show all N beats"), so the
   label can never disagree with what the rows are doing. */
.gcm-less { display: none; }
.group-cover[aria-expanded="true"] .gcm-more { display: none; }
.group-cover[aria-expanded="true"] .gcm-less { display: inline; }
.group-cover[aria-expanded="true"] .gcm-caret { transform: rotate(180deg); }
/* The file styles focus on text inputs only; this control had nothing but the UA
   default outline. Keyboard users get the collection's own accent instead. */
.group-cover:focus-visible { outline: 2px solid rgba(var(--acc-rgb), 0.85); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcm-caret { transition: none; } }
/* Hidden by default (no space reserved); shown only while the group is open, and
   never affects the cover's or the rows' position (it renders BELOW the cover,
   inside its own wrap — nothing above it shifts). */
.group-cover-link { display: none; font-size: 10.5px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-dim); text-decoration: none; text-align: center; }
.group-cover-link:hover { color: var(--ink); }
.tape-group.is-open .group-cover-link { display: inline-block; }
.group-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; min-width: 0; }
/* The rows run as one list: only the very first row of the whole list is capped. */
.tape-group:first-child .group-rows .track-row:first-child { border-top: 1px solid var(--hair); }
/* R44 accent — the collection tint belongs to the WHOLE collection: painted on the
   rows container so every track of the group carries it, revealed or not, and the
   group reads as one continuous coloured block. Colour only — no metric changes. */
.group-rows { border-radius: 0 6px 6px 0;
  background: radial-gradient(130% 170% at 5% -20%, rgba(var(--acc-rgb), 0.11), rgba(var(--acc-rgb), 0) 62%); }
/* The curated picks stay findable once the group is expanded: a stronger accent rule
   on the left edge plus a slight lift above the group tint. Subtle, dark, asymmetric,
   in the tape accent; never gold, never bright, never symmetric. Colour only. */
#track-list .track-row.is-pick { border-radius: 0 6px 6px 0;
  background: linear-gradient(90deg, rgba(var(--acc-rgb), 0.13), rgba(var(--acc-rgb), 0) 58%);
  box-shadow: inset 3px 0 0 rgba(var(--acc-rgb), 0.85); }
/* Pagination × search: paged rows hide until their group opens, Show-more expands,
   or a search is active. The [hidden] filter (set by site.js) beats all — R60.5. */
.track-row.row-paged { display: none; }
.tape-group.is-open .track-row.row-paged,
#track-list.is-expanded .track-row.row-paged,
#track-list.is-searching .track-row.row-paged { display: flex; }

/* Show-more (real beat count emitted by the build) */
.show-more { align-self: center; padding: 10px 22px; border-radius: 4px;
  border: 1px solid var(--hair); background: rgba(11, 16, 20, 0.5); color: var(--ink-dim);
  font-family: var(--disp); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  cursor: pointer; transition: color 0.18s ease, border-color 0.18s ease; }
.show-more:hover { color: var(--ink); border-color: rgba(var(--light-rgb), 0.6); }

/* R42 — tape page: cover header + accent (accent via --acc/--acc-rgb on .tape-page) */
.tape-cover-wrap { position: relative; display: flex; justify-content: center; width: 100%; }
.tape-cover-wrap::before { content: ""; position: absolute; left: 22%; right: 22%; top: -8%;
  height: 78%; background: radial-gradient(60% 80% at 50% 32%, rgba(var(--acc-rgb), 0.16), transparent 70%);
  filter: blur(26px); z-index: 0; pointer-events: none; }
/* The cover is one rem multiple like everything else on the page - see the
   TAPE-PAGE GEOMETRY block at the end of this file. */
.tape-cover { position: relative; z-index: 1; width: 15rem;
  aspect-ratio: 1 / 1; object-fit: cover; border-radius: 0.875rem;
  box-shadow: 0 1.125rem 2.875rem rgba(0, 0, 0, 0.5); display: block; }
/* R42 revised (2026-07-18): the accent moved off the title (now the page's normal
   ink colour) onto a translucent tint on every row of this tape's page only. */
.tape-page .track-row { border-radius: 6px; background: rgba(var(--acc-rgb), 0.08);
  transition: box-shadow 0.18s ease; }
.tape-page .track-row:hover { box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), 0.4); }

@media (max-width: 720px) {
  /* Narrow screens step down with the rest of the site's 720px re-layout. Still
     rem, still one unit; 3.4/4.875 of the reference so the ratio is preserved. */
  .tape-title { font-size: 3.4rem; min-height: calc(3.4rem * 0.95 * 2); }
  .tape-cover { width: 10.4rem; }
  .tape-group { grid-template-columns: 52px minmax(0, 1fr); column-gap: 10px; }
  .group-cover { width: 52px; height: 52px; }
  /* On a 52px thumbnail the words cannot be set at a readable size without covering
     most of the artwork, so the bar keeps the caret alone — still a permanent, visible
     "this opens" mark, still zero height cost. */
  .group-cover-more { height: 14px; gap: 0; }
  .gcm-more, .gcm-less { display: none; }
  .group-cover[aria-expanded="true"] .gcm-less { display: none; }
  .gcm-caret { width: 10px; height: 10px; }
}

/* R29 — catalog filter */
.cat-filter { width: 100%; padding: 12px 16px; border-radius: 4px; border: 1px solid var(--hair);
  background: rgba(11, 16, 20, 0.5); color: var(--ink); font-family: var(--sans); font-size: 14px; }
.cat-filter::placeholder { color: var(--ink-dim); }
.cat-filter:focus { outline: none; border-color: rgba(var(--light-rgb), 0.7); }
.cat-empty { margin: 0; text-align: center; font-size: 13px; color: var(--ink-dim); }

/* R26 — per-row quick actions */
.row-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.row-act { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid transparent; background: rgba(0, 0, 0, 0.22);
  color: var(--ink-dim); cursor: pointer; text-decoration: none; }
.row-act svg { width: 17px; height: 17px; }
.row-act:hover { border-color: var(--hair); }
/* Value coding: download cyan · share rosé (the logo "heart" action) ·
   buy/cart gold (the paid action — terminal right slot). Dusty + a little
   translucent — present, never loud; full saturation only on hover. */
.row-act--dl { color: rgba(94, 161, 177, 0.9); }
.row-act--dl:hover { color: var(--cyan-hi); border-color: rgba(var(--cyan-rgb), 0.4); }
.row-act--share { color: rgba(204, 129, 155, 0.9); }
.row-act--share:hover { color: var(--rose-hi); border-color: rgba(var(--rose-rgb), 0.4); }
.row-act--license { color: rgba(173, 149, 98, 0.92); }
.row-act--license:hover { color: var(--gold-hi); border-color: rgba(var(--gold-rgb), 0.5); }

/* R27 — brand-logo social icons (footer + /about/) */
.social-ic { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 50%;
  color: var(--ink-dim); border: 1px solid transparent; }
.social-ic svg { width: 17px; height: 17px; }
.social-ic:hover { color: var(--ink); border-color: var(--hair); }
/* Socials get their OWN footer row (never inline with About/Pricing/Privacy). */
.footer-socials { display: flex; justify-content: center; flex-wrap: wrap;
  gap: 8px; margin-top: 14px; }
.footer-socials .social-ic { width: 28px; height: 28px; }
.footer-socials .social-ic svg { width: 15px; height: 15px; }

/* R31 — /about/ releases, blog, contact */
.content ul.releases li { padding-left: 0; display: flex; align-items: baseline; gap: 12px; }
.content ul.releases li::before { display: none; }
.rel-meta { font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-dim); }
.blog-cta { align-self: flex-start; }
.contact-line { font-size: 14px; }

/* R2 — newsletter signup error line */
.nl-error { margin: 0; font-size: 13px; color: #feb4a8; }

/* ---- Beat-page hero action strip + scroll stages (track-page redesign) ----
   Four equal 48px actions in the homepage row order/colors; play carries a very
   subtle breathing glow (R44: never bright). Scroll targets are vh-positioned so
   the landed section owns the viewport on most devices (Visual_Design lesson). */
html { scroll-behavior: smooth; }

/* ============================================================================
   BEAT-PAGE SECTIONS — ONE unit drives everything (user 2026-07-19, rebuilt)

   The bug this replaces: the page mixed px type, vw logos and vh boxes, so any
   change of viewport or zoom moved them against each other.

   The mechanism now: ONE number, the ROOT FONT SIZE, is derived from the usable
   screen (viewport minus the topbar row minus the player's real height). Every
   size on this page — type, buttons, logo, pill, gaps, tiers, topbar, footer —
   is a `rem`, i.e. a multiple of that one number, so the whole composition holds
   its proportions exactly at every viewport and every zoom level. Viewport units
   appear in exactly one other place: the SECTION BOXES, which are by definition
   "one screen tall". No element size is ever a vh, a vw or a raw px.

   Reference design: 1280x800 at 100% zoom -> 1rem = 16px. 34.875 = the usable
   height there (558px) divided by 16, so the whole page is 34.875rem of screen.

   --dock-real: the player is `height: var(--dock-h)` AND `min-height: 140px`, so
   --dock-h alone under-reserves on short windows and the last element lands UNDER
   the player, where the canvas swallows the click. Always reserve the real height.
   --hinge: the logo + pill close section 1 and open section 2 (one copy in the
   DOM). Section 2's scroll-margin equals the hinge, so the cart button lands with
   the logo and pill on screen above the licences; section 2's own box is one
   screen minus the hinge, so hinge + section 2 = exactly one screen.
   ========================================================================== */
html {
  /* The 58px is the reference head (stage padding + topbar) at 1rem = 16px; it is
     a plain number here because a rem inside html's own font-size would resolve
     against 16px and quietly lie. Everywhere else the head is --head, in rem. */
  /* THE TIGHTEST CONSTRAINT WINS (third term added 2026-07-23, user's model).
     The shared number used to consider only two limits - the usable height and a
     flat 3.45% of the width - so a component that needed more width than that was
     simply not consulted, and the tape row silently overflowed the screen. The
     third term is the tracklist row's own requirement: play 2.375 + gaps 1.625 +
     meta ~7.8 + actions 6.75 + row padding 0.75 = ~19.3rem, rounded to 20, against
     the width the stage leaves it (the viewport minus its 26px padding each side).
     min() then picks whichever element is tightest on THIS device, upright or
     sideways, instead of assuming it is always the same one. Add a term here - do
     not special-case a page - whenever a new component's width can bind.
     `52px` is a plain number for the same reason `58px` is: a rem inside html's
     own font-size resolves against 16px and would quietly lie. */
  font-size: clamp(11px,
    min(calc((100dvh - max(var(--dock-h), 140px) - 58px) / 34.875),
        3.45vw,
        calc((100vw - 52px) / 20)), 19px);
}
:root {
  --dock-real: max(var(--dock-h), 140px);
  --head: 3.6rem;               /* stage top padding 2.1rem + topbar row 1.5rem */
  --screen: calc(100dvh - var(--dock-real));
  /* Measured, not guessed: logo 11 + gap 1.5 + pill 2.23 + section pad 1 = 15.73,
     rounded up so the logo's top edge always clears the viewport top by a hair. */
  --hinge: 16rem;
  /* Measured footer incl. its top margin = 7.55rem; the extra is clearance so the
     footer never ends flush against the player. */
  --footer-h: 8rem;
  --sec-gap: 1.4rem;
}

/* The page frame, in rem on beat pages only (every other page keeps its px). */
/* Bottom padding = the player's REAL height plus a little scroll runway, so the
   last section can actually reach the top of the viewport. */
body:has(.middle--beat) .stage { padding: 2.1rem 1.6rem calc(var(--dock-real) + 0.5rem); }
body:has(.middle--beat) .topbar .wordmark { font-size: 1.19rem; }
body:has(.middle--beat) .topbar .tagline { font-size: 0.66rem; padding-left: 0.88rem; margin-left: 0.19rem; }
body:has(.middle--beat) .footer { margin-top: 2.5rem; padding: 1.12rem 0 0.5rem; }
body:has(.middle--beat) .footer-nav { gap: 1.37rem; }
body:has(.middle--beat) .footer-nav a { font-size: 0.66rem; }
body:has(.middle--beat) .footer-socials { gap: 0.5rem; margin-top: 0.87rem; }
body:has(.middle--beat) .footer-socials .social-ic { width: 1.75rem; height: 1.75rem; }
body:has(.middle--beat) .footer-socials .social-ic svg { width: 0.94rem; height: 0.94rem; }

.middle--beat { justify-content: flex-start; gap: 0; max-width: 45rem; }
.sec { width: 100%; display: flex; flex-direction: column; align-items: center;
  gap: var(--sec-gap); text-align: center; }
/* Section 1 = exactly the space between the topbar and the player. The two auto
   margins split the leftover height evenly ABOVE the title and ABOVE the logo, so
   the hinge always ends the section at a fixed, known distance from its bottom —
   that distance IS --hinge, on every device. */
.sec--hero { min-height: calc(var(--screen) - var(--head));
  justify-content: center; padding-bottom: 1rem; }
.sec--hero .title, .sec--hero #logo { margin-top: auto; }
/* Section 2 lands with the hinge above it; its box is one screen minus the hinge. */
.sec--license { min-height: calc(var(--screen) - var(--hinge));
  justify-content: center; scroll-margin-top: var(--hinge); }
/* Section 3 = the outro block AND the footer; the footer's height is its share. */
.sec--outro { min-height: calc(var(--screen) - var(--footer-h));
  justify-content: center; scroll-margin-top: 0; }

/* Every element of the page, in rem — nothing drifts against anything else. */
/* The title BOX is fixed so every beat page's first screen has the same geometry;
   6.75rem is the tallest the longest title actually needs (two lines), so the rest
   of the box is dead space the logo and the breathing room get back. */
.middle--beat .title { height: 6.75rem; min-height: 0; line-height: 0.95;
  font-size: calc(var(--title-scale, 1) * 4.4rem); }
.middle--beat .specs { font-size: 0.78rem; gap: 1.37rem; }
.middle--beat .specs .spec + .spec::before { left: -0.75rem; width: 0.25rem; height: 0.25rem; }
.middle--beat .blurb { font-size: 0.87rem; max-width: 32.5rem; }
/* THE LOGO IS FIXED (user, 2026-07-19): 11rem = 176px at the 1280x800 reference,
   exactly the size `clamp(120px, 16vw, 176px)` produced. It is never resized to
   make room — everything else is sized around it. */
.middle--beat #logo { width: 11rem; }
.middle--beat .hero-actions { gap: 1.6rem; }
.middle--beat .hero-play,
.middle--beat .hero-act { width: 3rem; height: 3rem; }
.middle--beat .hero-play svg,
.middle--beat .hero-act svg { width: 1.25rem; height: 1.25rem; }
.middle--beat .tape-cta--prominent { font-size: 0.65rem; padding: 0.55rem 1rem; }
.middle--beat .tiers { width: min(27.5rem, 100%); gap: 0.5rem; }
.middle--beat .tier { padding: 0.75rem 1rem; gap: 0.87rem; }
.middle--beat .tier .tier-label { font-size: 0.81rem; }
.middle--beat .tier .tier-delivers { font-size: 0.65rem; }
.middle--beat .tier .tier-price { font-size: 1rem; }
.middle--beat .cta { padding: 0.94rem 1.87rem; font-size: 0.81rem; gap: 0.75rem; }
.middle--beat .cta .arrow { width: 0.94rem; height: 0.94rem; }
.middle--beat .microcopy { font-size: 0.69rem; }
.middle--beat .accept-copy { margin-top: -0.5rem; }
.middle--beat .nl-gate { width: min(27.5rem, 100%); margin-top: 0; }
.middle--beat .nl-summary { padding: 0.75rem 1rem; font-size: 0.75rem; }
.middle--beat .nl-reveal { padding: 0 1rem 1rem; }
.middle--beat .nl-email { padding: 0.81rem 1rem; font-size: 0.87rem; }
.middle--beat .nl-submit { padding: 0.81rem 1.37rem; }
.middle--beat .nl-trade { font-size: 0.69rem; }
.middle--beat .tape-cta { font-size: 0.75rem; }
.hero-actions { display: flex; align-items: center; justify-content: center; }
/* Subtle play-time visualizer BEHIND the blurb line (the SEO phrase — user
   2026-07-18: one line below the buttons, so the phrase stays auditable):
   absolutely centered on the paragraph, z-index -1 (paints behind the text but
   still above the fixed .room — the .stage stacking context contains it), never
   intercepts clicks, adds zero layout. Fades with the sb-playing class; JS draws
   nothing once settled. */
.middle--beat .blurb { position: relative; }
#hero-viz { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(22.5rem, 100%); height: 6rem; z-index: -1; pointer-events: none;
  opacity: 0; transition: opacity 700ms ease; }
body.sb-playing #hero-viz { opacity: 0.2; } /* user 2026-07-18: tuned 1 -> 0.6 -> 0.3 -> 0.2 (final) */
@media (prefers-reduced-motion: reduce) { #hero-viz { display: none; } }
.hero-play {
  width: 48px; height: 48px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; color: var(--ink); background: rgba(var(--light-rgb), 0.05);
  border: 1px solid rgba(var(--light-rgb), 0.6);
  animation: hero-breathe 3.4s ease-in-out infinite;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.hero-play:hover { border-color: rgba(var(--light-rgb), 0.95); background: rgba(var(--light-rgb), 0.1); }
.hero-play:active { transform: scale(0.96); }
.hero-play svg { width: 20px; height: 20px; }
.hero-play .icon-play { margin-left: 2px; }
.hero-play .icon-pause { display: none; }
body.sb-playing .hero-play .icon-play { display: none; }
body.sb-playing .hero-play .icon-pause { display: block; }
body.sb-playing .hero-play { animation: none; }
@keyframes hero-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--light-rgb), 0.016); }
  50% { box-shadow: 0 0 18px 4px rgba(var(--light-rgb), 0.04); }
}
.hero-act { width: 48px; height: 48px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; border: 1px solid; background: rgba(23, 31, 38, 0.5); cursor: pointer; transition: transform 0.15s ease, border-color 0.15s ease; }
.hero-act:hover { transform: translateY(-2px); }
.hero-act svg { width: 20px; height: 20px; }
.hero-act--dl { color: var(--cyan-hi); border-color: rgba(var(--cyan-rgb), 0.45); }
.hero-act--dl:hover { border-color: rgba(var(--cyan-rgb), 0.8); }
.hero-act--share { color: var(--rose-hi); border-color: rgba(var(--rose-rgb), 0.45); }
.hero-act--share:hover { border-color: rgba(var(--rose-rgb), 0.8); }
.hero-act--buy { color: var(--gold-hi); border-color: rgba(var(--gold-rgb), 0.42); }
.hero-act--buy:hover { border-color: rgba(var(--gold-rgb), 0.7); }
/* Closing stage: store link + newsletter + footer read as ONE section; the
   download anchor lands the box ~centered with the license block off-screen. */
/* The outro is its own section now, so the old 36vh spacer that used to push this
   block down the page is gone — the section does that job. */
.dl-store-link { margin: 0; }

/* ============================================================================
   TAPE-PAGE GEOMETRY - the beat page's root unit, applied here (2026-07-20)
   ----------------------------------------------------------------------------
   A tape page is now sized from the SAME root font size as a beat page, the one
   defined at the end of this file:

     html { font-size: clamp(11px, min((100dvh - dock - 58px) / 34.875, 3.45vw), 19px) }

   That number is the usable screen (viewport minus the player minus the head)
   divided by 34.875, so 34.875rem IS one usable screen by definition. Every size
   on this page is a multiple of it, which buys both properties at once:

     - ZOOM: one unit means the cover, title, blurb, pill and rows cannot drift
       against each other, at any zoom level. (The earlier fix pinned them to px,
       which held the ratios but froze the layout against the viewport.)
     - LANDING: the section-1 stack sums to ~34.1rem, so the "Back to home" pill
       finishes just above the player at EVERY window height instead of only at
       one. Before this it sat 125px behind the player at 640px tall and 229px
       above it at 1100px - a 350px drift, which is why one zoom level looked
       right and no other did.

   The section-1 budget, in rem (head is the stage's own padding + topbar):
     listing padding-top   1.500
     cover                15.000
     gap                   1.625
     title box             9.263   (4.875 font x 0.95 line-height x 2 lines)
     gap                   1.625
     blurb                 1.356   (0.875 font x 1.55 line-height)
     gap                   1.625
     pill                  2.100
     ----------------------------
     total                34.094   -> ~0.8rem of clearance under the player

   The clearance was taken from the TOP PADDING, not from the artwork: the cover
   stays at its approved 15rem. If anything is ever added to section 1 it has to
   take its space from something already there, exactly as on the beat page.

   Do not put a `vh`, a `vw` or a raw `px` size inside `.tape-page`.
   Manual: Manuals/Store/Section_Sizing.md, "Tape pages".
   ========================================================================== */

.listing.tape-page {
  max-width: 40rem;
  padding-top: 1.5rem;
  gap: 1.625rem;
}

.tape-page .tape-cover-wrap::before { filter: blur(1.625rem); }
.tape-page .blurb { font-size: 0.875rem; max-width: 32.5rem; }

.tape-page .tape-cta--prominent {
  font-size: 0.65rem;
  padding: 0.55rem 1rem;
  border-radius: 0.25rem;
}

.tape-page .track-row {
  gap: 0.8125rem;
  padding: 0.875rem 0.375rem;
  border-radius: 0.375rem;
}
.tape-page .row-play { width: 2.375rem; height: 2.375rem; }
.tape-page .row-play svg { width: 0.9375rem; height: 0.9375rem; }
.tape-page .row-title { font-size: 1.1875rem; }
.tape-page .row-specs { font-size: 0.6875rem; }
.tape-page .row-actions { gap: 0.375rem; }
.tape-page .row-act { width: 2rem; height: 2rem; }
.tape-page .row-act svg { width: 1.0625rem; height: 1.0625rem; }

/* PHONES: the row stacks instead of competing for one line (2026-07-23).
   One line could not hold play + name + meta + three actions: the name is the only
   item allowed to shrink, so it took the entire shortfall and collapsed to ZERO
   width - every track on a tape page was nameless at 360px, while the actions still
   pushed 33px past the screen edge. Grid areas give the name the full width of
   line 1 and put the meta and the actions on line 2, which is the standard mobile
   list item. Nothing is dropped and nothing is shrunk below its floor. */
@media (max-width: 520px) {
  .tape-page .track-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "play title title" "play specs actions";
    align-items: center; column-gap: 0.8125rem; row-gap: 0.25rem;
  }
  .tape-page .row-play    { grid-area: play; }
  .tape-page .row-title   { grid-area: title; margin-right: 0; }
  .tape-page .row-specs   { grid-area: specs; }
  .tape-page .row-actions { grid-area: actions; }
}

.tape-page .cta {
  font-size: 0.8125rem;
  gap: 0.75rem;
  padding: 0.9375rem 1.875rem;
  border-radius: 0.25rem;
}
.tape-page .cta .arrow { width: 0.9375rem; height: 0.9375rem; }
.tape-page .microcopy { font-size: 0.6875rem; }

.tape-page .quiet { margin-top: 0.25rem; }
.tape-page .quiet-link { font-size: 0.71875rem; }

/* ===========================================================================
   Cave-photo room background — live 2026-08-01. Every page's <html> carries
   data-bg="3" (build.py UTILITY_TPL + templates/*.html); variants 1-7 stay
   defined below for A/B swaps, only "3" is wired to a template.
   ======================================================================== */
html[data-bg] .room {
  background-image: var(--bg-photo);
  background-size: cover;
  background-position: 50% 12%;
  background-repeat: no-repeat;
}
/* the painted room becomes a dimming veil so the photo reads through it */
html[data-bg] .room::before {
  background:
    radial-gradient(76% 50% at 50% 1%, rgba(var(--light-rgb), 0.10) 0%, transparent 60%),
    linear-gradient(rgba(5, 7, 9, 0.58), rgba(5, 7, 9, 0.72));
}
html[data-bg="1"] { --bg-photo: url("/assets/img/bg/cave1.jpg"); }
html[data-bg="2"] { --bg-photo: url("/assets/img/bg/cave2.jpg"); }
html[data-bg="3"] { --bg-photo: url("/assets/img/bg/cave3.jpg"); }
html[data-bg="4"] { --bg-photo: url("/assets/img/bg/cave4.jpg"); }
html[data-bg="5"] { --bg-photo: url("/assets/img/bg/cave5.jpg"); }
html[data-bg="6"] { --bg-photo: url("/assets/img/bg/cave6.jpg"); }
html[data-bg="7"] { --bg-photo: url("/assets/img/bg/cave7.jpg"); }
/* 8-11 added 2026-08-01 — the client's own regenerated iterations (his prompts, not
   mine); 1-7 are the first batch and stay for comparison. */
html[data-bg="8"] { --bg-photo: url("/assets/img/bg/cave8.jpg"); }
html[data-bg="9"] { --bg-photo: url("/assets/img/bg/cave9.jpg"); }
html[data-bg="10"] { --bg-photo: url("/assets/img/bg/cave10.jpg"); }
html[data-bg="11"] { --bg-photo: url("/assets/img/bg/cave11.jpg"); }
html[data-bg="12"] { --bg-photo: url("/assets/img/bg/cave12.jpg"); }

/* --- /checkout/ — the store's own checkout (Stripe swap, 2026-08-05) ---------
   One centered column on the utility-page layout, same widths and same field
   styling as the newsletter form, so the page that takes money looks like the
   rest of the store rather than like a bolted-on payment screen. Stripe's card
   fields draw themselves inside their own frame; the Appearance API in
   checkout.js is what matches them to this palette. */
.checkout { width: 100%; max-width: 440px; display: flex; flex-direction: column;
  align-items: stretch; gap: 22px; text-align: left; }
.checkout .section-label { text-align: left; }

.co-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.co-item { display: grid; grid-template-columns: 1fr auto auto; align-items: center;
  gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--hair); }
.co-item-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.co-item-title { font-family: var(--disp); font-size: 15px; font-weight: 600;
  letter-spacing: -0.01em; color: var(--ink); }
.co-item-tier { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-dim); text-decoration: none; border-bottom: 1px solid transparent; }
.co-item-tier:hover { color: var(--ink); border-bottom-color: var(--hair); }
.co-item-price { font-family: var(--disp); font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink); }
.co-item-remove { width: 26px; height: 26px; display: grid; place-items: center;
  padding: 0; border: 1px solid var(--hair); border-radius: 4px; background: transparent;
  color: var(--ink-dim); font-size: 15px; line-height: 1; cursor: pointer; }
.co-item-remove:hover { color: var(--ink); border-color: var(--ink-dim); }
.co-total { display: flex; justify-content: space-between; align-items: baseline;
  margin: 12px 0 0; font-family: var(--disp); font-size: 19px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink); }
.co-empty { margin: 0; font-size: 14px; color: var(--ink-dim); }

.co-form, .co-pay { display: flex; flex-direction: column; gap: 14px; margin: 0; }
.co-field { display: flex; flex-direction: column; gap: 6px; }
.co-label { font-size: 10.5px; font-weight: 500; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink-dim); }
.co-input { width: 100%; padding: 13px 16px; border-radius: 4px; border: 1px solid var(--hair);
  background: rgba(11, 16, 20, 0.5); color: var(--ink); font-family: var(--sans); font-size: 14px; }
.co-input:focus { outline: none; border-color: rgba(var(--gold-rgb), 0.6); }

/* The two boxes the law requires BEFORE the card fields appear. Never pre-ticked;
   the build refuses to ship a page where they are. */
.co-check { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start;
  font-size: 13px; line-height: 1.45; color: var(--ink-dim); cursor: pointer; }
.co-check input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: var(--gold);
  cursor: pointer; }
.co-check:hover { color: var(--ink); }

.co-error { margin: 0; font-size: 13px; line-height: 1.45; color: var(--rose); }
.co-orderline { margin: 0; font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.co-back { align-self: flex-start; padding: 0; border: 0; background: transparent;
  font-family: var(--sans); font-size: 12.5px; color: var(--ink-dim); cursor: pointer; }
.co-back:hover { color: var(--ink); }
.cta.is-busy { opacity: 0.6; pointer-events: none; }

/* Thank-you page: the download of what was just paid for, above everything else. */
.order-status { width: 100%; max-width: 440px; display: flex; flex-direction: column;
  align-items: center; text-align: center; gap: 10px; }
.order-line { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-dim); }

/* The consent banner is fixed to the bottom of the window, so anything the page
   puts down there sits UNDERNEATH it and cannot be clicked. That is how the
   "Continue to payment" button on /checkout/ became unpressable for any visitor
   who had not yet answered the banner (found 2026-08-05). The head script flags
   the page while the banner is actually on screen; this reserves the room.
   Measured against the banner at its tallest wrap, plus its 16px bottom offset. */
html[data-consent="asking"] body { padding-bottom: 132px; }
/* …and scrolling to an element must stop short of the banner, or the browser
   parks it exactly underneath. */
html[data-consent="asking"] { scroll-padding-bottom: 148px; }
@media (max-width: 520px) {
  html[data-consent="asking"] body { padding-bottom: 184px; }
}


/* --- PANEL-COLOUR AUDIT (2026-08-18) ----------------------------------------
   A private switch for the client, built exactly like the ?bg= one: ?panel=1..4
   picks a panel look, ?panel=off returns to the live colour, and the choice is
   remembered so the whole store can be browsed in it. Visitors arrive without the
   parameter and see the live colour.

   Each variant sets ONLY --panel-bg (cart) and --menu-bg (menu). The four candidates
   are OPAQUE, because the 16% rule below describes a surface colour and an opaque
   surface is what it specifies; today's live panel is the translucent incumbent they
   are being judged against.

   1  white panel, dark text   — what all ten leading stores the client surveyed do
   2  pitch black, white text  — what Lamborghini and Universal Music do
   3  the page's own #0b1014 lifted 16% toward white — Material's dark-theme elevation
      overlay at its top step, which is where a panel hanging off a fixed strip sits
   4  as 3, with the menu darkened by the measured size effect: the menu subtends
      11.8 degrees against the cart's 10.5, which the CAM16 size-effect extension
      (Li & Luo, from Xiao's paint-panel data) puts at 1.7% of luminance — LESS than
      one step of 8-bit colour, so 3 and 4 differ by one point in blue and should be
      indistinguishable. Built at the client's request to see that for himself. */
[data-panel="1"] { --panel-bg: #ffffff; --menu-bg: #ffffff; }
/* The inks are flipped by re-declaring the tokens ON the panels: every child already
   reads them, so one block covers text, dividers, hovers, the licence picker and the
   focus rings without touching a single component rule. */
[data-panel="1"] .cart-panel, [data-panel="1"] .menu-panel {
  --ink: #0b1014;
  --ink-dim: #55636b;
  --light: #0b1014;
  --light-rgb: 11, 16, 20;
  --hair: rgba(11, 16, 20, 0.22);
  --cyan: #2c7c92;
  color: #0b1014;
}
[data-panel="2"] { --panel-bg: #000000; --menu-bg: #000000; }
[data-panel="3"] { --panel-bg: #33393d; --menu-bg: #33393d; }
[data-panel="4"] { --panel-bg: #33393d; --menu-bg: #33393c; }
