/* ==========================================================================
   TRAINING DASHBOARD — STYLES

   Everything visual is in this file. You can rewrite all of it without
   breaking any data.

   The block immediately below is the design system. Change a value there and
   it updates everywhere at once. Nothing further down hard-codes a colour or
   a size — it all points back to these.

   Edit, save, refresh the browser. No build step.
   ========================================================================== */


/* ==========================================================================
   0. FONTS
   ========================================================================== */

@font-face {
  font-family: 'SaansVF';
  src: url('SaansVF.woff2') format('woff2-variations'),
       url('SaansVF.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}


/* ==========================================================================
   1. DESIGN TOKENS — start here
   ========================================================================== */

:root {
  /* --- Type ------------------------------------------------------------ */
  --font-sans: 'SaansVF', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter",
               system-ui, sans-serif;
  --font-num: 'SaansVF', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter",
              system-ui, sans-serif;

  --text-hero: 46px;      /* the three big numbers */
  --text-xl: 22px;
  --text-lg: 17px;
  --text-md: 14px;
  --text-sm: 12px;
  --text-xs: 11px;

  --weight-normal: 400;
  --weight-medium: 550;
  --weight-bold: 700;

  --tracking-tight: -0.03em;
  --tracking-wide: 0.08em;

  /* --- Spacing scale ---------------------------------------------------- */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  /* --- Shape ------------------------------------------------------------
     Radius encodes hierarchy — the more important the container, the rounder.
     Measured off his Figma frame (node 8:223). See DESIGN.md.              */
  --radius-row: 8px;       /* rows with a coloured left edge */
  --radius-tile: 14px;      /* day tiles */
  --radius-button: 16px;    /* buttons */
  --radius-panel: 20px;     /* section panels */
  --radius-card: 32px;      /* the Today card — the one that matters */
  --radius-pill: 999px;

  --border-width: 1px;

  /* --- Layout ----------------------------------------------------------- */
  --page-max: 1080px;
  --page-pad: var(--space-5);
  --gap: var(--space-4);
  --chart-height: 300px;

  /* --- Motion ----------------------------------------------------------- */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --speed: 180ms;
}

/* --- DARK THEME (default) ------------------------------------------------ */
[data-theme="dark"] {
  --bg: #0d0f12;
  --surface: #16191e;
  --border: #262b34;

  --ink: #f2f4f7;

  /* Meaning colours. Used for both charts and text. */
  /* Dark is unfinished and currently disabled (DARK_MODE_READY in app.js).
     These carry the same semantics as light so nothing breaks when he
     designs it properly — the VALUES are placeholders, not decisions. */
  --done: #46d18b;
  --planned: #6f8fff;
  --ink-body: #e6e9ee;
  --ink-label: #9aa3b0;
  --ink-muted: #9aa3b0;
  --ink-subtle: #6b7280;
  --shadow-card: 0 1px 2px rgba(0,0,0,.40), 0 16px 40px rgba(0,0,0,.28);

  --accent: #4da3ff;       /* fitness */
  --warm: #ff9f45;         /* fatigue */
  --good: #46d18b;         /* fresh / positive */
  --bad: #ff5f6d;          /* tired / negative */
  --neutral: #6b7280;
  --black-primary: #141d21;  /* text on a coloured card — his variable */

  /* Text-safe variants. On dark the fills are already light enough to read as
     type, so these match them rather than darkening as they do on light. They
     exist here so anything using --good-text works in both themes — without
     them the colour silently falls back to nothing on dark. */
  --good-text: #46d18b;
  --bad-text: #ff5f6d;
  --warm-text: #ff9f45;
  --neutral-text: #9aa3b0;

  /* Two layers here too, and STRONGER than light — measured off the
     liquid-functionalism container in its dark mode: `.36` and `.24`, wider
     and softer (12px/30px). That is the opposite of the usual instinct to
     fade shadows on dark.

     It follows from how their dark card works: the surface is barely lighter
     than the page behind it, so the shadow is the only thing separating
     them. Lighten the card instead and you get a glowing panel, which is
     what dark modes usually get wrong. */
  --shadow: 0 1px 2px rgba(0,0,0,.36), 0 12px 30px rgba(0,0,0,.24);
  --shadow-tile: 0 1px 2px rgba(0,0,0,.32), 0 8px 20px rgba(0,0,0,.22);
  --shadow-today: 0 1px 2px rgba(0,0,0,.36), 0 8px 20px rgba(77,163,255,.25);
}

/* --- LIGHT THEME --------------------------------------------------------- */
[data-theme="light"] {
  --bg: #f7f7f5;
  --surface: #ffffff;
  --border: #e2e2de;

  --ink: #14161a;

  /* From his Figma frame (node 8:223). The three semantic colours are the
     heart of the design — see DESIGN.md.
       accent  = action ONLY. One primary CTA per screen, plus today.
       done    = a session that happened.
       planned = a session that hasn't.
     That green/blue split is what lets one grid hold past and future. */
  --accent: #e84d1a;
  --done: #2d7a3e;
  --planned: #3b5fcc;

  --ink-body: #2e2e2e;      /* body copy and the headline */
  --ink-label: #6b6b6b;     /* eyebrow labels */

  /* His two greys, straight from the Figma variables (Design — Colour).
     They existed in his file before they existed here — the calendar's day
     names and metadata are the first thing to need them. */
  --ink-muted: #5f6570;     /* the CTA arrows */
  --ink-subtle: #9aa0aa;    /* day names, durations, percentages */

  /* The four state colours — HIS values, pulled from Figma 10 Aug 2026.
     Used for fills, borders, stripes and swatches. */
  --warm: #e3704a;         /* caution, fatigue, heat — never failure */
  --good: #3faa6d;         /* a positive reading, not a completed session */
  --bad: #e03e3e;          /* something is wrong and needs action */
  --neutral: #8fa5b0;      /* inactive, or no data at all */
  --black-primary: #141d21;  /* text on a coloured card — his variable */

  /* The same four, darkened for TEXT. His call: "keep them, darken only for
     text."

     Three of his picks fall below the 4.5:1 contrast needed for small copy on
     white — good at 2.92 and neutral at 2.57 are genuinely hard to read at
     10-13px, which is the size these labels actually are. Hue and saturation
     are preserved; only lightness moves, and only as far as AA requires.

     `warm-text` is the exception: darkening it in place landed on hue 15,
     which is EXACTLY his accent. Caution would have read as an action. It is
     shifted 12 degrees to amber instead, so it stays a warning rather than
     becoming a dimmer primary button. */
  --warm-text: #b3601c;    /* 4.56:1 — amber, 12deg off accent */
  --good-text: #318555;    /* 4.55:1 */
  --bad-text: #de3333;     /* 4.54:1 */
  --neutral-text: #5f7987; /* 4.59:1 */

  /* Three shadows, and elevation MEANS something: a shadow says tappable —
     it opens a detail view. Never decorative.
     The tile shadow is measured off his calendar design (node 8:337); it is
     tighter and more spread than the card, so nine of them in a grid read as
     a set rather than nine floating cards.

     TWO LAYERS, measured off the liquid-functionalism container on
     15 Aug 2026 — he said he liked how those cards feel. Theirs is
     `0 1px 2px .08` + `0 8px 24px .07`: one tight layer that seats the card
     on the page, one wide soft layer that lifts it. A single layer can do
     one or the other, not both, which is why the old cards read as flat
     rectangles with a smudge under them.

     Worth recording, because it is the opposite of what he expected: THEIR
     CARDS ARE NOT GLASS. Measured on their own page — background is a solid
     fill, `backdrop-filter: none`, no border. What reads as "liquid" is the
     two-layer shadow and the radius, not transparency. Good news here,
     because glass over the new blob background would have cost contrast on
     the numbers. */
  --shadow: 0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.07);
  --shadow-card: 0 1px 2px rgba(0,0,0,.09), 0 16px 40px rgba(0,0,0,.09);
  --shadow-tile: 0 1px 2px rgba(0,0,0,.07), 0 6px 16px rgba(0,0,0,.06);
  /* Today's warm glow — the same geometry as the tile shadow, tinted. Only
     one tile in the grid ever carries it. */
  --shadow-today: 0 4px 8px 4px rgba(220,125,48,.15);
}


/* ==========================================================================
   2. BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* The ground colour sits on html; body and the blob field are transparent
   above it.

   Both used to carry `background: var(--bg)`, which painted an opaque sheet
   over anything at z-index -1 — that is why the old animated gradient was
   invisible, and why the blobs first showed up only below the fold. Putting
   the colour on html alone keeps the page from going white while letting the
   field behind body show through. */
html {
  background: var(--bg);
}

body {
  background: transparent;
}

html, body {
  margin: 0;
  padding: 0;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

/* ===========================================================================
   THE BACKGROUND — bouncing blurred blobs

   Replaces an animated linear-gradient that was set to opacity 0.15 and sat
   behind opaque panels, so it was effectively invisible. He said so:
   "možná už jsme to dělali ale v apce to nevidím".

   Technique from Taha Shashtari's "Smooth Liquid Background Effect"
   (https://codepen.io/tahazsh/full/gOqNZyw): coloured circles drift around
   and bounce off the viewport edges, with a single heavy backdrop-filter
   pane over the top melting them into each other. The blur is what makes it
   read as liquid rather than as moving dots.

   HIS COLOURS, not the pen's blue/purple/pink. The tokens are already
   semantic — accent is action, done is green, planned is blue — so this uses
   them at low opacity as pigment rather than introducing a second palette.
   =========================================================================== */

.blobs {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

.blobs__field {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.blobs__glass {
  position: absolute;
  inset: 0;
  z-index: 2;
  backdrop-filter: blur(140px);
  -webkit-backdrop-filter: blur(140px);
  pointer-events: none;
}

.blob {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 32vw;
  aspect-ratio: 1;
  border-radius: 50%;
  transform-origin: left top;
  will-change: transform;
}

/* Opacity does the work the pen's pastel palette did. His tokens are
   saturated because they mean something on a card; at full strength they
   would fight the content instead of sitting behind it. */
.blob--accent  { background: var(--accent);  opacity: 0.30; }
.blob--planned { background: var(--planned); opacity: 0.24; }
.blob--done    { background: var(--done);    opacity: 0.22; }
.blob--warm    { background: var(--warm);    opacity: 0.26; }

/* The small bright one. In the original this is white and sits above the
   others — it lifts the middle of the blend so the whole thing doesn't go
   muddy where colours overlap. */
.blob--light {
  background: var(--surface);
  opacity: 0.55;
  z-index: 2;
  width: 15vw;
}

@media (max-width: 1200px) {
  .blobs__glass {
    backdrop-filter: blur(120px);
    -webkit-backdrop-filter: blur(120px);
  }
}

@media (max-width: 700px) {
  .blob { width: 60vw; }
  .blob--light { width: 30vw; }
  .blobs__glass {
    backdrop-filter: blur(90px);
    -webkit-backdrop-filter: blur(90px);
  }
}

/* Someone who has asked the OS to stop animations gets the colours without
   the movement — the blend still looks like something, it just holds still. */
@media (prefers-reduced-motion: reduce) {
  .blob { animation: none !important; }
}

h1, h2, p { margin: 0; }
canvas { display: block; }
.hidden { display: none !important; }

/* An inline filename in an empty-state hint. Not a row, so it takes the pill
   rather than borrowing `radius/row` — that token means "a row with a
   coloured left edge" and this is a word inside a sentence. */
code {
  font-family: var(--font-num);
  font-size: 0.92em;
  background: var(--bg);
  padding: 2px 6px;
  border-radius: var(--radius-pill);
}

.app {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--page-pad);
  padding-bottom: var(--space-7);
}


/* ==========================================================================
   3. TOP BAR
   ========================================================================== */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding-block: var(--space-2) var(--space-6);
}

/* Logo 100px, 16px gap, quip 124px — measured off node 8:224. The width
   matters: the quip lines are authored as three, and without a fixed width
   "You usually ride Pavé on Wednesdays" wraps to five. */
.topbar__left {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-shrink: 0;
}

.topbar__logo {
  display: block;
  width: 100px;
  height: auto;
  flex-shrink: 0;
}

/* In dark mode the logo sits on a light plate.
   It is a pure-black mark on transparency, so on a dark surface it simply
   disappears — that single fact is why dark mode was pinned off for weeks.
   His fix: "v darkmodu bych logo zabalil do karty se světlým pozadím".

   Padding and radius rather than a bare rectangle, so it reads as a plate the
   mark belongs on instead of a patch covering a hole. No shadow: it opens
   nothing, and elevation means tappable. */
[data-theme="dark"] .topbar__logo {
  background: var(--surface-light, #f7f7f5);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-tile);
}

.topbar__title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}

.topbar__date {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* One of twelve lines beside the logo, picked at random each load.
   Figma component set "Logo typo" (node 29:766): Saans Heavy 16px, 100%
   line height, -1% tracking, three short lines. The tight leading is what
   makes it read as a stacked mark rather than a sentence — the line breaks
   are authored, not wrapped, so they are preserved rather than reflowed. */
.topbar__quip {
  width: 124px;
  flex-shrink: 0;
  font-size: 16px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--ink-body);
  white-space: pre-line;
  margin: 0;
}

/* Button above, freshness below — his layout, node 34:63.
   Figma: Auto layout vertical, 100 Fill × 25 Hug button, 92 Hug × 12 Hug freshness, 4px gap. */
.topbar__right {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}

.topbar__right .btn#refresh {
  width: 100%;
  padding: 0;
  height: 25px;
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  font-size: var(--text-lg);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* --- 24 hours without a refresh ---------------------------------------
   The button fills with accent and the tick becomes a warning. This is the
   one place accent appears outside a primary CTA, and it earns it: stale
   data is the failure the whole app rests on, because the coach keeps
   deciding from numbers that stopped moving. */
/* `.topbar__right .btn#refresh` sets the default colour and outranks a bare
   `.btn#refresh.is-stale`, so the glyph stayed grey on the accent fill.
   Matching that specificity is what makes the white actually apply. */
.topbar__right .btn#refresh.is-stale {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
}
.btn#refresh.is-stale:hover {
  color: var(--surface);
  border-color: var(--accent);
  opacity: .9;
}

