/* ============================================================================
   TalkNonStop! — colour system "Ink & Coral"
   Rules that the whole file obeys:
     1. The button colour marks the ONE primary action on a screen, never three.
     2. ~90% of the interface is neutral, and the neutrals are tinted toward coral.
     3. Three background layers: base (lists) -> card -> atmosphere (talk/onboarding only).
     4. No pure #fff and no pure #000. Body text is softer than headings.
     5. Focus is the button colour at 20%, 3px, and is never removed.
   Contrast: bright coral #FF5A3C is for the logo and graphics only (3.1:1 under white).
   Buttons use #D93A22 on light; on dark the coral fill carries DARK text.
   ============================================================================ */
:root {
  color-scheme: dark light;

  /* dark theme — the default */
  --bg: #101014;
  --card: #1a1a21;
  --card-2: #23232b;
  --line: #2e2e38;
  --heading: #f5f5f8;
  --text: #d7d7da;
  --muted: #9c9caa;

  --brand: #ff6b4e; /* logo + graphics on dark */
  --btn: #ff6b4e;
  --btn-text: #1a0b06;
  --btn-active: #ff8168;
  --chip-bg: #2a1712;
  --chip-text: #ff9e86;

  --ok: #34d399;
  --err: #f87171;
  --warn: #f0b357;

  /* Day-board colours, pre-mixed. They used to be built with color-mix() at runtime, which a
     browser that does not implement it drops on the floor — the cell then fell back to the card
     colour and the whole board read as blank tiles. Literals cannot fail.
     Named --day-*, NOT --sq-*: the grid already owns --sq-min as a SIZE, the cells inherit it,
     and `background: 7px` is silently invalid. */
  --day-goal: #34d399;
  --day-min: #2b725d;
  --day-part: #7f643f;
  --day-miss: #744146;
  --day-future: #1f1f26;

  --atmosphere: radial-gradient(100% 66% at 50% 112%, rgba(255, 107, 78, 0.24) 0%, rgba(255, 107, 78, 0) 66%);

  /* legacy aliases: older rules still speak the previous vocabulary */
  --accent: var(--btn);
  --accent-dim: #6b3423;
  --fail: var(--err);
  --slow: var(--warn);

  --radius: 18px;
  --pad: 18px;
}

/* Light theme: follows the system unless the learner picks one in Settings. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) {
    --bg: #f7f7f9;
    --card: #ffffff;
    --card-2: #f1f1f5;
    --line: #e4e4ea;
    --heading: #101014;
    --text: #2e2e32;
    --muted: #6e6e7a;

    --brand: #ff5a3c;
    --btn: #d93a22;
    --btn-text: #ffffff;
    --btn-active: #b82d18;
    --chip-bg: #ffece8;
    --chip-text: #b3341f;

    --ok: #0e7c57;
    --err: #c62828;
    --warn: #a86612;

    --day-goal: #0e7c57;
    --day-min: #8bbcae;
    --day-part: #d0b28f;
    --day-miss: #e1a5a7;
    --day-future: #f7f7fa;

    --atmosphere: radial-gradient(120% 78% at 50% -12%, #ffe7e1 0%, rgba(255, 231, 225, 0) 62%);
    --accent-dim: #f3b7a8;
  }
}

:root[data-theme='light'] {
  --bg: #f7f7f9;
  --card: #ffffff;
  --card-2: #f1f1f5;
  --line: #e4e4ea;
  --heading: #101014;
  --text: #2e2e32;
  --muted: #6e6e7a;

  --brand: #ff5a3c;
  --btn: #d93a22;
  --btn-text: #ffffff;
  --btn-active: #b82d18;
  --chip-bg: #ffece8;
  --chip-text: #b3341f;

  --ok: #0e7c57;
  --err: #c62828;
  --warn: #a86612;

  --day-goal: #0e7c57;
  --day-min: #8bbcae;
  --day-part: #d0b28f;
  --day-miss: #e1a5a7;
  --day-future: #f7f7fa;

  --atmosphere: radial-gradient(120% 78% at 50% -12%, #ffe7e1 0%, rgba(255, 231, 225, 0) 62%);
  --accent-dim: #f3b7a8;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 17px;
  line-height: 1.45;
  overscroll-behavior: none;
}

#app {
  max-width: 560px;
  margin: 0 auto;
  padding: max(10px, env(safe-area-inset-top)) 13px calc(10px + env(safe-area-inset-bottom));
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

h1,
h2,
h3 {
  margin: 0 0 8px;
  line-height: 1.25;
}
h1 {
  font-size: 26px;
}
h2 {
  font-size: 21px;
}
h3 {
  font-size: 17px;
  color: var(--muted);
  font-weight: 600;
}
p {
  margin: 0 0 10px;
}
.muted {
  color: var(--muted);
}
.small {
  font-size: 14px;
}
.center {
  text-align: center;
}
.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}
.warn {
  color: var(--warn);
  font-size: 14px;
  text-align: center;
}
.voice-sample {
  margin: 8px 0 0;
  min-height: 18px;
  font-style: italic;
}

/* ---------- cards ---------- */
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--pad);
}
.card--intro p {
  color: var(--muted);
}
/* The whole free area is the reveal target: on a phone the thumb should not have to travel to a
   thin band at the top of the screen. Flex keeps the text top-aligned inside the tall button. */
