/* Fluentish motion. Tokens, keyframes and state classes used by motion.js and brand.js.
   Rules: animate transform and opacity only (plus stroke-dashoffset on SVG and clip-path
   on the answer underline). Exits are faster than entrances. One primary motion per moment.
   Reduced motion: every movement distance is multiplied by --move (0 when reduced), and
   durations fall back to short crossfades. html[data-motion="reduce"] forces the same
   (used by the kit's toggle and available as a user setting). */

:root {
  --dur-press: 90ms;      /* :active scale */
  --dur-quick: 160ms;     /* hover, toggles, exits */
  --dur-base: 240ms;      /* state changes, underline sweep, sheet fade */
  --dur-card: 380ms;      /* card / view entrance */
  --dur-fill: 640ms;      /* progress, ring, field reveal */
  --dur-ripple: 720ms;    /* field ripple after a correct answer */
  --stagger: 28ms;        /* per item, max 8 items */

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in: cubic-bezier(0.55, 0, 0.75, 0.2);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  /* springs as linear(); mass 1. Safari 17.2+, Chrome 113+. Fallback is --ease-out. */
  --spring-snappy: var(--ease-out);   /* k 520, c 30: 6% overshoot, settles ~420ms */
  --spring-soft: var(--ease-out);     /* k 170, c 20: 2% overshoot, ~600ms */
  --spring-pop: var(--ease-out);      /* k 380, c 18: 19% overshoot, ~630ms. Check mark only. */

  --move: 1;              /* distance multiplier */
}
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --spring-snappy: linear(0, 0.072, 0.234, 0.421, 0.608, 0.763, 0.886, 0.97, 1.025, 1.053, 1.063, 1.061, 1.052, 1.04, 1.028, 1.018, 1.009, 1.003, 0.999, 0.997, 0.996, 0.996, 0.997, 0.997, 1);
    --spring-soft: linear(0, 0.048, 0.156, 0.291, 0.432, 0.565, 0.681, 0.778, 0.855, 0.914, 0.957, 0.986, 1.005, 1.016, 1.021, 1.023, 1.022, 1.019, 1.016, 1.013, 1.01, 1.007, 1.005, 1.003, 1);
    --spring-pop: linear(0, 0.114, 0.37, 0.654, 0.905, 1.078, 1.172, 1.194, 1.168, 1.118, 1.062, 1.015, 0.982, 0.966, 0.963, 0.968, 0.978, 0.989, 0.998, 1.004, 1.007, 1.007, 1.006, 1.004, 1);
  }
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) {
    --move: 0; --dur-card: 140ms; --dur-fill: 1ms; --dur-ripple: 1ms; --stagger: 0ms;
    --spring-snappy: linear(0, 1); --spring-soft: linear(0, 1); --spring-pop: linear(0, 1);
  }
}
:root[data-motion="reduce"] {
  --move: 0; --dur-card: 140ms; --dur-fill: 1ms; --dur-ripple: 1ms; --stagger: 0ms;
  --spring-snappy: linear(0, 1); --spring-soft: linear(0, 1); --spring-pop: linear(0, 1);
}

/* ---------- press feedback (every tappable control) ---------- */
.pressable { transition: transform var(--dur-press) var(--ease-out), background-color var(--dur-quick) var(--ease-out), color var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out), opacity var(--dur-quick) var(--ease-out); }
.pressable:active { transform: scale(calc(1 - 0.03 * var(--move))); transition-duration: 40ms; }

/* ---------- reveal on enter (motion.js reveal()) ---------- */
.js [data-reveal]:not(.is-in) { opacity: 0; transform: translateY(calc(10px * var(--move))); }
.js [data-reveal].is-in {
  opacity: 1; transform: none;
  transition: opacity var(--dur-card) var(--ease-out), transform var(--dur-card) var(--spring-soft);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}

/* ---------- view transitions (motion.js swap()) ---------- */
::view-transition-group(*) { animation-duration: var(--dur-card); animation-timing-function: var(--ease-out); }
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

/* The study card is named fx-card only while a card swap runs (data-vt is set before startViewTransition, so both
   snapshots see it): in a route change the card belongs to fx-view and leaves with it. These are the cards swap()
   animates; one per screen, as two elements with one name would make the browser skip the transition. */