/* Figma: 92 Hug × 12 Hug auto layout horizontal, 4px gap between icon and text. */
.freshness {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--ink-muted);
  font-size: 12px;
  line-height: 1;
  white-space: nowrap;
  height: 12px;
}

/* A tick when the data is current, a warning triangle when it isn't.
   Drawn as text rather than the exported vectors — two glyphs at 10px do
   not justify two more image requests, and they inherit colour for free. */
.freshness__icon {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1;
  font-weight: var(--weight-bold);
}
.freshness__icon.is-fresh { color: var(--done); }
.freshness__icon.is-stale { color: var(--accent); }

/* --- Syncing -----------------------------------------------------------
   The sync overlay appears centered on screen with a large 300px pump
   animation, visible and impossible to miss. The freshness text shows
   progress percentage. */

.sync-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--ink-body);
  display: flex;
  padding: var(--space-3) var(--space-4) var(--space-4);
  animation: fadeIn var(--speed) var(--ease);
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The card fills the ground, per his Figma node 29:469 — the same 32px
   radius and card shadow as the Today card, so the loading state belongs
   to the same family rather than looking like a system dialog. */
.sync-panel {
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 40px;
  padding: var(--space-5) var(--space-4);
  overflow: hidden;
}

/* Curved heading — Figma node 29:735, which is a TEXT_PATH. There is no HTML
   equivalent, so SVG <textPath> is the honest translation; the arc in the
   markup is that node's exact path data. Saans Bold 34px, -4% tracking. */
.sync-panel__curve {
  width: 318px;
  max-width: 100%;
  height: 42px;
  overflow: visible;
  display: block;
}

.sync-panel__curve text {
  font-family: var(--font-sans);
  font-size: 34px;
  font-weight: var(--weight-bold);
  letter-spacing: -0.04em;
  fill: var(--ink-body);
}

/* 300px wide, height auto to preserve full animation without cropping. */
.sync-panel__anim {
  width: 300px;
  max-width: 100%;
  height: auto;
  display: block;
}

.sync-panel__text {
  width: 100%;
  text-align: center;
  color: var(--ink-body);
}

.sync-panel__percent {
  font-size: 64px;
  font-weight: var(--weight-bold);
  line-height: 1.05;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  transition: none; /* Numbers will roll via JS, not CSS transitions */
}

.sync-panel__status {
  font-size: var(--text-sm);
  line-height: 1.35;
  color: var(--ink-body);
  text-align: center;
}

/* While syncing the button is not a button — pressing it again does nothing,
   and it should not look pressable. */
.btn#refresh:disabled {
  cursor: default;
  opacity: 0.5;
}

.freshness.is-syncing {
  color: var(--ink-muted);
}