.card--prompt {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  flex: 1;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  padding: 26px var(--pad) 18px;
  border-color: color-mix(in srgb, var(--line) 70%, var(--muted));
}
.card--prompt .tap-hint {
  margin-top: auto;
}
.card--prompt:active {
  background: var(--card-2);
}
.card--reveal {
  border-color: var(--accent-dim);
}
.card--warmup {
  text-align: center;
}
.card--recast {
  text-align: center;
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
}

/* Feedback line: success must be impossible to miss, not a grey sentence among grey sentences. */
.status {
  font-size: 14px;
  color: var(--muted);
  margin: 10px 0 0;
  padding: 9px 12px;
  border-radius: 12px;
  border: 1px solid transparent;
  transition: background 160ms ease, color 160ms ease, border-color 160ms ease;
}
.status--listening {
  color: var(--chip-text);
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--btn) 8%, transparent);
}
.status--ok {
  color: var(--btn-text);
  background: var(--ok);
  border-color: var(--accent);
  font-weight: 700;
  font-size: 15px;
}
.status--retry {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}

.prompt {
  font-size: 30px;
  line-height: 1.3;
  font-weight: 600;
  margin: 0 0 10px;
}
.prompt-sub {
  color: var(--muted);
  font-size: 15px;
  margin: 0 0 14px;
}
.target {
  font-size: 27px;
  font-weight: 600;
  color: var(--heading);
  margin: 6px 0 14px;
}
.target--warm {
  color: var(--text);
}
.tap-hint {
  color: var(--muted);
  font-size: 13px;
  margin: 12px 0 0;
  text-align: center;
}

/* ---------- countdown & progress ---------- */
.countdown {
  height: 6px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
  margin-top: 18px;
}
.countdown__fill {
  height: 100%;
  width: 100%;
  background: linear-gradient(90deg, var(--brand), var(--btn-active));
}
/* Speaking has started: the deadline is over, the bar becomes a calm "I'm listening" strip. */
.countdown__fill--speaking {
  background: var(--accent-dim);
  animation: breathe 1.8s ease-in-out infinite;
}
@keyframes breathe {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 0.9;
  }
}
.session-top {
  display: flex;
  align-items: center;
  gap: 12px;
}
.progress {
  flex: 1;
  height: 4px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
}
.btn--quit {
  border: none;
  color: var(--muted);
  font-size: 19px;
  line-height: 1;
  padding: 8px 4px 8px 10px;
  background: transparent;
}
.progress__fill {
  height: 100%;
  background: var(--accent);
  transition: width 200ms ease;
}
.progress__fill--warm {
  background: var(--muted);
}

/* ---------- chips ---------- */
.chip-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  min-height: 24px;
}
.chip {
  font-size: 12px;
  color: var(--muted);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 11px;
}
.chip--new {
  color: var(--btn-text);
  background: var(--btn);
  border-color: var(--accent);
}
.chip--relearn,
.chip--timeout {
  color: var(--fail);
  border-color: color-mix(in srgb, var(--err) 45%, var(--line));
}
.chip--warm {
  color: var(--warn);
}
.chip--mic {
  color: var(--chip-text);
  background: var(--chip-bg);
  border-color: transparent;
}
.chip--mic.mic--live {
  background: var(--btn);
  color: var(--btn-text);
}

