/* ============================================================================
   Scratchy - design tokens & chrome
   Calm, professional dark chrome; the playfulness lives in the canvas.
   Gradient appears in exactly two places: active tool pill + primary CTAs.
   ========================================================================== */
:root {
  --bg0: #0b0b10;
  --surface: #14141c;
  /* Four rules already reference --surface-1 (the more-menu list, the collab
     join card, the avatar rings) and it was never defined, so all four resolved
     to nothing: two transparent panels and two invisible rings. Defining it is
     the fix; it is the same value those callers clearly meant. */
  --surface-1: #14141c;
  --surface-2: #1c1c26;
  --surface-3: #262633;
  --line: rgba(255, 255, 255, 0.07);
  --line-strong: rgba(255, 255, 255, 0.12);
  --ink: #eceef6;
  --muted: #8d90a8;
  --accent: #7c5cff;
  --accent-2: #38d6ff;
  --accent-grad: linear-gradient(135deg, #7c5cff, #38b6ff);
  --accent-tint: rgba(124, 92, 255, 0.16);
  --good: #35d99a;
  --danger: #ff5470;
  --radius: 14px;
  --radius-lg: 20px;
  --touch: 48px;
  /* Tall enough to hold the tallest control with its padding: the mode button
     is 60 and the bar adds 4 above it. Sized to the contents rather than the
     contents squeezed to fit - the board and the sidebar measure this box, so
     it staying honest is what keeps them off the controls. */
  --bar-h: 68px;
  --shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
  /* Floating things get THREE layers, not one blur. A single wide shadow reads
     as a fuzzy halo; a real object above a surface has a tight contact shadow
     directly beneath it, a mid falloff, and a wide soft cast. The tint is
     #06060e rather than pure black because the backdrop is a blue-violet grey
     (#262632) - a neutral-black shadow on a tinted ground reads muddy, a
     shadow carrying the ground's own hue reads like light behaving properly.
     The 1px rim is the top-lit edge catching light, which is what actually
     separates an object from what is behind it. */
  --float-rim: 0 0 0 1px rgba(255, 255, 255, 0.045);
  --float-contact: 0 1px 2px rgba(6, 6, 14, 0.55);
  --float-mid: 0 6px 18px rgba(6, 6, 14, 0.42);
  --float-cast: 0 22px 60px rgba(6, 6, 14, 0.45);
  /* Glass. The top bar carries no fill of its own, so each control has to
     supply its own contrast against whatever artwork happens to be behind it -
     which is what a lens does and a flat chip does not.
     These numbers are #hint's, promoted rather than invented: that recipe is
     already shipping and already survived WebKit. The two insets are the part
     that turns a translucent rectangle into glass - a specular highlight along
     the top edge where light catches, and a shade along the bottom that grounds
     it. Without the pair you get a grey box you can see through. */
  --glass-fill: rgba(10, 10, 18, 0.58);
  --glass-press: rgba(10, 10, 18, 0.72);
  --glass-blur: blur(18px) saturate(160%);
  --glass-edge: rgba(255, 255, 255, 0.16);
  --glass-hi: inset 0 1px 0 rgba(255, 255, 255, 0.18);
  --glass-lo: inset 0 -1px 0 rgba(0, 0, 0, 0.28);
  /* No vertical offset. The controls sit ON the bottom edge of the screen, and
     a shadow cast downward has nowhere to land - it was being sliced off flat
     against the edge, which is worse than no shadow at all. A centred one has
     no far side to lose: it reads as the button lifting off the page in every
     direction equally, and clipping it at the bottom is imperceptible. */
  --glass-shadow: 0 0 16px rgba(0, 0, 0, 0.45);
  /* What the same capsule becomes where blur is unavailable, switched off for
     accessibility, or pointless because it sits on an opaque panel. Opaque, and
     light enough to read against --surface. */
  --glass-solid: #242430;
  /* One rainbow per mode. Every ring is a full spectrum - they are all
     recognisably Scratchy - but each STARTS on its own mode's colour and
     rotates from there, so the ring you see on the mode button matches the one
     on that mode's row in the menu. The mode's own hue leads; the rest follow.
     Defined once here because two places draw them and they must not drift. */
  --ring-scratch:  conic-gradient(from 200deg, #ff5470, #ffd23a, #35d99a, #38d6ff, #7c5cff, #ff5470);
  --ring-draw:     conic-gradient(from 265deg, #7c5cff, #ff5470, #ffd23a, #35d99a, #38d6ff, #7c5cff);
  --ring-magic:    conic-gradient(from 150deg, #38d6ff, #7c5cff, #ff5470, #ffd23a, #35d99a, #38d6ff);
  --ring-colour:   conic-gradient(from  30deg, #ffd23a, #ff9d3a, #ff5470, #7c5cff, #38d6ff, #ffd23a);
  --ring-maze:     conic-gradient(from 110deg, #35d99a, #38d6ff, #7c5cff, #ff5470, #ffd23a, #35d99a);
  --ring-dots:     conic-gradient(from 320deg, #ff9d3a, #ffd23a, #35d99a, #38d6ff, #7c5cff, #ff9d3a);
  --ring-squiggle: conic-gradient(from  75deg, #ff5470, #7c5cff, #38d6ff, #35d99a, #ffd23a, #ff5470);
  --glass-solid-press: #2e2e3c;
  --font-display: ui-rounded, "SF Pro Rounded", "Segoe UI Rounded", system-ui, sans-serif;
  --font-ui: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif;
  --ease: cubic-bezier(0.3, 0.9, 0.4, 1);
  /* Motion tokens for things ARRIVING and LEAVING. Deliberately not --ease:
     one curve for every purpose is what makes an interface read as merely
     fine. --ease-enter is Material 3's emphasized-decelerate, shaped for an
     object that comes to rest; --ease-exit is its accelerate counterpart, for
     something getting out of the way. Entering is given longer than leaving on
     purpose (NN/g: ~100ms reads as instant, 200-300ms suits substantial
     changes, past 500ms motion "starts to feel like a real drag"). */
  --ease-enter: cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-exit: cubic-bezier(0.3, 0, 0.8, 0.15);
  --dur-enter: 300ms;
  --dur-exit: 200ms;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
  /* The app is a fixed, non-scrolling canvas, so html/body ARE the app's height
     and every layer below is inset:0 inside them. Anything short shows as bare
     page background along the bottom - the black bar.
     100lvh is the browser's own answer to that and needs no JS at all. The
     @supports block below upgrades to it; this line is the floor for engines
     without it (iOS 15.0-15.3 is inside our deployment target). It must stay a
     separate declaration - putting dvh inside the var() fallback would make the
     whole declaration invalid at computed-value time on those engines, which
     collapses the layout rather than falling back to it.
     --app-height is now set ONLY while the on-screen keyboard is up, which is
     the one case dvh deliberately does not cover. */
  margin: 0; height: var(--app-height, 100vh); width: 100%;
  overscroll-behavior: none;
  /* The page canvas - html's own background - is painted over the WHOLE screen,
     including the strip behind the browser's toolbar that dvh deliberately
     excludes. Painting it the backdrop's outer colour is what makes the grey
     appear to run on behind the chrome instead of stopping dead against a
     near-black band. body keeps --bg0 for the full-screen surfaces (hub,
     gallery) that sit on it. */
  color: var(--ink);
  font-family: var(--font-ui); font-size: 15px;
  touch-action: none; user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none; /* iOS long-press callout would cancel spiral holds */
}
/* The page is deliberately ONE PIXEL taller than the screen, and html scrolls.
   Safari only gives a page the translucent, content-behind toolbar treatment
   when the page is scrollable; a fixed, non-scrolling page gets the opaque
   toolbar and a shortened viewport instead - which is the whole reason the
   bottom of the app has been fighting that strip.
   The app itself is still fixed and still cannot be scrolled by a finger:
   #app-root below is position:fixed, and body keeps touch-action:none. The
   extra pixel exists only to answer Safari's question with a yes. */
html {
  overflow-y: auto;
  height: 100%;
}
body {
  position: relative;
  min-height: calc(var(--app-height, 100vh) + 1px);
  overflow: hidden;
}
@supports (height: 100lvh) {
  body { min-height: calc(var(--app-height, 100lvh) + 1px); }
}

/* lvh, NOT dvh. dvh is the DYNAMIC viewport - it deliberately shrinks to leave
   room for the browser's URL bar and the home indicator, which is precisely the
   strip that kept showing as a black bar along the bottom. lvh is the LARGE
   viewport: the height as though that chrome were retracted, so the page runs
   behind it and fills the screen edge to edge, which is what viewport-fit=cover
   is for. Anything that must stay clear of the chrome uses env(safe-area-inset-*)
   already, so nothing important ends up underneath it. */
@supports (height: 100lvh) {
  html, body { height: var(--app-height, 100lvh); }
}
/* #262632 is the outer stop of #backdrop's gradient, so the seam where the app
   ends and the chrome begins is invisible. */
/* The backmost layer of the whole app is the light grey the backdrop settles to.
   Everything else paints on top of it, so any region no component covers - the
   safe area, behind browser chrome, the strip under a drawer - is that grey
   rather than a darker colour nobody put there.
   body is transparent on purpose: it used to paint --bg0, which sat in front of
   this and produced the dark band. The full-screen surfaces (hub, gallery) carry
   their own --bg0 background, so nothing loses its colour by this.
   The launch surface is exempt: it is opaque black by its own rule, so the app
   still opens on black and only becomes grey once it has gone. */
html { background: #262632; }
/* While the launch surface is up the whole screen is black, including the strip
   below body's box, so the surface reads as one unbroken sheet. */
html.launching { background: #000; }
body { background: transparent; }
button { font-family: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }
/* The drawers take focus themselves so nothing inside them shows a ring
   mid-slide (see show() in ui.js); the panel itself must not show one
   either - it is the surface, not a control. */
#hub:focus-visible, #morePopover:focus-visible { outline: none; }
.icn { display: block; flex: none; }

/* ----------------------------------------------------------------- launch */
/* The sheet and the chrome are separate on purpose. The SHEET is the opaque
   black that hides boot; it fades to nothing once the app is ready, revealing
   the board's own black cover underneath - same colour, so there is no visible
   handover. The CHROME (mark, spinner, Continue) outlives it and floats over
   the live canvas until the first stroke. */
#launch { position: fixed; inset: 0; z-index: 150; }
/* Black from the very first frame, in CSS - NOT waiting on the canvas.
   The cover is only painted once the engine is ready, so leaving the backdrop
   to the canvas meant the whole boot phase was a transparent sheet over a
   visible app: no black screen, no logo, just the app with a stray spinner. */
#launch[data-phase="boot"], #launch[data-phase="live"] { background: #000; }
#launch[data-phase="gone"] { display: none; }
#launchArt, #launchSheet { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#launchSheet { touch-action: none; }
/* The whole overlay fades on the way out; the surface itself never dissolves
   under the user - it is theirs to scratch until they leave it. */
#launch { transition: opacity 380ms var(--ease-exit); }
#launch[data-phase="fading"] { opacity: 0; pointer-events: none; }
/* While the launch surface owns the screen the app behind it is not merely
   covered, it is absent: no toolbar, no rail, no fitted board peeking around
   the edges. This is one full-bleed surface, not a lid on the app. */
body.launching #topbar,
body.launching #sidebar,
body.launching #board,
body.launching #hint { visibility: hidden; }

#launchChrome {
  /* It spans the whole overlay so the mark can sit centred, which means it
     would otherwise swallow every touch aimed at the surface underneath it.
     Only the button takes input. */
  pointer-events: none;
  position: absolute; inset: 0;
  /* Sat dead centre, the mark and the button were islands in the middle of the
     play area - the best part of the screen to scratch, and the button kept
     getting scribbled over. Pushed low: clean canvas above, chrome out of the
     way below. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px;
  transition: opacity 320ms var(--ease-exit), transform 320ms var(--ease-exit);
}
#launch[data-phase="fading"] #launchChrome { opacity: 0; transform: scale(0.97); }
/* Under the icon, which is centred. No spinner here: the surface is scratchable
   from the first frame, so there is nothing to wait for and nothing to say so. */
#launchHint {
  min-height: 44px; margin-top: 28vh;
  /* No plate, no border, no pill. It is an instruction printed on the surface,
     not a control - anything with a button's shape asks to be pressed, and
     pressing it is not what opens the app. */
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center; padding: 0 24px;
  animation: hintBreath 2.4s var(--ease) infinite;
}
.launch-hint-title {
  color: #fff; font-family: var(--font-display); font-weight: 800;
  /* Smaller than the 30px it started at: this is an instruction under the
     mark, not a headline of its own, and at 30 it was reading as the louder of
     the two things on screen. */
  font-size: 23px; letter-spacing: 0.2px; line-height: 1.1;
  /* The surface underneath is being scratched away to colour, so the words have
     to hold up over black AND over whatever gets revealed. */
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.55);
}
.launch-hint-sub {
  color: rgba(255, 255, 255, 0.72); font-size: 13px; font-weight: 500;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
}
#launchHint[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { #launchHint { animation: none; } }
/* The one thing that must stay tappable once the sheet has stopped taking
   input - it is the path for anyone who will not or cannot scratch. */

@media (prefers-reduced-motion: reduce) {
  #launchSheet, #launchChrome { transition: none; }
}

/* ---------------------------------------------------------------- stage */
#backdrop {
  position: absolute; inset: 0;
  /* lighter than the canvas so the black scratch board reads as a distinct
     sheet floating on the surface */
  /* The vertical radius is a PERCENTAGE of the height, not a fixed 820px, and
     it has to be: html paints this gradient's outer colour in the strip below
     the backdrop, so the gradient must have finished settling to that exact
     colour by the time it reaches the bottom edge - on EVERY screen. At a fixed
     radius it had not; on a 844pt phone it arrived at #292935 against a #262632
     strip, and that 3-level step is the seam visible across the bottom.
     The centre sits at 38% of the height, so the bottom edge is 62% of the
     height away; at a radius of 55% the outer stop (74% of it, i.e. 41%) lands
     well short of that on everything down to a 667pt screen. */
  background: radial-gradient(1200px 55% at 50% 38%, #3a3a48 0%, #262632 74%);
}
#stage { position: absolute; inset: 0; overflow: hidden; }
#board {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0; will-change: transform;
  /* Layered rather than one 70px blur, and a hair of rim light so the sheet's
     edge is legible against the backdrop instead of dissolving into it. The
     cast is deliberately the widest layer: the board is the biggest object on
     screen and needs the most air beneath it to sit above the grey.
     Radius stays 2px - there is no overflow:hidden here, so the radius shapes
     the shadow while the canvases inside stay square, and anything larger
     would show that mismatch at the corners. */
  box-shadow:
    var(--float-rim),
    0 2px 4px rgba(6, 6, 14, 0.5),
    0 10px 28px rgba(6, 6, 14, 0.45),
    0 34px 80px rgba(6, 6, 14, 0.5);
  border-radius: 2px;
}
#board canvas { position: absolute; top: 0; left: 0; display: block; touch-action: none; }
/* While a drawing is being built there is nothing worth looking at: show the
   spinner alone rather than a blank page, a stale one, or the scratch hint
   inviting a tap on something that is not there yet. The board keeps its box
   (see showLoading in ui.js) so measurements stay valid. */
body.canvas-loading #board,
body.canvas-loading #hint { visibility: hidden; }
/* The picture arrives. Opacity ONLY - #board's transform belongs to
   viewport.js, which inverts it to place every scratch (screenToDoc), so a
   competing transform animation here would land strokes in the wrong place.
   The sense of arrival comes from viewport.settleIn() instead, which does the
   same thing through the object that owns the property.
   The 60ms delay (with `backwards`, so it holds at 0 meanwhile) lets the
   spinner begin leaving first: the two cross-dissolve instead of handing off. */
@keyframes boardIn { from { opacity: 0; } to { opacity: 1; } }
body.canvas-revealing #board { animation: boardIn var(--dur-enter) var(--ease-enter) 60ms backwards; }
/* Staged, not simultaneous: the picture arrives first and the invitation to
   scratch follows it. Showing the hint at full strength over an empty board -
   which is what happened when both were ungated - reads as a caption with
   nothing to caption. */
body.canvas-revealing #hint {
  animation: boardIn 260ms var(--ease-enter) 300ms backwards;
  /* No blur while it fades. Animating the opacity of a backdrop-filtered
     surface is the exact combination that made the loading scrim flicker; the
     chip is translucent enough to read during the fade, and the blur arrives
     with the class being dropped. */
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
#reveal { z-index: 1; } #scratch { z-index: 2; } #overlay { z-index: 3; pointer-events: none; }

#stencilLayer { position: absolute; inset: 0; z-index: 20; pointer-events: none; }
/* At the TOP now: the controls moved to the bottom of the screen, and these
   were sitting on top of them. */
#stencilControls {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  display: flex; gap: 10px; pointer-events: auto;
}

/* desktop brush cursor ring */
#cursorRing {
  position: absolute; left: 0; top: 0; z-index: 15; pointer-events: none;
  border: 1.5px solid rgba(255, 255, 255, 0.85); border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  will-change: transform; /* positioned via transform from JS */
}

/* first-scratch shimmer hint */
#magicLoading {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  /* No backdrop-filter. Since the board is hidden while a drawing builds, the
     only thing behind this scrim is static chrome - blurring it bought nothing
     and cost a compositing layer that visibly flickered as the scrim faded.
     The alpha is a touch stronger to make up the lost separation. */
  background: rgba(6, 6, 14, 0.55);
}
/* Leaving: shorter than the arrival, and accelerating away rather than easing
   out.
   The SCRIM only fades - it must never scale. It is a full-screen fixed layer,
   so shrinking it drags its own edges inward across the screen and you see the
   boundary sweep past as a rectangular seam. The scale belongs on the spinner
   inside it, which is what was meant all along. */
@keyframes loaderOut { to { opacity: 0; } }
@keyframes spinnerOut { to { opacity: 0; transform: scale(0.9); } }
#magicLoading.is-leaving {
  animation: loaderOut var(--dur-exit) var(--ease-exit) forwards;
  pointer-events: none;
}
#magicLoading.is-leaving .scratchy-spinner {
  animation: spinnerOut var(--dur-exit) var(--ease-exit) forwards;
}

/* ---- Scratchy loading spinner: a rainbow felt-tip line traces a hand-wobbly
   circle, drawing forward and erasing behind - like a pen actually scratching
   it out, not a wedge sliding over a fixed ring. One shared <mask> (defined
   once in index.html) drives every instance in sync; only the size differs. */
.scratchy-spinner {
  width: var(--sp-size); height: var(--sp-size);
  position: relative; margin: 0 auto;
  /* No drop-shadow: on WebKit its layer bounds showed as a faint rectangle
     over the scrim's gradient - a box around the spinner.
     But the filter was ALSO promoting this to its own compositing layer, and
     losing that promotion is the most likely reason the pen started stalling
     while the main thread is busy. will-change asks for the same promotion
     with nothing drawn, so there is no box. It is honest here rather than a
     hack: the spinner really does animate transform on its way out.
     NOT confirmed by measurement - in Chromium the stroke kept advancing
     through a 1500ms main-thread block with the filter and without it, so
     that engine cannot tell the two apart. This is reasoned, and cheap to
     undo if it does not help. */
  will-change: transform;
}
.scratchy-spinner--lg { --sp-size: 72px; }
.scratchy-spinner--sm { --sp-size: 34px; margin: 4px auto 2px; }

/* The SVG path is visible (not inside a <mask>) so stroke-dashoffset
   animation works in WebKit. A rotating SVG linearGradient (SMIL
   <animateTransform>) gives the rainbow; CSS animates the dash. */
.scratchy-spinner__svg {
  width: 100%; height: 100%; overflow: visible;
}
/* The worker-drawn canvas replaces the SVG, but only once the hand-over has
   actually happened - data-wk is set after postMessage, so any failure along
   the way (no OffscreenCanvas, worker refused to start) leaves the SVG
   visible and animating exactly as before. */
.scratchy-spinner__cv { position: absolute; inset: 0; width: 100%; height: 100%; }
.scratchy-spinner[data-wk="1"] .scratchy-spinner__svg { visibility: hidden; }
.scratchy-spinner__stroke {
  stroke-dasharray: 0.34 0.66;
  animation: scratchyDraw 1.8s linear infinite;
}
/* NO rotation here, deliberately.
   A spin was added to keep the spinner alive while the main thread is blocked
   (stroke-dashoffset is a paint property and cannot advance then). It worked,
   and it was wrong: rotating the glyph overwhelmed the pen tracing the circle
   and turned a hand-drawn mark into an ordinary loading spinner. The drawing
   IS the character here, so the freeze is the better trade - and it is much
   rarer now that the analysis and dot-puzzle work run in a worker. */
@keyframes scratchyDraw { to { stroke-dashoffset: -1; } }

/* The hint sits on the artwork itself, so as plain white text it landed on
   whatever the picture happened to be - white on a pale sky, or on a busy
   patch, and became unreadable. It now rides a frosted chip that carries its
   own contrast, so the words are legible over any card.
   The fill is DARK rather than the lighter Apple-style glass: the text is
   white, and a light frost over bright artwork gives white-on-white again,
   which is the bug. */
#hint {
  position: absolute; z-index: 14; left: 50%; top: 50%; isolation: isolate;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 9px; white-space: nowrap;
  color: #fff; font-family: var(--font-display);
  /* Smaller than it was: now that the chip sits ON the canvas rather than
     floating over the middle of the screen, it covers the drawing it is
     inviting you to scratch, so it earns less room. */
  font-size: 16px; font-weight: 700; letter-spacing: 0.2px;
  pointer-events: none;
  padding: 9px 15px; border-radius: 999px;
  background: rgba(10, 10, 18, 0.44);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid transparent;
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.38);
}
/* The same rainbow ring the Fill/Done chips wear, and traced the same way. The
   plain hairline border is dropped for it - two rings would just be a thicker,
   muddier one. Sits on ::after because ::before is already the pulse's
   business here. */