:root[data-vt="forward"] :is(.pr-card, .wr-card, .wb-rcard),
:root[data-vt="back"] :is(.pr-card, .wr-card, .wb-rcard),
:root[data-vt="lift"] :is(.pr-card, .wr-card, .wb-rcard) { view-transition-name: fx-card; }
/* card: forward (next card) */
:root[data-vt="forward"]::view-transition-old(fx-card) { animation: vt-out-left var(--dur-quick) var(--ease-in) both; }
:root[data-vt="forward"]::view-transition-new(fx-card) { animation: vt-in-right var(--dur-card) var(--spring-snappy) 70ms both; }
/* card: back (previous card) */
:root[data-vt="back"]::view-transition-old(fx-card) { animation: vt-out-right var(--dur-quick) var(--ease-in) both; }
:root[data-vt="back"]::view-transition-new(fx-card) { animation: vt-in-left var(--dur-card) var(--spring-snappy) 70ms both; }
/* "I know this": the card lifts away, softly, and the next one rises into its place */
:root[data-vt="lift"]::view-transition-old(fx-card) { animation: vt-lift 300ms var(--ease-out) both; }
:root[data-vt="lift"]::view-transition-new(fx-card) { animation: vt-in-up var(--dur-card) var(--spring-soft) 90ms both; }
/* a round's chrome (header with the segments and count, the action row) never leaves the screen while the card
   swaps: it has its own names and no animation, and the page around the card holds still */
.pr-round .pr-top { view-transition-name: fx-roundhead; }
.pr-round .pr-actions { view-transition-name: fx-roundact; }
::view-transition-old(fx-roundhead), ::view-transition-old(fx-roundact) { animation: none; display: none; }
::view-transition-new(fx-roundhead), ::view-transition-new(fx-roundact) { animation: none; }
::view-transition-group(fx-roundhead), ::view-transition-group(fx-roundact) { animation: none; }
/* entering a round (only the new page has them): the header fades in and the action row rises, 120 ms late, once the
   app bar has slid away, so neither lands on the departing bar (design review S2) */
:root::view-transition-new(fx-roundhead):only-child { animation: vt-fade-in var(--dur-quick) var(--ease-out) 120ms both; }
:root::view-transition-new(fx-roundact):only-child { animation: vt-rise var(--dur-quick) var(--ease-out) 120ms both; }
:root[data-vt="forward"]::view-transition-old(fx-view), :root[data-vt="back"]::view-transition-old(fx-view) { animation: none; display: none; }
:root[data-vt="forward"]::view-transition-new(fx-view), :root[data-vt="back"]::view-transition-new(fx-view) { animation: none; }
/* view, safety net: any other named element that only the old view has (no partner to morph into) leaves with the
   page in 160 ms instead of the browser's 380 to 520 ms fade (the sparkline leaving Today, a round's header after
   End). Pairs that morph into each other (fx-disc, pg-known) are not :only-child and keep their own animation. */
:where(:root[data-vt="view"])::view-transition-old(*):only-child { animation: vt-fade-out var(--dur-quick) var(--ease-in) both; }
/* view: between tabs/routes. Content rises 8px; the bar and tab bar stay put. */
:root[data-vt="view"]::view-transition-old(fx-view) { animation: vt-fade-out var(--dur-quick) var(--ease-in) both; }
:root[data-vt="view"]::view-transition-new(fx-view) { animation: vt-rise var(--dur-card) var(--ease-out) 30ms both; }

/* The app bar and the phone tab bar are their own layers in every view transition (fx-bar, fx-tabs), so the page
   moves under them and they hold still. Their new picture is live: the tab dash slides inside it (components.css).
   Where a view hides them (a round, the exam runner, any full-screen flow) the old picture slides off its edge with
   a fade in 160 ms; where they come back the new one slides in on the card duration. Reduced motion: no view
   transition, so they appear and go at once. */
.bar { view-transition-name: fx-bar; }
.tabs-bottom { view-transition-name: fx-tabs; }
/* the phone dock (one per page) is chrome too: on both sides it holds still and its label just changes; on one side
   it slides off or on its bottom edge like the tab bar (design review S1) */
.dock { view-transition-name: fx-dock; }
::view-transition-group(fx-dock) { animation: none; }
::view-transition-old(fx-dock) { animation: none; opacity: 0; }
::view-transition-new(fx-dock) { animation: none; }
:root::view-transition-old(fx-dock):only-child { opacity: 1; animation: vt-off-down var(--dur-quick) var(--ease-in) both; }
:root::view-transition-new(fx-dock):only-child { animation: vt-on-up var(--dur-card) var(--ease-out) 30ms both; }
::view-transition-group(fx-bar), ::view-transition-group(fx-tabs) { animation: none; }
::view-transition-old(fx-bar), ::view-transition-old(fx-tabs) { animation: none; opacity: 0; }
::view-transition-new(fx-bar), ::view-transition-new(fx-tabs) { animation: none; }
:root::view-transition-old(fx-tabs):only-child { opacity: 1; animation: vt-off-down var(--dur-quick) var(--ease-in) both; }
:root::view-transition-old(fx-bar):only-child { opacity: 1; animation: vt-off-up var(--dur-quick) var(--ease-in) both; }
:root::view-transition-new(fx-tabs):only-child { animation: vt-on-up var(--dur-card) var(--ease-out) 30ms both; }
:root::view-transition-new(fx-bar):only-child { animation: vt-on-down var(--dur-card) var(--ease-out) 30ms both; }
@keyframes vt-off-down { to { opacity: 0; transform: translateY(calc(100% * var(--move))); } }
@keyframes vt-off-up   { to { opacity: 0; transform: translateY(calc(-100% * var(--move))); } }
@keyframes vt-on-up    { from { opacity: 0; transform: translateY(calc(100% * var(--move))); } }
@keyframes vt-on-down  { from { opacity: 0; transform: translateY(calc(-100% * var(--move))); } }