/* ---------- live microphone meter ----------
   Driven from JS by two custom properties: --lvl on the chip (0..1 overall loudness) and --b on
   each bar (0..1 for its frequency band). Everything here is transform/opacity/box-shadow so the
   25 Hz update never touches layout. */
/* An explicit display always beats the UA rule behind the hidden attribute — restate it. */
.miclevel[hidden] {
  display: none;
}
.miclevel {
  --lvl: 0;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding-left: 9px;
  isolation: isolate;
  /* The light spilling past the chip. Grows with the voice; the halo below carries it further. */
  box-shadow: 0 0 calc(4px + var(--lvl) * 18px) calc(var(--lvl) * 3px)
    color-mix(in srgb, var(--accent) calc(18% + var(--lvl) * 52%), transparent);
  transition: box-shadow 90ms linear, background-color 160ms ease;
}
/* A soft halo that reaches well beyond the chip itself — visible from the corner of the eye
   while the learner is looking at the sentence, not at the indicator. */
.miclevel::after {
  content: '';
  position: absolute;
  inset: -14px -18px;
  z-index: -1;
  border-radius: 999px;
  pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 55%, transparent), transparent 100%);
  opacity: calc(var(--lvl) * 0.5);
  transform: scale(calc(0.75 + var(--lvl) * 0.45));
  transition: opacity 110ms linear, transform 110ms ease-out;
}
.miclevel__bars {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 15px;
}
.miclevel__bar {
  --b: 0.12;
  display: block;
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: currentColor;
  opacity: calc(0.32 + var(--b) * 0.68);
  transform: scaleY(var(--b));
  transform-origin: 50% 50%;
  transition: transform 70ms ease-out, opacity 70ms linear;
}
.miclevel__text {
  white-space: nowrap;
}
/* Listening with no level to show: the recogniser has the microphone, so there is nothing to
   measure. A travelling wave says "on" without inventing a reading. */