.btn {
  font: inherit;
  font-size: var(--text-md);
  color: var(--ink-muted);
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: color var(--speed) var(--ease),
              border-color var(--speed) var(--ease);
}
.btn:hover { color: var(--ink); border-color: var(--ink-subtle); }


/* ==========================================================================
   4. STATES
   ========================================================================== */

.state {
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-tile);
  padding: var(--space-6);
  text-align: center;
}
.state--warn { border-color: var(--warm); }
.state__text { font-size: var(--text-lg); }


.login {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
  margin-top: var(--space-4);
}

.login__input {
  font: inherit;
  font-size: var(--text-md);
  color: var(--ink);
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  min-width: 220px;
}
.login__input:focus { outline: none; border-color: var(--accent); }

.login__input--code {
  min-width: 110px;
  font-family: var(--font-num);
  letter-spacing: 0.25em;
  text-align: center;
}

@media (max-width: 760px) {
  .login { flex-wrap: wrap; }
  .login__input { min-width: 0; flex: 1 1 100%; }
}
.state__hint { color: var(--ink-muted); font-size: var(--text-sm); margin-top: var(--space-2); }


/* ==========================================================================
   4b. TODAY'S INSTRUCTION
   The most important element on the page. Deliberately loud.
   ========================================================================== */

/* The card that matters most: the roundest radius, the biggest shadow, and
   no heavy border — elevation carries the emphasis instead. His design. */
.today {
  background: var(--surface);
  border: none;
  border-radius: var(--radius-card);
  padding: var(--space-5) 20px;
  margin-bottom: var(--gap);
  box-shadow: var(--shadow-card);
  /* His design spaces the groups with a real 24px gap rather than leaning
     on child margins, so the rhythm holds when a group is empty. */
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* NO coloured edge on the Today card. His call, 10 Aug 2026, and he is right
   twice over.

   First, the colour was wrong: a rest day was drawn with `--bad`, the red
   used for failures and illness. Resting is not a failure — it is often the
   correct answer, and painting it red makes the app disagree with its own
   advice.

   Second, and more important: "rest day is nothing to highlight". The card is
   already the loudest thing on the screen. A stripe down its side adds a
   second signal for a state the headline states in words.

   Highlighting belongs to the CALENDAR, where one day among nine needs
   picking out — orange border for today, per DESIGN.md. Here there is nothing
   to distinguish it from.

   His design (node 58:19) has no edge on any state, which is the version this
   follows. */

.today__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.today__eyebrow {
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-label);
}

.today__race {
  font-size: var(--text-xs);
  /* Information, not an action — accent is reserved for the CTA. */
  color: var(--ink-label);
  font-weight: var(--weight-medium);
}

/* His design: 40px, 105% line height, -1% tracking. Large and tight —
   this is the one sentence the whole app exists to deliver. */
.today__headline {
  font-size: 40px;
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
  line-height: 1.05;
  color: var(--ink-body);
}

.today__target {
  font-family: var(--font-num);
  font-size: var(--text-md);
  /* The prescribed numbers — planned, so blue. */
  color: var(--planned);
  margin-top: var(--space-2);
}
.today__target:empty { display: none; }

.today__reason {
  font-size: 13px;
  line-height: 1.35;
  color: var(--ink-body);
  max-width: 62ch;
}

/* The time he actually needs to clear in his day. Sits under the headline
   because it answers the first practical question — can I fit this today —
   before the reasoning. Never rounded down: see coach.duration_words(). */
.today__duration {
  font-size: var(--text-sm);
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

/* Heat. Warm rather than alarming — it is a cost to spend deliberately, not
   a warning to obey. Never appears below 24°C. */
.today__heat {
  font-size: 13px;
  line-height: 1.35;
  color: var(--warm-text);
  max-width: 62ch;
  margin-top: var(--space-2);
}
.today__heat:empty { display: none; }

/* Headline, reason and the drawn session sit together at 8px, inside the
   card's looser 24px rhythm. */
.today__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
}

/* --- The session, drawn ------------------------------------------------
   Figma node 29:593. Each block is a bar: WIDTH is duration, OPACITY is
   intensity. Both come from the steps already generated for the Wahoo, so
   the picture cannot disagree with what gets sent to the head unit.

   Bars sit on a shared baseline and grow upward, which is what makes the
   shape of the session readable at a glance — today's whole point is that
   the efforts come at the END, and that is visible here in a way the
   sentence has to spell out. */

.workout {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 119px;
  width: 100%;
  margin-top: var(--space-2);
  padding-right: var(--space-2);
}

/* Width is set inline as a percentage of the session's total duration, so
   the bars together fill the card. 2px is his floor — a two-minute block in
   a four-hour ride still has to be visible. flex-shrink is off so the floor
   actually holds; the row can overflow by a pixel or two on a session with
   many tiny blocks, which is the right trade. */
.workout__block {
  background: var(--planned);
  border-radius: var(--radius-button) var(--radius-button) 4px 4px;
  min-width: 2px;
  flex-shrink: 0;
}

.today__note {
  font-size: var(--text-sm);
  color: var(--ink-subtle);
  margin-top: var(--space-2);
  max-width: 62ch;
}
.today__note:empty { display: none; }

/* What the coach remembers about recent days */
.today__memory {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: var(--space-4);
  padding-left: var(--space-3);
  border-left: 2px solid var(--warm);
  max-width: 62ch;
}
.today__memory:empty { display: none; }


/* What this block is for — the arc behind today's session */
.arc {
  margin-top: var(--space-5);
  padding: var(--space-4);
  background: var(--bg);
  border-radius: var(--radius-tile);
  max-width: 62ch;
}
.arc:empty { display: none; }

.arc__phase {
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent);
}

.arc__why {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: var(--space-2);
}

.arc__outlook {
  font-size: var(--text-sm);
  color: var(--ink-subtle);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
}
.arc__outlook:empty { display: none; }

/* The off-season habit line. October to March only, and visually separate
   from the block numbers because it is a different claim: the habit held,
   not the fitness. Green because it is genuinely good news — but it sits
   apart so it can never read as progress against the load target. */
.arc__offseason {
  font-size: var(--text-sm);
  color: var(--done);
  margin-top: var(--space-2);
}
.arc__offseason:empty { display: none; }

.arc__verdict {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: var(--space-4);
}
.arc__verdict:empty { display: none; }


/* --- Renegotiation ------------------------------------------------------ */


.talk__open {
  font: inherit;
  font-size: var(--text-sm);
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.talk__open:hover { text-decoration: underline; }

/* LIQUID MERGE — the panel is one surface with the button above it.

   From the liquid-functionalism concepts he shared, applied on his terms:
   "aply but keep my radiuses sizes". So the morph is theirs and the geometry
   is his — `radius/tile` here, `radius/button` on the trigger, never the
   pen's 24px.

   `margin-top: 0` and the negative offset are what make it read as ONE
   control: the panel's top edge meets the button rather than floating below
   it with a gap. */
/* The panel sits inside `today__actions`, immediately after its trigger, so
   the two are adjacent siblings. It was a sibling of the whole actions block
   before, with two other buttons in between — a 95px gap that no negative
   margin could bridge, and the morph read as a separate thing appearing
   rather than one control unfolding.

   The negative margin cancels the flex gap so the panel's top edge meets the
   button's bottom edge exactly. */
/* Cancel most of the flex gap, not all of it. At the full -24px the panel
   overlapped the trigger and clipped its bottom edge; -16px leaves the two
   edges just touching, which is the "one surface" read without the collision. */
.talk { margin-top: calc(var(--space-4) * -1); }

.talk__body {
  padding: var(--space-4);
  padding-top: var(--space-5);
  background: var(--bg);
  border-radius: var(--radius-tile);
  transform-origin: top center;
  will-change: transform, opacity;
}

/* Only while morphing. Left on permanently it would animate every layout
   change inside the panel — a chip toggling would slide the whole thing. */
.talk__body.is-merging {
  transition: transform 220ms var(--ease), opacity 160ms var(--ease);
}

/* The trigger recedes as the panel takes over, so there is one control on
   screen rather than two. It stays in the layout — collapsing it would shift
   everything below at the moment the eye is following the morph. */
/* The food widget on the dashboard. Its insides are styled by
   food-widget.css; this only places it and its header. Two classes, because
   food-widget.css loads after this file and its `.fday` margin would win —
   which is how the card ended up touching the week below it. Same 16px
   bottom margin as every other panel. */
.fday.fday--home {
  margin: 0 0 var(--gap);
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-4);
}
.fday__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.fday__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  color: var(--ink);
  text-decoration: none;
}
.fday__snap {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  white-space: nowrap;
}