/* a shared element across a route (motion.js handoff/receive): a map group's disc grows into its page's header on
   the snappy spring, the old picture fading as the new one comes in (both kept to their own aspect) */
::view-transition-group(fx-disc) { animation-duration: 520ms; animation-timing-function: var(--spring-snappy); z-index: 2; }
::view-transition-old(fx-disc) { animation: vt-fade-out 260ms var(--ease-in) both; height: 100%; object-fit: cover; }
::view-transition-new(fx-disc) { animation: vt-fade-in 360ms var(--ease-out) 80ms both; height: 100%; object-fit: cover; }
@keyframes vt-fade-in { from { opacity: 0; } }

@keyframes vt-out-left  { to { opacity: 0; transform: translateX(calc(-28px * var(--move))) scale(calc(1 - 0.02 * var(--move))); } }
@keyframes vt-out-right { to { opacity: 0; transform: translateX(calc(28px * var(--move))) scale(calc(1 - 0.02 * var(--move))); } }
@keyframes vt-in-right  { from { opacity: 0; transform: translateX(calc(40px * var(--move))) scale(calc(1 - 0.015 * var(--move))); } }
@keyframes vt-in-left   { from { opacity: 0; transform: translateX(calc(-40px * var(--move))) scale(calc(1 - 0.015 * var(--move))); } }
@keyframes vt-fade-out  { to { opacity: 0; } }
@keyframes vt-rise      { from { opacity: 0; transform: translateY(calc(8px * var(--move))); } }
@keyframes vt-lift      { to { opacity: 0; transform: translateY(calc(-40px * var(--move))) scale(calc(1 - 0.03 * var(--move))); } }
@keyframes vt-in-up     { from { opacity: 0; transform: translateY(calc(18px * var(--move))); } }

/* WAAPI fallback uses the same keyframes by class */
.fx-out-left  { animation: vt-out-left var(--dur-quick) var(--ease-in) both; }
.fx-out-right { animation: vt-out-right var(--dur-quick) var(--ease-in) both; }
.fx-in-right  { animation: vt-in-right var(--dur-card) var(--spring-snappy) both; }
.fx-in-left   { animation: vt-in-left var(--dur-card) var(--spring-snappy) both; }
.fx-rise      { animation: vt-rise var(--dur-card) var(--ease-out) both; }
.fx-out-up    { animation: vt-lift 300ms var(--ease-out) both; }
.fx-in-up     { animation: vt-in-up var(--dur-card) var(--spring-soft) both; }

/* ---------- answer field: correct / wrong ---------- */
.answer { position: relative; }
.answer::after {               /* the underline that sweeps in on correct/wrong */
  content: ""; position: absolute; inset-inline-start: 0; inset-inline-end: 0; bottom: -1px; height: 2px; border-radius: 2px;
  background: var(--sweep, var(--ok));
  clip-path: inset(0 100% 0 0); transition: clip-path 1ms linear;
}
.answer.is-correct { --sweep: var(--ok); }
.answer.is-wrong { --sweep: var(--bad); }
.answer.is-correct::after, .answer.is-wrong::after { clip-path: inset(0 0 0 0); transition: clip-path var(--dur-base) var(--ease-out); }
:root[data-motion="reduce"] .answer::after { transition: none !important; }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) .answer::after { transition: none !important; } }

.answer.is-wrong .answer-input { animation: nudge 300ms var(--ease-out); }
@keyframes nudge {
  0% { transform: translateX(0); }
  22% { transform: translateX(calc(-7px * var(--move))); }
  48% { transform: translateX(calc(5px * var(--move))); }
  72% { transform: translateX(calc(-2px * var(--move))); }
  100% { transform: translateX(0); }
}