.miclevel.is-pulsing .miclevel__bar {
  animation: mic-wave 1.05s ease-in-out infinite;
}
.miclevel.is-pulsing .miclevel__bar:nth-child(2) { animation-delay: 0.12s; }
.miclevel.is-pulsing .miclevel__bar:nth-child(3) { animation-delay: 0.24s; }
.miclevel.is-pulsing .miclevel__bar:nth-child(4) { animation-delay: 0.36s; }
.miclevel.is-pulsing .miclevel__bar:nth-child(5) { animation-delay: 0.48s; }
@keyframes mic-wave {
  0%, 100% { transform: scaleY(0.22); opacity: 0.5; }
  50% { transform: scaleY(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .miclevel.is-pulsing .miclevel__bar { animation: none; transform: scaleY(0.6); }
}
/* The card the learner is looking at while they speak. The chip is in the corner of the eye; this
   is what actually tells them, without moving their gaze, that the voice is getting through. */
.mic-spill {
  --lvl: 0;
  box-shadow:
    0 0 0 calc(var(--lvl) * 2px) color-mix(in srgb, var(--accent) calc(var(--lvl) * 60%), transparent),
    0 0 calc(var(--lvl) * 42px) calc(var(--lvl) * -6px)
      color-mix(in srgb, var(--accent) calc(var(--lvl) * 45%), transparent);
  transition: box-shadow 90ms linear;
}
/* Not everyone wants a pulsing light in the corner of their eye; the bars still move, because
   they carry information the label does not, but the halo stops breathing. */
@media (prefers-reduced-motion: reduce) {
  .miclevel::after {
    display: none;
  }
  .miclevel,
  .mic-spill {
    box-shadow: none;
  }
}
.chip-row--center {
  justify-content: center;
  margin: 6px 0 2px;
}
.chip--ok {
  background: var(--ok);
  border-color: var(--ok);
  color: var(--btn-text);
}
.chip--bad {
  background: var(--err);
  border-color: var(--err);
  color: var(--btn-text);
}
/* Words as the recogniser hears them, live. Monospace so the text jumping about as it revises
   itself does not make the whole block shift. */
.live-words {
  margin: 4px 0 0;
  min-height: 18px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
}
.live-words[hidden] {
  display: none;
}

/* Shown only when the browser has no speech recogniser at all. */
.no-asr {
  margin: 6px 0 0;
  line-height: 1.3;
  color: var(--warn);
}
.no-asr[hidden] {
  display: none;
}
/* The wait before the next card. Visible on purpose: a screen that moves on by itself without
   warning feels like a misfire, and the same bar is what tells you a tap will skip it. */
.autonext {
  height: 3px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
  margin: 2px 0 0;
}
.autonext__fill {
  height: 100%;
  width: 0;
  background: var(--ok);
}
@media (prefers-reduced-motion: reduce) {
  .autonext__fill {
    transition: none !important;
  }
}

.heard {
  font-size: 19px;
  color: var(--muted);
  margin: 4px 0 10px;
}
.card--reveal.is-ok {
  border-color: var(--accent-dim);
}
.card--reveal.is-bad {
  border-color: color-mix(in srgb, var(--err) 45%, var(--line));
}

/* ---------- buttons ---------- */
.btn {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--card-2);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  border-radius: 14px;
  padding: 13px 16px;
  cursor: pointer;
}
.btn:active {
  transform: translateY(1px);
}
.btn[disabled] {
  opacity: 0.4;
  cursor: default;
}
.btn--ghost {
  background: transparent;
}
/* Declared AFTER --ghost on purpose: a button that gets promoted to primary at runtime must
   win, otherwise it ends up with dark text on a transparent dark background — invisible. */
.btn--primary {
  background: var(--btn);
  border-color: var(--btn);
  color: var(--btn-text);
}
.btn--danger {
  color: var(--fail);
  border-color: color-mix(in srgb, var(--err) 45%, var(--line));
}
.btn--wide {
  width: 100%;
}
.btn--huge {
  width: 100%;
  padding: 17px;
  font-size: 19px;
}
.btn--back {
  padding: 6px 12px;
  font-size: 20px;
}

.grades {
  display: grid;
  gap: 10px;
  margin-top: auto;
}
.btn--grade {
  padding: 17px;
  font-size: 17px;
}
.btn--fast {
  border-color: color-mix(in srgb, var(--ok) 45%, var(--line));
  color: var(--ok);
}
.btn--slow {
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
  color: var(--slow);
}
.btn--fail {
  border-color: color-mix(in srgb, var(--err) 45%, var(--line));
  color: var(--fail);
}

.actions {
  display: grid;
  gap: 10px;
}

/* The seven-day warning. Deliberately not styled as an error: it is advice, and it appears on
   the screen the learner sees most. */
.card--install {
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
  padding: 12px 14px;
}
.card--install h3 {
  margin: 0 0 4px;
  color: var(--heading);
  font-size: 15px;
}
.card--install p {
  margin: 0 0 8px;
}

/* ---------- home ----------
   Every pixel here is one the start button does not get. On a phone the whole screen should
   arrive without a scroll, so the chrome is trimmed to what actually earns its space. */
.home-header {
  text-align: center;
  padding-top: 2px;
}
.home-header .brand {
  font-size: 21px;
  margin: 0;
}
.brand {
  letter-spacing: -0.4px;
}
.home-main {
  display: grid;
  gap: 6px;
}
.home-main p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.35;
}
.footer-nav {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.nav-tile {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 9px 8px 8px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: transform 120ms ease, border-color 120ms ease;
}
.nav-tile:active {
  transform: translateY(1px) scale(0.985);
  border-color: var(--accent-dim);
}
.nav-tile__icon {
  color: var(--muted);
  display: block;
  width: 22px;
  height: 22px;
}
.nav-tile__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.btn--back svg {
  width: 20px;
  height: 20px;
  display: block;
}
/* An inline SVG with only a viewBox has no intrinsic size: without this the exit control
   rendered as an empty box. */
.btn--quit svg {
  width: 20px;
  height: 20px;
  display: block;
}
.nav-tile__label {
  font-size: 13px;
  color: var(--muted);
  letter-spacing: 0.1px;
}

.view-header {
  display: flex;
  align-items: center;
  gap: 10px;
}
.view-header h2 {
  margin: 0;
}

/* ---------- patterns ---------- */
.card--pattern {
  padding: 0;
  overflow: hidden;
}
.card--pattern.is-off {
  opacity: 0.55;
}
.pattern-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px var(--pad);
  cursor: pointer;
}
.pattern-head p {
  margin: 2px 0 0;
}
.frames {
  border-top: 1px solid var(--line);
}
.frame-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px var(--pad);
  border-bottom: 1px solid var(--line);
}
.frame-row p {
  margin: 2px 0 0;
}
.pill {
  font-size: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
  white-space: nowrap;
}
.pill--review {
  color: var(--accent);
  border-color: var(--accent-dim);
}
.pill--learning {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
}