.today__actions .btn--ghost.is-merged {
  opacity: 0.45;
  transform: scale(0.985);
  transition: opacity var(--speed) var(--ease), transform var(--speed) var(--ease);
}

/* Motion is a preference, not a decoration. Anyone who has asked the system
   to reduce it gets the panel with no morph at all. */
@media (prefers-reduced-motion: reduce) {
  .talk__body.is-merging { transition: none; }
  .today__actions .btn--ghost.is-merged { transition: none; }
}

.talk__q {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-subtle);
  margin-bottom: var(--space-2);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.chip {
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: all var(--speed) var(--ease);
}
.chip:hover { border-color: var(--ink-subtle); color: var(--ink); }
.chip.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
  font-weight: var(--weight-medium);
}

.talk__result {
  padding: var(--space-3) 0;
  margin-bottom: var(--space-3);
  border-top: var(--border-width) solid var(--border);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  white-space: pre-wrap;
}

.talk__actions { display: flex; gap: var(--space-2); }

/* The action buttons in his design are full-width blocks with a 16px radius,
   not pills — the pill shape stays for the small icon buttons in the top bar. */
/* Full-width stacked blocks, as in his design — not shrink-wrapped pills.
   The container had no display:flex, so the flex rule below did nothing. */
.today__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.today__actions .btn {
  border-radius: var(--radius-button);
  padding: var(--space-3) var(--space-5);
  width: 100%;
  text-align: center;
  font-size: var(--text-md);
}

/* The outlined secondary from his design (node 58:39): black border, black
   text. The base button's grey-on-grey is right for the small controls
   elsewhere, but on the Today card it read as disabled next to the dark
   primary above it. */
.today__actions .btn--ghost {
  color: var(--ink);
  border-color: var(--ink);
}
.today__actions .btn--ghost:hover { border-color: var(--ink); opacity: 0.7; }

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
  font-weight: var(--weight-medium);
}
.btn--primary:hover { color: var(--bg); opacity: .9; }

.today__verdict {
  font-size: var(--text-sm);
  color: var(--ink-subtle);
  margin-top: var(--space-5);
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
}


/* ==========================================================================
   5. THE THREE HEADLINE NUMBERS
   ========================================================================== */

.hero {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  margin-bottom: var(--gap);
}

/* THE INACTIVE STATE, which is what he asked to settle: border, no shadow.
   These three numbers are read-only — nothing opens when you tap them — so
   under his own rule they must not carry elevation. They had BOTH a border
   and a shadow, which said "tappable" and "inactive" at the same time. */
.stat {
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-5);
}

.stat__label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.stat__value {
  font-family: var(--font-num);
  font-size: var(--text-hero);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1.05;
  margin-top: var(--space-3);
  font-variant-numeric: tabular-nums;
}

.stat__meta {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: var(--space-2);
}

/* Colour hooks the script applies to convey meaning */
.is-good { color: var(--good-text); }
.is-bad { color: var(--bad-text); }
.is-warm { color: var(--warm-text); }
.is-accent { color: var(--accent); }


/* ==========================================================================
   6. PANELS
   ========================================================================== */

/* Panels are containers, not controls — so no shadow. Under his rule a shadow
   promises a detail view, and a panel doesn't open one. */
.panel {
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-4);
  margin-bottom: var(--gap);
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  margin-bottom: var(--space-5);
}

.panel__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
}

.panel__note {
  font-size: var(--text-xs);
  color: var(--ink-subtle);
}


/* ==========================================================================
   7. SEGMENTED CONTROL
   ========================================================================== */

.segmented {
  display: inline-flex;
  background: var(--bg);
  border-radius: var(--radius-pill);
  padding: 3px;
  gap: 2px;
}

.segmented__btn {
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: background var(--speed) var(--ease), color var(--speed) var(--ease);
}
.segmented__btn.is-active { background: var(--surface); color: var(--ink); }


/* ==========================================================================
   8. CHART
   ========================================================================== */

.chart { height: var(--chart-height); position: relative; }
.chart--short { height: calc(var(--chart-height) * 0.72); }

/* Zone mix bar — how much easy / middle / hard riding you've done */
.mix {
  display: flex;
  height: 10px;
  border-radius: var(--radius-pill);
  overflow: hidden;
  margin-top: var(--space-2);
  background: var(--bg);
}
.mix__seg { height: 100%; }
.mix__seg--easy { background: var(--good); }
.mix__seg--middle { background: var(--warm); }
.mix__seg--hard { background: var(--bad); }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.legend__item { display: flex; align-items: center; gap: var(--space-2); }

.legend__swatch {
  width: 10px; height: 10px;
  border-radius: 3px;
  flex: none;
}


/* ==========================================================================
   9. WEEK STRIP
   ========================================================================== */

.week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-2);
}

/* Day tiles, per DESIGN.md:
     grey border, no shadow  = inactive day, nothing to do
     no border, shadow       = a session — tappable, opens the detail
     orange border           = today
   Elevation is the affordance, not decoration. */
.day {
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-tile);
  padding: var(--space-3) var(--space-2);
  min-height: 92px;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* A day with a real session: raised and borderless. */
.day.has-session {
  border-color: transparent;
  box-shadow: var(--shadow);
}

/* Today. */
.day.is-today {
  border-color: var(--accent);
  box-shadow: var(--shadow);
}

/* Done vs planned — the split that lets one grid hold past and future. */
.day.is-done .day__note { color: var(--done); }
.day.is-planned .day__note { color: var(--planned); }

.day.is-today { border-color: var(--accent); }
.day.is-rest { opacity: .5; }

.day__name {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.day__load {
  font-family: var(--font-num);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}

.day__note {
  font-size: var(--text-xs);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.day__tag {
  font-size: var(--text-xs);
  color: var(--accent);
  margin-top: auto;
}


/* ==========================================================================
   10. WEEK PROGRESS BAR
   ========================================================================== */

.weekbar { margin-top: var(--space-5); }

.weekbar__track {
  height: 6px;
  background: var(--bg);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.weekbar__fill {
  height: 100%;
  width: 0;
  background: var(--accent);
  border-radius: var(--radius-pill);
  transition: width var(--speed) var(--ease);
}

.weekbar__label {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: var(--space-2);
}


/* ==========================================================================
   11. RIDE LIST
   ========================================================================== */

.rides { display: flex; flex-direction: column; }

.ride {
  display: grid;
  grid-template-columns: 68px 1fr auto auto auto;
  align-items: center;
  gap: var(--space-4);
  padding-block: var(--space-3);
  border-bottom: var(--border-width) solid var(--border);
}
.ride:last-child { border-bottom: 0; }

.ride__date {
  font-family: var(--font-num);
  font-size: var(--text-sm);
  color: var(--ink-subtle);
}

.ride__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ride__metric {
  font-family: var(--font-num);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 62px;
}

.ride__load {
  font-family: var(--font-num);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 46px;
}


/* ==========================================================================
   12. RECOVERY MINI-GRID
   ========================================================================== */

.mini {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--gap);
}

.mini__item {
  background: var(--bg);
  border-radius: var(--radius-tile);
  padding: var(--space-4);
}

.mini__label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.mini__value {
  font-family: var(--font-num);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  margin-top: var(--space-2);
}

.mini__meta { font-size: var(--text-xs); color: var(--ink-muted); margin-top: var(--space-1); }


/* ==========================================================================
   13. FOOTER
   ========================================================================== */

.foot {
  text-align: center;
  color: var(--ink-subtle);
  font-size: var(--text-xs);
  padding-top: var(--space-5);
}


/* ==========================================================================
   13b. THE WEEK AHEAD — the calendar
   His design, Figma node 8:337, with the card taxonomy from node 44:151.

   THE IDEA: one grid holds past and future at once. It works because the
   colour carries the tense — green is a ride that happened, blue is one that
   has not. Nothing else in the tile needs to say which it is.

   SIX CARD TYPES, named by him:

     .is-ride-done        a ride that happened      shadow, green, tick
     .is-rest-done        a rest day that passed    flat, dimmed, grey tick
     .is-today-planned    today, still to ride      orange border + glow
     .is-rest-planned     a future easy/open day    flat, dimmed, no shadow
     .is-training-planned a future session          shadow, blue
     .is-cta              navigation, not a day     flat, centred arrow

   Shadow still means tappable, per DESIGN.md — the three types that carry one
   are exactly the three that will open a detail. The flat ones will not.
   ========================================================================== */

/* Remove panel styling from calendar - it appears directly on background */
.panel.calendar {
  background: transparent;
  border: none;
  padding: 0;
}

.calendar__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-3);
}

