/* ============================================================
   Art Daily — sketchbook theme.
   The page is a working artist's notebook: warm paper, graphite
   ink, watercolor accents, washi tape, index cards. Handwritten
   display type is Caveat (vendored, OFL — see fonts/LICENSE.txt);
   annotations and numbers stay monospace, the SadeAli signature.
   Paper-first: light is the default here, dark is "night studio".
   Token NAMES match the network's so the game drills inherit the
   look through their own vendored copies of this block.
   ============================================================ */

@font-face {
  font-family: 'Caveat';
  src: url('../fonts/caveat-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
}

:root {
  color-scheme: light;
  --bg: #F6EFDF;
  --card: #FDFAF1;
  --ink: #33291E;
  --muted: #766850; /* AA on the paper card (was #8A7B66 at ~3.9:1) */
  --line: #D9CBB2;
  --shadow: rgba(59, 47, 32, 0.16);
  --focus: #C4622F;
  --link: #3E6C8E;
  --prompt: #C4622F; /* the "pencil" accent: sienna */

  --coral: #E26D5A;      /* terracotta wash */
  --sunny: #E9A93D;      /* ochre */
  --mint: #56A382;       /* viridian */
  --sky: #5C93B8;        /* cerulean */
  --lilac: #907BC0;      /* violet wash */
  --bubblegum: #D077A0;  /* rose madder */

  --hand: 'Caveat', 'Segoe Print', 'Comic Sans MS', cursive;
  --mono: ui-monospace, "Cascadia Code", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #191511;
  --card: #221D16;
  --ink: #EFE7D7;
  --muted: #A39682;
  --line: #443A2C;
  --shadow: rgba(0, 0, 0, 0.5);
  --focus: #E88B4F;
  --link: #7FB2D8;
  --prompt: #E88B4F;

  --coral: #F08262;
  --sunny: #F2B345;
  --mint: #5FBF97;
  --sky: #6FADD6;
  --lilac: #A78FDF;
  --bubblegum: #E58AB5;
}

* { box-sizing: border-box; }

/* Stage-0 surfaces rely on [hidden] beating component display rules. */
[hidden] { display: none !important; }

/* scrollbar-gutter is the scroll lock's other half. openPlayer sets
   overflow: hidden on <html> so the page cannot scroll behind the dialog —
   and on every classic-scrollbar desktop (Windows/Linux Chrome & Firefox)
   that takes the scrollbar away, widens the viewport by ~15px, and re-centres
   every max-width block on the page: the hero sheet, all six catalogue
   spreads, the toast. The backdrop is only 55% opaque, so the whole page
   visibly jumps sideways the instant a drill opens and jumps back when it
   closes. Reserving the gutter permanently costs nothing here — this page
   always scrolls — and it is inert where there is nothing to fix (overlay
   scrollbars on macOS, iOS and Android have no width to remove). */
html { height: 100%; scrollbar-gutter: stable; scrollbar-color: var(--muted) var(--bg); }

/* Selecting text runs a highlighter over it. */
::selection { background: #F3DF7E; color: #33291E; }
:root[data-theme="dark"] ::selection { background: #B99A2E; color: #191511; }

body {
  margin: 0;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  background:
    radial-gradient(1100px 500px at 85% -5%, color-mix(in srgb, var(--sunny) 7%, transparent), transparent 60%),
    radial-gradient(900px 600px at -10% 40%, color-mix(in srgb, var(--sky) 6%, transparent), transparent 60%),
    radial-gradient(color-mix(in srgb, var(--ink) 7%, transparent) 1px, transparent 1.4px) 0 0 / 22px 22px,
    var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  font-variant-ligatures: none;
  font-variant-numeric: tabular-nums lining-nums;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

:is(a, button, input, [tabindex]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ===== Paper sheet — the base surface ===== */

.sheet {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  /* a hair off-square, like a cut sheet */
  border-radius: 6px 14px 8px 12px;
  box-shadow: 0 2px 0 var(--shadow), 0 12px 28px -18px var(--shadow);
}

/* ===== Topbar ===== */

.topbar { display: flex; align-items: center; gap: 10px; padding: 16px 18px 6px; }
.topbar-spacer { flex: 1; }

/* Every control on this page carries min-height: 44px. A bare finger's
   contact patch is ~9mm ≈ 44 CSS px; the old sizes here (backlink 37px,
   theme toggle 38px, jump tabs 35px, "open full page" 38px, ✕ 42px,
   "next warmup" 40px, "no thanks" 34px) were all comfortably tappable
   with a mouse and all a coin-flip with a thumb. */
.backlink {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  font-weight: 700;
  font-size: 0.82rem;
  text-decoration: none;
  padding: 7px 14px;
  border: 1px solid var(--line);
  border-radius: 3px 10px 4px 9px;
  background: var(--card);
  box-shadow: 0 2px 0 var(--shadow);
  transform: rotate(-0.6deg);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.backlink:hover { transform: rotate(0deg) translateY(-2px); box-shadow: 0 4px 0 var(--shadow); }

/* flex: none is the whole reason this stays 44px wide. `width` on a flex item
   is only its BASE size — .topbar is a flex row, and once the backlink and a
   live streak chip stop fitting, every shrinkable item gives up pixels. The
   theme toggle was the item that gave them up: measured in Firefox against
   this stylesheet with a "🔥 5 days ❄️1" chip in the bar, it rendered
   33 x 44 on a 320px phone and 21 x 44 on a 280px cover screen — a control
   declared 44 square, shaved to under half a fingertip on exactly the
   cheapest hardware. .player-x already carries this for the identical reason
   ("a squeezed bar must not shave the ✕ below 44px"); the lesson never made
   it up to the topbar. The two text items either side of it can wrap and
   lose pixels harmlessly. A square icon cannot. */
.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 10px 4px 9px 5px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  padding: 0;
  box-shadow: 0 2px 0 var(--shadow);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.iconbtn:hover { transform: translateY(-2px) rotate(2deg); box-shadow: 0 4px 0 var(--shadow); }

/* Light (default) shows the moon (click → dark); dark shows the sun. */
.ic-sun { display: none; }
:root[data-theme="dark"] .ic-sun { display: inline; }
:root[data-theme="dark"] .ic-moon { display: none; }

/* Streak = a rubber stamp */
.streakchip {
  font-family: var(--hand);
  font-size: 1.15rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--coral);
  /* 1.15rem = 18.4px, a hair UNDER the 18.66px large-text threshold — and
     Caveat's strokes are thinner than the sans it is measured against, so
     this is small text and owes 4.5:1. Dark theme clears it (#F08262 on the
     8% coral wash = 6.2:1); on paper, --coral on its own wash was 2.6:1 —
     the streak count, the one number the whole habit hangs on, was the
     lowest-contrast text on the page. Deepened below, same hue family as
     the other paper inks. */
  padding: 4px 12px 5px;
  border: 2px solid color-mix(in srgb, var(--coral) 75%, transparent);
  border-radius: 8px 3px 10px 4px;
  transform: rotate(3deg);
  background: color-mix(in srgb, var(--coral) 8%, transparent);
  -webkit-mask-image: radial-gradient(circle at 30% 70%, transparent 1px, #000 2px);
}
/* 4.9:1 on the chip's wash even under the body's warm gradient (measured
   against bg + the 7% sunny radial, which sits exactly over the topbar). */
:root:not([data-theme="dark"]) .streakchip { color: #A33F30; }

/* ===== Hero — a notebook page ===== */

.hero { padding: 14px 20px 8px; }

.hero-sheet {
  width: 100%;
  max-width: 730px;
  margin: 0 auto;
  padding: 30px 34px 30px 64px;
  overflow: hidden;
}
/* punch holes down the left edge */
.hero-sheet::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 0;
  bottom: 0;
  width: 16px;
  background: radial-gradient(circle 7px at 50% 26px, var(--bg) 6px, color-mix(in srgb, var(--ink) 16%, transparent) 6.6px, transparent 8px) 0 0 / 16px 64px repeat-y;
  pointer-events: none;
}
/* the red margin rule */
.hero-sheet::after {
  content: "";
  position: absolute;
  left: 46px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: color-mix(in srgb, var(--coral) 45%, transparent);
  pointer-events: none;
}

.hero-eyebrow {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.hero-title {
  margin: 0.05em 0 0;
  font-family: var(--hand);
  font-weight: 700;
  font-size: clamp(3rem, 9vw, 4.4rem);
  line-height: 1.02;
  letter-spacing: 0.01em;
}
.hero-title .tilt { display: inline-block; transform: rotate(-2deg); color: var(--prompt); }

.hero-sub {
  margin: 2px 0 20px;
  font-family: var(--hand);
  font-size: clamp(1.25rem, 3.4vw, 1.55rem);
  font-weight: 500;
  color: var(--muted);
}

/* today's warmup = a handwritten checklist */
.today-head {
  margin: 0 0 6px;
  font-family: var(--hand);
  font-size: 1.5rem;
  font-weight: 700;
  text-decoration: underline wavy color-mix(in srgb, var(--sunny) 80%, transparent) 2px;
  text-underline-offset: 6px;
}

.today-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }

.today-slot {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 9px 10px;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px dashed color-mix(in srgb, var(--line) 80%, transparent);
  cursor: pointer;
  border-radius: 6px;
  transition: background 0.15s ease, transform 0.15s ease;
}
.today-slot:hover { background: color-mix(in srgb, var(--accent, var(--sunny)) 10%, transparent); transform: translateX(3px); }

.today-tick {
  font-family: var(--hand);
  font-size: 1.35rem;
  font-weight: 700;
  width: 1.2em;
  text-align: center;
  color: var(--muted);
  flex: none;
}
.today-slot.done .today-tick { color: var(--accent-ink, var(--mint)); }
.slot-icon { font-size: 1.15rem; flex: none; }
/* name over "what you'll do" (or the score, once done) */
.slot-body { display: flex; flex-direction: column; min-width: 0; }
.slot-name { font-family: var(--hand); font-size: 1.3rem; font-weight: 600; line-height: 1.15; }
.slot-what { font-size: 0.74rem; font-weight: 600; line-height: 1.3; color: var(--muted); }
.today-slot.done .slot-name { text-decoration: line-through; text-decoration-thickness: 2px; text-decoration-color: color-mix(in srgb, var(--ink) 55%, transparent); }
.today-slot.done .slot-what { color: var(--accent-ink, var(--mint)); font-weight: 700; }
.today-slot .skillchip { margin-left: auto; flex: none; }

.today-done { margin: 12px 0 0; font-size: 0.76rem; font-weight: 700; color: var(--muted); }

/* The day-one → day-two bridge under the checklist (renderToday). Quieter
   than .today-done — a full sentence, not a scoreboard — so regular weight. */
.today-note { margin: 6px 0 0; font-size: 0.76rem; color: var(--muted); }

.sharebtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  margin-top: 12px;
  padding: 7px 16px;
  font-family: var(--hand);
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--ink);
  background: color-mix(in srgb, var(--sunny) 22%, var(--card));
  border: 1px solid color-mix(in srgb, var(--sunny) 55%, var(--line));
  border-radius: 4px 12px 5px 10px;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--shadow);
  transform: rotate(-1deg);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.sharebtn:hover { transform: rotate(0deg) translateY(-2px); box-shadow: 0 4px 0 var(--shadow); }
.sharebtn.copied { background: color-mix(in srgb, var(--mint) 24%, var(--card)); border-color: var(--mint); }

/* "add a daily reminder 📅" on the closing card — a link to /reminder.ics,
   dressed as the sticker button beside it (share = sunny, reminder = sky)
   and tilted the other way so the pair reads as two stickers, not a set. */
.remindbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  margin-top: 12px;
  margin-left: 10px;
  padding: 7px 16px;
  font-family: var(--hand);
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  background: color-mix(in srgb, var(--sky) 22%, var(--card));
  border: 1px solid color-mix(in srgb, var(--sky) 55%, var(--line));
  border-radius: 4px 12px 5px 10px;
  box-shadow: 0 2px 0 var(--shadow);
  transform: rotate(1deg);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.remindbtn:hover { transform: rotate(0deg) translateY(-2px); box-shadow: 0 4px 0 var(--shadow); }

/* ===== Session closure — the end of the day's warmup ===== */

.closing {
  margin-top: 16px;
  padding: 14px 16px 16px;
  border: 1px dashed color-mix(in srgb, var(--mint) 60%, var(--line));
  border-radius: 6px 14px 8px 12px;
  background: color-mix(in srgb, var(--mint) 8%, transparent);
}
.closing-head {
  margin: 0;
  font-family: var(--hand);
  font-size: 1.6rem;
  font-weight: 700;
}
.closing-scores { margin: 2px 0 0; font-size: 0.9rem; font-weight: 700; line-height: 1.6; }
/* the drill name and its number must never be split across a line break —
   a wrapped "Vanishing Point / Hunt 71" is the emoji cipher all over
   again. The trailing tag — "best ★" or "first go" — is allowed to drop
   instead (its leading space is inside the tag span, so normal white-space
   there restores exactly one break opportunity), which keeps the longest
   name in the registry inside a 320px hero sheet. "first go" is one
   character wider than "best ★" and wraps the same way. */
.closing-score { white-space: nowrap; }
.closing-pb, .closing-first { white-space: normal; font-size: 0.76rem; font-weight: 700; }
/* Two different facts, two different inks. "best ★" is the reward and keeps
   the accent green; "first go" is a baseline note, not an achievement, so it
   sits in the same --muted the rest of the card's annotations use — the star
   stops being wallpaper on the days when every score is a first. --muted is
   5.2:1 on paper and 5.8:1 at night against --card. */
.closing-pb { color: var(--mint); }
:root:not([data-theme="dark"]) .closing-pb { color: #3A7A5E; }
.closing-first { color: var(--muted); }
/* the summary gets focus when "see today's card →" is pressed */
.closing:focus { outline: 2px dashed var(--focus); outline-offset: 4px; }
.closing-streak { margin: 6px 0 0; font-size: 0.82rem; font-weight: 700; color: var(--muted); }
.closing-tomorrow {
  margin: 8px 0 0;
  font-family: var(--hand);
  font-size: 1.2rem;
  color: var(--muted);
}
.closing-btns { margin: 10px 0 0; }
.closing .sharebtn { margin-top: 0; }

/* The ask: quiet, once, dismissible, and only ever visible when a
   support account is actually configured (see buildAsk in app.js). */
.ask {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px dashed var(--line);
}
.ask-head {
  margin: 0;
  font-family: var(--hand);
  font-size: 1.25rem;
  font-weight: 700;
}
.ask-body { margin: 4px 0 10px; font-size: 0.8rem; color: var(--muted); max-width: 54ch; }
.ask-btn {
  display: inline-block;
  padding: 7px 16px;
  font-family: var(--hand);
  font-size: 1.15rem;
  font-weight: 700;
  text-decoration: none;
  color: var(--ink);
  background: color-mix(in srgb, var(--coral) 22%, var(--card));
  border: 1px solid color-mix(in srgb, var(--coral) 55%, var(--line));
  border-radius: 4px 12px 5px 10px;
  box-shadow: 0 2px 0 var(--shadow);
}
.ask-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 0 var(--shadow); }
.ask-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0; }
/* Same 1.5:1 boundary problem as the paint tube, on a control where it is
   worse: an email field on a card-coloured background with a --line border
   is a 1.53:1 outline around a 1.06:1 box, so there is nothing on screen to
   say a text field is there at all. (Dormant today — buildAsk returns null
   until a Buttondown name is configured — but it renders the moment one is.) */
.ask-form input {
  flex: 1 1 12rem;
  min-height: 44px;
  padding: 6px 12px;
  font: inherit;
  font-size: 0.85rem;
  color: var(--ink);
  background: var(--card);
  border: 1px solid color-mix(in srgb, var(--muted) 85%, transparent);
  border-radius: 4px 10px 5px 9px;
}
.ask-form .ask-btn { border: 1px solid color-mix(in srgb, var(--mint) 55%, var(--line)); background: color-mix(in srgb, var(--mint) 22%, var(--card)); cursor: pointer; font-family: var(--hand); font-size: 1.15rem; min-height: 44px; }

.ask-no {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-left: 10px;
  padding: 7px 4px;
  font: inherit;
  font-size: 0.78rem;
  color: var(--muted);
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}

/* ===== Practice record ===== */

.record { margin: 4px 0 14px; }
.record-stats { margin: 0 0 8px; font-size: 0.84rem; font-weight: 600; color: var(--muted); }
.record-stats b { font-family: var(--hand); font-size: 1.35rem; font-weight: 700; color: var(--ink); }
.record-strip { display: flex; flex-wrap: wrap; gap: 4px; }
/* "filled = you practised" is the whole message of the strip, and the strip
   could not say it: on paper an empty day sat 1.07:1 against the page and a
   practised one 1.50:1, so thirty days of habit rendered as a barely-there
   smudge, and the two states were a tenth of a step apart.
   Now the state that carries the meaning carries the contrast. An empty day
   is an outline that clears 3:1 by itself (--muted 85% = 3.6:1 on paper,
   4.9:1 at night); a practised day is ringed AND filled in the deepened mint
   ink, 3.2:1 paper / 5.6:1 night for one round and 4.4:1 / 8.1:1 for three.
   One round versus three is deliberately only a tonal step on top of that —
   it is a bonus tier, and the note under the strip never claims otherwise.
   The strip is aria-hidden either way; the same counts are in the record
   line above it in words. */
.rdot { width: 12px; height: 12px; border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--muted) 85%, transparent);
  background: transparent; }
.rdot.r1 { background: color-mix(in srgb, var(--mint) 80%, transparent); border-color: var(--mint); }
.rdot.r3 { background: var(--mint); border-color: var(--mint); }
:root:not([data-theme="dark"]) .rdot.r1 { background: color-mix(in srgb, #3A7A5E 80%, transparent); border-color: #3A7A5E; }
:root:not([data-theme="dark"]) .rdot.r3 { background: #3A7A5E; border-color: #3A7A5E; }
.record-note { margin: 6px 0 0; font-size: 0.72rem; color: var(--muted); }
/* The record's only "are you getting better?" line, so it does not sit at
   the same weight as the strip caption below it. It follows the stats line
   directly (which has 8px under it already), reads at the stats' own size,
   and keeps --muted — 5.2:1 on paper, 5.8:1 at night — rather than reaching
   for an accent, because it is a fact about the player, not a reward. */
.record-improve { margin: -4px 0 10px; font-size: 0.78rem; font-weight: 600; max-width: 62ch; }

/* ===== Milestone toast ===== */

/* pointer-events: none is load-bearing, not tidiness. This is a fixed,
   centred box up to 460px wide sitting over the bottom of the viewport for
   the whole time a milestone is up — directly on top of the footer links,
   and on top of the catalogue cards whenever the page is scrolled. Without
   it, earning a badge quietly ate every tap in that strip and the player
   just experienced a card that did not open. */
.page-toast {
  position: fixed;
  left: 50%;
  bottom: 20px;
  z-index: 20;
  pointer-events: none;
  transform: translateX(-50%);
  padding: 9px 18px;
  font-family: var(--hand);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px 14px 8px 12px;
  box-shadow: 0 3px 0 var(--shadow), 0 16px 30px -18px var(--shadow);
  max-width: min(92vw, 460px);
  text-align: center;
}
/* The box has to stay in the DOM between messages so the live region is
   never torn out of the accessibility tree (see index.html) — so an empty
   one has to paint nothing at all. :empty matches the moment the text node
   is removed, and with no padding, border, background or shadow a
   zero-by-zero fixed box is invisible and (pointer-events: none above)
   untouchable. */
.page-toast:empty { padding: 0; border: 0; background: none; box-shadow: none; }

/* ===== Jump nav — pencil tabs ===== */

/* ONE row, always. The six tabs need ~724px, so between 481px and ~760px
   they used to wrap to two rows and the bar grew to 115px — taller than
   .cat-section's scroll-margin-top, so tapping a tab parked the heading
   you jumped to UNDERNEATH the bar. `safe center` centres the row when it
   fits and falls back to flex-start when it does not, which keeps the
   first tab reachable inside the scroller. */
.jumpnav {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: thin;   /* a hairline hint, not a second toolbar */
  gap: 6px 10px;
  justify-content: center;
  justify-content: safe center;
  padding: 10px 16px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(3px);
  border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
}
.jumpnav a {
  display: inline-flex;
  align-items: center;
  flex: none;          /* never squash a tab instead of scrolling */
  min-height: 44px;
  font-family: var(--hand);
  font-size: 1.12rem;
  font-weight: 600;
  text-decoration: none;
  padding: 2px 12px;
  border-radius: 999px;
  border: 1px solid transparent;
  color: var(--ink);
}
.jumpnav a:hover { border-color: var(--line); background: var(--card); }
.jumpnav a .n { font-family: var(--mono); font-size: 0.68rem; color: var(--muted); vertical-align: 2px; margin-left: 3px; }

/* ===== Catalogue — sketchbook spreads ===== */

main { flex: 1; width: 100%; }

.section { width: 100%; max-width: 1080px; margin: 0 auto; padding: 30px 20px 4px; }

/* Anchor jumps must clear the sticky jumpnav. Measured: 65px on desktop,
   up to 73px on a phone once the horizontal scrollbar is a classic one.
   4.5rem (72px) lost that race by a pixel; 5.5rem (88px) clears it with
   room for the tab row to grow.
   The same clearance is owed to KEYBOARD focus, which was never given it:
   the browser scrolls a newly focused element into view with the same
   scroll-margin machinery, and Shift+Tab back up the catalogue aligns the
   card to the TOP of the viewport — i.e. underneath the pinned tab bar,
   focus ring and all. Every focusable thing that lives below the sticky bar
   gets the margin, not just the anchor targets. */
.cat-section, .card, .resetlink { scroll-margin-top: 5.5rem; }

.cat-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 4px;
}
.cat-head h2 {
  margin: 0;
  font-family: var(--hand);
  font-weight: 700;
  font-size: clamp(1.7rem, 4.5vw, 2.1rem);
  text-decoration: underline wavy var(--accent, var(--sunny)) 2px;
  text-underline-offset: 7px;
}
.cat-head .cat-icon { font-size: 1.3rem; }
.cat-count { font-size: 0.72rem; font-weight: 700; color: var(--muted); }
.cat-note { margin: 6px 0 16px; font-family: var(--hand); font-size: 1.18rem; color: var(--muted); }

/* ===== Index cards ===== */

.grid {
  list-style: none;
  margin: 14px 0 0;
  padding: 0 0 14px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(235px, 1fr));
  gap: 26px 22px;
}

@media (prefers-reduced-motion: no-preference) {
  .grid > li { animation: settle 0.45s ease backwards; }
  .grid > li:nth-child(2) { animation-delay: 0.05s; }
  .grid > li:nth-child(3) { animation-delay: 0.1s; }
  .grid > li:nth-child(4) { animation-delay: 0.15s; }
  .grid > li:nth-child(n+5) { animation-delay: 0.2s; }
}
@keyframes settle {
  from { opacity: 0; transform: translateY(10px) rotate(0deg); }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  height: 100%;
  padding: 26px 16px 14px;
  text-align: center;
  text-decoration: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 5px 12px 6px 11px;
  box-shadow: 0 2px 0 var(--shadow), 0 14px 26px -20px var(--shadow);
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.grid > li:nth-child(odd) .card { transform: rotate(-1.1deg); }
.grid > li:nth-child(even) .card { transform: rotate(0.8deg); }

/* washi tape holding each card */
.card::before {
  content: "";
  position: absolute;
  top: -11px;
  left: 50%;
  width: 86px;
  height: 22px;
  transform: translateX(-50%) rotate(-2.5deg);
  background:
    repeating-linear-gradient(105deg, transparent 0 7px, rgba(255, 255, 255, 0.28) 7px 12px),
    color-mix(in srgb, var(--accent, var(--sunny)) 55%, transparent);
  border-radius: 2px;
  box-shadow: 0 1px 2px var(--shadow);
}

a.card:hover, a.card:focus-visible {
  transform: rotate(0deg) translateY(-6px);
  border-color: color-mix(in srgb, var(--accent) 60%, var(--line));
  box-shadow: 0 4px 0 var(--shadow), 0 20px 34px -18px var(--shadow);
}

/* watercolor blob behind the icon */
.card-blob {
  display: grid;
  place-items: center;
  width: 66px;
  height: 62px;
  font-size: 32px;
  border-radius: 58% 42% 55% 45% / 48% 60% 40% 52%;
  background:
    radial-gradient(at 32% 28%, color-mix(in srgb, var(--accent, var(--sunny)) 38%, transparent), color-mix(in srgb, var(--accent, var(--sunny)) 16%, transparent) 70%, transparent);
}

.card-title { margin: 0; font-family: var(--hand); font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
.card-tagline { margin: 0; font-family: var(--hand); font-size: 1.14rem; font-weight: 500; color: var(--muted); line-height: 1.25; }

.card-skills { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 2px; }

.skillchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0 9px;
  font-size: 0.66rem;
  font-weight: 700;
  color: var(--muted);
  border: 1px solid color-mix(in srgb, var(--line) 85%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--line) 18%, transparent);
}

.card-meta { margin: 3px 0 0; font-size: 0.72rem; font-weight: 700; color: var(--muted); }
.meta-done { color: var(--mint); font-weight: 900; }
:root:not([data-theme="dark"]) .meta-done { color: #3A7A5E; }

/* how-it's-scored pencil mark, top-right corner */
.tagmark {
  position: absolute;
  top: 8px;
  right: 10px;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--muted);
  border-bottom: 2px solid color-mix(in srgb, var(--accent, var(--sunny)) 55%, transparent);
}

/* played-today: a little grader's stamp in the corner */
.card.is-done::after {
  content: "✓";
  /* Generated content is NOT invisible to a screen reader — Chromium, WebKit
     and Gecko all fold ::before/::after text into the accessible name. So
     every finished card's link ended "…best 88/100 · today 70, check mark":
     a glyph read out as a noun, restating in the worst possible words the
     "today 70" immediately before it. fillMeta moved the tick OUT of the text
     for exactly this reason ("the ✓ moved out of the text because
     .card.is-done::after already stamps one") — it moved into the one place
     that is still spoken.
     Declared twice on purpose. A browser that does not parse the `/ alt-text`
     syntax drops the second line as invalid and keeps the plain glyph above,
     so the stamp can never vanish; one that does parse it takes the empty alt
     and the tick becomes what it always looked like — decoration. */
  content: "✓" / "";
  position: absolute;
  top: 6px;
  left: 10px;
  font-family: var(--hand);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--accent-ink, var(--mint));
  border: 2px solid color-mix(in srgb, var(--accent-ink, var(--mint)) 65%, transparent);
  border-radius: 999px;
  width: 26px;
  height: 26px;
  line-height: 22px;
  transform: rotate(-8deg);
}

.card-soon { border-style: dashed; box-shadow: none; }
.card-soon::before { opacity: 0.5; }
.card-soon .card-title, .card-soon .card-tagline { color: var(--muted); }

/* JS-off fallback list */
.noscript-note { margin: 8px 0; font-size: 0.85rem; font-weight: 600; color: var(--muted); }
.plainlist { margin: 0; padding-left: 1.4em; font-size: 0.88rem; }
.plainlist a { color: var(--link); font-weight: 700; }

.aboutline {
  width: 100%;
  max-width: 1080px;
  margin: 26px auto 0;
  padding: 0 20px;
  font-family: var(--hand);
  font-size: 1.3rem;
  color: var(--muted);
}

.legendline {
  width: 100%;
  max-width: 1080px;
  margin: 6px auto 0;
  padding: 0 20px;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--muted);
}
.legendline b { color: var(--ink); border-bottom: 2px solid color-mix(in srgb, var(--sunny) 55%, transparent); }