/* ---------- forms ---------- */
.field {
  display: grid;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.field:last-child {
  border-bottom: none;
}
.field--row {
  grid-template-columns: 1fr auto;
  align-items: center;
}
.select,
input[type='range'] {
  width: 100%;
  font: inherit;
}
.select {
  background: var(--card-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 11px;
}
.switch {
  width: 46px;
  height: 28px;
  accent-color: var(--accent);
}
.segmented {
  display: flex;
  gap: 6px;
}
.seg {
  flex: 1;
  background: var(--card-2);
  border: 1px solid var(--line);
  color: var(--muted);
  border-radius: 12px;
  padding: 10px;
  font: inherit;
  cursor: pointer;
}
.seg--on {
  background: var(--ok);
  border-color: var(--ok);
  color: var(--btn-text);
  font-weight: 700;
}

/* ---------- stats ---------- */
.row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}
.row:last-child {
  border-bottom: none;
}
.bars {
  display: grid;
  gap: 8px;
  margin: 12px 0;
}
.bar__label {
  font-size: 13px;
  color: var(--muted);
}
.bar__track {
  height: 8px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
  margin-top: 4px;
}
.bar__fill--fast {
  height: 100%;
  background: var(--ok);
}
.bar__fill--slow {
  height: 100%;
  background: var(--slow);
}
.bar__fill--fail {
  height: 100%;
  background: var(--fail);
}
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 15px;
}
.table th {
  text-align: left;
  color: var(--muted);
  font-weight: 500;
  font-size: 13px;
  padding: 6px 0;
}
.table td {
  padding: 7px 0;
  border-top: 1px solid var(--line);
}
.table td:not(:first-child),
.table th:not(:first-child) {
  text-align: right;
  width: 22%;
}

/* ---------- rating & charts ---------- */
.rating {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 2px 0 6px;
}
.rating__value {
  font-size: 44px;
  font-weight: 700;
  color: var(--heading);
  line-height: 1;
}
.rating__max {
  color: var(--muted);
  font-size: 15px;
}
.rating__delta {
  margin-left: auto;
  font-size: 15px;
  font-weight: 600;
}
.rating__delta.is-up {
  color: var(--ok);
}
.rating__delta.is-down {
  color: var(--slow);
}
.meters {
  display: grid;
  gap: 12px;
  margin: 14px 0 6px;
}
.meter__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
.meter__track {
  height: 6px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
  margin: 5px 0 3px;
}
.meter__fill {
  height: 100%;
  background: linear-gradient(90deg, color-mix(in srgb, var(--ok) 55%, var(--card-2)), var(--ok));
}
.chart {
  width: 100%;
  height: auto;
  display: block;
  margin: 6px 0 2px;
}
.legend {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
}
.legend__item::before {
  content: '';
  display: inline-block;
  width: 12px;
  height: 3px;
  border-radius: 2px;
  margin-right: 5px;
  vertical-align: middle;
}
.legend__item--rating::before {
  background: var(--ok);
}
.legend__item--lat::before {
  background: var(--brand);
}
.legend__item--bars::before {
  background: var(--line);
}

/* ---------- toast ---------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translate(-50%, 20px);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px 18px;
  opacity: 0;
  transition: opacity 200ms ease, transform 200ms ease;
  z-index: 20;
  max-width: 90vw;
}
.toast--in {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* test page */
.test-pass {
  color: var(--accent);
}
.test-fail {
  color: var(--fail);
  font-weight: 700;
}