.calendar__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  color: var(--ink);
  line-height: 1.45;
}

.calendar__note {
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  line-height: 1.45;
}

.calendar__summary {
  color: var(--ink-body);
  margin-bottom: var(--space-3);
  line-height: 1.5;
  font-size: var(--text-sm);
}

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Equal rows: nine tiles of different content lengths must still read as a
     grid, not as a ragged stack. */
  grid-auto-rows: 1fr;
  gap: var(--space-2);
}

/* ---- the tile ---------------------------------------------------------- */

.cal {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-2);
  border-radius: var(--radius-tile);
  background: var(--surface);
  border: 1px solid transparent;
  min-height: 78px;
  text-align: left;
  font: inherit;
  overflow: hidden;
}

.cal__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-1);
  width: 100%;
}

.cal__day {
  font-size: 10px;
  letter-spacing: 0.6px;
  color: var(--ink-subtle);
  line-height: 1.45;
  white-space: nowrap;
}

.cal__dur {
  font-size: 10px;
  letter-spacing: 0.6px;
  color: var(--ink-subtle);
  line-height: 1.45;
  white-space: nowrap;
  /* Shrink rather than push. Both header labels are `nowrap`, so anything
     longer than expected used to force the tick out of the tile — which is
     what happened when the duration read "2 rides · 121 min". The icon is
     `flex: none`, so this is the piece that must give. */
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* How many rides, when there was more than one. Its own line, because his
   header (node 44:116) is day + duration + tick and has no room for a fourth
   thing. Sits above the load so the tile reads top-down: when, how many, what
   it cost, how hard. */
.cal__count {
  font-size: 10px;
  letter-spacing: 0.6px;
  color: var(--ink-subtle);
  line-height: 1.45;
}

.cal__icon { width: 16px; height: 16px; flex: none; }
.cal__icon img { display: block; width: 100%; height: 100%; }

/* Forecast temperature, top right. Today and ahead only — a past day's
   weather is history and the tile already shows what it cost.
   Turns warm at 28°C, which is where his own numbers turn: above it his heart
   runs about 6 bpm higher for the same power. */
.cal__temp {
  font-size: 10px;
  line-height: 1.45;
  color: var(--ink-subtle);
  white-space: nowrap;
  flex: none;
}
.cal__temp.is-hot { color: var(--warm-text); font-weight: var(--weight-medium); }

.cal__label {
  font-size: 11px;
  font-weight: var(--weight-medium);
  line-height: 1.45;
  color: var(--ink);
}

.cal__meta {
  font-size: 10px;
  color: var(--ink-subtle);
  line-height: 1.45;
}

/* ---- 1. a ride that happened ------------------------------------------- */
.cal.is-ride-done { box-shadow: var(--shadow-tile); }

/* A day he trained, but not on the bike. Deliberately quieter than a ride:
   no shadow (nothing to open), no accent, and the neutral tone the design
   uses for description rather than verdict. It says the day was not empty —
   it does not claim the day was a bike session. */
.cal.is-off-bike {
  background: var(--surface);
  border: var(--border-width) dashed var(--border);
  color: var(--ink-muted);
}
.cal.is-off-bike .cal__label {
  color: var(--ink);
  text-transform: none;
  letter-spacing: 0;
}
.cal.is-ride-done .cal__label { color: var(--done); }

/* The shadow already said tappable; this makes it behave like it. Only tiles
   with a data-day open anything, so only they get the cursor. */
.cal[data-day] { cursor: pointer; }
.cal[data-day]:hover { box-shadow: var(--shadow-card); }

/* ---- 2. a rest day that passed ----------------------------------------- */
/* Dimmed and flat: it is real, it is over, and there is nothing to open. */
.cal.is-rest-done {
  border-color: var(--border);
  opacity: 0.55;
}
.cal.is-rest-done .cal__label {
  font-size: 18px;
  font-weight: var(--weight-bold);
  color: var(--ink);
}

/* ---- 3. today, still to ride ------------------------------------------- */
/* Orange border and a warm glow — the only tile that gets either. */
.cal.is-today-planned {
  border-color: var(--accent);
  box-shadow: var(--shadow-today);
}
.cal.is-today-planned .cal__label { color: var(--planned); }

/* ---- 4. a future day he rarely rides ----------------------------------- */
/* Present so the shape is legible, dimmed so nothing is implied to be owed. */
.cal.is-rest-planned {
  border-color: var(--border);
  opacity: 0.55;
}

/* ---- 5. a future session ----------------------------------------------- */
.cal.is-training-planned { box-shadow: var(--shadow-tile); }
.cal.is-training-planned .cal__label { color: var(--planned); }

/* ---- 6. navigation, not a day ------------------------------------------ */
.cal.is-cta {
  border-color: var(--border);
  opacity: 0.55;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}
.cal.is-cta .cal__day { text-align: center; }
.cal.is-cta .cal__arrow {
  font-size: 24px;
  color: var(--ink-muted);
  line-height: 1.45;
}

.ahead__foot {
  margin-top: var(--space-4);
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  line-height: 1.5;
}


/* ==========================================================================
   13e. WHAT CHANGED — the swipeable stack, and the alternatives
   His design, node 58:19. Two doors from the rest-day card.

   The deck stacks with scale and a small upward push, so it reads as a deck
   rather than one card. Swiping loops — a card goes to the BACK, never away —
   which is what he asked for and what removes the need for an end state.
   ========================================================================== */

/* The sheet. Full screen, above everything, like the loading overlay — his
   request: "it needs to be above the content or standalone place so cards
   have the effect, Like loading screen for example."

   It also fixes a real mechanical problem. The deck's cards fly off past the
   viewport edge when swiped; inside a card with `overflow: hidden` that
   motion was clipped and the gesture read as a stutter. */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  padding: var(--space-4);
  overflow-y: auto;
  animation: sheetIn var(--speed) var(--ease);
}

@keyframes sheetIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

.sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-4);
  flex: none;
}

.sheet__title {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-label);
}

/* A word, not a glyph. "Done" is a bigger target than × and says what it
   does — there has to be an obvious way back out. */
.sheet__close {
  border: var(--border-width) solid var(--border);
  background: var(--surface);
  border-radius: var(--radius-button);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
  cursor: pointer;
  padding: var(--space-2) var(--space-4);
}

.sheet__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-1);
  /* min() against the VIEWPORT, not the parent: `100%` resolves against the
     sheet, which is already the full width, so it never clamped anything. On
     a 393-wide phone a fixed 420 pushed the card off the right edge. The
     32px is the sheet's own left+right padding. */
  width: 100%;
  max-width: min(420px, calc(100vw - var(--space-4) * 2));
  margin: 0 auto;
  min-height: 0;
}

.sheet__hint {
  margin-top: var(--space-4);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--ink-subtle);
}

/* The deck. Fixed height because the cards are absolutely positioned — they
   have to overlap for the stack to exist at all. */
.swipe__deck {
  position: relative;
  /* Every card is the same height because they overlap — the deck cannot size
     to its contents when the contents are stacked on top of each other.
     Rebuilt 18 Aug 2026 to his Figma frame (node 131:618 and siblings), where
     the card is 566 tall inside an 852 sheet. On a phone the sheet is the
     viewport, so the deck takes the height it is given and the card fills it;
     the min-height stops a short viewport collapsing the hero. */
  flex: 1 1 auto;
  min-height: 420px;
  max-height: 566px;
  touch-action: pan-y;
  /* So the value can size itself against the CARD rather than the window.
     `vw` looked right on a phone only because the two happen to match
     there; anywhere else the number was scaled off a width it does not
     occupy. */
  container-type: inline-size;
}

/* The card is FULLY COLOURED, not a white panel with a coloured edge. That is
   the idea of his design: the colour is the verdict, so it has to be the card.

   Rebuilt again 18 Aug 2026 against node 131:618 in dev mode, after he said
   it still was not right. Measured, rather than adjusted by eye:

     Today (the card)   361 x 566, padding 24/20, gap 0, align CENTER,
                        justify MIN — content sits at the TOP, and the empty
                        space belongs at the bottom. My previous pass used
                        gap 16 with the closing text pushed down by
                        `margin-top: auto`, which put the gap in the middle.
     Frame 3            321 wide, HUGs its height, holds the arc + figure
     Frame 11           321 wide, holds the closing paragraph, gap 16
     closing text       24px / 120% line-height
     swipe__dots        18 tall, 12 padding-top, 4 gap

   The arc label is a real TEXT_PATH in his file, 90 x 48, sitting inside
   Frame 3 at x 115.5 — so it is centred, small, and tight. Font 11px with
   0.6px tracking. It is NOT a wide shallow arc across the card, which is
   what I had. */