#hint::after {
  /* Opaque core, like the app's own no-blur fallback: the ring technique needs
     the interior to hide the gradient, and a translucent one would let it show
     through as a tint. The chip keeps its blur - this only sits behind it. */
  --ring-inner: #14141f;
  content: ''; position: absolute; inset: -1px; border-radius: 999px;
  pointer-events: none; z-index: -1;
  /* A background-clip ring, NOT a mask. Two backgrounds: the gradient
     painted over the border box, and an opaque fill over the padding box on
     top of it - so only the border-width strip of gradient is left showing.
     This replaced a mask-composite ring, which iOS could hand back from a
     restored layer with the composite dropped: the gradient then filled the
     whole shape and the button came back as a solid rainbow pill until a tap
     forced it to repaint. There is nothing here to lose - it is two ordinary
     background layers. */
  background:
    linear-gradient(var(--ring-inner, var(--glass-solid)) 0 0) padding-box,
    var(--ring, var(--ring-scratch)) border-box;
  border: 2px solid transparent;
  animation: chipTrace 900ms var(--ease-enter) both;
}
@media (prefers-reduced-motion: reduce) { #hint::after { animation: none; clip-path: none; } }
/* No backdrop-filter support: lean on a more opaque fill instead, because the
   whole point of the chip is that the text stays readable. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #hint { background: rgba(10, 10, 18, 0.78); }
}
/* The pulse lives on the CONTENTS, never on the chip. A backdrop-filtered
   surface re-samples what is behind it, and animating that surface forever is
   precisely the combination that made the loading scrim flicker. The chip
   holds still; the words breathe. */
#hint > * { animation: hintBreath 2.4s var(--ease) infinite; }
@keyframes hintBreath {
  /* The floor stays high on purpose. This is a legibility fix first and an
     invitation second - dipping to 0.6 put the words back into grey, which is
     the problem it was meant to solve. */
  0%, 100% { opacity: 0.78; }
  50% { opacity: 1; }
}

/* transient zoom chip */
#zoomChip {
  position: absolute; z-index: 40; top: calc(env(safe-area-inset-top, 0px) + 10px);
  left: 50%; transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink); font-weight: 600; font-size: 13px; padding: 7px 14px;
  cursor: pointer; font-variant-numeric: tabular-nums;
  transition: opacity 0.2s;
}

/* Magic Picture find-the-creatures progress */

/* ---------------------------------------------------------------- top bar */
/* At the BOTTOM, where the thumb is - the platform pattern for a phone's
   primary controls, and it puts the tools by the hand rather than at the far
   end of the reach. The id is unchanged on purpose: viewport.js, the inert
   handling and the card/launch modes all address it by name.
   Everything that reserved room for it now reserves at the bottom - see
   barInset() in viewport.js, the single place that decides. */
#topbar {
  position: absolute; bottom: 0; left: 0; right: 0; z-index: 30;
  height: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px));
  padding: 4px max(10px, env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px) max(10px, env(safe-area-inset-left, 0px));
  /* The page runs BEHIND the browser's toolbar on purpose (lvh - it is what
     stops a dead strip appearing along the bottom), which was fine while the
     controls were at the top and is not now they are at the bottom: in a
     browser they sat underneath the toolbar.
     `100lvh - 100dvh` is exactly the chrome's height - lvh is the viewport with
     it retracted, dvh with it showing - so this is zero in an installed app or
     a native build and precisely the toolbar's height in a browser. No user
     agent sniffing, no display-mode guess, and it tracks the toolbar if it
     retracts while scrolling.
     The HEIGHT grows with it because viewport.js measures offsetHeight to
     decide how much room to leave the board. */
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  /* No fill, no blur, no hairline. The bar is pure layout now: the controls
     float in the picture rather than sitting on a slab drawn across the top of
     it, and each one carries its own contrast (see the glass tokens).
     The HEIGHT still matters even though nothing paints - js/viewport.js
     measures #topbar.offsetHeight to centre the board and the sidebar beneath
     it, so changing the box here moves the whole canvas. */
  background: transparent;
}
@supports (height: 100lvh) and (height: 100dvh) {
  #topbar {
    /* The chrome gap MINUS the safe-area inset, floored at zero.
       In a browser, 100lvh - 100dvh is the toolbar, which is taller than the
       home indicator, so this leaves the toolbar's height and the bar clears it.
       In an installed app the two are the SAME strip - the home indicator - and
       adding both lifted the controls a whole indicator's height off the bottom
       of the screen, which is what this is fixing. */
    --chrome-gap: max(0px, (100lvh - 100dvh) - env(safe-area-inset-bottom, 0px));
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--chrome-gap));
    height: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + var(--chrome-gap));
  }
}
/* While the canvas is being worked on, the chrome steps aside. The picture is
   the point; a tool rail parked on top of it is only useful between strokes.
   pointer-events go with it so a tap into the gap during the fade cannot hit an
   invisible button.
   The blur is dropped for the whole hidden state, not just the fade: animating
   the opacity of an ancestor of a backdrop-filtered element is exactly the
   combination that flickered on WebKit before (see #hint). Restoring it is
   DELAYED by the length of the fade - see the base rule - so the blur switches
   back on only once the buttons are fully opaque again, never mid-fade.

   Leaving and returning are deliberately NOT the same move. Getting out of the
   way should feel instant - the hand is already moving, and anything slower
   reads as the app lagging behind it - so the exit is quick and on the
   accelerating curve. Coming back is the opposite: nothing is waiting on it, so
   it takes its time on the decelerating curve and settles rather than snapping.
   Same asymmetry the app already uses for things arriving and leaving
   (--ease-enter / --ease-exit, --dur-enter / --dur-exit).
   Each surface also drifts a few px toward its own edge on the way out, which
   is what makes it read as stepping aside rather than merely dimming. It is
   `translate`, not `transform`: viewport.js writes an inline transform on
   #sidebar and would overwrite a transform set here. */
body.canvas-busy #topbar,
body.canvas-busy #sidebar,
body.canvas-busy #zoomChip,
body.canvas-busy #collabBar,
body.canvas-busy #squiggleBar {
  /* They leave the screen rather than dissolve on the spot. Fading in place
     reads as sudden however long you make it - the thing is simply somewhere,
     then not - whereas travelling off an edge and back is a movement the eye
     can follow, so the same 200ms feels unhurried. Each goes out through its
     OWN nearest edge: the bar upward, the rail left, the presence pill right,
     the chips up past the header. Distances overshoot on purpose, so nothing
     is left peeking on a taller safe area. */
  opacity: 0;
  pointer-events: none;
  transition-duration: 180ms;
  transition-timing-function: var(--ease-exit);
}
#topbar, #sidebar, #zoomChip, #collabBar, #squiggleBar {
  /* Coming back is longer than going away, and the opacity is deliberately
     quicker than the movement: they are already most of the way in before they
     finish becoming solid, so they arrive rather than materialise. */
  transition: opacity 160ms var(--ease-enter),
              translate 260ms var(--ease-enter);
  translate: 0 0;
}
body.canvas-busy #topbar { translate: 0 100%; }
body.canvas-busy #collabBar { translate: calc(100% + 24px) 0; }
body.canvas-busy #sidebar { translate: calc(-100% - 24px) 0; }
body.canvas-busy #zoomChip { translate: 0 calc(-100% - 90px); }
body.canvas-busy #squiggleBar { translate: 0 calc(-100% - 90px); }
body.canvas-busy #topbar .bar-btn,
body.canvas-busy #topbar .icon-btn,
body.canvas-busy #toolSwitcher {
  backdrop-filter: none; -webkit-backdrop-filter: none;
  transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  #topbar, #sidebar, #zoomChip, #squiggleBar { transition: none; }
}

.bar-group { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* Centred on the SCREEN. As a flex:1 group between two groups of different
   widths it centred in the leftover space instead, so it sat off-centre by
   half the difference - and that difference changes per mode. width:max-content
   because an absolutely-positioned box offset by left:50% may otherwise only
   shrink-to-fit the remaining half. */
.bar-group.center {
  position: absolute; left: 50%; transform: translateX(-50%);
  width: max-content; max-width: calc(100vw - 200px); justify-content: center;
}


/* Every button that used to sit on chrome is now its own capsule: the shape,
   the rim and the two insets are the app's treatment now, not the top bar's,
   and they apply everywhere .icon-btn and .bar-btn appear.
   The FILL is where it splits. A lens only does something when there is
   something behind it to refract - on the opaque panels (drawers, sheets,
   pickers) a blur samples nothing but that flat colour, so it costs
   compositing, buys no legibility, and inside a sliding drawer it makes WebKit
   sample the wrong backdrop mid-animation. So opaque is the DEFAULT and the
   real glass is opted into just below, by the surfaces that float over
   artwork. */
.bar-btn,
.icon-btn {
  background: var(--glass-solid);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-hi), var(--glass-lo), var(--glass-shadow);
}
.bar-btn:active, .icon-btn:active { background: var(--glass-solid-press); }
@media (hover: hover) {
  .bar-btn:hover, .icon-btn:hover { background: var(--glass-solid-press); }
}

/* The surfaces with live artwork behind them - the only places the blur is
   doing real work. */
#topbar .bar-btn,
#topbar .icon-btn,
.replay-controls .icon-btn {
  background: var(--glass-fill);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}
/* The delayed half of the fade-in described above: switching the blur back on
   is held until the bar has finished becoming opaque again. 0s duration - this
   is a switch with a wait in front of it, not an animation. */
#topbar .bar-btn, #topbar .icon-btn, #toolSwitcher {
  transition: transform 0.12s var(--ease), background 0.15s, border-color 0.15s,
              backdrop-filter 0s linear 220ms, -webkit-backdrop-filter 0s linear 220ms;
}
#topbar .bar-btn:active,
#topbar .icon-btn:active,
.replay-controls .icon-btn:active { background: var(--glass-press); }
@media (hover: hover) {
  #topbar .bar-btn:hover,
  #topbar .icon-btn:hover,
  .replay-controls .icon-btn:hover { background: var(--glass-press); }
}

/* The mode button is a column, not a row: icon over label, the way a tab reads.
   It is wider and shorter-typed than the other controls on purpose - it is the
   only one that says what it is rather than relying on its glyph. */
/* Every control in the bar is the height of the tool switcher beside it -
   46px of button plus its 3px padding and 1px border on each side. They read as
   one row of controls rather than three sizes, and the mode name stopped being
   clipped once the button was tall enough to hold icon and label. */
/* Bigger than everything else in the bar, but only just: 60 against 54, and a
   wider minimum. It is the one control that changes what the whole app is
   doing, so it should be the first thing the eye lands on - and a button that
   is obviously the biggest reads as primary without needing a colour or a
   badge to say so. Any more than this and it stops being a bar. */
.mode-btn {
  flex-direction: column; gap: 3px !important;
  justify-content: center; align-items: center;
  height: 60px !important; padding: 0 14px !important;
  min-width: 76px;
}
.mode-name { font-size: 11px; }
#topbar .icon-btn { width: 54px; height: 54px; }
.mode-name {
  font-size: 10px; font-weight: 700; letter-spacing: 0.2px;
  color: var(--muted); line-height: 1; text-align: center;
  max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mode-btn .mode-ic { display: block; line-height: 0; }

.bar-btn {
  display: flex; align-items: center; gap: 7px;
  height: 44px; padding: 0 14px; cursor: pointer;
  border-radius: 999px; font-size: 14px; font-weight: 600;
  transition: transform 0.12s var(--ease), background 0.15s;
}
.bar-btn:active { transform: scale(0.96); }

.icon-btn {
  width: 46px; height: 46px; flex: none;
  display: grid; place-items: center;
  /* A capsule, not a rounded square - a lens has no corners. */
  border-radius: 999px;
  cursor: pointer;
  transition: transform 0.12s var(--ease), background 0.15s, border-color 0.15s;
}
.icon-btn:active { transform: scale(0.94); }
.icon-btn:disabled { opacity: 0.35; cursor: default; }

/* tool switcher - sliding pill */
#toolSwitcher {
  position: relative; display: flex; gap: 2px;
  background: var(--glass-fill);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-hi), var(--glass-lo), var(--glass-shadow);
  padding: 3px; border-radius: 999px;
}