/* ---------- goal celebration ---------- */
.cheer {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  z-index: 30;
  display: flex;
  justify-content: center;
  padding: calc(8px + env(safe-area-inset-top)) 12px 0;
  pointer-events: none;
}
.cheer__inner {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  max-width: 360px;
  padding: 12px 15px;
  border-radius: 16px;
  background: var(--card);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 12px 34px rgba(16, 16, 20, 0.35);
  transform: translateY(-96px);
  opacity: 0;
  transition: transform 260ms cubic-bezier(0.2, 0.9, 0.25, 1), opacity 180ms linear;
}
.cheer.is-in .cheer__inner {
  transform: translateY(0);
  opacity: 1;
}
.cheer.is-out .cheer__inner {
  transform: translateY(-96px);
  opacity: 0;
  transition-duration: 200ms;
}
.cheer__mark {
  flex: 0 0 52px;
  width: 52px;
  height: 52px;
}
.cheer__text {
  display: grid;
  gap: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .ring__val,
  .cheer__inner {
    transition: none;
  }
  .flame--s4 .flame__inner {
    animation: none;
  }
  /* The breathing "I am listening" bar is the app's only infinite animation. */
  .countdown__fill--speaking {
    animation: none;
    opacity: 0.75;
  }
}

/* ---------- brand, atmosphere, focus ---------- */
/* Layer three: only the talking screens and onboarding get atmosphere. Lists stay flat so the
   eye has somewhere to rest. */
body[data-route='/session'],
body[data-route='/'] .card--intro {
  background-image: var(--atmosphere);
  background-repeat: no-repeat;
  background-attachment: fixed;
}

.brand {
  letter-spacing: -0.4px;
  color: var(--heading);
}
.brand__accent {
  color: var(--brand);
}

/* Rule 5: focus is always visible, and it is the button colour at 20%. */
:where(button, a, input, select, [tabindex]):focus-visible {
  outline: 3px solid color-mix(in srgb, var(--btn) 20%, transparent);
  outline-offset: 2px;
  border-radius: 12px;
}

/* Headings carry the strongest ink; paragraphs are deliberately softer (rule 4). */
h1,
h2 {
  color: var(--heading);
}
strong {
  color: var(--heading);
}

/* Second block of the intro: a heading inside the card, not a new screen — one more step in
   onboarding is one more reason to skip it unread. */
.intro__h {
  color: var(--heading);
  font-size: 17px;
  font-weight: 700;
  margin: 18px 0 8px;
}

/* ---------- About ---------- */
.about {
  display: grid;
  gap: 8px;
  margin-top: 6px;
}
.about__kicker {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  color: var(--muted);
}
.about__name {
  font-size: 21px;
  font-weight: 700;
  color: var(--heading);
  margin: 0;
  letter-spacing: -0.3px;
}
.about .card--intro {
  margin-top: 4px;
}

/* ---------- progress: goal bar, tiles, history ---------- */
.progress-block {
  display: grid;
  gap: 8px;
}

.goalbar {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 12px 14px;
}
.goalbar.is-done {
  border-color: var(--ok);
}
.goalbar__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 8px;
}
.goalbar__sub {
  display: block;
  margin-top: 5px;
  font-size: 11px;
  line-height: 1;
  color: var(--muted);
  opacity: 0.75;
}
.goalbar__track {
  height: 8px;
  background: var(--card-2);
  border-radius: 999px;
  overflow: hidden;
}
.goalbar__fill {
  height: 100%;
  background: var(--btn);
  transition: width 500ms cubic-bezier(0.22, 0.85, 0.25, 1);
}
.goalbar.is-done .goalbar__fill {
  background: var(--ok);
}

/* Four squares, two per row: same shape means the numbers can be compared at a glance. */
/* Three across. Fluency is the point of the app, so it takes two of them and the big number;
   the counters share one column each, which is what makes them comparable at a glance. */
.tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.tile--wide {
  grid-column: span 2;
}
.tile {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 8px 10px 8px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-height: 0;
  min-width: 0;
}
.tile--accent {
  border-color: color-mix(in srgb, var(--btn) 40%, var(--line));
}
.tile__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}
.tile__label {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  color: var(--muted);
  white-space: nowrap;
}
.tile__delta {
  font-size: 12px;
  font-weight: 700;
}
.tile__delta.is-up {
  color: var(--ok);
}
.tile__delta.is-down {
  color: var(--warn);
}
.tile__value {
  display: flex;
  align-items: baseline;
  gap: 3px;
  margin: 1px 0 2px;
}
.tile__n {
  font-size: 22px;
  line-height: 1;
  font-weight: 700;
  color: var(--heading);
  letter-spacing: -0.8px;
}
.tile--wide .tile__n {
  font-size: 36px;
  letter-spacing: -1.3px;
}
.tile--accent .tile__n {
  color: var(--btn);
}
.tile__unit {
  font-size: 13px;
  color: var(--muted);
}
.tile__note {
  font-size: 10px;
  color: var(--muted);
  margin-top: auto;
  line-height: 1.25;
}
/* ---------- the streak tile: a flame and a bar toward the next milestone ----------
   --heat (0..1) grows over the first month, then the flame burns at full size. It stays hollow
   until today is actually closed, so the tile answers "did I do it today?" before "how long?". */