.swipe__card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;   /* his primaryAxisAlignItems: MIN */
  text-align: center;
  /* itemSpacing 80 on the Fitness card, and the arc label sits inside the
     first group rather than above it — which is where the missing air at the
     top came from. He spotted it: "still this missing some spacing at the
     top". Padding is his 24/20; the extra room above the arc is the label's
     own margin, below. */
  gap: 0;
  padding: var(--space-5) var(--space-5);
  border-radius: var(--radius-card);        /* 32 */
  overflow: hidden;
  background: var(--neutral);
  color: var(--black-primary, #141d21);
  box-shadow: 0 6px 8px rgba(0, 0, 0, 0.06), 0 1px 1px rgba(0, 0, 0, 0.07);
  transform-origin: 50% 100%;
  transition: transform var(--speed) var(--ease), opacity var(--speed) var(--ease);
  will-change: transform;
  cursor: grab;
  user-select: none;
}

.swipe__card.is-moving { transition: none; cursor: grabbing; }

/* Colour carries the verdict. Nothing turns red — a number going the wrong
   way is information, not a failure, which is why "worse" is his warm tone
   and not an alarm. */
.swipe__card.is-better { background: var(--good); }
.swipe__card.is-worse  { background: var(--warm); }
.swipe__card.is-flat   { background: var(--neutral); }
.swipe__card.is-goal   { background: var(--planned); color: #ffffff; }
.swipe__card.is-dark   { background: var(--black-primary, #141d21); color: #f2f1ec; }

/* --- the arced label ------------------------------------------------------
   A 90x48 arc, curving UP over the number. The path is his, lifted verbatim
   out of the TEXT_PATH node:

     M 90 48 C 90 21.49 69.85 0 45 0 C 20.15 0 0 21.49 0 48

   Rendered as an SVG textPath rather than faked with rotated letters —
   letter-spacing along a curve is not something per-character rotation gets
   right, and the glyphs have to stand perpendicular to the tangent. */
.swipe__label {
  flex: 0 0 auto;
  width: 90px;
  height: 48px;
  overflow: visible;
  /* The arc needs air above it as well as below. In his frame the label's
     glyphs stand clear of the card's top edge; with only the 24px padding
     they sat tight against it. */
  margin: var(--space-3) 0 var(--space-5);
}

.swipe__label text {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: var(--weight-normal);
  letter-spacing: 0.6px;
  fill: currentColor;
}

/* 120px in the frame. On a 393-wide phone that is right for "63.9" and far
   too wide for "194 W", so it scales with the viewport and caps at his
   value. clamp keeps the weight and tracking constant, which is what makes
   it read as the same type at every size. */
.swipe__value {
  font-weight: 790;
  letter-spacing: -0.04em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  margin: 0;
  width: 100%;
  text-align: center;
  /* A value is one line, always. "194 W" broke after the digits and set the
     unit on its own line, which turned a number into a two-line headline.
     But nowrap alone just overflows the card, so the SIZE has to adapt to
     the character count — see `--fit` below, set per card in app.js. */
  white-space: nowrap;
  font-size: clamp(40px, calc(var(--fit, 26) * 1cqw), 120px);
}

/* The unit sits UNDER the number, which is what his "You rode twice" card
   does — "125" over "load". Keeping it on the same line made a four-glyph
   number six characters wide, which forced the type down and pushed the
   last glyph under the badge. This was the real cause of the collision I
   had been patching with padding. */
.swipe__unit {
  width: 100%;
  margin: 0;
  font-size: 13px;
  font-weight: var(--weight-normal);
  line-height: 1.35;
  opacity: 0.8;
  text-align: center;
}

/* Word headlines drop to 64px and lose some of the tracking, exactly as the
   "Done as asked." card does. */
.swipe__value.is-words {
  font-size: clamp(38px, 17cqw, 64px);
  letter-spacing: -0.03em;
}

/* The number and its delta badge share a row. In his frame the badge is a
   56px circle at x 230 in a 321 column, overlapping the number's top-right —
   but his numbers are four narrow glyphs. "1.32" at a phone's clamped size
   runs wider, so the badge is nudged clear of the digits with padding rather
   than allowed to sit on them: a badge that covers the figure it annotates
   is worse than no badge. */
/* --- the number and its badge --------------------------------------------
   Measured properly this time, after he said I was going in circles — and
   he was right, because I had been nudging pixels instead of reading the
   structure. His file is unambiguous:

     Frame 7        321 wide, the content column
     the number     321 wide, textAlignHorizontal CENTER
     Frame 13       layoutPositioning ABSOLUTE, x 230, y -4.56, 56x56

   The number fills the WHOLE column and centres inside it. The badge is
   taken out of the layout entirely and floats at a fixed position. They are
   not siblings in a row, which is how I had built it — and putting them in a
   row is what pushed the number off centre, because the badge was taking
   width from it.

   So the badge is absolute here too, at 230/321 = 71.65% from the left. That
   keeps it in his place at every card width, and the number is centred in
   the card exactly as his is. */
.swipe__figure {
  position: relative;
  width: 100%;
  flex: 0 0 auto;
}

.swipe__delta {
  position: absolute;
  left: 71.65%;
  top: -4px;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  font-size: 18px;
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;

  /* THE RULE IS CONTRAST, NOT SENTIMENT. Measured against every card colour
     (WCAG ratio; below about 1.6 two fills read as one):

                       border  dark   green  red
       green card       2.25   5.85   1.00x  1.46x
       slate card       1.98   6.66   1.14x  1.66
       accent card      2.94   4.49   1.30x  1.12x
       warm card        2.42   5.44   1.07x  1.36x
       dark card       13.17   1.00x  5.85   4.01
       blue card        4.38   3.01   1.94   1.33x

     He caught this twice — green on green, then red on orange at 1.12. Both
     times the badge was picked for what it MEANT rather than how it looked
     against the card. Only his border tone clears every card, and it is what
     his own frames use on a coloured card. The sign in front of the number
     carries the direction; it needs no colour to do that. */
  background: var(--border);
  color: var(--black-primary, #141d21);
}

/* The scale line sits directly under the number — small, and it explains
   what the number means before the statement interprets it. */
.swipe__detail {
  font-size: var(--text-sm);
  line-height: 1.35;
  opacity: 0.78;
  width: 100%;
  margin: var(--space-2) 0 0;
  flex: 0 0 auto;
}

/* The closing statement. 24px at 120% — the size that makes it a statement
   rather than a caption. It follows the scale line with his 16px gap, and
   the remaining space falls BELOW it, at the bottom of the card. */
.swipe__note {
  font-size: clamp(18px, 6.4cqw, 24px);
  line-height: 1.2;
  width: 100%;
  margin: var(--space-4) 0 0;
  flex: 0 0 auto;
}

.swipe__bar {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--bg);
  overflow: hidden;
  margin: var(--space-2) 0;
}
.swipe__bar span { display: block; height: 100%; background: var(--accent); }

.swipe__dots {
  display: flex;
  gap: var(--space-1);
  justify-content: center;
  margin-top: var(--space-3);
}
.swipe__dot {
  width: 6px; height: 6px;
  border-radius: var(--radius-pill);
  background: var(--border);
}
.swipe__dot.is-on { background: var(--ink-subtle); }

/* --- Alternatives ------------------------------------------------------- */

.alts__ready {
  font-size: var(--text-sm);
  color: var(--ink-body);
  margin-bottom: var(--space-3);
}

.alts__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.alt {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  text-align: left;
  font: inherit;
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-tile);
  cursor: pointer;
}
.alt.is-picked { border-color: var(--accent); box-shadow: var(--shadow-today); }

.alt__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}
.alt__label { font-size: var(--text-lg); color: var(--ink); }
.alt__spec  { font-size: var(--text-xs); color: var(--ink-subtle); white-space: nowrap; }
.alt__note  { font-size: var(--text-sm); color: var(--ink-body); line-height: 1.35; }

/* Said out loud rather than hidden: 2 gym sessions is not a pattern, and the
   card should not imply the number was learned from him. */
.alt__caveat { font-size: 10px; color: var(--ink-subtle); }

#alt-push { margin-top: var(--space-3); width: 100%; }

/* Declining the day, and taking that back. Deliberately the quietest thing
   on the card: easy to reach, never the obvious choice. No border, no fill —
   it reads as a link rather than an option being offered. */
.btn--quiet {
  background: none;
  border-color: transparent;
  color: var(--ink-subtle);
  font-size: var(--text-sm);
}
.btn--quiet:hover { color: var(--ink-body); }

/* The dark primary from his design (node 61:110). */
.btn--dark {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
}
.btn__icon { width: 24px; height: 24px; }


/* ==========================================================================
   13c. WHAT YOUR BODY IS SAYING
   The signals behind the decision — HRV, load, efficiency factor, decoupling,
   durability. A number and a sentence, never a score: the underlying data
   does not support the precision a 0-100 index would imply.

   No shadow on these tiles. Per DESIGN.md shadow means tappable, and there is
   nothing behind them to open.
   ========================================================================== */

.signals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-2);
}