/* ===== Progress — paint tubes ===== */

.meters {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
  max-width: 560px;
}

.meter { display: flex; align-items: center; gap: 10px; font-size: 0.82rem; font-weight: 700; }
.meter-icon { width: 1.4em; text-align: center; }
.meter-name { width: 7rem; font-family: var(--hand); font-size: 1.15rem; font-weight: 600; }

/* The tube's OUTLINE is the control, and it was drawn in --line on --card:
   1.53:1 on paper, 1.50:1 at night, against a page background --card itself
   only clears 1.06:1. So an empty tube — a skill sitting exactly on a level
   line, and every tube on the day the meters first appear — rendered as
   nothing at all, and a part-full one showed a fill floating in mid-air with
   no vessel around it and no way to see how much was left to go. That is the
   whole metaphor of the section, failing 1.4.11 in both themes.
   --muted at 85% is the same ink .rdot already uses for exactly this job:
   3.8:1 against the track and 3.4:1 against the page on paper, 4.6:1 / 4.4:1
   at night. The fill is unchanged (its lightest end is 3.1:1 on paper). */
.meter-track {
  flex: 1;
  min-width: 60px;
  height: 12px;
  border-radius: 999px 4px 999px 4px;
  border: 1px solid color-mix(in srgb, var(--muted) 85%, transparent);
  background: var(--card);
  overflow: hidden;
}
.meter-fill {
  display: block;
  height: 100%;
  width: var(--w, 0%);
  border-radius: inherit;
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent-ink, var(--mint)) 75%, transparent), var(--accent-ink, var(--mint)));
  transition: width 0.3s ease;
}
.meter-lv { min-width: 4.5ch; text-align: right; font-size: 0.74rem; color: var(--muted); }