/* check mark: scales in with the pop spring while its stroke draws */
.check { opacity: 0; transform: scale(calc(1 - 0.4 * var(--move))); }
.check path { stroke-dasharray: 24; stroke-dashoffset: 24; }
.is-correct .check { opacity: 1; transform: none; transition: opacity 80ms linear, transform 420ms var(--spring-pop); }
.is-correct .check path { stroke-dashoffset: 0; transition: stroke-dashoffset var(--dur-base) var(--ease-out) 60ms; }

/* the correct answer, revealed under a wrong attempt */
.reveal-answer { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out); }
.reveal-answer > * { overflow: hidden; }
.reveal-answer.is-open { grid-template-rows: 1fr; opacity: 1; }

/* Reduced motion (DESIGN: "the check and underline appear without sweeping", "fills jump"): what --move cannot reach.
   The answer reveal opens at once (wrong() and Show me add .is-open directly, not through disclose()), the check's
   stroke and the round segments jump to their end, and the segmented thumb (a translateX set from JS) does not slide. */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) :is(.reveal-answer, .is-correct .check path, .seg-thumb, .tabs-dash),
  :root:not([data-motion="full"]) .segments i::after { transition: none; }
}
:root[data-motion="reduce"] :is(.reveal-answer, .is-correct .check path, .seg-thumb, .tabs-dash),
:root[data-motion="reduce"] .segments i::after { transition: none; }

/* ---------- round progress segments ---------- */
.segments i::after { transform: scaleX(0); transform-origin: left; transition: transform var(--dur-base) var(--spring-snappy); }
.segments i.is-done::after, .segments i.is-miss::after, .segments i.is-seen::after, .segments i.is-now::after { transform: scaleX(1); }

/* ---------- fills: bars and rings ---------- */
.fill { transform-origin: left; transform: scaleX(var(--p, 0)); transition: transform var(--dur-fill) var(--spring-soft); }
.ring-arc { transition: stroke-dasharray var(--dur-fill) var(--spring-soft), opacity 1ms; }

/* ---------- odometer (motion.js odometer()) ---------- */
.odo { display: inline-flex; overflow: hidden; height: 1em; line-height: 1; vertical-align: bottom; font-variant-numeric: tabular-nums lining-nums;
  -webkit-mask-image: linear-gradient(transparent, #000 12%, #000 88%, transparent); mask-image: linear-gradient(transparent, #000 12%, #000 88%, transparent); }
.odo-col { display: inline-flex; flex-direction: column; transition: transform var(--dur-fill) var(--spring-snappy); transition-delay: calc(var(--i, 0) * 40ms); }
.odo-col > span { height: 1em; display: block; text-align: center; }
.odo-sep { display: inline-block; }

/* ---------- toast ---------- */
.toast { transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-card) var(--spring-snappy); }
.toast:not(.is-in) { opacity: 0; transform: translate(-50%, calc(16px * var(--move))); }

/* ---------- one-shot attention (today cell, new streak day) ---------- */
@keyframes land { 0% { transform: scale(calc(1 - 0.5 * var(--move))); opacity: 0; } 100% { transform: none; opacity: 1; } }
.land { animation: land 520ms var(--spring-pop) both; }

/* ---------- grade4: rise in on a stagger, the pick pops, the others step back ---------- */
.grade4 .grade4-b { opacity: 0; transform: translateY(calc(12px * var(--move))) scale(calc(1 - 0.04 * var(--move))); }
.grade4.is-in .grade4-b { opacity: 1; transform: none; transition: transform var(--dur-card) var(--spring-snappy), opacity var(--dur-base) var(--ease-out), background-color var(--dur-quick) var(--ease-out);
  transition-delay: calc(var(--i) * var(--stagger)); }
.grade4.is-in .grade4-b:active { transform: scale(calc(1 - 0.05 * var(--move))); transition-duration: 60ms; transition-delay: 0ms; }
.grade4 .grade4-b.is-picked { animation: grade4-pick 420ms var(--spring-pop) both; }
.grade4 .grade4-b.is-other { opacity: 0.3; transition-delay: 0ms; }
@keyframes grade4-pick { from { transform: scale(calc(1 - 0.1 * var(--move))); } to { transform: none; } }

/* Right to left (C3a): the bars that grow with scaleX grow from the start side. transform-origin has no logical
   keyword, so each bar that names `left` above is flipped here; nothing matches in a left-to-right page. */
.segments i:dir(rtl)::after, .fill:dir(rtl), .pr-bar .pr-seen:dir(rtl), .pr-tbar .fill:dir(rtl), .wb-weld:dir(rtl), .wb-timer i:dir(rtl),
.wb-gw:dir(rtl), .sc-w:dir(rtl)::after, .sc-r:dir(rtl)::after, .sc-rw:dir(rtl)::after, .sc-cell.is-fill:dir(rtl) { transform-origin: right; }