.signal {
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-tile);
  background: var(--surface);
}

.signal__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.signal__label {
  font-size: var(--text-xs);
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.signal__value {
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.signal__unit {
  font-size: 10px;
  color: var(--ink-subtle);
  line-height: 1.45;
  margin-bottom: var(--space-2);
}

/* The sentence is the point — it is what makes the number mean something. */
.signal__says {
  font-size: var(--text-sm);
  color: var(--ink-body);
  line-height: 1.45;
}

.signal__meta {
  margin-top: var(--space-1);
  font-size: 10px;
  color: var(--ink-subtle);
  line-height: 1.45;
}


/* ==========================================================================
   13i. HOW HARD WAS THAT
   Three buttons, asked once, after a quality session that actually happened.

   Deliberately NOT colour-coded good/bad. "Had more in me" is not a failure
   and "couldn't finish" is not a disgrace — both are information the app
   needs, and tinting them would make one of them the answer he wants to give.
   ========================================================================== */

.rating {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.rate {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: var(--space-3) var(--space-4);
  text-align: left;
  font: inherit;
  color: var(--ink-body);
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-button);
  cursor: pointer;
}

.rate:hover { border-color: var(--ink-subtle); }
.rate:disabled { opacity: 0.5; cursor: default; }

.rate__label {
  font-size: 15px;
  font-weight: 600;
}

.rate__meaning {
  font-size: 13px;
  color: var(--ink-muted);
}

/* ==========================================================================
   13h. WHAT THE SESSIONS ARE BUILT FROM
   FTP, threshold HR, max HR — the value, where it came from, and an override.

   The source line is the point of this panel, not the input. He read "240 W"
   for months without knowing it was one of three FTP figures in his own data.
   ========================================================================== */

.numbers {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.num__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.num__label {
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-label);
}

.num__value {
  margin-left: auto;
  font-size: 22px;
  font-weight: 600;
  color: var(--ink-body);
  font-variant-numeric: tabular-nums;
}

.num__unit {
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-subtle);
  margin-left: 2px;
}

.num__why {
  margin: var(--space-1) 0 0;
  font-size: 13px;
  color: var(--ink-muted);
}

.num__meta {
  margin: var(--space-2) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  font-size: 12px;
  color: var(--ink-subtle);
}

/* A value he chose reads differently from one the app guessed. */
.num__src.is-mine { color: var(--good-text); }

.num__edit {
  margin-top: var(--space-3);
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

.num__input {
  width: 92px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-body);
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-button);
}

.num__msg {
  margin: var(--space-2) 0 0;
  font-size: 12px;
}

.num__msg.is-good { color: var(--good-text); }
.num__msg.is-bad  { color: var(--bad-text); }


/* ==========================================================================
   13g. AM I GETTING BETTER
   Power on his own climbs, weight, and W/kg — month by month.

   Bars, not a line chart, because there is no chart library and there isn't
   going to be one. Each row scales against its own best month so the shape of
   the year reads at a glance; the current month is marked.

   The delta only takes a colour when the change clears 2%. Colouring noise
   green is how a progress panel stops being believed.
   ========================================================================== */

.progress {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.prog__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.prog__label {
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-label);
}

.prog__value {
  margin-left: auto;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink-body);
  font-variant-numeric: tabular-nums;
}

.prog__delta {
  font-size: 12px;
  color: var(--ink-subtle);
  font-variant-numeric: tabular-nums;
}

.prog__delta.is-good { color: var(--good-text); }
.prog__delta.is-bad  { color: var(--bad-text); }

.prog__bars {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 44px;
}

.prog__bar {
  flex: 1;
  min-width: 0;
  background: var(--neutral);
  border-radius: var(--radius-row) var(--radius-row) 2px 2px;
  opacity: 0.45;
}

/* The month being reported. Not "good" or "bad" — just where you are now. */
.prog__bar.is-now {
  background: var(--ink-body);
  opacity: 1;
}

/* A month with no ride long enough to hold that duration. Drawn rather than
   skipped, so the gaps in his winter stay visible instead of closing up. */
.prog__bar.is-empty {
  height: 3px;
  opacity: 0.18;
}

/* A weight inherited from an earlier month rather than measured. Outlined
   rather than filled — it holds its place in the trend without claiming to
   be a reading. */
.prog__bar.is-carried {
  background: transparent;
  border: 1px solid var(--neutral);
  opacity: 0.3;
}

/* ===========================================================================
   13d. PRESCRIBED AND RIDDEN
   What the coach asked for against what happened.

   No red, no percentage badge, no streak. A missed session usually means the
   session was the wrong size for the week — his time is genuinely
   unpredictable, which is a founding finding of this whole app. The colour
   only marks the outcome quietly on the left edge.
   ========================================================================== */

.adherence {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.adh {
  display: grid;
  grid-template-columns: 64px 1fr auto;
  grid-template-areas:
    "day what nums"
    "day outcome outcome";
  align-items: baseline;
  gap: 2px var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--border);
  border-radius: 0 var(--radius-row) var(--radius-row) 0;
  background: var(--surface);
}

.adh__day {
  grid-area: day;
  font-size: var(--text-xs);
  color: var(--ink-subtle);
}