/* Two ways the glass can be unavailable, and the bar has no fill to fall back
   on, so in both the controls have to become opaque or they stop being
   readable over artwork. This is a legibility requirement now, not polish. */
/* These have to out-specify the #topbar opt-in above - a media query adds no
   specificity of its own, so a bare .icon-btn here would simply lose to it and
   the fallback would never apply. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #topbar .bar-btn, #topbar .icon-btn, .replay-controls .icon-btn, #toolSwitcher {
    background: var(--glass-solid);
  }
  #topbar .bar-btn:active, #topbar .icon-btn:active,
  .replay-controls .icon-btn:active { background: var(--glass-solid-press); }
}
/* The accessibility setting this whole look is expected to honour. */
@media (prefers-reduced-transparency: reduce) {
  #topbar .bar-btn, #topbar .icon-btn, .replay-controls .icon-btn, #toolSwitcher {
    background: var(--glass-solid);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  #topbar .bar-btn:active, #topbar .icon-btn:active,
  .replay-controls .icon-btn:active { background: var(--glass-solid-press); }
}
#toolPill {
  position: absolute; top: 3px; bottom: 3px; left: 3px; width: 48px;
  border-radius: 999px; background: var(--accent-grad);
  box-shadow: 0 2px 12px rgba(124, 92, 255, 0.45);
  transition: transform 0.2s var(--ease), width 0.2s var(--ease);
}
.tool-btn {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  height: 46px; min-width: 52px; padding: 0 13px;
  border: 0; border-radius: 999px; background: transparent; cursor: pointer;
  color: var(--muted); font-size: 14px; font-weight: 600;
  transition: color 0.15s;
}
.tool-btn.active { color: #fff; }
.tool-btn .lbl { display: none; }
@media (min-width: 900px) { .tool-btn .lbl { display: inline; } }

/* current pattern dot */
#patternDot {
  width: 26px; height: 26px; border-radius: 50%;
  background-size: cover; background-position: center;
  border: 2px solid var(--line-strong); display: block;
}
/* The ring says "this is the thing that is open". .bar-btn is in the list now
   because the mode button opens the hub and had no pressed state at all. */
#patternBtn[data-open="1"] #patternDot,
.bar-btn[data-open="1"],
.icon-btn[data-open="1"] { border-color: var(--accent); }

/* premium locks */
[data-locked="1"] { position: relative; }
.lock-badge {
  position: absolute; top: -3px; right: -3px; z-index: 2;
  width: 17px; height: 17px; border-radius: 50%;
  background: var(--surface-3); border: 1px solid var(--line-strong);
  display: grid; place-items: center; color: var(--muted);
  pointer-events: none;
}

/* responsive top bar */
.show-sm { display: none; }
@media (max-width: 599px) {
  .hide-sm { display: none; }
  .show-sm { display: flex; }
  .bar-btn .lbl { display: none; }
  .bar-btn { padding: 0 12px; }
}

/* ---------------------------------------------------------------- popovers */
.popover {
  position: absolute; z-index: 45;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 12px;
  box-shadow: var(--shadow);
  transform-origin: top right;
  animation: popIn 0.16s var(--ease-enter);
}
@keyframes popIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
/* Side popovers are pinned with translateY(-50%); the plain popIn above
   animates `transform: scale()`, which both drops that offset mid-animation
   (panel opens low, snaps up) and shifts the edges as it scales. Fade only -
   no transform - so the panel appears exactly where it stays, no movement. */
@keyframes popInSide { from { opacity: 0; } to { opacity: 1; } }

/* "scroll for more" cue: a down chevron pinned to the bottom of a scroll area
   that fades out at the end. Sticky + negative margin = zero layout footprint;
   pointer-events:none so taps reach the content under it. */
.scroll-hint {
  position: sticky; bottom: 0; z-index: 2;
  height: 30px; margin-top: -30px;
  display: flex; align-items: flex-end; justify-content: center;
  pointer-events: none; opacity: 0; transition: opacity 0.2s;
  background: linear-gradient(to bottom, transparent, var(--surface));
  color: var(--accent-2);
}
.scroll-hint.show { opacity: 1; }
.scroll-hint svg { width: 18px; height: 18px; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)); animation: scrollHintBob 1.5s ease-in-out infinite; }
@keyframes scrollHintBob { 0%, 100% { transform: translateY(-1px); } 50% { transform: translateY(3px); } }
@media (prefers-reduced-motion: reduce) { .scroll-hint svg { animation: none; } }
.popover h3 {
  margin: 2px 4px 10px; font-size: 13px; font-weight: 600;
  color: var(--muted); letter-spacing: 0.4px; text-transform: uppercase;
}
/* The same contained side sheet the colour picker uses, at every width. It
   is anchored beside the sidebar rather than filling the screen, so the
   drawing stays visible behind it and the interaction matches the colour
   picker exactly. max-width keeps it clear of the sidebar it hangs off. */
#patternPopover {
  left: calc(env(safe-area-inset-left, 0px) + 74px); right: auto;
  /* Centred in the band BELOW the topbar rather than on the whole screen, so
     a tall sheet can never sit over the header. top/bottom bound it and
     margin-block:auto centres it inside that band. */
  top: calc(env(safe-area-inset-top, 0px) + 8px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
  transform: none; margin-block: auto; height: fit-content;
  transform-origin: left center;
  /* Sized to the three-column grid it contains, not to the colour picker's
     width - at 400px it carried a column's worth of empty space. */
  width: min(300px, 88vw); max-width: calc(100vw - 90px);
  max-height: 620px; overflow-y: auto;
  animation: popInSide 0.18s var(--ease-enter);
}
#patternPopover .pattern-cell { font-size: 11.5px; padding: 8px 2px 6px; }
#patternPopover #patternGrid { gap: 6px; }
#patternGrid { display: grid; grid-template-columns: repeat(3, 84px); gap: 8px; }
.pattern-cell {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 10px 4px 8px; border: 1px solid transparent; border-radius: var(--radius);
  background: transparent; cursor: pointer; font-size: 12px; color: var(--muted);
  transition: background 0.15s, border-color 0.15s, transform 0.12s var(--ease);
}
.pattern-cell:active { transform: scale(0.95); }
.pattern-cell .dot {
  width: 44px; height: 44px; border-radius: 50%;
  background-size: cover; background-position: center;
  border: 2px solid var(--line-strong);
  display: grid; place-items: center; color: var(--ink);
}
.pattern-cell.active { background: var(--accent-tint); border-color: var(--accent); color: var(--ink); }
.pattern-cell.active .dot { border-color: var(--accent); }

.popover.menu { display: flex; flex-direction: column; gap: 2px; min-width: 200px; }
#sizePopover { top: calc(env(safe-area-inset-top, 0px) + 8px); right: 10px; }
.menu-row {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--touch); padding: 0 12px;
  border: 0; border-radius: 10px; background: transparent; cursor: pointer;
  font-size: 15px; font-weight: 500; color: var(--ink); text-align: left;
  transition: background 0.12s;
}
.menu-row:active { background: var(--surface-3); }
@media (hover: hover) { .menu-row:hover { background: var(--surface-2); } }
.menu-row.active { background: var(--accent-tint); color: var(--ink); }
.menu-row.active .icn { color: var(--accent-2); }


/* ---------------------------------------------------------------- sidebar */
#sidebar {
  position: absolute; z-index: 30;
  left: calc(env(safe-area-inset-left, 0px) + 10px);
  /* top is owned by Viewport._positionSidebar (viewport.js), which centers
     the sidebar on wherever the canvas actually rests and - the one hard
     rule - never lets it start above the canvas, even when the sidebar's
     content (e.g. Free Draw's extra colour/texture rows) is taller than a
     small board. This 50%/translateY is only the fallback for the brief
     window before the first fitToScreen() runs. */
  top: 50%;
  transform: translateY(-50%);
  max-height: calc(100% - var(--bar-h) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 20px);
  overflow-y: auto;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  /* A whisper of a vertical gradient - two steps of lightness across the whole
     rail - so the surface itself looks lit from above rather than filled flat.
     Paired with the inset highlight below it does the same job a rim light
     does on the board. */
  background: linear-gradient(180deg, #1a1a24 0%, var(--surface) 62%);
  border: 1px solid var(--line);
  padding: 10px 7px; border-radius: 24px;
  box-shadow:
    var(--float-contact),
    var(--float-mid),
    var(--float-cast),
    /* the lit top edge, inset so it sits just inside the border */
    inset 0 1px 0 rgba(255, 255, 255, 0.055);
}
/* Narrower horizontal padding on phones only - the buttons themselves stay
   44px (the app's baseline touch target elsewhere too), this just trims the
   pill's own girth so fitToScreen (viewport.js) has to reserve less room for
   it beside the board. */
@media (max-width: 599px) {
  #sidebar { padding: 12px 3px; }
}/* margin-bottom clears the thumb's own overflow below the track (26px thumb,
   `bottom:0%` + translateY(50%) center-pins it ON the track's bottom edge at
   minimum brush size, so it overhangs ~13px past the track), PLUS the
   opacity slider immediately below it overhangs ~13px above ITS OWN track
   at max opacity (`bottom:100%`, same center-pin trick) - the two thumbs
   face each other, so clearing one thumb's overflow (18px) wasn't enough;
   both can overflow toward each other at once. */
/* Track height scales with the viewport. Free Draw and Colouring add an
   opacity slider AND two more buttons (colour, texture) to this rail - ten
   items instead of seven - which pushed it past the room it has, so it
   clipped and scrolled. The buttons stay 44px (the app's touch-target
   baseline everywhere), so the height comes out of the tracks and the gaps
   instead. The vh term shrinks it further on short phones and the floor
   keeps it draggable. */
/* margin-top is the same idea as margin-bottom, for the other end. At the
   largest brush size the thumb sits at `bottom:100%` and translateY(50%)
   centre-pins it ON the track's TOP edge, so ~13px of it lands outside the
   slider's box - and #sidebar's overflow-y:auto makes overflow-x compute to
   auto too, so the rail clips in both axes and shaves the top off the thumb.
   13px = half the 26px thumb. */
#brushSlider { height: clamp(90px, 12vh, 168px); width: 40px; display: grid; place-items: center; touch-action: none; margin-top: 13px; margin-bottom: 28px; }
/* That 28px clears TWO thumbs facing each other - this one's overhang below
   its track, and the opacity slider's above its own. With the opacity slider
   hidden (every mode but Free Draw) only this thumb overhangs, so half the
   gap is dead space and the rail is taller than it needs to be. */
#brushSlider:has(+ #opacitySlider[hidden]) { margin-bottom: 15px; }
#brushTrack { position: relative; width: 10px; height: 100%; background: var(--surface-3); border-radius: 999px; }
#brushFill { position: absolute; bottom: 0; left: 0; right: 0; border-radius: 999px; background: var(--accent); opacity: 0.8; }
#brushThumb {
  position: absolute; left: 50%; transform: translate(-50%, 50%);
  width: 26px; height: 26px; border-radius: 50%;
  background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
/* Brush opacity (Free Draw only) - a checkerboard track makes "transparent
   to solid" legible at a glance, same fill/thumb shape as the size slider.
   margin-bottom matches #brushSlider's fix: the 26px thumb overhangs ~13px
   below the track at minimum opacity, so 4px let it overlap the row below. */
#opacitySlider { height: clamp(60px, 8vh, 100px); width: 40px; display: grid; place-items: center; touch-action: none; margin-bottom: 18px; }
#opacityTrack {
  /* No overflow:hidden here - border-radius already clips the checkerboard
     background to the pill, and #opacityFill rounds itself, so clipping isn't
     needed; it only clipped the thumb (a child of the track), cutting off its
     top at high opacity. */
  position: relative; width: 10px; height: 100%; border-radius: 999px;
  background-color: var(--surface-3);
  background-image: repeating-conic-gradient(rgba(255, 255, 255, 0.18) 0% 25%, transparent 0% 50%);
  background-size: 6px 6px;
}
#opacityFill { position: absolute; bottom: 0; left: 0; right: 0; border-radius: 999px; background: var(--accent); opacity: 0.8; }
#opacityThumb {
  position: absolute; left: 50%; transform: translate(-50%, 50%);
  width: 26px; height: 26px; border-radius: 50%;
  background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
/* Short screens: the rail is tightest in Free Draw / Colouring, where it
   carries ten items. Buttons stay 44px - the touch target is not the thing to
   trade - so the remaining slack comes from the thumbs (a smaller thumb needs
   less clearance below its track, which is what those margins exist for) and
   from the gaps. */
@media (max-height: 720px) {
  /* padding-block only - the phone rule above deliberately narrows the
     horizontal padding, and a shorthand here would undo it. */
  #sidebar { gap: 3px; padding-block: 8px; }
  #brushThumb, #opacityThumb { width: 20px; height: 20px; }
  /* thumbs are 20px here, so the overhang each way is 10px, not 13px */
  #brushSlider { margin-top: 10px; margin-bottom: 22px; }
  #opacitySlider { margin-bottom: 13px; }
}
.side-btn {
  width: 44px; height: 44px; flex: none;
  display: grid; place-items: center;
  border: 1px solid transparent; border-radius: 12px;
  background: transparent; cursor: pointer; color: var(--ink);
  transition: transform 0.12s var(--ease), background 0.15s, color 0.15s;
}
.side-btn:active { transform: scale(0.94); background: var(--surface-2); }
@media (hover: hover) { .side-btn:hover { background: var(--surface-2); border-color: var(--line); } }
/* highlight a sidebar button while its popover is open (colour / brush) */
.side-btn.on, .side-btn[data-open="1"] { background: var(--accent-tint); color: var(--accent-2); border-color: var(--accent); }
.side-btn:disabled { opacity: 0.32; cursor: default; }

#brushPreview {
  position: absolute; z-index: 31; pointer-events: none;
  left: calc(env(safe-area-inset-left, 0px) + 76px); top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 12px;
}
#brushPreviewDot { border-radius: 50%; background: radial-gradient(circle, #fff, #cfd3ff); box-shadow: 0 0 22px rgba(124, 92, 255, 0.55); }
#brushPreviewVal {
  font-variant-numeric: tabular-nums; font-weight: 700; font-size: 15px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px; padding: 5px 11px;
}

#sidePill {
  position: absolute; z-index: 40; pointer-events: none;
  left: calc(env(safe-area-inset-left, 0px) + 76px); top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  padding: 8px 14px; font-weight: 600; font-size: 14px;
  box-shadow: var(--shadow); opacity: 0; transition: opacity 0.15s;
}
#sidePill.show { opacity: 1; }
#symmetryTray,
#textureTray { left: calc(env(safe-area-inset-left, 0px) + 74px); top: 50%; transform: translateY(-50%); transform-origin: left center; animation: popInSide 0.18s var(--ease-enter); }
#textureTray { max-height: min(74vh, 560px); width: min(430px, 92vw); max-width: calc(100vw - 90px); }

/* ---------------------------------------------------------------- trays */
/* The shape and spiral pickers come up from the BOTTOM, above the bar that
   opens them, and sit over the tool rail rather than beside it - they are a
   choice you are making right now, and the rail is not part of that choice.
   z-index 31 clears the rail and the bar (both 30); it used to be 29, which put
   it underneath them.
   Same growth as the menus: out of the button, not in from an edge. */
