@font-face { font-family: "OS"; src: url("../fonts/os-semibold.woff2") format("woff2"); font-weight: 600; font-display: swap; }
@font-face { font-family: "OS"; src: url("../fonts/os-regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }

html { background: #000; color: #e9e9ec; -webkit-text-size-adjust: 100%; }
body { margin: 0; background: #000; font-family: var(--font-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", "Inter", "Segoe UI", Roboto, Arial, sans-serif); }

/* scroll length of the story; the canvas stays fixed (pinned) while this scrolls */
#track { height: calc(675 * var(--svh, 1svh)); position: relative; }  /* calc(710 * var(--svh, 1svh)) minus the 0.35 screens before the lock (Rob 2026-10-08) */
.reduce #track { height: calc(100 * var(--svh, 1svh)); }

/* static viewport probes: read once per real resize, never on iOS toolbar moves */
.probe { position: absolute; top: 0; left: 0; width: 1px; visibility: hidden; pointer-events: none; }
.probe-s { height: 100svh; }
.probe-l { height: 100lvh; }

/* calc(100 * var(--lvh, 1lvh)): the canvas does not resize when the iOS toolbar shows or hides */
#gl { position: fixed; top: 0; left: 0; right: 0; width: 100%; height: calc(100 * var(--lvh, 1lvh)); display: block; }
/* Rob 2026-10-08: the first thing seen is the exact logo, so the canvas fades in only once the logo is sharp
   (3 s fallback, so a failed build still shows the page) */
#gl { opacity: 0; transition: opacity .6s ease; animation: gl-late .6s ease 3s forwards; }
.crisp #gl, .reduce #gl, .snap #gl { opacity: 1; }
@keyframes gl-late { to { opacity: 1; } }
/* the landing: the hint is in the HTML (an image of the very first WebGL frame, at the same pixels), so it is
   there at first paint; once WebGL has drawn the same frame under it, it fades out and the canvas carries on.
   Its box: centred on the lid point the camera aims at (the middle of the small viewport), as wide as the
   logo lock width (min(80vw, 480px, calc(160 * var(--svh, 1svh))), see main.js) times the image's share of it. */
.hint { --hint-k: 1.24167; }
.hint { position: fixed; left: 50%; top: calc(50 * var(--svh, 1svh)); width: calc(min(80vw, 480px, calc(160 * var(--svh, 1svh))) * var(--hint-k)); height: auto;
  transform: translate(-50%, -50%); pointer-events: none; }
.gl-on .hint { opacity: 0; transition: opacity .5s linear; }
.snap .hint, .reduce .hint { display: none; }

/* the name for screen readers; the page shows it as the logo */
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* the scroll cue: its wrapper fades in half a second after the page is up (CSS), the line itself is taken away by the first scroll (JS) */
.cue-w { position: fixed; left: 50%; bottom: calc(30px + env(safe-area-inset-bottom)); transform: translateX(-50%); opacity: 0; pointer-events: none; z-index: 3; }
.lit .cue-w { opacity: 1; transition: opacity 1.2s cubic-bezier(.25,0,.2,1) .5s; }
.snap .cue-w { transition: none; }
/* Rob 2026-10-08: bigger; half a second after the page is up, ONE slow, smooth pulse (light swells through the words
   while the arrow travels down and back), then it rests until the first scroll fades it out */
.cue { display: flex; flex-direction: column; align-items: center; gap: 10px; color: #b9b9bf; font-size: 15px; font-weight: 500; letter-spacing: .16em; text-transform: uppercase; }
/* the two labels share one place, so one can crossfade into the other (Rob 2026-10-09) */
.cue-t { display: grid; justify-items: center; } .cue-t > span { grid-area: 1 / 1; white-space: nowrap; } .cue-t .t2 { opacity: 0; }
.cue svg { width: 26px; height: 26px; }
.lit .cue { animation: cue-pulse 2.8s cubic-bezier(.45,0,.2,1) .9s 1 both; }
.lit .cue svg { animation: cue-arrow 2.8s cubic-bezier(.45,0,.2,1) .9s 1 both; }
/* "Keep scrolling" (Rob 2026-10-09): the same single slow pulse as it takes over from "Scroll down" */
.cue.again { animation: cue-pulse 2.8s cubic-bezier(.45,0,.2,1) .4s 1 both; }
.cue.again svg { animation: cue-arrow 2.8s cubic-bezier(.45,0,.2,1) .4s 1 both; }
/* colour, glow and movement only: the first scroll fades the cue through its opacity (JS), which an opacity keyframe would override */
@keyframes cue-pulse { 0% { color: #8a8a90; text-shadow: 0 0 0 rgba(255,255,255,0); transform: scale(1); }
  40% { color: #ffffff; text-shadow: 0 0 22px rgba(255,255,255,.35); transform: scale(1.06); }
  100% { color: #c4c4ca; text-shadow: 0 0 0 rgba(255,255,255,0); transform: scale(1); } }
.cue[hidden] { display: none; }
@keyframes cue-arrow { 0% { transform: translateY(0); } 40% { transform: translateY(10px); } 75%, 100% { transform: translateY(0); } }


.debug-rect { position: fixed; outline: 1px solid #f0f; pointer-events: none; z-index: 9; }

.gl-lost #gl { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .cue-w, .hint { display: none; }
}

/* the page text (placeholder copy, Rob 2026-10-08): it scrolls up over the black screen the camera pushed into */
.story { position: relative; z-index: 2; margin-top: 0; padding: 0 clamp(24px, 7vw, 120px); }
.beat { min-height: calc(78 * var(--svh, 1svh)); display: flex; flex-direction: column; justify-content: center; max-width: 42rem; }
.beat h2 { font-weight: 600; font-size: clamp(34px, 7.6vw, 78px); line-height: 1.04; letter-spacing: -0.022em; margin: 0 0 .42em; color: #f2f2f4; text-wrap: balance; }
.beat p { font-size: clamp(18px, 2.1vw, 22px); line-height: 1.5; margin: 0; color: #8e8e95; max-width: 31rem; text-wrap: pretty; }
.beat a { color: #e9e9ec; text-underline-offset: .18em; }
.btn { display: inline-block; align-self: flex-start; margin-top: 1.5em; padding: 15px 28px; border-radius: 999px; background: #f2f2f4; color: #000 !important;
  text-decoration: none; font-weight: 600; font-size: 17px; letter-spacing: -0.01em; }
/* the second kind of button (Download & verify): an outline, so the pre-order stays the one white button */
.btn-line { background: transparent; color: #f2f2f4 !important; box-shadow: inset 0 0 0 1.5px rgba(242,242,244,.4); transition: background .25s ease, color .25s ease; }
.btn-line:hover { background: #f2f2f4; color: #000 !important; }
/* Rob 2026-10-11: a small, simple link to the software in the simulator, under the download button */
.sim-link { display: block; width: fit-content; margin-top: 1.1em; padding: 6px 0; font-size: 15px; color: #c9c9ce;
  text-decoration: underline; text-decoration-color: rgba(242,242,244,.35); text-underline-offset: 4px; transition: color .25s ease, text-decoration-color .25s ease; }
.sim-link:hover { color: #f2f2f4; text-decoration-color: currentColor; }
.foot { padding: calc(6 * var(--svh, 1svh)) 0 calc(4 * var(--svh, 1svh)); }
.foot p { font-size: 12px; line-height: 1.5; color: #8a8a90; max-width: 34rem; }
@supports (animation-timeline: view()) {
  .beat > * { animation: beat-in linear both; animation-timeline: view(); animation-range: entry 5% cover 32%; }
}
@keyframes beat-in { from { opacity: 0; transform: translateY(32px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .beat > * { animation: none; } }

/* the first line: drawn on the 3D screen by WebGL, then this HTML copy takes over at the same pixels (main.js).
   Its stage is sticky: fixed with the canvas while the story scrolls, then it scrolls away; no per-frame moves */
.sui-stage { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; } /* the line sits at one place in the page (Rob 2026-10-08: it scrolls, never pins) */
.screen-ui { position: absolute; left: 0; top: 0; box-sizing: border-box; transform-origin: 0 0; visibility: hidden; opacity: 0;
  text-align: center; z-index: 2; pointer-events: none; color: #f2f2f4; }
.screen-ui h2 { font-weight: 600; letter-spacing: 0; color: #f2f2f4; }
.screen-ui p { font-weight: 400; margin: 0; color: #8e8e95; }
.screen-ui span { display: block; white-space: nowrap; }
.screen-ui.live { pointer-events: auto; user-select: text; -webkit-user-select: text; }
.reduce .screen-ui { visibility: visible; opacity: 1; position: relative; transform: none; margin: calc(40 * var(--svh, 1svh)) auto 0; }
.screen-ui em { font-style: normal; color: #e9e9ec; }
/* Rob 2026-10-10: the heading in a slight silver, the vertical gradient of the page titles (.ch h2), one per line
   (line-height 1.08em everywhere); main.js draws the same stops (SUI_SILVER) into the 3D screen line */
.screen-ui h2 { --lh: 1.08em;
  background: linear-gradient(180deg, #fafafb 0%, #ececf0 38%, #c3c3ca 60%, #dadadf 82%, #eeeef1 100%) 0 0 / 100% var(--lh) repeat-y;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
/* the header (Rob 2026-10-08): the Bitsaga logo and one quiet pre-order pill over a slight blur; main.js fades it in
   (opacity, pointer-events) only once the 3D story has ended */
.topbar { position: fixed; top: 0; left: 0; right: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between;
  padding: calc(18px + env(safe-area-inset-top)) calc(26px + env(safe-area-inset-right)) 36px calc(26px + env(safe-area-inset-left));
  opacity: 0; pointer-events: none; --fade: 28px; }
/* the backdrop sits on its own layer so it never softens the logo or the pill. Rob 2026-10-10: the bar itself is solid
   black, so the logo and the pill always sit on clean black over any text that moves behind; only its last --fade px
   fade out (eased), so the page slides under it without a hard line */
.topbar::before { content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(to bottom, #000 0, #000 calc(100% - var(--fade)), rgba(0,0,0,.84) calc(100% - var(--fade) * .8),
    rgba(0,0,0,.56) calc(100% - var(--fade) * .55), rgba(0,0,0,.26) calc(100% - var(--fade) * .3), rgba(0,0,0,.07) calc(100% - var(--fade) * .1), rgba(0,0,0,0) 100%); }
.brand { line-height: 0; }
.brand img { width: 150px; height: auto; opacity: .8; transition: opacity .3s ease; }
.brand:hover img { opacity: 1; }
.topbar-cta { padding: 9px 20px; border-radius: 999px; box-shadow: inset 0 0 0 1px rgba(242,242,244,.34); color: #f2f2f4; text-decoration: none;
  font-weight: 600; font-size: 15px; line-height: 1.2; letter-spacing: -0.005em; transition: background-color .2s ease, color .2s ease, opacity .4s ease; }
.topbar-cta:hover { background: #f2f2f4; color: #000; }
/* the pre-order screen has its own button (main.js sets at-buy on <html> while that button is in view) */
.at-buy .topbar-cta { opacity: 0; pointer-events: none; transition: opacity .4s ease; }

/* ---------- the page chapters (stand-in, placeholder copy, Rob 2026-10-08: A1 idea first, B1 real screens, C stand-in) */
.ch { position: relative; min-height: calc(92 * var(--svh, 1svh)); display: flex; flex-direction: column; justify-content: center; max-width: 64rem; margin: 0 auto; padding: calc(8 * var(--svh, 1svh)) 0; }
.ch h2 { font-weight: 600; font-size: clamp(34px, 6.4vw, 72px); line-height: 1.04; letter-spacing: -0.022em; margin: 0 0 .42em; color: #f2f2f4; text-wrap: balance; max-width: 15em; }
.ch > p, .ch p { font-size: clamp(18px, 2vw, 21px); line-height: 1.5; margin: 0; color: #8e8e95; max-width: 32rem; text-wrap: pretty; }
/* the titles and the card list: anodised silver (Rob 2026-10-08). The light falls the same on every line: the
   gradient is one line-height tall and repeats down, with a faint raking light on top; still near white, no glow */
.ch h2, .holds li { --lh: 1.04em;
  background: linear-gradient(100deg, rgba(255,255,255,0) 18%, rgba(255,255,255,.22) 42%, rgba(255,255,255,0) 66%),
    linear-gradient(180deg, #fafafb 0%, #ececf0 38%, #c3c3ca 60%, #dadadf 82%, #eeeef1 100%) 0 0 / 100% var(--lh) repeat-y;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.holds li { --lh: 1.1em; }
.ch h3 { font-weight: 600; font-size: 19px; line-height: 1.3; margin: 18px 0 6px; color: #e9e9ec; }
@supports (animation-timeline: view()) {
  .ch > h2, .diy-pin > h2, .ch > p, .ch > a, .steps > li, .specs > div { animation: beat-in linear both; animation-timeline: view(); animation-range: entry 0% cover 30%; }
}
/* cards: one device, many cards (a fan that opens on scroll); one pinned group: the fan, then the text */
/* Rob 2026-10-10 ("distance between the fan out 5 and above those should decrease"): the fan sat about 0.9 screens
   under the opening's last line; it now comes up under that line while it is still on screen (-73 svh, was -30) */
.ch-cards { min-height: 0; display: block; padding: 0 0 calc(6 * var(--svh, 1svh)); margin-top: calc(-73 * var(--svh, 1svh)); view-timeline: --cards block; }
.reduce .ch-cards { margin-top: calc(-30 * var(--svh, 1svh)); } /* reduced motion: the opening is a still on a one-screen track, so the fan keeps its place under it */
.ch-cards .pin { padding-top: calc(9 * var(--svh, 1svh)); } /* Rob 2026-10-08: no sticky hold, the fan scrolls 1:1 while it opens */
.fan { position: relative; height: calc(44 * var(--svh, 1svh)); width: min(84vw, 900px); margin: 0 auto calc(3 * var(--svh, 1svh)); }
.fan .card { position: absolute; left: 50%; top: 52%; width: min(58vw, 330px); aspect-ratio: 85.6 / 53.98; border-radius: 4.6% / 7.3%;
  background: radial-gradient(120% 140% at 20% 0%, #232327 0%, #0d0d0f 55%, #121214 100%); box-shadow: inset 0 0 0 1px #2c2c31, 0 24px 60px #000;
  transform: translate(-50%, -50%) rotate(calc(var(--i, 0) * 1.2deg)); transform-origin: 50% 150%; }
.fan .card .cardart-chip { width: 15%; }
.fan .fp { position: absolute; right: 7%; bottom: 10%; font: 500 13px var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace); letter-spacing: .04em; color: #6c6c73; }
@supports (animation-timeline: view()) {
  .fan .card { animation: fan-out linear both; animation-timeline: --cards; animation-range: entry 75% cover 50%; }
  .ch-cards h2, .ch-cards .pin > p { animation: beat-in linear both; animation-timeline: --cards; animation-range: entry 45% entry 95%; }
}
/* five cards (Rob 2026-10-08: five, fanned wider): the middle one (--i 2) stays put, the others open to both sides.
   The step is in vw so the whole fan stays inside the screen at any width (about 95 % of a phone, 67 % at 1440) */
.ch-cards .pin > p { font-size: clamp(21px, 2.6vw, 28px); max-width: 30em; text-wrap: balance; }
.fan .card { width: min(40vw, 330px); --spread: min(8vw, 150px); --turn: 9deg; }
@media (max-width: 700px) { .fan .card { width: 44vw; --turn: 7deg; } }
@keyframes fan-out { from { transform: translate(-50%, -50%) rotate(calc(var(--i, 0) * 1.2deg)); } to { transform: translate(calc(-50% + (var(--i, 0) - 2) * var(--spread)), -50%) rotate(calc((var(--i, 0) - 2) * var(--turn))); } }
/* what a card holds */
.holds { list-style: none; margin: 0 0 calc(3 * var(--svh, 1svh)); padding: 0; }
.holds-wrap { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); align-items: center; gap: 6vw; }
.holds li { position: relative; padding-left: .7em; font-weight: 600; font-size: clamp(32px, 5.2vw, 68px); line-height: 1.1; letter-spacing: -0.025em; color: #f2f2f4; }
.holds li::before { content: ''; position: absolute; left: 0; top: .5em; width: .2em; height: .2em; border-radius: 50%; background: #8e8e95; }
.sk { perspective: 1400px; }
.sk-card { position: relative; width: 100%; max-width: 420px; aspect-ratio: 85.6 / 53.98; border-radius: 4.6% / 7.3%; margin-left: auto;
  background: linear-gradient(140deg, #26262b 0%, #101012 38%, #08080a 60%, #1b1b1f 100%); box-shadow: inset 0 0 0 1px #34343a, inset 0 1px 0 #4a4a52, 0 40px 90px #000;
  transform: rotateY(-24deg) rotateX(14deg) rotateZ(-4deg); overflow: hidden; }
.sk-card::after { content: ''; position: absolute; inset: -50%; background: linear-gradient(110deg, transparent 44%, rgba(255,255,255,.12) 50%, transparent 56%); transform: translateX(-55%); }
.sk-card .cardart-chip { left: 10%; top: 36%; }
.sk-mark { position: absolute; right: 8%; bottom: 11%; font: 600 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: .14em; text-transform: uppercase; color: #5b5b62; }
@supports (animation-timeline: view()) {
  .sk-card { animation: sk-turn linear both; animation-timeline: view(); animation-range: entry 0% exit 30%; }
  .sk-card::after { animation: sheen linear both; animation-timeline: view(); animation-range: entry 30% exit 10%; }
}
@keyframes sk-turn { from { transform: rotateY(-76deg) rotateX(90deg) rotateZ(-20deg) translateY(60px); opacity: 0; } /* Rob 2026-10-13: starts completely flat (lying down), so it turns more */ 35% { opacity: 1; } to { transform: rotateY(10deg) rotateX(6deg) rotateZ(2deg); opacity: 1; } }
@media (max-width: 760px) { .holds-wrap { grid-template-columns: 1fr; gap: calc(6 * var(--svh, 1svh)); } .sk { order: -1; } .sk-card { margin: 0 auto; max-width: 300px; } }
@supports (animation-timeline: view()) { .holds li { animation: beat-in linear both; animation-timeline: view(); animation-range: entry 10% cover 35%; } }
/* 2. the card */
.cardart { width: min(72vw, 420px); aspect-ratio: 85.6 / 53.98; border-radius: 4.6% / 7.3%; margin: 0 0 calc(7 * var(--svh, 1svh));
  background: radial-gradient(120% 140% at 20% 0%, #232327 0%, #0d0d0f 55%, #121214 100%); box-shadow: inset 0 0 0 1px #2c2c31, 0 30px 80px #000;
  position: relative; overflow: hidden; transform: perspective(1200px) rotateX(18deg) rotateZ(-6deg); }
.cardart::after { content: ''; position: absolute; inset: -40%; background: linear-gradient(105deg, transparent 42%, rgba(255,255,255,.09) 50%, transparent 58%); transform: translateX(-60%); }
.cardart-chip { position: absolute; left: 11%; top: 34%; width: 15%; aspect-ratio: 1.25; border-radius: 14%; background: linear-gradient(135deg, #8a7a4e, #c9b47a 45%, #7a6a40);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.35); }
.cardart-chip::before { content: ''; position: absolute; inset: 22% 0; border-top: 1px solid rgba(0,0,0,.35); border-bottom: 1px solid rgba(0,0,0,.35); }
@supports (animation-timeline: view()) {
  .cardart { animation: card-tilt linear both; animation-timeline: view(); animation-range: entry 0% exit 20%; }
  .cardart::after { animation: sheen linear both; animation-timeline: view(); animation-range: entry 20% exit 0%; }
}
@keyframes card-tilt { from { transform: perspective(1200px) rotateX(48deg) rotateZ(-14deg) translateY(40px); opacity: 0; } 40% { opacity: 1; } to { transform: perspective(1200px) rotateX(4deg) rotateZ(-2deg); opacity: 1; } }
@keyframes sheen { from { transform: translateX(-55%); } to { transform: translateX(60%); } } /* the start of .sk-card::after (explicit: see the explode keyframes) */
/* 3. three steps with real firmware screens */
.steps { list-style: none; margin: calc(5 * var(--svh, 1svh)) 0 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; }
.scr { margin: 0; padding: 10px; background: #000; border-radius: 14px; box-shadow: inset 0 0 0 1px #26262a; }
.scr img { display: block; width: 100%; height: auto; image-rendering: pixelated; border-radius: 4px; }
.steps p { font-size: 16px; }
@media (max-width: 700px) { .steps { grid-template-columns: 1fr; gap: 40px; } }
/* 4. sign by QR: the wallet's transaction -> QR -> CardSigner (overview, address, approve, signed QR) -> back */
.ch-sign { min-height: 0; height: calc(360 * var(--svh, 1svh)); display: block; padding: 0; view-timeline: --sign block; }
.ch-sign .pin { position: sticky; top: calc(6 * var(--svh, 1svh)); display: flex; flex-direction: column; align-items: center; text-align: center; }
/* iOS jitter (Rob 2026-10-10): .sign-bar (fixed) sits outside the pin, because WebKit moves a fixed layer inside a sticky
   one with a per-frame correction in its scrolling tree. No will-change on the pin: WebKit gives a sticky element its own
   layer already, and in Chrome a transform layer here drew the pin on half pixels (soft text, resampled screens) */
.ch-sign .pin > h2, .ch-sign .pin > p { margin-left: auto; margin-right: auto; }
.flow { display: grid; grid-template-columns: minmax(0, 1fr) 56px minmax(0, 1.25fr) 56px minmax(0, 1fr); align-items: center; gap: 10px; width: min(94vw, 1080px); margin: calc(5 * var(--svh, 1svh)) 0 calc(3 * var(--svh, 1svh)); }
.tx { border-radius: 18px; padding: 18px 18px 16px; background: linear-gradient(180deg, #121215, #0b0b0d); box-shadow: inset 0 0 0 1px #24242a; text-align: left; }
.tx-k { font-size: 12px; color: #6c6c73; letter-spacing: .04em; text-transform: uppercase; margin: 0 0 4px; }
.tx-amt { font-weight: 600; font-size: clamp(20px, 2.2vw, 26px); color: #f2f2f4; margin: 0 0 12px; }
.tx-addr { font: 500 13px/1.45 var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace); color: #c9c9ce; word-break: break-all; margin: 0 0 14px; }
.tx-ok { font-weight: 600; font-size: 20px; color: #f2f2f4; }
.tx-ok::before { content: ''; display: inline-block; width: .5em; height: .5em; border-radius: 50%; background: #c3c3ca; margin-right: .5em; vertical-align: .1em; }
.tx-qr { position: relative; aspect-ratio: 1; background: #ececee; border-radius: 10px; overflow: hidden; }
.tx-qr img { filter: brightness(.925); } /* white #fff -> #ececec, black stays black */
.anim img { position: absolute; inset: 0; width: 100%; height: 100%; image-rendering: pixelated; opacity: 0; animation: qrframe 1.2s linear infinite; }
.anim img:nth-child(1) { animation-delay: 0s; } .anim img:nth-child(2) { animation-delay: -1s; } .anim img:nth-child(3) { animation-delay: -.8s; }
.anim img:nth-child(4) { animation-delay: -.6s; } .anim img:nth-child(5) { animation-delay: -.4s; } .anim img:nth-child(6) { animation-delay: -.2s; }
/* the hard cut as two keyframes 0.001 % apart, linear: WebKit runs no steps() animation on the compositor (GraphicsLayerCA) */
@keyframes qrframe { 0%, 16.666% { opacity: 1; } 16.667%, 100% { opacity: 0; } }
.dev { position: relative; aspect-ratio: 4 / 3; padding: 12px; }
.dev .st { position: absolute; inset: 12px; width: calc(100% - 24px); height: calc(100% - 24px); opacity: 0; image-rendering: pixelated; border-radius: 4px; will-change: opacity; } /* own layers before their range starts, so a cut is a compositor change, not a new layer */
.dev .st4 { background: #000; }
.arrow { position: relative; height: 2px; background: #2a2a2f; overflow: hidden; }
.arrow i { position: absolute; top: 0; bottom: 0; left: 0; width: 40%; background: linear-gradient(90deg, transparent, #e9e9ec, transparent); animation: flowdot 1.4s linear infinite; }
.arrow::after { content: ''; position: absolute; right: 0; top: -4px; border: 5px solid transparent; border-left-color: #2a2a2f; }
@keyframes flowdot { from { transform: translateX(-100%); } to { transform: translateX(250%); } } /* = left -40% to 100% of the arrow */
.flow-steps { list-style: none; counter-reset: step; margin: 0 auto; padding: 0; max-width: 54rem; min-height: 4.6em; position: relative; width: 100%;
  font-size: clamp(22px, 2.6vw, 31px); text-wrap: balance; }
/* Rob 2026-10-08: the steps way bigger, each with its number highlighted (the orange of the CardSigner screens); the
   number sits centred above its text, in the same place for every step, and the text is centred under it */
.flow-steps li { position: absolute; left: 0; right: 0; top: 0; display: flex; flex-direction: column; align-items: center; gap: .4em;
  font-weight: 500; line-height: 1.3; letter-spacing: -0.01em; color: #e9e9ec; text-align: center; opacity: 0; counter-increment: step; }
.flow-steps li::before { content: counter(step); flex: none; display: grid; place-items: center; width: 1.6em; height: 1.6em;
  border-radius: 50%; box-shadow: inset 0 0 0 1.5px rgba(255,159,10,.75); color: #ff9f0a; font-size: .9em; font-weight: 600; font-variant-numeric: tabular-nums; }
.ch-works { min-height: 0; padding: calc(12 * var(--svh, 1svh)) 0 calc(10 * var(--svh, 1svh)); align-items: center; } /* Rob: more room above "Works with" */
.ch-works .works-k { margin: 0 0 22px; font-size: 12px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: #6c6c73; text-align: center; }
.wallets { list-style: none; display: flex; flex-wrap: wrap; justify-content: center; gap: 30px; margin: 0; padding: 0; }
.wallets a { display: block; width: 64px; height: 64px; opacity: .8; transition: opacity .2s ease; }
.wallets a:hover { opacity: 1; }
.wallets img { width: 100%; height: 100%; object-fit: contain; }
@supports (animation-timeline: view()) {
  .tx:first-child, .a1, .dev, .a2, .tx-done { animation: beat-in linear both; animation-timeline: --sign; }
  .tx:first-child { animation-range: contain 0% contain 6%; }
  .a1 { animation-range: contain 6% contain 12%; }
  .dev { animation-range: contain 12% contain 18%; }
  /* hard cuts like the device itself: each screen is on only inside its own range (outside it the animation has no
     effect, so opacity is 0), and the ranges touch without overlap, so two screens are never on at once */
  .dev .st { animation: st-on linear; animation-timeline: --sign; }
  .dev .st1 { animation-fill-mode: backwards; animation-range: contain 0% contain 40%; }
  .dev .st2 { animation-range: contain 40% contain 57%; }
  .dev .st3 { animation-fill-mode: forwards; animation-range: contain 57% contain 100%; }
  .a2 { animation-range: contain 76% contain 82%; }
  .tx-done { animation-range: contain 82% contain 88%; }
  .flow-steps li:nth-child(1) { animation: st-show linear both; animation-timeline: --sign; animation-range: contain 0% contain 18%; }
  .flow-steps li:nth-child(2) { animation: st-show linear both; animation-timeline: --sign; animation-range: contain 16% contain 58%; }
  .flow-steps li:nth-child(3) { animation: st-show linear both; animation-timeline: --sign; animation-range: contain 56% contain 74%; }
  .flow-steps li:nth-child(4) { animation: st-show-last linear both; animation-timeline: --sign; animation-range: contain 72% contain 100%; }
}
/* without scroll timelines (Firefox, older Safari): the signing chapter is one static block, no pinned stretch: the
   approve screen, all four steps as a numbered list, no progress bar. Every step spans the list's width, so its number
   and text sit centred like the animated steps (Rob 2026-10-14: on an iPhone the short step 3 sat to the left) */
@supports not (animation-timeline: view()) {
  .ch-sign { height: auto; padding: calc(8 * var(--svh, 1svh)) 0; }
  .ch-sign .pin { position: static; }
  .dev .st3 { opacity: 1; }
  .flow-steps { display: flex; flex-direction: column; align-items: stretch; gap: .9em; min-height: 0; width: fit-content; max-width: 100%; }
  .flow-steps li { position: relative; opacity: 1; justify-content: flex-start; }
  .sign-bar { display: none; }
}
/* Rob 2026-10-08: a subtle vertical bar at the right edge that fills while the signing screen holds still, so the
   scroll position shows; it lives only in the pinned stretch (the contain range of the section) */
.sign-bar { position: fixed; right: calc(18px + env(safe-area-inset-right)); top: 50%; width: 2px; height: calc(34 * var(--svh, 1svh)); margin-top: calc(-17 * var(--svh, 1svh)); border-radius: 2px;
  background: rgba(242,242,244,.12); overflow: hidden; opacity: 0; pointer-events: none; z-index: 3; }
.sign-bar i { position: absolute; inset: 0; background: rgba(242,242,244,.7); transform: scaleY(0); transform-origin: 50% 0; }
@supports (animation-timeline: view()) {
  .sign-bar { animation: bar-show linear both; animation-timeline: --sign; animation-range: contain 0% contain 100%; }
  .sign-bar i { animation: bar-fill linear both; animation-timeline: --sign; animation-range: contain 0% contain 100%; }
}
@keyframes bar-show { 0% { opacity: 0; } 5% { opacity: 1; } 95% { opacity: 1; } 100% { opacity: 0; } }
@keyframes bar-fill { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@media (max-width: 760px) { .sign-bar { right: calc(9px + env(safe-area-inset-right)); height: calc(28 * var(--svh, 1svh)); margin-top: calc(-14 * var(--svh, 1svh)); } }
@keyframes st-show { 0% { opacity: 0; } 12% { opacity: 1; } 88% { opacity: 1; } 100% { opacity: 0; } }
@keyframes st-show-last { 0% { opacity: 0; } 12% { opacity: 1; } 100% { opacity: 1; } }
@keyframes st-on { from, to { opacity: 1; } }
@media (max-width: 760px) {
  .flow { grid-template-columns: 1fr; width: min(88vw, 420px); gap: 14px; }
  .arrow { width: 2px; height: 34px; justify-self: center; }
  .arrow i { width: auto; height: 40%; left: 0; right: 0; animation-name: flowdot-v; }
  .arrow::after { right: auto; left: -4px; top: auto; bottom: 0; border-left-color: transparent; border-top-color: #2a2a2f; }
  .ch-sign .pin { top: calc(70px + env(safe-area-inset-top)); } /* below the header's solid black and its fade */
  .ch-sign h2 { font-size: 30px; margin-bottom: .3em; }
  .tx:first-child { display: grid; grid-template-columns: 1fr 34%; column-gap: 12px; align-items: center; padding: 14px; }
  .tx:first-child .tx-qr { grid-column: 2; grid-row: 1 / span 4; width: 100%; }
  .tx-amt { font-size: 20px; margin-bottom: 8px; } .tx-addr { font-size: 11px; margin-bottom: 0; }
  .flow { margin: calc(2 * var(--svh, 1svh)) 0 calc(2 * var(--svh, 1svh)); gap: 8px; } .arrow { height: 18px; } .tx-done { padding: 10px 14px; } .tx-ok { font-size: 16px; }
  .flow-steps { font-size: 19px; min-height: 6em; margin-top: calc(1.6 * var(--svh, 1svh)); }
  .ch-sign { height: calc(420 * var(--svh, 1svh)); }
}
@keyframes flowdot-v { from { transform: translateY(-100%); } to { transform: translateY(250%); } }
/* 5. what is absent: struck through one by one */
.strike { list-style: none; margin: 0 0 calc(6 * var(--svh, 1svh)); padding: 0; }
.strike li { position: relative; width: max-content; font-weight: 600; font-size: clamp(40px, 8vw, 96px); line-height: 1.02; letter-spacing: -0.03em; color: #7c7c84; } /* Rob 2026-10-10: readable, still quieter than the title under it */
.strike li::after { content: ''; position: absolute; left: -2%; right: -2%; top: 55%; height: .07em; background: #e9e9ec; transform-origin: 0 50%; transform: scaleX(0); }
@supports (animation-timeline: view()) {
  .strike li::after { animation: strike linear both; animation-timeline: view(); animation-range: cover calc(28% + var(--i, 0) * 6%) cover calc(38% + var(--i, 0) * 6%); }
}
@keyframes strike { from { transform: scaleX(0); } to { transform: scaleX(1); } } /* an explicit start: see the explode keyframes */
/* the three lines are the title: each must still fit on one line, on a phone too */
.ch-absent h2 { font-size: min(5.7vw, 68px); max-width: none; } /* the longest line ("Anyone can read it and check it.") is about 14.6 em */
/* 7. DIY: a stand-in exploded line drawing (until the STEP files) */
/* Rob 2026-10-08: no hold; the drawing breaks apart while it scrolls in and the page moves on at once */
.ch-diy { min-height: 0; display: block; padding: calc(10 * var(--svh, 1svh)) 0 calc(12 * var(--svh, 1svh)); view-timeline: --diy block; }
.diy-pin { position: relative; }
.ch-diy { overflow-x: clip; } /* the rotated labels reach past the screen edge on a phone */
.explode { position: relative; height: calc(50 * var(--svh, 1svh)); width: min(86vw, 620px); margin: calc(4 * var(--svh, 1svh)) auto 0; --rz: -28deg; transform: rotateX(58deg) rotateZ(var(--rz)); transform-style: preserve-3d; }
.lay { position: absolute; left: 10%; right: 10%; top: 40%; height: 26%; border: 1.5px solid rgba(233,233,236,.55); border-radius: 12px; transform-style: preserve-3d; } /* preserve-3d: the label's counter-rotation then makes it flat to the screen */
.l-top, .l-bot { border-radius: 999px; left: 0; right: 0; height: 36%; top: 32%; }
.l-screen { left: 30%; right: 30%; }
.l-cam { left: 44%; right: 44%; height: 12%; top: 47%; border-radius: 50%; }
.lay span { position: absolute; left: 104%; top: 50%; white-space: nowrap; font-size: 13px; color: #8e8e95; transform: rotateZ(calc(-1 * var(--rz))) rotateX(-58deg); transform-origin: 0 0; opacity: 0; }
.l-top { --dy: -150%; } .l-screen { --dy: -95%; } .l-pcb { --dy: -40%; } .l-pi { --dy: 20%; } .l-cam { --dy: 160%; } .l-bot { --dy: 140%; }
/* every label starts just past the widest layer (104 % of the drawing, in the drawing's own plane), so no label sits on a line */
.l-screen span { left: 185%; } .l-pcb span, .l-pi span { left: 117.5%; } .l-cam span { left: 500%; }
/* on a phone the drawing turns less and is smaller, and the section clips at the screen edge (not at the page margin),
   so the drawing and all labels fit between the two screen edges */
@media (max-width: 700px) {
  .ch-diy { margin: 0 calc(-1 * clamp(24px, 7vw, 120px)); padding: calc(10 * var(--svh, 1svh)) clamp(24px, 7vw, 120px) calc(12 * var(--svh, 1svh)); }
  .explode { --rz: -18deg; width: 50vw; height: calc(34 * var(--svh, 1svh)); margin-left: 7vw; }
  .lay span { font-size: 11.5px; }
}
/* a tablet (701-1198 px) without WebGL: the centred 620 px drawing put its labels past the section's clip edge (Rob
   2026-10-14: "check for other flaws like it"), so the drawing is smaller and set left there, like on a phone */
@media (min-width: 701px) and (max-width: 1198px) {
  .explode { --rz: -22deg; width: min(34vw, 520px); margin-left: calc(14 * var(--svh, 1svh) + 2vw); } /* the top shell reaches about 12 svh left of the drawing (it rises with the height); fitted on 15 tablet sizes, at least 23 px clear */
}
@supports (animation-timeline: view()) {
  .lay { animation: linear both; animation-timeline: --diy; animation-range: entry 35% cover 50%; }
  .l-top { animation-name: explode-top; } .l-screen { animation-name: explode-screen; } .l-pcb { animation-name: explode-pcb; }
  .l-pi { animation-name: explode-pi; } .l-cam { animation-name: explode-cam; } .l-bot { animation-name: explode-bot; }
  .lay span { animation: label-in linear both; animation-timeline: --diy; animation-range: cover 42% cover 55%; }
}
/* every scroll-driven keyframe set names its start (and the explode holds no var(), to be safe): in WebKit an
   implicit start keyframe can keep the animation off the compositor (WebKit bug 312140, fixed 2026-04-13), so on an
   iPhone it ran on the main thread, a frame behind the finger, and jittered against the page. So one explode set
   per layer (named above), the same --dy values */
@keyframes explode-top { from { transform: none; } to { transform: translateY(-150%); } }
@keyframes explode-screen { from { transform: none; } to { transform: translateY(-95%); } }
@keyframes explode-pcb { from { transform: none; } to { transform: translateY(-40%); } }
@keyframes explode-pi { from { transform: none; } to { transform: translateY(20%); } }
@keyframes explode-cam { from { transform: none; } to { transform: translateY(160%); } }
@keyframes explode-bot { from { transform: none; } to { transform: translateY(140%); } }
@keyframes label-in { from { opacity: 0; } to { opacity: 1; } }
/* 8. ports, 9. specs, 10. buy */
.specs { margin: calc(4 * var(--svh, 1svh)) 0 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 40px; }
.specs > div { padding: 18px 0; border-top: 1px solid #1f1f23; }
.specs dt { font-size: 13px; color: #8a8a90; margin: 0 0 6px; }
.specs dd { margin: 0; font-size: 17px; color: #e9e9ec; }
.specs dd a { color: inherit; text-underline-offset: .2em; }
@media (max-width: 700px) { .specs { grid-template-columns: 1fr; } }
.ch-buy { align-items: flex-start; }

/* USB-C and microSD: text beside a drawing; the plug and the card slide in on scroll */
.ch-port { min-height: 0; padding: calc(10 * var(--svh, 1svh)) 0; display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); align-items: center; gap: 5vw; }
.ch-port h2 { font-size: clamp(34px, 5.2vw, 60px); }
/* step 1 (microSD) then step 2 (USB-C): one pair, so the gap between them is small */
.ch-sd { padding-bottom: calc(6 * var(--svh, 1svh)); }
.ch-power { padding-top: calc(6 * var(--svh, 1svh)); }
.nowrap { white-space: nowrap; }
.ch-port .step { display: grid; place-items: center; width: 1.5em; height: 1.5em; margin: 0 0 .8em; border-radius: 50%; box-shadow: inset 0 0 0 1.5px rgba(255,159,10,.75);
  color: #ff9f0a; font-size: 22px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; } /* Rob 2026-10-08: like the numbers of the signing steps */
.ch-port .prod { width: min(58vw, 250px); height: auto; justify-self: center; filter: drop-shadow(0 30px 60px rgba(0,0,0,.6)); }
/* the product images are not inserted: they float in and settle (Rob 2026-10-08) */
@supports (animation-timeline: view()) {
  .prod-sd { animation: prod-in linear both; animation-timeline: view(); animation-range: entry 10% cover 50%; --r0: 8deg; }
}
.ch-port .prod-sd { width: min(30vw, 120px); } /* Rob 2026-10-08: the microSD card smaller again (the cable stays) */
.prod-usb { -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 97%); mask-image: linear-gradient(to bottom, #000 62%, transparent 97%); }
@keyframes prod-in { from { opacity: 0; transform: translateY(70px) rotate(var(--r0)) scale(.94); } to { opacity: 1; transform: none; } }
@media (max-width: 760px) { .ch-port { grid-template-columns: 1fr; gap: calc(4 * var(--svh, 1svh)); } }
/* pre-order: fully centred */
/* Rob 2026-10-10 ("final reveal ... needs to be slower"; later: "below that should by regular scroll appear the
   preorder"): .ch-buy is an empty runway (--fin-run: the canvas shows the device coming up over it, main.js) and then
   the last screen, .buy-pin, one screen tall in plain page flow, so its copy scrolls in like any section and the page
   ends on that screen, no scrolling past it.
   Rob 2026-10-10 (later: "The preorder text must come way sooner and reveal itself be way slower"): the reveal starts
   with the specs' last lines on screen and its length follows the heading (main.js FIN_COPY), so the runway sets it:
   Rob 2026-10-10 ("This should not conflict"): the reveal starts once the specs text has gone under the topbar.
   Rob 2026-10-11 ("that pre-order CTA should come even sooner, way sooner, immediately after"): the reveal starts while
   the specs are still on the view (main.js FIN_START) and the heading comes up as the specs' last line goes under the
   topbar: 30 svh (38 until Rob's "make the pre-order text ... come a little bit sooner"). On a wide screen the specs
   chapter has more room under its list, hence less runway */
.story { --fin-run: calc(30 * var(--svh, 1svh)); }
@media (min-width: 1000px) { .story { --fin-run: calc(13 * var(--svh, 1svh)); } }
.ch-buy { display: block; text-align: center; min-height: 0; padding: var(--fin-run) 0 0; }
.buy-pin { position: relative; height: calc(100 * var(--svh, 1svh)); display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ch-buy .foot { position: absolute; left: 0; right: 0; bottom: calc(20px + env(safe-area-inset-bottom)); padding: 0; }
.ch-buy .foot p { margin: 0 auto; font-size: 12px; line-height: 1.5; color: #8a8a90; max-width: 34rem; }
.foot a { color: inherit; text-underline-offset: .18em; }
.ch-buy h2, .ch-buy p { margin-left: auto; margin-right: auto; }
.buy-pin > p { max-width: 46rem; }
.buy-pin > p .ln { display: block; text-wrap: balance; } /* each line wraps on its own and evenly on a phone */
@media (min-width: 1000px) { .buy-pin > p { max-width: none; } } /* on a desktop, both lines of the price text stay unbroken */
.ch-buy .btn { align-self: center; }

/* the card line stays on one line on wide screens (Rob) */
@media (min-width: 1000px) { .holds-text p { max-width: none; white-space: nowrap; } }

@media (max-width: 700px) {
  .topbar { --fade: 18px; padding: calc(12px + env(safe-area-inset-top)) calc(18px + env(safe-area-inset-right)) 22px calc(18px + env(safe-area-inset-left)); }
  .brand img { width: 124px; } .topbar-cta { padding: 8px 17px; font-size: 14px; }
}
@media (max-width: 700px) { .strike li { font-size: 8.4vw; } } /* the struck words stay under the open-source title, which leads */

/* static mode: main.js may have stopped before it set the per-item indices, so the CSS sets them (the same values) */
.no-gl .fan .card:nth-child(1), .no-gl .strike li:nth-child(1) { --i: 0; } .no-gl .fan .card:nth-child(2), .no-gl .strike li:nth-child(2) { --i: 1; }
.no-gl .fan .card:nth-child(3), .no-gl .strike li:nth-child(3) { --i: 2; } .no-gl .fan .card:nth-child(4), .no-gl .strike li:nth-child(4) { --i: 3; }
.no-gl .fan .card:nth-child(5) { --i: 4; }
/* without scroll timelines every chapter shows its end state: the fan open, the lines struck, the SeedKeeper turned,
   the drawing exploded with its labels, and no empty pinned stretch */
@supports not (animation-timeline: view()) {
  .fan .card { transform: translate(calc(-50% + (var(--i, 0) - 2) * var(--spread)), -50%) rotate(calc((var(--i, 0) - 2) * var(--turn))); }
  .strike li::after { transform: scaleX(1); }
  .sk-card { transform: rotateY(10deg) rotateX(6deg) rotateZ(2deg); }
  .ch-diy { height: auto; padding: calc(12 * var(--svh, 1svh)) 0 calc(20 * var(--svh, 1svh)); }
  .diy-pin { position: static; }
  .lay { transform: translateY(var(--dy)); }
  .lay span { opacity: 1; }
}

/* ---------- static mode (main.js adds html.no-gl): no WebGL, a 3D build that failed or is too slow (JS without the
   JIT on Vanadium, Lockdown Mode), or an error in the 3D path. The page is then a normal page: a short hero with a
   still of the device and the first line as real HTML, the header shown, then the chapters. !important where the
   story writes inline styles, so a late write can never hide the text again */
.still { display: none; }
.no-gl #gl, .no-gl .cue-w { display: none !important; }
.no-gl #track { height: auto !important; padding: calc(78px + env(safe-area-inset-top)) clamp(24px, 7vw, 120px) calc(14 * var(--svh, 1svh)); }
/* only the one image that matches loads (a background, and only under .no-gl): 1600 px wide for a desktop, 800 for a phone */
.no-gl .still { display: block; width: min(100%, 760px, calc(72 * var(--svh, 1svh))); aspect-ratio: 1960 / 1120; margin: 0 auto calc(7 * var(--svh, 1svh));
  background: url("../img/still-1600.webp") center / contain no-repeat; }
@media (max-width: 700px) { .no-gl .still { background-image: url("../img/still-800.webp"); margin-bottom: calc(5 * var(--svh, 1svh)); } }
.no-gl .sui-stage { position: static; height: auto; pointer-events: auto; }
.no-gl .screen-ui { position: static; visibility: visible !important; opacity: 1 !important; transform: none !important;
  -webkit-mask-image: none !important; mask-image: none !important; width: auto !important; height: auto !important; padding: 0 !important;
  max-width: 60rem; margin: 0 auto; pointer-events: auto; user-select: text; -webkit-user-select: text; }
.no-gl .screen-ui h2 { font-size: clamp(36px, 9.5vw, 88px); line-height: 1.08; margin: 0 0 .95em; }
.no-gl .screen-ui p + p, .reduce .screen-ui p + p { margin-top: 1.3em; } /* the two sentences apart, as on the 3D screen */
.no-gl .screen-ui p { font-size: clamp(21px, 3vw, 32px); line-height: 1.62; text-wrap: balance; }
.no-gl .story, .no-gl .ch-specs { clip-path: none !important; }
.no-gl .ch-cards { margin-top: 0; }
.no-gl .topbar { opacity: 1 !important; pointer-events: auto !important; }
/* short phones: the pinned signing block must fit the screen with the bigger numbered steps (Rob 2026-10-08) */
@media (max-width: 760px) and (max-height: 860px) {
  .ch-sign .dev { width: 76%; justify-self: center; }
  .ch-sign .pin > p { font-size: 16px; }
  .flow-steps { font-size: 17px; min-height: 5.6em; }
  .flow { gap: 5px; } .arrow { height: 12px; }
}
@media (max-width: 760px) and (max-height: 700px) {
  .ch-sign h2 { font-size: 26px; }
  .ch-sign .dev { width: 62%; }
  .tx:first-child { padding: 10px 12px; }
  .flow { margin: calc(1.2 * var(--svh, 1svh)) 0; }
  .ch-sign .pin > p { display: none; } /* the four numbered steps say the same */
}
/* 7b. DIY in 3D (diy.js adds .has-3d once the real case is loaded; static mode keeps the line drawing) */
.diy-gl { display: none; }
/* 72 svh on a wide screen (Rob 2026-10-13: "too small on desktop"): the stack 1.24x, still wholly in view through the
   explode (to p 0.595); at 78 its top would go under the bar before the explode ends */
.has-3d .diy-gl { display: block; width: min(100%, 980px); height: calc(72 * var(--svh, 1svh)); margin: calc(3 * var(--svh, 1svh)) auto 0; }
.has-3d .explode { display: none; }
.no-gl .diy-gl { display: none; } /* its labels are drawn in the canvas (diy.js: 13 px, 11.5 px on a phone, #8e8e95) */
.no-gl .ch-diy .explode { display: block; }
/* on a phone the canvas runs to the screen's edges, and the picture fits the width between the page margins (diy.js
   padX). No labels on a phone (Rob 2026-10-13), so the picture takes that width: 60 svh tall makes the exploded stack
   as wide as the text column (1.37x the labelled one) */
@media (max-width: 700px) { .has-3d .diy-gl { width: calc(100% + 2 * clamp(24px, 7vw, 120px)); height: calc(60 * var(--svh, 1svh)); margin-left: calc(-1 * clamp(24px, 7vw, 120px)); } }