.adh__what {
  grid-area: what;
  font-size: var(--text-sm);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.adh__nums {
  grid-area: nums;
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  white-space: nowrap;
}

.adh__outcome {
  grid-area: outcome;
  font-size: 10px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.adh.is-followed { border-left-color: var(--done); }
.adh.is-followed .adh__outcome { color: var(--done); }

.adh.is-partial { border-left-color: var(--warm); }
.adh.is-partial .adh__outcome { color: var(--warm-text); }

/* Missed stays neutral on purpose. It is a fact about the week, not a fault. */
.adh.is-missed { border-left-color: var(--border); opacity: 0.65; }


/* ==========================================================================
   13a. AFTER YOUR RIDE — the card stack
   Native scroll-snap, so swiping works on touch with real momentum and no
   library. One card fills the rail; the dots follow scroll position.
   ========================================================================== */

.cards {
  display: grid;
  grid-auto-flow: column;
  /* Two at a time on a wide screen — one card per screen wastes desktop
     space and makes the stack feel longer than it is. Phones get one, in
     the media query at the bottom of this file. */
  grid-auto-columns: calc(50% - var(--space-4) / 2);
  gap: var(--space-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.cards::-webkit-scrollbar { display: none; }

.card {
  scroll-snap-align: start;
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-6) var(--space-5);
  min-height: 190px;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The ride's own numbers lead with the load figure. */
.card__headline {
  font-size: var(--text-hero);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  color: var(--accent);
}

.card__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  color: var(--ink);
  margin: 0;
}

.card__facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.card__facts li {
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* The ride card's facts are short measurements, so they read better inline. */
.card.is-ride .card__facts {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
}
.card.is-ride .card__facts li {
  font-family: var(--font-num);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.card.is-compare { border-color: var(--good); }
.card.is-compare .card__title { color: var(--good-text); }
.card.is-goal { border-color: var(--accent); }

/* Conditions are context, not a verdict — heat and headwind are neither an
   achievement nor a failure, so this card stays neutral. Green would read as
   praise for riding in the wind; red would read as an excuse. */
.card.is-conditions { border-color: var(--warm); }
.card.is-conditions .card__title { color: var(--warm-text); }

.cards__dots {
  display: flex;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.cards__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--border);
  transition: background var(--speed) var(--ease);
}
.cards__dot.is-on { background: var(--ink-subtle); }

/* Shown when a newer ride exists that the API won't give us detail for. */
.panel__note.is-stale { color: var(--warm-text); }


/* ==========================================================================
   13d. OFF THE BIKE — Whoop
   ========================================================================== */

.whoop__line {
  color: var(--ink-muted);
  line-height: 1.55;
  margin-bottom: var(--space-4);
}

/* The `hidden` attribute is only a DEFAULT style (display: none), so any
   explicit `display` here silently overrides it — which left "Connect Whoop"
   showing on a panel that was already connected and pulling data. Anything
   with a display rule needs this alongside it. */
[hidden] { display: none !important; }

.btn--accent {
  display: inline-block;
  text-decoration: none;
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: var(--weight-medium);
  padding: var(--space-2) var(--space-5);
}
.btn--accent:hover { color: var(--bg); opacity: 0.9; }


/* ==========================================================================
   13f. WHEN SOMETHING FAILS
   The app's one real-world action is putting a workout on the head unit.
   A failure there is found at 6am, so it gets said plainly rather than
   flashing "Failed" for two seconds.
   ========================================================================== */

.today__pusherror {
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--bad);
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--bad) 8%, transparent);
  color: var(--ink);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.talk__result.is-bad {
  color: var(--bad-text);
}

/* Stale or failed data in the header. Quiet, but not invisible. */
.freshness.is-bad { color: var(--bad-text); }
.freshness.is-warm { color: var(--warm-text); }


/* ==========================================================================
   13e. THE PLAN — the run to race day
   Shape, not sessions. A phase change is the thing worth spotting at a
   glance, so those weeks get the accent; everything else stays quiet.
   ========================================================================== */

.plan {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.plan__week {
  display: grid;
  grid-template-columns: 96px 1fr auto auto auto;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--border);
  border-radius: 0 var(--radius-row) var(--radius-row) 0;
}

.plan__when {
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  font-family: var(--font-num);
}

.plan__phase {
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.plan__detail {
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  font-family: var(--font-num);
  text-align: right;
}

/* Which week of the block. Quiet — it explains the load beside it rather
   than competing with it. */
.plan__block {
  font-size: var(--text-xs);
  color: var(--ink-subtle);
  font-family: var(--font-num);
  text-align: right;
  min-width: 42px;
}

/* Projected fitness. Slightly darker than the load detail beside it, because
   this is the number he asked to watch — but still quiet: it is a projection,
   not a measurement. */
.plan__ctl {
  font-size: var(--text-xs);
  color: var(--ink-muted);
  font-family: var(--font-num);
  text-align: right;
  min-width: 84px;
}

/* Where the block changes character — the weeks worth noticing. */
.plan__week.is-phase-start {
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.plan__week.is-phase-start .plan__phase {
  color: var(--ink);
  font-weight: var(--weight-medium);
}

/* This week, and the race itself. */
.plan__week.is-now { background: var(--bg); }
.plan__week.is-now .plan__when { color: var(--accent); }

.plan__week.is-race {
  border-left-color: var(--good);
  background: color-mix(in srgb, var(--good) 7%, transparent);
}
.plan__week.is-race .plan__phase { color: var(--good-text); }


/* ==========================================================================
   14. SMALL SCREENS
   ========================================================================== */

@media (max-width: 760px) {
  :root {
    --page-pad: var(--space-4);
    --text-hero: 34px;
    --chart-height: 240px;
  }

  .hero { grid-template-columns: 1fr; }
  .mini { grid-template-columns: repeat(2, 1fr); }

  /* The week becomes a horizontal scroll rather than seven tiny columns */
  .week {
    grid-template-columns: repeat(7, minmax(96px, 1fr));
    overflow-x: auto;
    padding-bottom: var(--space-2);
  }

  /* Same treatment for the week ahead — the labels carry real words
     ("1½h low intensity"), so they need more room than the day cards. */
  .ahead {
    grid-template-columns: repeat(7, minmax(120px, 1fr));
    overflow-x: auto;
    padding-bottom: var(--space-2);
  }

  .ride { grid-template-columns: 56px 1fr auto; }
  .ride__metric:nth-of-type(n+2) { display: none; }

  /* One card per swipe on a phone — this is the format he asked for. */
  .cards { grid-auto-columns: 100%; }
  .card { padding: var(--space-5) var(--space-4); }

  /* The plan stacks its detail rather than scrolling sideways — reading the
     arc matters more on a phone than seeing every column at once. */
  .plan__week {
    grid-template-columns: 88px 1fr;
    row-gap: 2px;
  }
  .plan__detail { grid-column: 2; text-align: left; }
  .plan__ctl { grid-column: 2; text-align: left; min-width: 0; }
  .plan__block { grid-column: 2; text-align: left; min-width: 0; }
}

/* ==========================================================================
   WHAT THE RIDE MOVED — on the today card
   ==========================================================================

   Added 9 Sep 2026. The "Done for today" card announced the session was
   finished and then offered to send a workout to the Wahoo and adjust it —
   his words: "tahle celá karta moc nedává smysl". Nothing on it answered the
   question a finished day actually raises.

   These are the same three cards the calendar detail shows, in the place he
   looks first. Desktop puts them side by side; a phone scrolls and snaps,
   which gives him the swipe he asked for using the browser's own gesture
   rather than a second drag implementation to keep in step with the sheet.
   ========================================================================== */

.today__after {
  margin-top: var(--space-5);
}

.after {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
}

/* The deck's cards are absolutely positioned for the stack in the sheet.
   Here they sit in the flow, so that has to be undone rather than inherited. */
.after .swipe__card {
  position: relative;
  inset: auto;
  min-height: 260px;
  padding: 52px var(--space-4) var(--space-4);
  opacity: 1;
  pointer-events: auto;
  cursor: default;
}

/* The hero number carries the card in the sheet, where it has a full phone
   width. In a third of a panel it has to come down or it wraps. */
.after .swipe__value {
  font-size: clamp(34px, 7cqw, 56px);
}
.after .swipe__note {
  font-size: clamp(14px, 2.2cqw, 17px);
}
.after .swipe__label { top: var(--space-4); }
/* The sheet pins the badge at 71.65% of the card — his frame's position
   against a 120px number. These cards are narrower and the number is clamped
   smaller, so that percentage lands on the digits, and lower down it crosses
   the closing text. Measured at 393px: the badge sat over the note.
   Anchoring it to the card's top-right corner keeps it clear of both at any
   width. The overlap in the sheet is a property of that size, not the rule. */
/* The badge anchors to `.swipe__figure`, which is `position: relative` — so
   "top right" means the top right of the NUMBER's row, not the card, and at
   this width that lands on the digits and the note below them. Measured at
   393px: it overlapped both. Dropping the figure's positioning context lets
   it anchor to the card, where the corner is actually empty. */
.after .swipe__figure { position: static; }

.after .swipe__delta {
  width: 44px;
  height: 44px;
  font-size: 14px;
  left: auto;
  right: var(--space-4);
  top: var(--space-4);
}

/* Room for it, so the arc label does not run underneath. */
.after .swipe__label { margin-right: 52px; }

/* And the figure no longer reserves space for a badge that has moved out of
   its way. */
.after .swipe__value { padding-right: 0; }

.after__dots { display: none; }

@media (max-width: 760px) {
  /* One card per screen, snapped. `scroll-snap-type` is what makes this feel
     like the sheet's swipe without being one. */
  .after {
    display: flex;
    gap: var(--space-3);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    margin-inline: calc(var(--space-4) * -1);
    padding-inline: var(--space-4);
  }
  .after::-webkit-scrollbar { display: none; }

  .after .swipe__card {
    flex: 0 0 100%;
    scroll-snap-align: center;
    min-height: 300px;
  }

  .after__dots {
    display: flex;
    gap: 4px;
    justify-content: center;
    padding-top: var(--space-3);
  }
  .after__dots i {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--ink);
    opacity: 0.18;
  }
  .after__dots i.on { opacity: 0.85; }
}

/* --- FUELLING ------------------------------------------------------------
   Panel, not a card: an instruction rather than a verdict, and per DESIGN.md
   a shadow promises a detail view that does not exist here. */

.fuel {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.fuel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.fuel__label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-label);
}

/* The number he will check the arithmetic against. */
.fuel__target {
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

/* Bottles and gels, not grams — grams are the research's unit and the wrong
   one to act on at six in the morning. */
.fuel__carry {
  margin: var(--space-2) 0 0;
  font-size: var(--text-md);
  color: var(--ink);
}

.fuel__when {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.fuel__when li {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--ink-body);
  padding-left: var(--space-3);
  border-left: 2px solid var(--border);
}

.fuel__when b {
  font-weight: var(--weight-medium);
  color: var(--ink);
}

/* The gut ramp — a standing instruction, quieter than the kit list. */
.fuel__gut {
  margin: var(--space-3) 0 0;
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
  font-size: var(--text-sm);
  color: var(--ink-body);
}