.tray {
  position: absolute; z-index: 31;
  left: 50%; transform: translateX(-50%);
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 18px);
  /* Wraps into ROWS and grows upward, instead of running off to the right.
     It used to be a fixed 4 rows with columns flowing sideways, so every extra
     shape made the strip longer and had to be scrolled to - the shapes past the
     edge were invisible unless you knew to swipe. Filling the width and then
     adding a row upward means the whole set is on screen at once; it is
     anchored at the bottom, so the growth happens away from the controls.
     Four rows' worth is the floor before it can start growing (min-height),
     which keeps a small set from looking like a stray fragment. */
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
  width: min(calc(100vw - 24px), 420px);
  gap: 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 8px;
  box-shadow: var(--shadow);
  /* Vertical is the only direction that can ever need scrolling now, and only
     when the set is genuinely too big for the screen. */
  max-height: calc(100dvh - var(--bar-h) - env(safe-area-inset-top, 0px)
                   - env(safe-area-inset-bottom, 0px) - 46px);
  overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  touch-action: pan-y; /* global touch-action:none would make the tray unscrollable */
  /* Grows up out of the bar, like the menus. The -50% has to be carried in
     every keyframe: it is the centring, not part of the animation, and a
     keyframe that omits it slides the tray to the right edge as it plays. */
  transform-origin: bottom center;
  animation: popInTray 240ms var(--ease-enter);
}
@keyframes popInTray {
  from { opacity: 0; transform: translateX(-50%) scale(0.88) translateY(14px); }
  to   { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); }
}
/* The divider separated COLUMNS when the flow was sideways; now that rows wrap
   it has to span the full width and be a horizontal rule instead. */
.tray .tray-divider {
  grid-column: 1 / -1; width: auto; min-width: 0; height: 1px;
  align-self: center; margin: 3px 0;
}
/* The fade hinted that more shapes were off to the RIGHT. Nothing goes off to
   the right any more, and a fade over the last row would hide shapes rather
   than hint at them. */
.tray-item {
  width: auto; min-width: 0; height: 56px;
  display: grid; place-items: center;
  border: 1px solid transparent; border-radius: 12px;
  background: transparent; cursor: pointer; color: var(--muted);
  transition: background 0.15s, color 0.15s, transform 0.12s var(--ease), border-color 0.15s;
}
.tray-item:active { transform: scale(0.93); }
@media (hover: hover) { .tray-item:hover { background: var(--surface-2); } }
.tray-item.active { background: var(--accent-tint); color: var(--ink); border-color: var(--accent); }
.tray.shapes svg { width: 40px; height: 40px; }
.tray-item.emoji { font-size: 30px; line-height: 1; }
.tray-divider { width: 1px; min-width: 1px; align-self: stretch; margin: 2px 3px; background: var(--line-strong); }
.tray-item.any-emoji {
  border: 1.5px dashed var(--accent); color: var(--accent-2);
  display: flex; flex-direction: column; gap: 2px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.2px;
}
#emojiInput {
  width: 100%; box-sizing: border-box; margin: 4px 0 16px;
  font-size: 44px; text-align: center; padding: 10px; min-height: 76px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: 14px; color: var(--ink);
}
#emojiInput::placeholder { font-size: 15px; color: var(--muted); font-family: var(--font-ui); }
#emojiInput:focus { outline: 2px solid var(--accent); }

/* ---------------------------------------------------------------- canvas maker */
/* The maker is a fixed frame, not a long page: preview and saved canvases
   pinned at the top, the controls scrolling between, and the actions always
   reachable at the bottom. Saving is the point of the screen and it used to
   sit below however far the controls happened to run. */
/* A sheet beside the sidebar, sized and structured exactly like the colour
   picker: a fixed frame with one scrolling region inside it and the action
   pinned below, rather than a full page you scroll through. */
.maker-popover {
  left: calc(env(safe-area-inset-left, 0px) + 74px);
  /* Centred in the band BELOW the topbar rather than on the whole screen, so
     a tall sheet can never sit over the header. top/bottom bound it and
     margin-block:auto centres it inside that band. */
  top: calc(env(safe-area-inset-top, 0px) + 8px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
  /* No height:fit-content here: with top and bottom both set that would
     over-constrain the box and let it grow past the band, back over the
     header. Letting it fill the band (capped below) is also what makes the
     maker use the height available. */
  transform: none; margin-block: auto;
  transform-origin: left center; animation: popInSide 0.18s var(--ease-enter);
  width: min(400px, 92vw); max-width: calc(100vw - 90px);
  max-height: 780px; overflow: hidden;
  display: flex; flex-direction: column; padding: 14px 14px 0;
}
.maker-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.maker-head h2 { font-family: var(--font-display); font-size: 19px; margin: 0; }
/* No bottom-sheet variant: it stays a contained sheet on phones too, so the
   drawing remains visible behind it and it behaves like the colour picker
   everywhere. The pieces inside shrink instead of the sheet growing. */
@media (max-width: 599px) {
  .maker-popover { padding: 12px 12px 0; }
  .maker-head h2 { font-size: 17px; }
  .maker-preview-wrap { max-width: min(84px, 22vw); }
  #makerHint { font-size: 11.5px; }
  .pal-dot { width: 28px; height: 28px; border-width: 2px; }
  .style-chip { padding: 7px 11px; font-size: 13px; }
  #makerSaved { max-height: 84px; }
}
#canvasMaker .maker-body {
  flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 8px;
  max-width: 720px; width: 100%; margin: 0 auto;
}
/* This screen is a lot of controls in a small space, so the vertical rhythm is
   tightened throughout rather than each section keeping page-sized margins. */

#canvasMaker .maker-controls h4 { margin: 2px 0 6px; }
#canvasMaker #makerPalette { margin-bottom: 8px; }
#canvasMaker .maker-slider { margin-bottom: 0; }
#canvasMaker .chip-row { margin-bottom: 8px; }
.maker-top { flex: none; display: flex; flex-direction: column; gap: 6px; align-items: center; }
/* Under the preview, across the full width - they read as "or start from one
   of these" rather than as something parked beside the picture. */
#makerSaved {
  flex: none; width: 100%; display: flex; flex-wrap: wrap; gap: 10px;
  justify-content: center; max-height: 108px; overflow-y: auto;
  /* The delete badge sits at top:-6px right:-6px, OUTSIDE the chip, and this
     is a scroll container - so without room of its own it was clipped away.
     Pad the box rather than move the badge, which reads correctly where it is. */
  padding: 8px 8px 2px;
}
.maker-actions {
  flex: none; position: sticky; bottom: 0; display: flex;
  flex-direction: column; gap: 8px;
  padding: 10px 0 calc(env(safe-area-inset-bottom, 0px) + 10px);
  border-top: 1px solid var(--line); background: var(--surface);
}
/* Only Save lives down here now, so the bar is one button tall. */
.maker-actions #makerSave { width: 100%; }
/* Clear and Mix it up as icons, stacked beside the picture they act on.
   The picture stays CENTRED on the screen and the buttons sit off to its
   right: an empty flexible spacer mirrors the button column, so the preview
   centres itself without either side having to know the other's width. */
.maker-preview-row { display: flex; align-items: center; gap: 12px; width: 100%; }
.maker-preview-row::before { content: ''; flex: 1; }
.maker-quick { flex: 1; display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.maker-quick .icon-btn { background: var(--surface); border-color: var(--line); }
.maker-preview-wrap { flex: none; display: flex; flex-direction: column; gap: 6px; max-width: min(90px, 20vw); }
#makerPreview {
  width: 100%; border-radius: 12px;
  border: 1px solid var(--line-strong); background: #000;
  touch-action: none; cursor: crosshair; /* paintable in Paint style */
}
#makerHint { margin: 0; text-align: center; max-width: min(430px, 94%); font-size: 12.5px; }
/* overflow-x: clip is load-bearing. Setting overflow-y:auto makes overflow-x
   compute to auto as well, so anything a pixel too wide - a colour swatch row,
   a style chip - turned the whole panel into a sideways scroller. The contents
   wrap; they should never pan. */
.maker-controls { flex: 1; min-height: 0; overflow-y: auto; overflow-x: clip; min-width: 0; padding-right: 2px; }
#makerPalette, #makerStyles { max-width: 100%; }
.maker-controls h4 { margin: 4px 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.4px; color: var(--muted); }
#makerPalette { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; }
.pal-dot {
  width: 32px; height: 32px; border-radius: 50%; cursor: pointer;
  border: 3px solid transparent; padding: 0;
  transition: transform 0.12s var(--ease), border-color 0.12s;
}
.pal-dot.on { border-color: #fff; transform: scale(1.12); }
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.style-chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--muted);
  border-radius: 999px; padding: 9px 14px; font-size: 14px; font-weight: 600; cursor: pointer;
}
.style-chip .icn { width: 16px; height: 16px; flex: none; }
.style-chip.on { background: var(--accent-tint); border-color: var(--accent); color: var(--ink); }
.maker-slider { display: block; font-size: 13.5px; font-weight: 600; color: var(--muted); margin-bottom: 4px; }
.maker-slider input {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 44px; background: transparent; display: block;
  /* border-box, or the range's own padding pushes it past its container and
     the scroll parent (overflow-y:auto implies overflow-x:auto) starts panning
     sideways. */
  box-sizing: border-box; max-width: 100%;
  touch-action: pan-x; /* global touch-action:none would break the drag */
}
.maker-slider input::-webkit-slider-runnable-track {
  height: 8px; border-radius: 999px; background: var(--surface-3);
}
.maker-slider input::-webkit-slider-thumb {
  -webkit-appearance: none; width: 28px; height: 28px; border-radius: 50%;
  background: #fff; border: 3px solid var(--accent); margin-top: -10px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.maker-slider input::-moz-range-track { height: 8px; border-radius: 999px; background: var(--surface-3); }
.maker-slider input::-moz-range-thumb {
  width: 24px; height: 24px; border-radius: 50%;
  background: #fff; border: 3px solid var(--accent);
}
.maker-controls .row { margin-bottom: 8px; }
.save-strong { border-color: var(--accent) !important; color: var(--accent-2) !important; }
#makerShuffle:active .icn { animation: wiggle 0.3s var(--ease); }
@keyframes wiggle {
  0%, 100% { transform: rotate(0); }
  30% { transform: rotate(-18deg) scale(1.2); }
  65% { transform: rotate(14deg) scale(1.15); }
}
/* Spacing for this row lives with the maker layout rules above; this one
   only carried a margin-top that fought them. */
.saved-canvas { position: relative; }
.saved-canvas canvas { width: 46px; height: 46px; border-radius: 50%; border: 2px solid var(--line-strong); display: block; }
.saved-canvas .del.arm { background: var(--danger); border-color: var(--danger); color: #fff; }
.saved-canvas .del {
  position: absolute; top: -6px; right: -6px; width: 20px; height: 20px;
  border-radius: 50%; border: 1px solid var(--line-strong); cursor: pointer;
  background: var(--surface-3); color: var(--muted); display: grid; place-items: center;
}
.tray.shapes svg path, .tray.shapes svg circle { fill: none; stroke: currentColor; stroke-width: 6; stroke-linejoin: round; stroke-linecap: round; }
.tray.shapes .tray-item.active svg { color: var(--accent-2); }

/* Glass, like everything else in the chrome now - but these two get a twist the
   other controls do not: a rainbow line that draws itself all the way around
   the button when it appears. It is the app's own identity (colour hiding under
   black, revealed by a line travelling across it) applied to a button, and it
   makes the two most consequential controls on screen announce themselves
   without shouting in solid orange and green.
   The line is a conic gradient in the border box, revealed by an animated
   `mask` sweeping round; the fill sits in the padding box and is untouched. */
.chip-btn {
  position: relative; isolation: isolate;
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; font-weight: 700; font-size: 16px;
  padding: 0 20px; height: var(--touch); border-radius: 999px;
  color: var(--ink);
  background: var(--glass-fill);
  backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-hi), var(--glass-lo), var(--glass-shadow);
  transition: transform 0.12s var(--ease);
}
.chip-btn::before {
  content: ''; position: absolute; inset: -2px; border-radius: 999px;
  pointer-events: none; z-index: -1;
  /* Ring, not disc: two mask layers - the inner box and the whole box -
     combined so only the 2px between them survives.
     ORDER MATTERS. `mask` is a shorthand and resets mask-composite to `add`,
     so the composite must be declared AFTER it; the other way round the
     composite is silently thrown away and the gradient fills the whole
     button, which is exactly what it did the first time. */
  /* A background-clip ring, NOT a mask. Two backgrounds: the gradient
     painted over the border box, and an opaque fill over the padding box on
     top of it - so only the border-width strip of gradient is left showing.
     This replaced a mask-composite ring, which iOS could hand back from a
     restored layer with the composite dropped: the gradient then filled the
     whole shape and the button came back as a solid rainbow pill until a tap
     forced it to repaint. There is nothing here to lose - it is two ordinary
     background layers. */
  background:
    linear-gradient(var(--ring-inner, var(--glass-solid)) 0 0) padding-box,
    var(--ring, var(--ring-scratch)) border-box;
  border: 2px solid transparent;
  animation: chipTrace 900ms var(--ease-enter) both;
}
/* The sweep: the ring is clipped to a growing wedge, so the line appears to be
   drawn around the button once, then stays. */
@keyframes chipTrace {
  from { clip-path: polygon(50% 50%, 50% -60%, 50% -60%); opacity: 0; }
  15%  { opacity: 1; }
  to   { clip-path: polygon(50% 50%, 50% -60%, 160% -60%, 160% 160%, -60% 160%, -60% -60%, 50% -60%); opacity: 1; }
}
/* The mode button wears the same ring - it is the one control that says what
   you are doing, so it gets the app's own flourish. Its own animation, not
   just the shared rule: it redraws whenever the mode changes (see
   setModeButton), rather than only when it first appears. */
/* The mode's colour lives on its ICON, not as an outline around the button.
   An outline is a box drawn around the thing; the icon IS the thing, and it is
   what the eye goes to first in both the bar and the menu.
   These are SVG paint servers (see the <defs> in index.html) rather than CSS
   gradients: an inline <svg> can take `stroke: url(#id)` directly, whereas a
   CSS gradient cannot paint a stroke at all. They are userSpaceOnUse across the
   24x24 icon grid, so every path in a multi-path glyph shares one continuous
   sweep instead of each restarting the gradient in its own bounding box. */
/* The button is outlined in the same mode colours its icon is painted with -
   the icon says which mode at a glance, the ring makes the whole button read as
   belonging to it. Same masked-gradient ring as the Fill/Done chips, thinner
   and unanimated: this one is permanent chrome, not a momentary announcement. */
.mode-btn { position: relative; isolation: isolate; }
.mode-btn::before {
  content: ''; position: absolute; inset: -1px; border-radius: 999px;
  pointer-events: none; z-index: -1;
  /* A background-clip ring, NOT a mask. Two backgrounds: the gradient
     painted over the border box, and an opaque fill over the padding box on
     top of it - so only the border-width strip of gradient is left showing.
     This replaced a mask-composite ring, which iOS could hand back from a
     restored layer with the composite dropped: the gradient then filled the
     whole shape and the button came back as a solid rainbow pill until a tap
     forced it to repaint. There is nothing here to lose - it is two ordinary
     background layers. */
  background:
    linear-gradient(var(--ring-inner, var(--glass-solid)) 0 0) padding-box,
    var(--ring, var(--ring-scratch)) border-box;
  border: 2px solid transparent;
}
#homeBtn[data-mode="scratch"]  { --ring: var(--ring-scratch); }
#homeBtn[data-mode="draw"]     { --ring: var(--ring-draw); }
#homeBtn[data-mode="magic"]    { --ring: var(--ring-magic); }
#homeBtn[data-mode="colour"]   { --ring: var(--ring-colour); }
#homeBtn[data-mode="maze"]     { --ring: var(--ring-maze); }
#homeBtn[data-mode="dots"]     { --ring: var(--ring-dots); }
#homeBtn[data-mode="squiggle"] { --ring: var(--ring-squiggle); }

#homeBtn[data-mode="scratch"]  .icn * { stroke: url(#gScratch); }
#homeBtn[data-mode="draw"]     .icn * { stroke: url(#gDraw); }
#homeBtn[data-mode="magic"]    .icn * { stroke: url(#gMagic); }
#homeBtn[data-mode="colour"]   .icn * { stroke: url(#gColour); }
#homeBtn[data-mode="maze"]     .icn * { stroke: url(#gMaze); }
#homeBtn[data-mode="dots"]     .icn * { stroke: url(#gDots); }
#homeBtn[data-mode="squiggle"] .icn * { stroke: url(#gSquiggle); }

#hubBlank    .hub-ic .icn * { stroke: url(#gScratch); }
#hubFreeDraw .hub-ic .icn * { stroke: url(#gDraw); }
#hubMagic    .hub-ic .icn * { stroke: url(#gMagic); }
#hubColour   .hub-ic .icn * { stroke: url(#gColour); }
#hubMazes    .hub-ic .icn * { stroke: url(#gMaze); }
#hubDots     .hub-ic .icn * { stroke: url(#gDots); }
#hubSquiggle .hub-ic .icn * { stroke: url(#gSquiggle); }
@media (prefers-reduced-motion: reduce) {
  .chip-btn::before { animation: none; clip-path: none; }
}
/* The icon keeps each button's own meaning colour - green for done, warm for
   fill - now that the button itself is no longer a block of it. */
.chip-btn.done .icn { color: var(--good); }
.chip-btn.fill .icn { color: #ffb03a; }
.chip-btn:active { transform: scale(0.95); background: var(--glass-press); }

/* ------------------------------------------------ Mystery Card recipient */
/* A visitor opening a shared #c= link gets a sealed card and this bar; the
   studio chrome (sidebar tools, new/pattern buttons) is hidden - they're
   here to scratch one card, not to draw. */
/* Flip: a labelled pill at the TOP. It used to sit at the bottom, which put it
   straight on top of the tool row - it covered the spiral and emoji buttons on
   a phone. The top is also where every other in-canvas chip lives. */
#flipBtn {
  position: absolute; z-index: 40; left: 50%; transform: translateX(-50%);
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line-strong);
  color: var(--ink); font-family: var(--font-display);
  font-size: 15px; font-weight: 800; white-space: nowrap;
  padding: 11px 20px 11px 16px; border-radius: 999px; box-shadow: var(--shadow);
  cursor: pointer;
}
#flipBtn:active { transform: translateX(-50%) scale(0.95); }
/* Two-player squiggle shows the whose-turn bar in that same spot, so on
   player 2's turn - the only turn Flip appears - the pill drops below it. */