.tile--streak.is-lit {
  border-color: color-mix(in srgb, var(--warn) 42%, var(--line));
}
.flame {
  --heat: 0;
  display: inline-block;
  width: calc(19px + var(--heat) * 6px);
  height: calc(19px + var(--heat) * 6px);
  margin-right: 2px;
  align-self: center;
  line-height: 0;
  transform-origin: 50% 92%;
}
.flame svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}
.flame__body {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.4;
}
.flame__core {
  fill: none;
}
.flame.is-lit .flame__body {
  fill: color-mix(in srgb, var(--warn) calc(55% + var(--heat) * 45%), transparent);
  stroke: none;
  filter: drop-shadow(0 0 calc(2px + var(--heat) * 6px) color-mix(in srgb, var(--warn) 55%, transparent));
  animation: flame-body 1.5s ease-in-out infinite alternate;
}
.flame.is-lit .flame__core {
  fill: color-mix(in srgb, #ffe9a8 calc(45% + var(--heat) * 55%), transparent);
  animation: flame-core 0.9s ease-in-out infinite alternate;
}
/* Two rates that do not divide evenly: the flicker never repeats the same shape twice in a row. */
@keyframes flame-body {
  from {
    transform: scale(1) translateY(0);
  }
  to {
    transform: scale(1.06, 1.1) translateY(-0.4px);
  }
}
@keyframes flame-core {
  from {
    transform: scaleY(0.86);
    opacity: 0.75;
  }
  to {
    transform: scaleY(1.12);
    opacity: 1;
  }
}
.flame.is-cold {
  opacity: 0.55;
}
/* The bar in the streak tile: distance to the next round number, not another sparkline. */
.fuel {
  height: 6px;
  border-radius: 999px;
  background: var(--card-2);
  overflow: hidden;
  margin: 6px 0 6px;
}
.fuel__fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--warn) 55%, var(--btn)), var(--warn));
  transition: width 260ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .flame.is-lit .flame__body,
  .flame.is-lit .flame__core {
    animation: none;
  }
}

/* ---------- fluency chart: one bar per day ----------
   Taller than a sparkline on purpose. This is the number the app exists to move, and the wide
   tile has the room; the bars also speak the same language as the day board further down. */
.fchart {
  height: 44px;
  margin: 3px 0 4px;
  flex: 0 0 auto;
}
.fchart svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}
/* One lit segment. Rectangular on purpose — same decision as the day cells: chrome is rounded,
   data is sharp — and the thin dark gap between segments is what makes it read as a meter
   instead of a bar chart. */
.fseg {
  fill: var(--fgrad);
  opacity: 0.72;
  shape-rendering: crispEdges;
}
/* The peak cap: the segment that hangs at the top of the stack, like the LED that lingers at
   the maximum on a hardware meter. */
.fseg--peak {
  opacity: 1;
}
/* Today is the bar the learner is here to change, so its whole stack burns at full strength. */
.fseg--today {
  opacity: 0.95;
}
.fseg--today.fseg--peak {
  opacity: 1;
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--ok) 70%, transparent));
}
/* A day that did not happen. Dark, but present — a gap the eye can count. */
.fseg--off {
  fill: var(--muted);
  opacity: 0.28;
}
/* The ramp: amber at the bottom, green at the top, so a short day stays amber and a strong one
   climbs into green. The colour carries the reading before the number does. */
.fstop--low {
  stop-color: var(--warn);
}
.fstop--mid {
  stop-color: color-mix(in srgb, var(--ok) 55%, var(--warn));
}
.fstop--high {
  stop-color: var(--ok);
}
.fchart.is-empty .fseg--off {
  opacity: 0.16;
}

/* The current level — the same number printed above, in the same colour as that number, so the
   link between them needs no legend. Coral appears here and nowhere else on this screen except
   the figure it belongs to. */