/* ===== Per-drill trends (renderTrends) ===== */

.trends { margin-top: 18px; }
.trends-head { margin: 0; font-family: var(--hand); font-size: 1.3rem; font-weight: 700; }
.trends-list {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* flex on the <li> removes its list-item box, so renderTrends marks each
   row role="listitem" — the same double-cover the meters carry. */
.trend { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.trend-name { font-weight: 700; font-size: 0.82rem; }
.trend-words { flex: 1 1 16rem; font-size: 0.78rem; color: var(--muted); }
.trend-svg { flex: 0 1 180px; min-width: 120px; height: 32px; align-self: center; }
.trend-base { stroke: var(--line); stroke-width: 1; }
/* --accent-ink comes from the row's .accent-* class: already AA-deepened
   per hue in light mode, the raw accent in the night studio. */
.trend-dot { fill: var(--accent-ink); }

.storenote { margin: 14px 0 0; font-size: 0.76rem; color: var(--muted); }
/* "reset it" sits inside a sentence at 0.76rem, so its line box is only
   ~19px tall — half a fingertip. It cannot be padded without shoving the
   sentence apart, so the hit area is grown with an invisible box instead:
   19px tall on screen, 44px tall to a thumb. */
.resetlink {
  position: relative;
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  cursor: pointer;
}
.resetlink::after {
  content: "";
  position: absolute;
  left: -8px;
  right: -8px;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.resetlink:hover { color: var(--focus); }

/* ===== Accent helpers ===== */
/* --accent paints washes/tape; --accent-ink is the same hue as *text or
   fill*, deepened on paper so it clears AA. */
.accent-coral { --accent: var(--coral); --accent-ink: var(--coral); }
.accent-sunny { --accent: var(--sunny); --accent-ink: var(--sunny); }
.accent-mint { --accent: var(--mint); --accent-ink: var(--mint); }
.accent-sky { --accent: var(--sky); --accent-ink: var(--sky); }
.accent-lilac { --accent: var(--lilac); --accent-ink: var(--lilac); }
.accent-bubblegum { --accent: var(--bubblegum); --accent-ink: var(--bubblegum); }
:root:not([data-theme="dark"]) .accent-coral { --accent-ink: #B04434; }
:root:not([data-theme="dark"]) .accent-sunny { --accent-ink: #8A6210; }
:root:not([data-theme="dark"]) .accent-mint { --accent-ink: #2F7A5B; }
:root:not([data-theme="dark"]) .accent-sky { --accent-ink: #2F6A93; }
:root:not([data-theme="dark"]) .accent-lilac { --accent-ink: #6A50A3; }
:root:not([data-theme="dark"]) .accent-bubblegum { --accent-ink: #AC4B7C; }

/* ===== Player dialog — a sheet clipped over the page ===== */

.player {
  width: min(96vw, 900px);
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: visible;
}
.player::backdrop { background: rgba(30, 24, 15, 0.55); }

/* dvh-driven: the sheet may never outgrow the viewport (landscape
   phones must always reach the ✕), the frame flexes, chrome stays. */
.player-sheet {
  width: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  max-height: calc(100dvh - 20px);
}

/* tape across the dialog's top corners */
.player-sheet::before {
  content: "";
  position: absolute;
  top: -10px;
  left: 24px;
  width: 76px;
  height: 20px;
  transform: rotate(-4deg);
  background:
    repeating-linear-gradient(105deg, transparent 0 7px, rgba(255, 255, 255, 0.28) 7px 12px),
    color-mix(in srgb, var(--sunny) 55%, transparent);
  border-radius: 2px;
}

.player-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px 8px 18px;
  border-bottom: 1px dashed var(--line);
}
.player-title { font-family: var(--hand); font-size: 1.3rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.player-spacer { flex: 1; }
.player-link { display: inline-flex; align-items: center; min-height: 44px; padding: 10px 8px; font-size: 0.72rem; font-weight: 700; color: var(--link); text-decoration: none; white-space: nowrap; }
.player-link:hover { text-decoration: underline; text-decoration-thickness: 2px; }

.player-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;   /* a squeezed bar must not shave the ✕ below 44px */
  width: 44px;
  height: 44px;
  padding: 0;
  font: inherit;
  font-size: 1.05rem;
  font-weight: 900;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
}
.player-x:hover { color: var(--focus); background: color-mix(in srgb, var(--line) 40%, transparent); }

.player-frame {
  display: block;
  width: 100%;
  flex: 1;
  aspect-ratio: 16 / 10;
  /* The floor has to YIELD, or it clips the very thing it was raised to
     protect. .player-sheet is a column flexbox with overflow: hidden and
     max-height: calc(100dvh - 20px); the bar cannot shrink (its ✕ is 44px,
     + 18px padding = 63px measured) and .player-foot is flex: none (its
     "next warmup" button is 44px, + 12px padding = 57px measured). So the
     sheet's floor is 63 + min-height + 57, and with a hard 220px that is
     340px against a box only 100dvh - 20 tall: every viewport under 360px
     of dvh pushed the FOOT out through overflow: hidden.
     Measured in Firefox against this stylesheet, "next" shown: 568x320
     lost 41px of a 57px foot, 780x340 lost 21px. That is a phone held
     SIDEWAYS — which is exactly how a finger holds a phone to pull a long
     stroke — and what vanished was the score reveal ("recorded ✓ 62/100 —
     new best…") and the only way on to the next drill. The ✕ survived, so
     the round simply ended with nothing to show for it.
     min(220px, 48dvh) holds the foot on screen down to a 280px-tall
     viewport — a Galaxy Fold cover screen turned sideways, the shortest
     thing that browses — and resolves to EXACTLY 220px on anything 459px
     tall or more, i.e. every phone in portrait, every tablet and every
     desktop, unchanged. (50dvh was one pixel short at 280: the sheet's own
     1px top and bottom borders are part of the box too.) Between the two,
     min-height does not bind at all — flex: 1 already grows the frame past
     it — so the canvas only ever gives up the pixels the chrome needs. */
  min-height: min(220px, 48dvh);
  /* dvh, not vh: vh is the LARGE viewport on phones, so with the address
     bar shown a 74vh frame is taller than the screen and the foot (score
     line, "next warmup") gets clipped by the sheet's overflow:hidden. */
  max-height: 74dvh;
  border: 0;
  background: var(--bg);
}

/* HARDWARE FAIRNESS — the drill's canvas is the page's one real tolerance.
   .player is width: min(96vw, 900px), so on a 390px phone the frame was
   374 x 234 (16/10) = 87k px², against 900 x 562 = 506k px² for a desktop
   mouse. Drill tolerances scale with the canvas (ArtDaily.ease multiplies
   a FRACTION of the reference length), so the smallest canvas shrinks the
   allowance in absolute pixels — while a fingertip's wobble stays the same
   physical size. The least precise pointer was being handed the least room
   to be imprecise in, which is backwards.
   3/4 makes it 374 x 499 = 187k px², 2.1x the area, and still leaves the
   title bar, the ✕ and the foot on screen: 44 + 499 + 38 = 581 of a ~750px
   dvh. Anything shorter is caught by max-height: 74dvh above, which the
   frame's flex-shrink honours down to min-height: 220px.

   PORTRAIT IS THE WHOLE CONDITION — the `max-width: 640px` this rule used
   to carry left every portrait TABLET behind, which is the single most
   finger-driven device class there is. Measured in Firefox against this
   stylesheet: an iPad mini (744 x 1133) got a 712 x 445 frame and an iPad
   (820 x 1180) got 785 x 491 — a canvas that ended 490px down a 1180px
   screen, with 560px of viewport sitting empty underneath it, because a
   745px-wide tablet is not a 640px-wide phone. A finger's contact patch is
   ~44px whatever it is resting on, so the tablet was scoring the least
   precise pointer on the second-smallest canvas on the site.
   No drill meets a new shape from this: 3/4 is exactly what every phone
   already hands them, and 74dvh + flex-shrink still cap the height, so a
   barely-portrait window (1000 x 1050) lands on the dvh cap, not on a
   1200px frame. */
@media (orientation: portrait) {
  .player-frame { aspect-ratio: 3 / 4; }
}

/* wrap, not overflow: the status line now carries the score AND the delta
   in words ("recorded ✓ 62/100 — 19 under your best of 81"), which on a
   phone is wider than the foot. Without wrapping the flex row pushed the
   "next" button past the sheet's overflow: hidden edge. */
.player-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 0;
  min-height: 2.2em;
  padding: 6px 10px 6px 16px;
  border-top: 1px dashed var(--line);
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--muted);
  flex: none;
}
#playerStatus { min-width: 0; flex: 1 1 auto; }
#playerStatus.is-best { color: var(--mint); }
:root:not([data-theme="dark"]) #playerStatus.is-best { color: #3A7A5E; }
.player-foot-spacer { flex: 1 1 0; }

.player-next {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--hand);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--ink);
  padding: 5px 14px;
  border: 1px solid color-mix(in srgb, var(--mint) 55%, var(--line));
  border-radius: 4px 10px 5px 9px;
  background: color-mix(in srgb, var(--mint) 18%, var(--card));
  cursor: pointer;
  white-space: nowrap;
}
.player-next:hover { transform: translateY(-1px); box-shadow: 0 2px 0 var(--shadow); }

/* ===== Footer ===== */

/* Solid --bg, deliberately: the body paints a 1px dot grid, and --muted
   over a dot that passes under a glyph measures 4.26 — under the 4.5 the
   bold footnav links and the fineprint owe. "Measure against what is
   actually behind the mark" is this project's own rule; a clean ground
   behind the footer is the cheapest way to keep it. (The drills' vendored
   footer rules belong to the central chrome re-sync, not to this file.) */
.footer { text-align: center; padding: 44px 20px 28px; color: var(--muted); background: var(--bg); }
.footnav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-weight: 700;
  font-size: 0.85rem;
}
.footnav a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 2px; text-decoration: none; }
.footnav a:hover { text-decoration: underline wavy; text-decoration-thickness: 2px; }
#supportLinks { display: contents; }
.heart-ink { color: var(--coral); }
.fineprint { margin: 12px 0 0; font-size: 0.76rem; }

/* ===== Small screens ===== */

@media (max-width: 480px) {
  .hero { padding: 8px 14px 6px; }
  .hero-sheet { padding: 24px 20px 24px 52px; }
  .hero-sheet::before { left: 8px; }
  .hero-sheet::after { left: 36px; }
  .section { padding: 24px 14px 2px; }
  .aboutline, .legendline { padding: 0 14px; }
  .grid { gap: 24px 14px; }
  .today-slot .skillchip { display: none; }
  .meter-name { width: 5.8rem; }
  /* the single row is set up above; just tighten the padding here */
  .jumpnav { padding: 8px 12px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ===== Forced colors (Windows High Contrast) =====
   The UA strips background-color, forces color/border-color to the system
   palette and drops box-shadow — and almost everything stateful on this page
   is a background: the habit strip's practised days, the paint tubes' fill,
   the trend sparkline's SVG inks (SVG is NOT auto-adjusted, so the author
   accents would sit unreadably on the user's own ground). Re-express each
   state in a channel forced-colors preserves, using ONLY system colours —
   never forced-color-adjust: none, which pins the author palette back in
   over the palette the user chose, a strictly worse failure than the one
   being fixed. Everything with a border already keeps its bounds (borders
   survive, forced to the system ink); the toast/aria layers were always
   words-first, so nothing here is load-bearing for meaning — it restores
   the visual redundancy layer. Verified by inspection against the spec's
   preserved-property list; no HCM environment on this machine, so nothing
   below strays off the guaranteed channels. */
@media (forced-colors: active) {
  /* the 30-day habit strip: a practised day becomes a Highlight block, the
     3-drill day adds a heavier double border — empty stays outline-only */
  .rdot { background: Canvas; }
  .rdot.r1 { background: Highlight; }
  .rdot.r3 { background: Highlight; border: 2px solid CanvasText; }

  /* the paint tubes: gradients may not paint at all in forced mode; a
     Highlight block is a system colour and therefore legal */
  .meter-fill { background: Highlight; transition: none; }

  /* trend sparklines: SVG keeps author colours in forced mode, so hand it
     the system inks explicitly */
  .trend-dot { fill: CanvasText; }
  .trend-base { stroke: GrayText; }

  /* the checklist's done state: the ✓/☐ glyph already carries it without
     colour; a doubled border makes the row itself say it too */
  .today-slot.done { border-style: double; border-width: 3px; }
}