body.squiggle-prompt #flipBtn { top: calc(env(safe-area-inset-top, 0px) + 68px); }
#flipBtn svg { color: var(--accent-2); }
#flipBtn[hidden] { display: none; }
/* the recipient of a card gets a stripped-back studio - no view controls */
body.cardview #flipBtn { display: none; }

/* Squiggle two-player: whose-turn prompt, and the pass-over screen.
   Lives at the TOP: the bottom is taken by the flip pill, and a full-width
   bar down there also covered the sidebar's last button on a phone. It wraps
   to two rows rather than squeezing the button - the prompt plus "Pass it
   over" is wider than a phone screen, which clipped the button's label. */
#squiggleBar {
  position: absolute; z-index: 41; left: 50%; transform: translateX(-50%);
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 8px 12px; text-align: center;
  background: var(--surface); border: 1px solid var(--line-strong);
  color: var(--ink); font-family: var(--font-display);
  font-size: 15px; font-weight: 700; line-height: 1.25;
  padding: 10px 16px; border-radius: 22px; box-shadow: var(--shadow);
  /* max-content, capped: without it the absolutely-positioned box shrink-wraps
     far narrower than the room available and breaks the prompt across lines */
  width: max-content; max-width: min(94vw, 560px);
}
#squiggleBar[hidden] { display: none; }
#squiggleBarText { min-width: 0; }
#squigglePass { white-space: nowrap; flex: 0 0 auto; }
.fullscreen-veil {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center; padding: 24px;
  background: rgba(8, 8, 16, 0.86); backdrop-filter: blur(6px);
}
.fullscreen-veil[hidden] { display: none; }
.handover-card {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center; max-width: 420px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 22px; padding: 30px 26px; box-shadow: var(--shadow);
}
.handover-card svg { color: var(--accent-2); }
.handover-card h2 { font-family: var(--font-display); font-size: 26px; margin: 0; }
.handover-card .big-btn { margin-top: 6px; }

#cardBar {
  position: absolute; z-index: 40; left: 50%; transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 18px);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  pointer-events: none;
}
#cardMsg {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line-strong);
  color: var(--ink); font-family: var(--font-display);
  font-size: 16px; font-weight: 700; white-space: nowrap;
  padding: 10px 18px; border-radius: 999px; box-shadow: var(--shadow);
}
#cardMsg svg { color: var(--accent-2); }
#cardNote {
  max-width: min(340px, 84vw); text-align: center;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--muted); font-size: 14px; line-height: 1.4;
  padding: 8px 14px; border-radius: 14px; box-shadow: var(--shadow);
}
#cardActions {
  display: flex; gap: 8px; pointer-events: auto;
  flex-wrap: wrap; justify-content: center; max-width: 94vw;
}
#cardActions .chip-btn { color: #0b0b10; white-space: nowrap; flex: none; padding: 0 16px; }

/* compose sheet fields */
/* A suggested value reads as a suggestion, not as something the child wrote -
   otherwise there is no way to tell whose words are whose. */
.card-field input.is-suggest, .card-field textarea.is-suggest { color: var(--muted); font-style: italic; }
.card-field { display: block; margin: 0 0 12px; text-align: left; }
.card-field > span {
  display: block; font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--muted); margin-bottom: 5px;
}
.card-field input, .card-field textarea {
  width: 100%; box-sizing: border-box; resize: none;
  background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--ink);
  border-radius: 10px; padding: 10px 12px; font: inherit; font-size: 15px;
  user-select: text; -webkit-user-select: text;
}
.card-field input:focus, .card-field textarea:focus { outline: 2px solid var(--accent); }
#cardSendGo { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 4px; }
#cardActions #cardReplay { background: var(--accent-grad); color: #fff; }
body.cardview #sidebar > :not(#brushSlider),
body.cardview #topbar .bar-group.center,
body.cardview #topbar .bar-group.right { display: none; }

/* Chips and bars are revealed by toggling [hidden]. `display` is discrete, so
   without allow-discrete the entrance never runs at all. OPACITY only, quite
   deliberately: every one of these is centred with translateX(-50%), and an
   entrance transform would have to compose with that - one missed pairing and
   the element jumps half its width across the screen.
   Older engines ignore @starting-style and simply show them, which is a clean
   degradation needing no fallback. */
#flipBtn, #squiggleBar, #dotsChip, #mazeTimerChip, #dotsConnectChip, #dotsDoneBar {
  transition: opacity var(--dur-enter) var(--ease-enter),
              display var(--dur-enter) allow-discrete;
}
@starting-style {
  #flipBtn:not([hidden]), #squiggleBar:not([hidden]), #dotsChip:not([hidden]),
  #mazeTimerChip:not([hidden]), #dotsConnectChip:not([hidden]), #dotsDoneBar:not([hidden]) { opacity: 0; }
}

/* Sending can take a moment now: a photo canvas uploads its image before the
   share sheet can open. The button says so rather than looking dead. */
.big-btn.is-busy { opacity: 0.75; pointer-events: none; }
.big-btn.is-busy > [data-icon], .big-btn.is-busy > .icn { display: none; }
.btn-spinner { --sp-size: 20px; margin: 0; flex: none; }

/* ---------------------------------------------------------------- toasts */
/* At the top, with the rest of the transient text. The bar and the shape and
   spiral trays own the bottom of the screen now, and a toast there covered the
   very controls the toast is telling you about. */
.toast {
  position: absolute; z-index: 60; left: 50%;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  transform: translateX(-50%) translateY(-6px);
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); padding: 10px 18px; border-radius: 999px;
  font-weight: 600; font-size: 14px;
  box-shadow: var(--shadow); opacity: 0;
  transition: opacity 0.18s, transform 0.18s var(--ease);
  /* max-content, capped: an absolutely-positioned box offset by left:50%
     may only shrink-to-fit the remaining HALF of the containing block, so
     this wrapped at 50% of the screen with empty space either side. Same
     fix as #squiggleBar. */
  width: max-content; max-width: min(94vw, 560px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.sync-status {
  position: absolute; bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);
  right: 12px; z-index: 40;
  font-size: 12px; color: var(--muted);
  background: var(--surface); border: 1px solid var(--line);
  padding: 5px 11px; border-radius: 999px;
}

/* ---------------------------------------------------------------- sheets */
.sheet-backdrop {
  position: absolute; inset: 0; z-index: 70;
  background: rgba(5, 5, 10, 0.62);
  backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
  display: grid; place-items: center; padding: 20px;
}
.sheet-backdrop:not([hidden]) { animation: backdropIn 0.18s ease-out; }
@keyframes backdropIn { from { opacity: 0; } to { opacity: 1; } }
.sheet {
  position: relative; width: min(430px, 92vw);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 24px; padding: 26px;
  box-shadow: 0 26px 90px rgba(0, 0, 0, 0.65);
  animation: popIn 0.18s var(--ease-enter);
}
.sheet.small { width: min(350px, 90vw); text-align: center; }

/* Share is a popover off the Share button, not a modal. Same treatment as the
   "..." menu: no dimming, sat just above the bar on the right, growing out of
   the button rather than appearing in the middle of the screen. It keeps the
   .sheet-backdrop machinery (Escape, tap-away, the [data-close] buttons) - only
   where it sits and how it arrives change. */
#shareSheet {
  background: transparent;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  place-items: end stretch;
  padding: 0;
}
#shareSheet .sheet {
  /* Narrower than a modal wants to be: it hangs off the Share button rather
     than owning the middle of the screen, so it should look like a menu. */
  width: min(320px, calc(100% - 20px));
  margin: 0 max(10px, env(safe-area-inset-right, 0px))
            calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 8px) auto;
  padding: 18px;
  box-shadow: var(--float-rim), var(--float-contact), var(--float-mid), var(--float-cast);
  transform-origin: bottom right;
  animation: popFromButton 240ms var(--ease-enter);
}
.sheet h2 {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-display); margin: 0 0 6px; font-size: 24px;
}
.sheet .sub { color: var(--muted); margin: 0 0 16px; font-size: 14.5px; line-height: 1.45; }
.sheet .sub.warn { color: var(--danger); font-weight: 600; }
.sub2 { color: var(--muted); font-size: 13.5px; line-height: 1.4; }
.sheet-close {
  position: absolute; top: 12px; right: 12px;
  width: 44px; height: 44px; display: grid; place-items: center;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  border-radius: 50%; cursor: pointer;
  transition: transform 0.12s var(--ease), background 0.15s;
}
.sheet-close:active { transform: scale(0.92); background: var(--surface-3); }
#deleteThumb {
  width: 92px; height: 118px; object-fit: cover;
  border-radius: 12px; border: 1px solid var(--line-strong);
  margin: 0 auto 12px; display: block; background: #000;
}
#videoPreview {
  width: 100%; max-height: 240px; object-fit: contain;
  border-radius: 12px; border: 1px solid var(--line-strong);
  margin: 4px 0 12px; display: block; background: #000;
}
#videoSave { display: flex; align-items: center; justify-content: center; gap: 4px; }
.replay-controls.recording #replayProgressFill { background: var(--danger); }
#recLabel {
  color: var(--danger); font-weight: 700; font-size: 13px;
  padding: 0 4px 0 10px; letter-spacing: 0.3px;
}
#photoPreview {
  width: 100%; max-height: 190px; object-fit: cover;
  border-radius: 12px; border: 1px solid var(--line-strong);
  margin: 4px 0 6px; display: block; background: #000;
}
#photoPreview.pop { filter: saturate(1.6) brightness(1.1); }

.coach-row { display: flex; gap: 14px; align-items: center; text-align: left; padding: 10px 2px; }
.coach-ic {
  width: 50px; height: 50px; flex: none; display: grid; place-items: center;
  border-radius: 14px; background: var(--accent-tint); color: var(--accent-2);
}
#coachGo { margin-top: 14px; width: 100%; }

.benefits { list-style: none; padding: 0; margin: 0 0 16px; }
.benefits li { padding: 7px 0 7px 26px; position: relative; font-size: 15px; }
.benefits li::before { content: ""; position: absolute; left: 2px; top: 12px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-grad); }
.price-row { display: flex; gap: 10px; margin-bottom: 16px; }
.price-opt { flex: 1; border: 1.5px solid var(--line-strong); border-radius: 14px; padding: 13px; text-align: center; cursor: pointer; }
.price-opt.active { border-color: var(--accent); background: var(--accent-tint); }
.price-opt b { display: block; font-size: 18px; }
.price-opt small { color: var(--muted); }
.fineprint { color: var(--muted); font-size: 12.5px; text-align: center; margin: 12px 0 0; }
.toggle-row { display: flex; align-items: center; gap: 8px; justify-content: center; font-size: 16px; margin: 8px 0 16px; }
.toggle-row input { width: 20px; height: 20px; accent-color: var(--accent); }

.row { display: flex; gap: 10px; }
.big-btn {
  flex: 1; min-width: 0; border: 0; cursor: pointer; font-weight: 700; font-size: 16px;
  padding: 14px; border-radius: 14px; min-height: var(--touch);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  text-align: center;
  transition: transform 0.12s var(--ease), filter 0.15s;
}
.big-btn:active { transform: scale(0.97); }
.big-btn.primary { background: var(--accent-grad); color: #fff; }
.big-btn.danger { background: var(--danger); color: #fff; }
.big-btn.ghost { background: var(--surface-2); border: 1px solid var(--line); color: var(--ink); }
.big-btn.hold { position: relative; overflow: hidden; }
.big-btn.hold::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 100%;
  width: var(--hold, 0%); background: rgba(255, 255, 255, 0.28);
  transition: width 0.1s linear;
}

/* ---------------------------------------------------------------- gallery */
.fullscreen {
  position: absolute; inset: 0; z-index: 65; background: var(--bg0);
  /* The bottom inset is not optional now the page runs behind the chrome (lvh):
     a flat 24px used to be enough only because the page stopped above the home
     indicator. Without it the last thing on a screen - the hub's footer links -
     sits underneath it. */
  padding: calc(env(safe-area-inset-top, 0px) + 10px)
           max(16px, env(safe-area-inset-right, 0px))
           calc(env(safe-area-inset-bottom, 0px) + 24px)
           max(16px, env(safe-area-inset-left, 0px));
  overflow-y: auto;
}
/* [hidden] toggles display, so un-hiding re-triggers the enter animation */
.fullscreen:not([hidden]) { animation: screenIn 0.26s var(--ease-enter); }

/* ------------------------------------------------------------- hub drawer */
/* The hub is a menu, not a screen: it slides in from the left over three
   quarters of the width, leaving the canvas visible in the remaining strip so
   it never feels like you have left your drawing behind. */
#hub {
  /* A floating card, not a full-height slab. Edge to edge it had to be as tall
     as the screen whether or not it had that much to say - so the list ended
     two thirds down and the rest was empty panel, and the very bottom ran under
     the home indicator and looked cut off. Pinned at the top and free at the
     bottom, it is exactly as tall as its contents and stops short of both ends,
     which is what makes it read as a menu that opened rather than a screen you
     were taken to. */
  /* Anchored to the bottom-left, just above the bar, because that is where the
     Home button that opens it now lives. */
  inset: auto auto calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 8px) max(10px, env(safe-area-inset-left, 0px));
  height: auto;
  max-height: calc(100% - var(--bar-h) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 20px);
  width: 83.333%; max-width: 520px;
  border: 1px solid var(--line-strong);
  border-radius: 24px;
  /* The app's three-layer float instead of one big offset blur. A single wide
     directional shadow has a visible outer edge, and where the panel sits
     against the side of the screen that edge gets clipped and reads as a hard
     line drawn down the shadow. These are tighter and centred, so there is no
     far edge to slice. */
  box-shadow: var(--float-rim), var(--float-contact), var(--float-mid), var(--float-cast);
  /* The card sits inside the safe area now, so its padding is plain padding -
     the insets are already spent on the offsets above. */
  padding: 16px 18px 16px 16px;
  /* It grows OUT OF the button rather than sliding in from an edge: the origin
     is the corner the Home button sits in, so the panel appears to be the
     button opening up. Scale and fade together over a short distance - the
     movement is small because the point is the connection to the button, not
     a journey across the screen. */
  transform-origin: bottom left;
  animation: popFromButton 240ms var(--ease-enter);
}
@keyframes popFromButton {
  from { transform: scale(0.82) translateY(12px); opacity: 0; }
  to { transform: none; opacity: 1; }
}
/* Closing is the opening played backwards, and slightly quicker - leaving is
   allowed to be brisker than arriving. The class is put on for the length of
   the animation and the panel is hidden when it finishes (see closeMoreMenu
   and closeHub), so the element is still there to animate. */
@keyframes popToButton {
  from { transform: none; opacity: 1; }
  to { transform: scale(0.82) translateY(12px); opacity: 0; }
}
#hub.is-closing,
#morePopover.is-closing .drawer-panel {
  animation: popToButton 180ms var(--ease-exit) forwards;
}
/* The strip beside it dims, so a tap there reads as "close this" rather than
   as part of the drawing. */
#hubScrim {
  /* Invisible on purpose. It still exists and still catches the tap that
     closes the panel - that job is unchanged - but it no longer dims the
     drawing behind it. The panel is a card that pops open over the canvas, and
     darkening the whole screen for it made a menu feel like leaving the page. */
  position: absolute; inset: 0; z-index: 64;
  background: transparent;
}

/* ------------------------------------------------------------ share sheet */
/* Four ways to send a drawing somewhere, as tappable cards rather than another
   list of menu rows - this is a destination, not a continuation of the menu. */