.flevel {
  stroke: var(--btn);
  stroke-width: 1.25;
  stroke-dasharray: 4 3;
  opacity: 0.85;
  vector-effect: non-scaling-stroke;
}
/* Nothing recorded yet: draw the empty slots rather than a blank space, which on the most
   important tile reads as a broken widget. */
.spark {
  height: 34px;
  margin: 2px 0 4px;
}
.spark svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}
/* Fluency gets its own colour, and it is NOT the button colour: rule 1 of the palette says the
   coral marks the one action on the screen, and a chart is not an action. Green reads as "how
   well", which is exactly what fluency is. */
.spark__line {
  stroke: var(--ok);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.spark__area {
  fill: color-mix(in srgb, var(--ok) 16%, transparent);
}
.spark__dot {
  fill: var(--ok);
}
/* The counters are counts, not judgements — a thin neutral line, so the eye goes to fluency. */
.tile:not(.tile--accent) .spark {
  height: 17px;
  margin: 1px 0 2px;
}
.tile:not(.tile--accent) .spark__line {
  stroke: var(--muted);
  stroke-width: 1.5;
  opacity: 0.75;
}
.tile:not(.tile--accent) .spark__dot {
  fill: var(--muted);
}
.tile:not(.tile--accent) .spark__line {
  fill: none;
}
/* Counters draw a line only, no filled area: the fill is what makes fluency read as the subject
   of the screen, and five filled charts would flatten that back out. */
.tile:not(.tile--accent) .spark__area {
  fill: none;
}

/* One square per day, wrapping onto as many rows as a year of practice needs. */
.history {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 9px 11px 10px;
}
.history__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 7px;
}
.history__grid {
  /* --cols is set by historyGrid() so the layout and the row padding can never disagree. */
  --sq-min: 7px;
  --sq-gap: 2.5px;
  display: grid;
  /* auto-fill, not a fixed count: the board takes the whole card and answers a wider screen with
     MORE days rather than bigger ones. Doing the arithmetic here rather than in JS means the
     squares are the right size the instant the width changes, with no measurement to go stale —
     historyGrid() reads the count back out of this rule only to pad the last row. */
  grid-template-columns: repeat(auto-fill, minmax(var(--sq-min), 1fr));
  gap: var(--sq-gap);
}
/* Square corners, not rounded. Two different radii on one screen is a signal, not an accident:
   the chrome (cards, chips, buttons) is generously rounded, the DATA is sharp. Heat maps and
   matrix displays are drawn with square cells for the same reason — at 8px a radius is 25% of
   the cell and it stops reading as a unit of a grid and starts reading as a decoration. */
.sq {
  aspect-ratio: 1;
  border-radius: 0;
  background: var(--card-2);
  border: 1px solid transparent;
}
.sq--goal {
  background: var(--day-goal);
}
.sq--min {
  background: var(--day-min);
}
.sq--part {
  background: var(--day-part);
}
.sq--miss {
  background: var(--day-miss);
}
.sq--freeze {
  background: var(--card-2);
  border-color: var(--brand);
}
.sq--pause {
  background: var(--card-2);
  border-color: var(--muted);
}
/* Days that have not happened yet: visible, empty, waiting. */
.sq--future {
  background: var(--day-future);
}
/* Today carries no marker at all, and that is the considered choice rather than an omission.
   A ring in the card colour is a white hairline on the light theme — the very thing that was
   being removed — and any coloured ring is a second bright spot on a screen whose own rule
   allows one. The boundary does the work: today is always the last lived square before the
   empty ones, so the eye lands on it from the shape of the board. GitHub's contribution graph
   marks today the same way, which is to say it does not.
   The class stays for the date tooltip and for the tests. */
/* The cell whose day is currently shown in the header. A ring in the heading colour is fine
   here: it is a direct response to a tap, not ambient decoration. */
.sq.is-picked {
  outline: 1.5px solid var(--heading);
  outline-offset: 1px;
}
.history__readout {
  text-align: right;
}
.sq.is-today {
  /* intentionally no visual treatment — see above */
}

.cheer__ring {
  fill: none;
  stroke: var(--ok);
  stroke-width: 2.5;
  opacity: 0.35;
}
.cheer__tick {
  fill: none;
  stroke: var(--ok);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