.share-grid { display: grid; gap: 8px; margin: 4px 0 14px; }
.share-opt {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;
  column-gap: 14px; align-items: center;
  padding: 12px 14px; min-height: var(--touch);
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); cursor: pointer; text-align: left;
  transition: background 0.15s, border-color 0.15s, transform 0.12s var(--ease);
}
.share-opt:active { transform: scale(0.98); background: var(--surface-2); }
@media (hover: hover) { .share-opt:hover { border-color: var(--accent); background: var(--surface-2); } }
.share-ic { grid-row: 1 / 3; color: var(--accent-2); display: grid; place-items: center; }
.share-lbl { font-size: 15px; font-weight: 600; color: var(--ink); }
.share-sub { font-size: 12.5px; color: var(--muted); }

/* ------------------------------------------------------- more menu drawer */
/* The "..." menu, mirrored: same drawer, opposite edge. Narrower than the hub
   because its rows are single lines of text, not tiles with subtitles. */
#morePopover.menu-drawer {
  /* The section is a full-height box that only POSITIONS the panel - it is
     transparent, but it was still a hit target, and it covers the right 78% of
     the screen including the "..." button itself. So every tap in the empty
     area above the panel landed on the section and did nothing: the menu would
     not close on a tap away, and tapping "..." again did nothing either.
     Letting taps pass through to the scrim underneath is the fix; the panel
     takes input back for itself. */
  pointer-events: none;
  display: flex; flex-direction: column; justify-content: flex-end;
  inset: 0 0 0 auto;
  width: 78%; max-width: 400px;
  /* The section itself never moves and never scrolls. It is only the box the
     panel lives in - see the markup comment. Transparent so the inner panel
     provides the surface. */
  background: none;
  border: 0;
  padding: 0;
  /* visible, not hidden: the panel's drop shadow is drawn OUTSIDE the panel,
     and this box ends exactly at the panel's edge - so hiding the overflow
     sliced the shadow off in a hard straight line down the side. */
  overflow: visible;
  animation: none;
}
/* The inner panel is what slides, and it is an ordinary in-flow block rather
   than an absolutely-positioned full-height layer. That is the whole fix: iOS
   was painting the section's contents at their resting position while the
   section itself was still travelling, so the close button appeared on its own
   to the left of the drawer and then jumped into place. */
#morePopover .drawer-panel {
  pointer-events: auto;
  /* The mirror of the hub: a card that is as tall as its menu and no taller,
     held clear of both ends rather than running the full height of the screen.
     The section around it still spans the screen - it is only the box the panel
     travels in - so the slide-in is unchanged. */
  height: auto;
  max-height: calc(100% - var(--bar-h) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 20px);
  /* Pushed to the bottom of its box so it sits directly above the "..." button
     it belongs to. margin-top:auto is what does it - the section is still a
     full-height box, and only the panel moved. */
  margin: auto max(10px, env(safe-area-inset-right, 0px)) calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 8px) 0;
  display: flex; flex-direction: column;
  box-sizing: border-box;
  padding: 6px;
  background: var(--bg0);
  border: 1px solid var(--line-strong);
  border-radius: 22px;
  /* The app's three-layer float instead of one big offset blur. A single wide
     directional shadow has a visible outer edge, and where the panel sits
     against the side of the screen that edge gets clipped and reads as a hard
     line drawn down the shadow. These are tighter and centred, so there is no
     far edge to slice. */
  box-shadow: var(--float-rim), var(--float-contact), var(--float-mid), var(--float-cast);
  transform-origin: bottom right;
  animation: popFromButton 240ms var(--ease-enter);
}
#moreScrim {
  /* Invisible on purpose. It still exists and still catches the tap that
     closes the panel - that job is unchanged - but it no longer dims the
     drawing behind it. The panel is a card that pops open over the canvas, and
     darkening the whole screen for it made a menu feel like leaving the page. */
  position: absolute; inset: 0; z-index: 64;
  background: transparent;
}
/* The close button sits top-RIGHT here - its natural place - so it lands under
   the same thumb that opened the menu. The hub reverses its own for the same
   reason on the other side. */
/* The panel and the list are the same element now, so this only contributes
   the list behaviour - the surface, border and radius come from .drawer-panel.
   They used to be two nested cards doing one card's job. */
#morePopover .menu-list {
  display: flex; flex-direction: column; gap: 2px;
  /* scrolls inside the sliding panel, so neither the section nor the panel is
     a scroll container while the transform is running */
  overflow-y: auto; overflow-x: clip;
  -webkit-overflow-scrolling: touch;
}
/* The hub's h1 is the app's name and earns its size; "Menu" does not - it is
   a label for the list below it, so it sits back rather than shouting. */
#morePopover .screen-head h1 { font-size: 20px; }
#morePopover .menu-row { min-height: var(--touch); border-radius: 12px; }
#morePopover .menu-row + .menu-row { border-top: 1px solid var(--line); }
/* Rows arrive from the right, matching the direction the panel came from. */
}
/* Same here: the panel is the thing that moves. */

/* The rows are one grouped list - the shape a menu has everywhere else - with
   hairline separators rather than seven floating cards. */
#hub .hub-grid {
  display: block; gap: 0; max-width: none; margin: 6px 0 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 18px; overflow: hidden;
}
/* Condensed: the hub is a menu of eight rows, and at the old row height it was
   nearly the height of the screen. Tighter than it was, deliberately still
   roomier than the "..." menu - these are the app's main choices and they are
   tapped by children. */
#hub .hub-tile {
  padding-top: 11px !important; padding-bottom: 11px !important;
  min-height: 0 !important;
}
#hub .hub-ic .icn { width: 23px !important; height: 23px !important; }
#hub .hub-label { font-size: 16px !important; }
#hub .hub-grid { gap: 0 !important; margin-top: 2px !important; }

#hub .hub-tile {
  display: flex; flex-direction: row; align-items: center; gap: 14px;
  width: 100%; padding: 15px 14px; border: 0; border-radius: 0;
  border-bottom: 1px solid var(--line);
  background: transparent; text-align: left;
}
#hub .hub-tile:last-child { border-bottom: 0; }
#hub .hub-tile:active { transform: none; background: var(--surface-3); }
@media (hover: hover) { #hub .hub-tile:hover { border-color: var(--line); background: var(--surface-3); transform: none; } }
#hub .hub-ic .icn { width: 26px; height: 26px; }
#hub .hub-label { font-size: 17px; }
/* The label and its one-liner stack; the chevron is pushed to the far edge. */
#hub .hub-tile > .hub-label { flex: 1; }
#hub .hub-sub { display: none; }
.hub-chev { margin-left: auto; color: var(--muted); display: inline-flex; line-height: 0; }

/* The close control belongs where a menu's does - top left. */
#hub .screen-head { justify-content: flex-start; gap: 12px; }
#hub .screen-head .icon-btn { order: -1; }
@keyframes screenIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* The hub tiles slide in from the left, top to bottom, and overshoot slightly
   before settling.
   The curve is the standard back-out: its third control point sits above 1, so
   progress runs PAST the target and eases back - the element slides a few px
   beyond its resting spot and returns. That is the whole trick; there is no
   spring library involved.
   Kept deliberately quick. An earlier pass ran 540ms end to end and read as
   broken rather than choreographed, and an overshoot needs a little more time
   than a hard stop to be legible at all - so 220ms each, 28ms apart, which
   still finishes the whole run inside 400ms. */
}
/* No per-item animation at all. The drawer itself slides in; its contents ride
   along with it, which is what a panel moving as one object looks like. The
   items used to bump sideways on their own on top of that, and two motions in
   the same direction at once read as busy rather than lively. */
/* No stagger. Everything arrives together on the same beat, so the drawer reads
   as one panel sliding in rather than a list dealing itself out row by row -
   which is the part that made it feel slow. The sideways overshoot stays: that
   is the character, the sequencing was not. */
/* The overshoot carries each tile a few px PAST its resting position, which on
   a narrow screen is enough to widen the page and flash a horizontal
   scrollbar. Clip it at the container rather than shrink the motion. */
#hub .hub-grid { overflow-x: clip; }

/* Gallery cards fade without the rise: the grid re-renders on every filter
   change, and a moving stagger there would draw the eye on each toggle. */
.grid > .card:nth-child(-n+12) { animation: boardIn 260ms var(--ease-enter) backwards; }
.grid > .card:nth-child(2) { animation-delay: 25ms; }
.grid > .card:nth-child(3) { animation-delay: 50ms; }
.grid > .card:nth-child(4) { animation-delay: 75ms; }
.grid > .card:nth-child(n+5) { animation-delay: 100ms; }
.screen-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.screen-head h1 { font-family: var(--font-display); font-size: 26px; margin: 4px 0; }
/* Was --line/--surface, which quietly overrode the shared capsule on every
   drawer and picker head and left them on the old flat look. Pointed at the
   same tokens so a close button is the same object wherever it appears. */
.screen-head .icon-btn { border-color: var(--glass-edge); background: var(--glass-solid); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 12px; }
.card {
  position: relative;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; overflow: hidden;
  transition: transform 0.12s var(--ease), border-color 0.15s;
}
@media (hover: hover) { .card:hover { border-color: var(--line-strong); transform: translateY(-2px); } }
.card .thumb { width: 100%; aspect-ratio: 3/4; object-fit: cover; background: #000; display: block; cursor: pointer; }
.card .meta { padding: 8px 10px 2px; display: flex; align-items: center; gap: 6px; }
.card .title { font-weight: 600; font-size: 13.5px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card .title[contenteditable="true"]:focus { outline: 1px solid var(--accent); border-radius: 4px; white-space: normal; }
.card .rename-btn { border: 0; background: none; color: var(--muted); cursor: pointer; width: 40px; height: 40px; margin: -8px; display: grid; place-items: center; }
.card .date { color: var(--muted); font-size: 11.5px; padding: 0 10px; }
.card .card-actions { display: flex; gap: 4px; padding: 8px; }
.card .card-actions button {
  flex: 1; height: 44px; display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
  border-radius: 10px; cursor: pointer; transition: color 0.15s, background 0.15s;
}
.card .card-actions button:active { background: var(--surface-3); color: var(--ink); }
@media (hover: hover) { .card .card-actions button:hover { color: var(--ink); } }
.card.readonly .thumb { opacity: 0.75; }
.card .lock-note {
  display: flex; align-items: center; gap: 5px;
  color: var(--muted); font-size: 11px; padding: 0 10px 6px;
}
.empty {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  color: var(--muted); text-align: center; margin-top: 60px; line-height: 1.5;
}

/* ---- gallery multi-select ---- */
.screen-head .head-actions { display: flex; align-items: center; gap: 10px; }
#gallerySelect, #gallerySelectAll {
  background: var(--surface); color: var(--ink); box-shadow: none;
  border: 1px solid var(--line); font-size: 14px; font-weight: 600;
  height: 38px; padding: 0 16px;
}
.sel-check {
  /* sized for small fingers in multi-select mode */
  position: absolute; top: 6px; left: 6px; width: 38px; height: 38px;
  border-radius: 999px; display: none; place-items: center; z-index: 2;
  background: rgba(6, 6, 16, 0.55); border: 2px solid #fff; color: transparent;
  backdrop-filter: blur(2px); cursor: pointer;
}
.shared-badge {
  position: absolute; top: 6px; right: 6px; width: 26px; height: 26px;
  border-radius: 999px; display: grid; place-items: center; z-index: 2;
  background: rgba(6, 6, 16, 0.55); border: 2px solid #fff; color: #fff;
  backdrop-filter: blur(2px); pointer-events: none;
}
.shared-badge .icn { width: 14px; height: 14px; }
.gallery-filter-empty { grid-column: 1 / -1; color: var(--muted); text-align: center; padding: 40px 20px; }
.grid.selecting .sel-check { display: grid; }
.grid.selecting .thumb { cursor: pointer; }
/* keep the last row clear of the fixed Delete bar */
.grid.selecting { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 84px); }
.card.selected { outline: 3px solid var(--accent); outline-offset: -3px; }
.card.selected .sel-check { background: var(--accent); border-color: var(--accent); color: #fff; }
#galleryBulkBar {
  /* fixed to the viewport so it sticks to the bottom while the grid scrolls */
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--line);
  padding: 8px 10px 8px 14px; border-radius: 999px; box-shadow: var(--shadow); z-index: 70;
  /* never wider than the screen; the count absorbs the squeeze */
  max-width: calc(100vw - 24px); box-sizing: border-box;
}
#gallerySelCount {
  color: var(--muted); font-size: 14px; font-variant-numeric: tabular-nums;
  /* the flexible one: if the pill runs out of room this gives way, not the
     button whose label people are about to tap */
  flex: 1 1 auto; min-width: 0; text-align: center;
}
/* .big-btn is flex:1/min-width:0 because it was built for the two-up rows in
   dialogs. Here that let the button shrink under its own label, so "Delete 199"
   wrapped onto two lines and spilled past the pill's edge once the count hit
   three digits. Size it to its content instead. */
#galleryDeleteSel { flex: 0 0 auto; white-space: nowrap; height: 44px; padding: 0 20px; }
#galleryDeleteSel:disabled { opacity: 0.45; }

/* ---------------------------------------------------------------- home hub */
/* min-height:100% was what forced the empty tail: it made the column as tall as
   the screen, and `margin-top:auto` on the footer then pushed the links all the
   way down to meet it. The card sizes to its contents instead. */
#hub { display: flex; flex-direction: column; }
.hub-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px; max-width: 920px; margin: 10px auto 0;
  /* #hub is a flex column; without a definite width the grid's auto-fit
     resolves against max-content and overflows sideways. Pin it to the row. */
  width: 100%; box-sizing: border-box;
}
.hub-tile {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  padding: 22px 20px; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); border-radius: 20px;
  color: var(--ink); transition: transform 0.12s var(--ease), border-color 0.15s;
}
.hub-tile:active { transform: scale(0.98); }
@media (hover: hover) { .hub-tile:hover { border-color: var(--accent); transform: translateY(-2px); } }
.hub-ic { color: var(--accent); display: inline-flex; line-height: 0; }
.hub-ic .icn { width: 34px; height: 34px; stroke-width: 1.75; }
/* each doorway gets its own colour - faster to recognise for pre-readers */
#hubMagic .hub-ic { color: var(--accent-2); }
#hubMazes .hub-ic { color: var(--good); }
@media (hover: hover) {
  #hubBlank:hover { border-color: var(--accent); }
  #hubMagic:hover { border-color: var(--accent-2); }
  #hubMazes:hover { border-color: var(--good); }
}

/* ---- Your drawings: a strip, deliberately not an eighth tile -------------
   The seven tiles above start something new; this opens work that already
   exists. Real thumbnails in a row (newest first) say "your stuff" at a
   glance in a way another icon-and-label card never could - and for a
   pre-reader, their own picture is the only label that works. */
.hub-recent {
  width: 100%; box-sizing: border-box; max-width: 920px; margin: 22px auto 0;
}
/* The "My Gallery" button IS the heading now - it names the section and opens
   it, so a separate label above the thumbnails was saying the same thing
   twice. */
.hub-recent-head {
  display: flex; align-items: center; justify-content: flex-start; gap: 12px;
  margin-bottom: 10px;
}
/* Quiet on purpose: .chip-btn's default is a solid white primary pill, which
   out-shouted the thumbnails - the drawings are the content here, this is just
   the way through to all of them. Ghost styling, but still a full-size tap
   target because small children are the ones aiming at it. */
#hubGallery {
  display: inline-flex; align-items: center; gap: 6px;
  background: transparent; color: var(--muted);
  border: 1px solid var(--line); box-shadow: none;
  font-size: 14px; height: auto; min-height: 40px; padding: 0 14px;
}
#hubGallery .icn { width: 16px; height: 16px; }
@media (hover: hover) {
  #hubGallery:hover { color: var(--ink); border-color: var(--accent); }
}
.recent-strip {
  display: flex; gap: 12px; overflow-x: auto; overflow-y: hidden;
  padding: 2px 2px 8px; scroll-snap-type: x proximity;
  /* momentum scrolling on iOS, and no scrollbar gutter stealing height */
  -webkit-overflow-scrolling: touch; scrollbar-width: thin;
}
.recent-strip::-webkit-scrollbar { height: 6px; }
.recent-strip::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
.recent-item {
  flex: 0 0 auto; width: 108px; scroll-snap-align: start; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  padding: 0; overflow: hidden; position: relative; line-height: 0;
  transition: transform 0.12s var(--ease), border-color 0.15s;
}
.recent-item img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; background: #000; }
.recent-item:active { transform: scale(0.97); }
@media (hover: hover) { .recent-item:hover { border-color: var(--accent); transform: translateY(-2px); } }
/* Shared-canvas marker, mirroring the gallery card's badge */
.recent-item .shared-badge { position: absolute; top: 6px; right: 6px; }
.recent-empty { font-size: 13px; color: var(--muted); padding: 6px 2px 10px; }
.hub-label { font-family: var(--font-display); font-size: 19px; font-weight: 700; }
.hub-sub { font-size: 13px; color: var(--muted); }
.hub-links { margin-top: auto; text-align: center; padding-top: 16px; display: flex; gap: 8px; justify-content: center; align-items: center; color: var(--muted); font-size: 13px; }
.hub-links a,
.hub-links button { color: var(--muted); text-decoration: none; font-weight: 600; padding: 4px 2px;
  background: none; border: 0; font-family: inherit; font-size: inherit; cursor: pointer; }
.hub-links a:active { color: var(--ink); }
@media (hover: hover) { .hub-links a:hover, .hub-links button:hover { color: var(--ink); } }
.hub-version { display: block; text-align: center; font-size: 11px; color: var(--muted); opacity: 0.5; padding: 8px 0 4px; }
@media (max-height: 730px), (max-width: 479px) {
  .hub-grid { gap: 10px; margin-top: 4px; }
  .hub-tile {
    flex-direction: row; align-items: center; gap: 14px;
    padding: 14px 18px; border-radius: 16px;
  }
  .hub-ic .icn { width: 28px; height: 28px; }
  .hub-sub { display: none; }
  .hub-label { font-size: 17px; }
}

/* ---------------------------------------------------------------- maze picker */
.maze-mods { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 16px; }
.mods-label { font-size: 14px; color: var(--muted); }
.mod-chip {
  display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
  padding: 8px 16px; border-radius: 999px; font-size: 14px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted); cursor: pointer;
  transition: transform 0.12s var(--ease), background 0.15s;
}
.mod-chip:active { transform: scale(0.95); }
.mod-chip .icn { width: 16px; height: 16px; }
.mod-chip.on { border-color: var(--accent); color: var(--ink); background: rgba(124, 92, 255, 0.16); }
.mod-chip.on .icn { color: var(--accent); }
.win-star { display: inline-flex; vertical-align: -3px; color: #ffb14a; }
.win-star .icn { fill: #ffce5a; stroke: #e0921f; }
.maze-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 14px; max-width: 1080px; margin: 0 auto;
}

/* Squiggle picker: tiles ARE the squiggle, rendered live from the seed.
   Laid out like the gallery (same auto-fill/148px track) so all 30 can be
   scanned at a glance - wider tracks gave one enormous tile per row on a
   phone and turned choosing into scrolling. */
.squiggle-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 12px; max-width: 1080px; margin: 0 auto; padding-bottom: 24px;
}
.squiggle-card {
  padding: 0; border: 1px solid var(--line); border-radius: 16px;
  overflow: hidden; background: #000; cursor: pointer; line-height: 0;
  transition: transform 0.12s var(--ease), border-color 0.15s;
}
@media (hover: hover) {
  .squiggle-card:hover { border-color: var(--line-strong); transform: translateY(-2px); }
}
.squiggle-card:active { transform: scale(0.97); }
.squiggle-thumb { width: 100%; aspect-ratio: 1/1; display: block; }
.maze-tile {
  display: flex; flex-direction: column; gap: 4px; padding: 0 0 12px; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  color: var(--ink); cursor: pointer; transition: transform 0.12s var(--ease), border-color 0.15s;
}
.maze-tile:active { transform: scale(0.98); }
@media (hover: hover) { .maze-tile:hover { border-color: var(--accent); transform: translateY(-2px); } }
.maze-thumb { width: 100%; height: auto; display: block; background: #0a0a14; }
.maze-label { font-family: var(--font-display); font-size: 17px; font-weight: 700; padding: 8px 12px 0; }
.maze-tag { font-size: 12.5px; color: var(--muted); padding: 0 12px; }

/* Maze mode: the game locks the toys that could cheat through walls. */
body.maze-mode .tool-btn:not([data-tool="scratch"]),
body.maze-mode #symmetryBtn { opacity: 0.35; pointer-events: none; }

/* ---------------------------------------------------------- dots mode */
/* Scratch only: the other toys are parked while a puzzle is on the board. */
body.dots-mode .tool-btn:not([data-tool="scratch"]),
body.dots-mode #symmetryBtn { opacity: 0.35; pointer-events: none; }

/* ------------------------------------------------------- squiggle mode */
/* Squiggle: the squiggle is pre-scratched and the kid carries on from it,
   so the same scratch-only lock applies (setTool also guards this in JS). */
body.squiggle-mode .tool-btn:not([data-tool="scratch"]),
body.squiggle-mode #symmetryBtn { opacity: 0.35; pointer-events: none; }

#dotsChip, #mazeTimerChip {
  position: absolute; z-index: 40; top: calc(env(safe-area-inset-top, 0px) + 10px);
  left: 50%; transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink); font-weight: 600; font-size: 13px; padding: 7px 14px;
  font-variant-numeric: tabular-nums; pointer-events: none;
  width: max-content; max-width: min(94vw, 560px);
}
#dotsConnectChip {
  position: absolute; z-index: 40; top: calc(env(safe-area-inset-top, 0px) + 48px);
  left: 50%; transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  color: var(--muted); font-weight: 600; font-size: 12.5px; padding: 6px 12px; cursor: pointer;
}
#dotsConnectChip.on { color: var(--ink); border-color: var(--accent); }
#dotsConnectChip.on .icn { color: var(--accent); }
@media (hover: hover) { #dotsConnectChip:hover { border-color: var(--accent); } }
/* quiet post-win bar: no popup - a view toggle and a next-puzzle button */
#dotsDoneBar {
  position: absolute; z-index: 40; bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  /* same floating panel as the sidebar: the buttons need contrast against
     whatever picture ends up behind them */
  background: var(--surface); border: 1px solid var(--line);
  padding: 7px; border-radius: 999px;
  box-shadow: var(--shadow);
}
#dotsViewChip, #dotsNextBtn {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 999px;
  color: var(--ink); font-weight: 600; font-size: 13.5px; padding: 9px 16px; cursor: pointer;
  white-space: nowrap;
}
#dotsViewChip .icn { color: var(--accent); }
#dotsNextBtn {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
#dotsNextBtn .icn { color: #fff; }
@media (hover: hover) {
  #dotsViewChip:hover { border-color: var(--accent); }
  #dotsNextBtn:hover { filter: brightness(1.1); }
}

.tier-preview {
  display: block; width: 100%; max-height: 200px; object-fit: contain;
  border-radius: 12px; background: #050508; margin: 2px 0 4px;
}
.tier-hint { color: var(--muted); font-size: 12px; text-align: center; margin: 0 0 6px; }
.tier-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.tier-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  color: var(--ink); padding: 12px 14px; cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.tier-row .tier-name { font-weight: 700; font-size: 15px; }
.tier-row .tier-dots { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }
.tier-row.on { border-color: var(--accent); background: rgba(124, 92, 255, 0.14); }
@media (hover: hover) { .tier-row:hover { border-color: var(--accent); } }

/* ---------------------------------------------------------- magic picker */
.magic-lede { color: var(--muted); margin: -6px 0 16px; font-size: 14px; }
.shelf { margin-bottom: 22px; }
.shelf-head {
  font-family: var(--font-display); font-size: 19px; margin: 0 0 10px;
  display: flex; align-items: center; gap: 8px;
}
.shelf-emoji { font-size: 22px; }
.shelf-row {
  display: flex; gap: 12px; overflow-x: auto; padding: 2px 2px 10px;
  /* proximity, not mandatory: a mandatory-snap row wins slightly-diagonal
     drags on iOS, hijacking vertical scrolls that start on a card */
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  /* pan-y too: a vertical drag that starts on a card must scroll the picker,
     not get swallowed by the row (the browser axis-locks from the first move) */
  touch-action: pan-x pan-y;
  overscroll-behavior-x: contain;
}
.shelf-row::-webkit-scrollbar { height: 6px; }
.shelf-row::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
.magic-card {
  position: relative; flex: 0 0 auto; width: 210px;
  scroll-snap-align: start; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; overflow: hidden; padding: 0;
  transition: transform 0.12s var(--ease), border-color 0.15s;
}
.magic-card:active { transform: scale(0.98); }
@media (hover: hover) { .magic-card:hover { border-color: var(--accent); transform: translateY(-2px); } }
.magic-card .mc-thumb {
  width: 100%; aspect-ratio: 32/21; object-fit: cover; display: block; background: #05050e;
  /* Thumbnails render through a sequential queue, so they land one at a time.
     Snapping in reads as stuttering; fading in reads as deliberate. The dark
     card background is the placeholder underneath. */
  opacity: 0; transition: opacity 220ms var(--ease-enter);
}
.magic-card .mc-thumb.loaded { opacity: 1; }
.magic-card .mc-title {
  display: block; padding: 8px 10px 10px; font-weight: 600; font-size: 13.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left;
}
.magic-card.make-own {
  display: grid; place-items: center; aspect-ratio: 32/21;
  border: 1.5px dashed var(--line-strong); background: transparent; color: var(--muted);
}
.magic-card.make-own .mo-inner { display: flex; flex-direction: column; align-items: center; gap: 8px; font-weight: 600; }
.magic-card.make-own .mo-inner .icn { color: var(--accent); }
@media (hover: hover) { .magic-card.make-own:hover { border-color: var(--accent); color: var(--ink); } }
.magic-card.locked .mc-thumb.loaded { opacity: 0.82; }
#sizePopover h3 { margin: 2px 6px 8px; font-size: 13px; color: var(--muted); }
.size-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.size-row small { color: var(--muted); font-variant-numeric: tabular-nums; }
.size-row.active { color: var(--accent); }
.magic-card .mc-lock {
  position: absolute; top: 8px; left: 8px;
  width: 26px; height: 26px; display: grid; place-items: center;
  background: rgba(6,6,16,0.72); color: #fff; border-radius: 999px;
  backdrop-filter: blur(2px);
}

/* ---------------------------------------------------------------- replay */
#replayOverlay { position: absolute; inset: 0; z-index: 55; }
.replay-controls {
  /* sits just above the bottom menu bar so it never overlaps the invite
     button or the tool switcher */
  position: absolute;
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 12px);
  left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--line);
  padding: 7px; border-radius: 999px;
  box-shadow: var(--shadow);
  min-width: 440px; /* wide enough for all buttons + REC label */
}
#replayProgress {
  width: min(34vw, 220px); height: 4px; border-radius: 999px;
  background: var(--surface-3); overflow: hidden; margin: 0 6px;
}
#replayProgressFill { height: 100%; width: 0%; background: var(--accent-grad); border-radius: 999px; }
#replaySpeed { font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
#replayTimer { font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; color: var(--muted); padding: 0 2px; }

/* ------------------------------------------------------- maze leaderboard */
#mazeWinTime { margin: -8px 0 16px; font-weight: 700; color: var(--ink); }
#mazeNameEntry { margin: 0 0 16px; text-align: left; }
#mazeNameInput {
  width: 100%; box-sizing: border-box; margin: 0 0 16px;
  font-size: 16px; font-family: var(--font-ui); padding: 11px 14px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: 12px; color: var(--ink);
}
#mazeNameInput::placeholder { color: var(--muted); }
#mazeNameInput:focus { outline: 2px solid var(--accent); }
#mazeRecentNames:empty { margin: 0; }
#mazeRecentNames:not(:empty) { margin: 0 0 16px; }
.recent-name {
  background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--muted);
  height: 34px; padding: 0 14px; font-size: 13.5px; box-shadow: none;
}
.recent-name:hover, .recent-name:active { border-color: var(--accent); color: var(--ink); }
#mazeSaveScore { width: 100%; }

/* one scrolling row, not a wrap - 7 difficulty labels of very different
   lengths wrapped into ragged, unevenly-centered rows otherwise */
#mazeBoardTabs {
  display: flex; flex-wrap: nowrap; flex-direction: row; justify-content: flex-start;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  gap: 8px; margin: 0 0 16px; padding: 2px 2px 8px;
}
#mazeBoardTabs::-webkit-scrollbar { height: 6px; }
#mazeBoardTabs::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
#mazeBoardTabs .mod-chip { flex: 0 0 auto; white-space: nowrap; }
#mazeBoardList { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; }
/* It was inheriting a shrink-to-fit button with no gap above it, so it sat
   jammed against the last score with the sheet's edge crowding its right. Full
   width and given room, it reads as the sheet's action rather than a stray
   chip in the corner. */
#mazeBoardAgain { width: 100%; margin-top: 18px; }
.board-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 12px;
  padding: 10px 14px;
}
.board-row:nth-child(1) { border-color: #ffce5a; }
.board-rank {
  font-family: var(--font-display); font-weight: 800; font-size: 15px;
  color: var(--muted); min-width: 20px;
}
.board-row:nth-child(1) .board-rank { color: #ffb14a; }
.board-name { flex: 1; font-weight: 600; font-size: 14.5px; display: flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-badges { display: inline-flex; color: var(--muted); flex: none; }
.board-badges .icn { width: 13px; height: 13px; }
.board-time { font-weight: 700; font-size: 14.5px; font-variant-numeric: tabular-nums; color: var(--accent-2); }

/* ---------------------------------------------------------------- install / update */
.install-hint, .update-toast {
  position: absolute; z-index: 62; border: 1px solid var(--line); cursor: pointer;
  /* Sit above the bottom toolbar (#topbar, despite the name) rather than a
     flat 16px, or the two overlap - same clearance as .tray. */
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 18px);
  right: max(16px, env(safe-area-inset-right, 0px));
  background: var(--surface); color: var(--ink); font-weight: 600; font-size: 14px;
  padding: 11px 17px; border-radius: 999px; box-shadow: var(--shadow);
}
.update-toast {
  /* above every surface (fullscreen 65, replay 55, sheets 80) so "Update
     ready" is reachable no matter which screen is open, not just the canvas */
  z-index: 90;
  background: var(--accent-grad); color: #fff; border: 0;
  left: 50%; right: auto; transform: translateX(-50%);
  /* same 50% shrink-to-fit cap as .toast - see there */
  width: max-content; max-width: min(94vw, 560px);
}

/* ---------------------------------------------------------------- motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  #hint > * { animation: none; opacity: 1; }
  /* the spinner is feedback, not decoration - freezing it solid reads as a hang */
  .scratchy-spinner__stroke { animation-duration: 3.4s !important; }
}

/* ---------------------------------------------------------------- free draw */
/* Free Draw hides the chrome that doesn't apply: there's no swappable
   reveal pattern (colour lives on the stroke, not the canvas), and
   stencils/spirals aren't surfaced in this mode (still work if triggered). */
body.freedraw-mode #patternBtn,
body.freedraw-mode .tool-btn[data-tool="stencils"],
body.freedraw-mode .tool-btn[data-tool="spirals"] { display: none; }

#colorDot {
  width: 24px; height: 24px; border-radius: 50%; flex: none;
  /* the ring shows the Secondary colour, set inline alongside the Primary
     fill (see syncFreeDrawChrome in ui.js) - this is just the fallback
     before Free Draw's first sync. */
  border: 4px solid rgba(255, 255, 255, 0.6);
}

/* Two-panel brush library (sidebar tray): left = brush sets,
   right = brushes in the selected set with name + live stroke preview. */
.brushlib-body { display: flex; gap: 4px; }
.brushlib-groups {
  flex: 0 0 84px; display: flex; flex-direction: column; gap: 2px;
  max-height: min(60vh, 460px); overflow-y: auto;
  border-right: 1px solid var(--line); padding-right: 6px; margin-right: 4px;
}
.brushlib-group {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 9px 4px; border-radius: 12px; border: 1px solid transparent;
  background: transparent; color: var(--muted); cursor: pointer;
  font-size: 10.5px; font-weight: 700; line-height: 1.1;
}
.brushlib-group svg { width: 20px; height: 20px; }
.brushlib-group.on { background: var(--accent-tint); color: var(--ink); border-color: var(--accent); }
@media (hover: hover) { .brushlib-group:hover:not(.on) { background: var(--surface-2); } }
.brushlib-brushes {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
  max-height: min(60vh, 460px); overflow-y: auto;
}
.brushlib-brush {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 10px;
  border: 1px solid transparent; background: transparent; color: var(--ink);
  cursor: pointer; text-align: left; font: inherit;
}
.brushlib-brush .label { font-size: 14px; font-weight: 600; flex: none; }
.brushlib-brush .tex-preview { border-radius: 6px; background: #000; flex: none; }
.brushlib-brush.on { background: var(--surface-2); border-color: var(--line-strong); }
@media (hover: hover) { .brushlib-brush:hover:not(.on) { background: var(--surface-2); } }

/* ------------------------------------------------------------ colour panel */
/* Colour popover: a side card off the sidebar colour button (same placement
   as the brush library), a self-contained tabbed overlay - fixed swatch
   header + scrolling middle + a bottom tab bar. */
.color-popover {
  left: calc(env(safe-area-inset-left, 0px) + 74px); top: 50%; transform: translateY(-50%);
  transform-origin: left center; animation: popInSide 0.18s var(--ease-enter);
  width: min(400px, 92vw); max-width: calc(100vw - 90px);
  max-height: min(84vh, 640px); overflow: hidden;
  display: flex; flex-direction: column; padding: 16px 16px 0;
}
.color-panel { user-select: none; -webkit-user-select: none; display: flex; flex-direction: column; flex: 1; min-height: 0; }
.cp-scroll { flex: 1; min-height: 0; overflow-y: auto; }
#colorDone { margin: 12px 0; flex: none; }
.cp-head { display: flex; align-items: flex-start; justify-content: center; gap: 14px; margin-bottom: 12px; }
.cp-slot { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.cp-slot-lbl { font-size: 11px; font-weight: 600; color: var(--muted); }
.cp-primary, .cp-secondary {
  width: 56px; height: 34px; border: 2px solid var(--line-strong); padding: 0; border-radius: 10px;
  cursor: pointer; transition: box-shadow 0.15s, border-color 0.15s;
}
/* the active (selected) slot - the one a colour pick will fill - gets a ring */
.cp-primary.active, .cp-secondary.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
}
.cp-sync {
  width: 26px; height: 26px; border-radius: 50%; flex: none; padding: 0; margin-top: 4px;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--muted);
}
.cp-sync svg { width: 15px; height: 15px; }
.cp-sync.on { background: var(--accent-tint); border-color: var(--accent); color: var(--accent-2); }
/* bottom tab bar - equal-width segments across the foot of the card */
.cp-tabs {
  display: flex; gap: 2px; flex: none;
  padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--line);
}
.cp-tab {
  flex: 1; border: 0; background: transparent; color: var(--muted);
  border-radius: 10px; padding: 9px 2px; font-size: 12px; font-weight: 600; cursor: pointer;
}
.cp-tab.on { background: var(--accent-tint); color: var(--accent-2); }
.cp-pane[hidden] { display: none; }
.cp-disc, .cp-sv { display: block; width: 100%; touch-action: none; cursor: crosshair; }
.cp-disc { max-width: 260px; aspect-ratio: 1; margin: 0 auto 10px; }
.cp-sv { height: 150px; border-radius: 12px; margin-bottom: 10px; }
.cp-sliders { display: flex; flex-direction: column; gap: 6px; }
.cp-slider { display: flex; align-items: center; gap: 10px; }
.cp-sl-lbl { width: 18px; font-size: 13px; font-weight: 700; color: var(--muted); text-align: center; }
.cp-track {
  flex: 1; height: 26px; border-radius: 999px; background: var(--surface-3);
  overflow: hidden; touch-action: none; cursor: pointer; position: relative;
}
.cp-fill { position: absolute; inset: 0 auto 0 0; background: var(--accent); opacity: 0.75; border-radius: 999px; }
/* colour sliders (HSB / RGB): the track IS the gradient, with a round handle
   marking the value so it's obvious what the slider adjusts. */
.cp-track.grad { overflow: visible; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14); }
.cp-handle {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 50%; background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
.cp-num {
  width: 52px; text-align: center; font-variant-numeric: tabular-nums;
  background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--ink);
  border-radius: 8px; padding: 6px 4px; font-size: 14px; font-weight: 600;
}
.cp-hexrow { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cp-hexlbl { font-size: 13px; font-weight: 700; color: var(--muted); }
.cp-hex {
  flex: 1; background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--ink);
  border-radius: 10px; padding: 9px 12px; font-size: 15px; font-weight: 600;
  font-family: ui-monospace, monospace; text-transform: lowercase;
}
.cp-sub { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); margin: 10px 0 6px; }
.cp-harmhint { text-align: center; font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: 8px; }
.cp-harmsw-lbl { text-align: center; font-size: 12px; font-weight: 600; color: var(--muted); margin-top: 10px; }
.cp-harmsw { display: flex; gap: 8px; justify-content: center; margin-top: 8px; }
/* the currently-chosen accent match gets a ring */
.cp-harmsw .cp-sw.on { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }
.cp-sw {
  width: 34px; height: 34px; border-radius: 50%; cursor: pointer; padding: 0; flex: none;
  border: 2px solid rgba(255, 255, 255, 0.25);
}
.cp-palettes-head { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.cp-newpal {
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--ink);
  border-radius: 999px; padding: 6px 12px; font-size: 13px; font-weight: 600; cursor: pointer;
}
.cp-palettes { display: flex; flex-direction: column; gap: 12px; }
/* read-only default-palette title, and the divider above the defaults */
.cp-pal-name-ro { font-size: 13px; font-weight: 700; color: var(--muted); margin-bottom: 6px; }
.cp-pal-divider {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--muted); margin: 6px 0 2px; padding-top: 12px; border-top: 1px solid var(--line);
}
.cp-pal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 13px; font-weight: 700; color: var(--muted); margin-bottom: 6px;
}
.cp-pal-name {
  flex: 1; min-width: 0; border: 0; background: transparent; color: inherit;
  font: inherit; padding: 3px 5px; border-radius: 6px;
}
.cp-pal-name:focus { background: var(--surface-2); outline: 2px solid var(--accent); }
.cp-pal-btns { display: flex; gap: 6px; flex: none; }
.cp-pal-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.cp-add, .cp-pal-del {
  width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--ink);
  font-size: 16px; font-weight: 700; line-height: 1;
}
.cp-pal-del { color: var(--muted); font-size: 15px; }
.cp-sw-hold { transition: transform 0.55s linear; transform: scale(0.7); }
.cp-hist-label { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); margin: 12px 0 6px; }
.cp-history { display: flex; gap: 8px; flex-wrap: wrap; min-height: 34px; margin-bottom: 12px; }
.cp-empty { font-size: 13px; color: var(--muted); align-self: center; }

/* ---- Collaboration ---- */
/* The presence pill: its own row under the header, right-aligned, so it reads
   as information about the canvas rather than another toolbar button. Sits at
   z-index 29, below the bar itself, and hides with the rest of the chrome while
   someone is drawing. */
.collab-bar {
  position: absolute; z-index: 29;
  top: calc(env(safe-area-inset-top, 0px) + 6px);
  right: max(10px, env(safe-area-inset-right, 0px));
  display: flex; align-items: center; gap: 10px;
  padding: 5px 14px 5px 6px;
  border-radius: 999px;
  background: var(--glass-fill);
  backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-hi), var(--glass-lo), var(--glass-shadow);
}
.collab-bar[hidden] { display: none; }
/* The empty state: no faces yet, so the pill is an invitation instead. The
   whole thing is tappable (see ui.js), hence the pointer and the pressed
   state - it has to read as a button, not as a status line. */
.collab-bar.is-invite { cursor: pointer; padding-left: 12px; }
.collab-bar.is-invite:active { transform: scale(0.96); }
.collab-bar.is-invite .collab-count { font-weight: 700; }
/* A card recipient is here to scratch one card open, not to start a session. */
body.cardview .collab-bar { display: none; }
/* Fill and Done live in the same top strip, centred, and a wide presence pill
   ran straight into them - covering the Fill button entirely with three
   people on the canvas. While a stencil is being placed the pill steps down a
   row rather than fighting for the space. */
body:has(#stencilLayer:not([hidden])) .collab-bar {
  top: calc(env(safe-area-inset-top, 0px) + 66px);
}
.collab-avatars { display: flex; align-items: center; }
.collab-avatar {
  width: 32px; height: 32px; border-radius: 50%; color: #fff;
  font-size: 14px; font-weight: 700; display: flex; align-items: center; justify-content: center;
  /* The ring is what separates one face from the one it overlaps, so it has to
     be the pill's own colour rather than a panel colour - and opaque, or the
     face underneath shows through the gap. */
  border: 2px solid #1b1b26; position: relative;
}
.collab-avatar + .collab-avatar { margin-left: -10px; }
.collab-invite {
  width: 32px; height: 32px; margin-left: -10px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%; cursor: pointer;
  color: var(--ink); background: var(--surface-3);
  border: 2px solid #1b1b26;
  transition: transform 0.12s var(--ease), background 0.15s;
}
.collab-invite:active { transform: scale(0.92); background: var(--surface-2); }
@media (hover: hover) { .collab-invite:hover { background: var(--surface-2); } }
.collab-count { font-size: 13px; font-weight: 600; color: var(--ink); white-space: nowrap; }
.collab-status-dot {
  width: 9px; height: 9px; border-radius: 50%; position: absolute; bottom: -1px; right: -1px;
  border: 2px solid #1b1b26;
}
.collab-status-dot.connected { background: #4ade80; }
.collab-status-dot.reconnecting { background: #fbbf24; }

.collab-link-box {
  display: flex; gap: 8px; align-items: center; margin-top: 8px;
}
.collab-link-box input {
  flex: 1; font-size: 13px; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  font-family: inherit;
}

.collab-join-overlay {
  position: fixed; inset: 0; z-index: 9999;
  background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center;
}
.collab-join-card {
  background: var(--surface-1); border-radius: 16px; padding: 24px; width: 300px; max-width: 90vw;
  text-align: center;
}
.collab-join-card h2 { margin: 0 0 4px; font-size: 18px; color: var(--ink); }
.collab-join-card label {
  display: block; text-align: left; font-size: 13px; color: var(--muted); margin: 20px 0 0;
}
.collab-join-card input {
  /* the label text and input are both children of the same <label> - the
     label's own margin only spaces the whole block, not text-from-input,
     so the gap between them has to live here or they sit flush/overlap
     on narrow (mobile) viewports */
  display: block; margin-top: 8px;
  width: 100%; box-sizing: border-box; font-size: 16px; padding: 10px 12px;
  border-radius: 10px; border: 1px solid var(--line); background: var(--surface-2);
  color: var(--ink); font-family: inherit;
}
.collab-join-card .big-btn { width: 100%; margin-top: 16px; }

[hidden] { display: none !important; }

/* ---- Photo cropper ---- */
/* Above the loading veil (90) and everything else it could appear over, but
   below the launch surface (150) - it can never be up at the same time as
   that, and the ordering should still say so. */
.cropper {
  position: fixed; inset: 0; z-index: 120;
  display: flex; flex-direction: column;
  background: var(--bg0);
  padding: calc(env(safe-area-inset-top, 0px) + 12px)
           max(12px, env(safe-area-inset-right, 0px))
           calc(env(safe-area-inset-bottom, 0px) + 14px)
           max(12px, env(safe-area-inset-left, 0px));
  animation: screenIn 0.22s var(--ease-enter);
}
.crop-stage { flex: 1; min-height: 0; display: grid; place-items: center; }
/* touch-action:none because the whole gesture vocabulary here is ours - the
   page must not try to pan or zoom underneath a drag on the frame. */
.crop-canvas { touch-action: none; border-radius: 12px; }
.crop-bar { display: flex; gap: 10px; padding-top: 14px; }
.crop-bar .big-btn { flex: 1; }


/* The app's layers are FIXED, so the one scrollable pixel that keeps Safari
   treating this as a scrollable page can never move the interface. This lives
   at the end of the file on purpose: each of these elements declares its own
   position later in the cascade, and an override earlier simply lost to it -
   which showed up as the bottom bar sitting one pixel below the fold. */
#backdrop, #stage, #topbar, #sidebar, #hint,
#zoomChip, #collabBar, #squiggleBar, #stencilControls {
  position: fixed;
}

/* ---------------------------------------------------------- onboarding */
/* Above the launch surface (150), because it takes over the moment that one
   begins to fade - the app is never seen before it. */
#onboard {
  position: fixed; inset: 0; z-index: 160;
  background: var(--bg0);
  display: flex; flex-direction: column;
  padding: calc(env(safe-area-inset-top, 0px) + 14px)
           max(18px, env(safe-area-inset-right, 0px))
           calc(env(safe-area-inset-bottom, 0px) + 16px)
           max(18px, env(safe-area-inset-left, 0px));
  /* Ours to interpret: a swipe between screens must not scroll the page. */
  touch-action: none; user-select: none; -webkit-user-select: none;
}
#onboard[hidden] { display: none; }
/* The heading takes focus on every screen so a screen reader says what the
   screen IS before how to leave it - but a visible ring on a heading nobody
   tabbed to reads as a bug. */
#onboard h2:focus, #onboard h2:focus-visible { outline: none; }

.ob-skip {
  position: absolute; z-index: 2;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: max(16px, env(safe-area-inset-right, 0px));
  background: none; border: 0; cursor: pointer;
  color: var(--muted); font-family: inherit; font-size: 15px; font-weight: 700;
  padding: 10px 12px;
}
@media (hover: hover) { .ob-skip:hover { color: var(--ink); } }

.ob-screen {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; justify-content: space-between; gap: 12px;
  animation: obIn 260ms var(--ease-enter) both;
}
.ob-screen[hidden] { display: none; }
@keyframes obIn { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ob-screen { animation: none; } }

.ob-body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; text-align: center; }
#onboard h2 { font-family: var(--font-display); font-size: 30px; font-weight: 800; margin: 0; line-height: 1.1; }
.ob-sub { color: var(--muted); font-size: 15px; margin: 0 0 6px; max-width: 30ch; }
.ob-hint { color: var(--muted); font-size: 13px; font-weight: 600; margin: 0; min-height: 18px; }

/* The scratch card. Its own layers, exactly as the app's board is built:
   erasing a single canvas would reveal the page, not a picture. */
.ob-card {
  position: relative; width: min(78vw, 300px); aspect-ratio: 3 / 4;
  border-radius: 20px; overflow: hidden;
  box-shadow: var(--float-rim), var(--float-mid), var(--float-cast);
}
.ob-card canvas { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }

/* A screenshot of the real app, framed like a phone. */
/* Wide, because the image is now a CROP of one panel rather than a whole
   phone shrunk down - the controls in it are meant to be readable at a glance,
   which was the point of cropping. */
.ob-shot { margin: 0; width: min(80vw, 320px); }
/* The highlight is measured against the IMAGE, so it needs a box that is
   exactly the image - the figure also contains the caption, and positioning
   against that put the ring below the picture entirely. */
.ob-frame { position: relative; line-height: 0; }
.ob-shot img {
  display: block; width: 100%; height: auto;
  /* No max-height and no object-fit. Constraining one axis while the other is
     auto is how an image ends up letterboxed or squashed; width alone keeps
     the crop at its true proportions, whatever shape it came out. */
  border-radius: 14px; border: 1px solid var(--line-strong);
  box-shadow: var(--float-rim), var(--float-mid), var(--float-cast);
}
/* Annotation is a RING on the thing being pointed at, with the words beneath
   the picture - never a label on top of it. A caption pill placed over the
   screenshot covered the exact control it was describing, which is the one
   thing an annotation must not do.
   --x/--y/--w/--h are set per slide in the markup, as percentages of the
   image, so re-aiming a highlight after a UI change is four numbers. */
.ob-spot {
  position: absolute;
  left: var(--x); top: var(--y); width: var(--w); height: var(--h);
  border: 2px solid var(--accent); border-radius: 12px;
  box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.25), 0 0 18px rgba(124, 92, 255, 0.5);
  pointer-events: none;
  animation: obSpot 1.8s var(--ease) infinite;
}
@keyframes obSpot {
  0%, 100% { box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.25), 0 0 18px rgba(124, 92, 255, 0.5); }
  50%      { box-shadow: 0 0 0 6px rgba(124, 92, 255, 0.12), 0 0 26px rgba(124, 92, 255, 0.65); }
}
@media (prefers-reduced-motion: reduce) { .ob-spot { animation: none; } }

.ob-note {
  margin-top: 12px;
  color: var(--ink); font-size: 14px; font-weight: 600; line-height: 1.3;
  max-width: 26ch; margin-inline: auto;
}

.ob-foot { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.ob-foot .big-btn { width: 100%; max-width: 420px; }
.ob-link { background: none; border: 0; cursor: pointer; color: var(--muted); font-family: inherit; font-size: 14px; font-weight: 700; padding: 10px; }
@media (hover: hover) { .ob-link:hover { color: var(--ink); } }

.ob-dots { display: flex; justify-content: center; gap: 8px; padding: 12px 0 2px; }
.ob-dot {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
  background: var(--surface-3); cursor: default;
  transition: background 0.2s, width 0.2s var(--ease);
}
.ob-dot.past { background: var(--muted); cursor: pointer; }
.ob-dot.on { width: 22px; border-radius: 999px; background: var(--accent); }
@media (prefers-reduced-motion: reduce) { .ob-dot { transition: none; } }

/* Present but unavailable until the card has been scratched: hiding it left
   people with nothing to aim at, and enabling it straight away let them skip
   the one thing this screen exists to teach. */
.ob-next[disabled] { opacity: 0.4; cursor: default; filter: saturate(0.5); }
