:root {
  --bg: #F5F5F7;
  --card: #FFFFFF;
  --ink: #1D1D1F;
  --secondary: #6E6E73;
  --tertiary: #86868B;
  --fill: #E8E8ED;
  --track: #F2F2F5;
  --ghost: #E5E5EA;
  --accent: #FF5E3A;    /* bars when no FTP is set */
  --strava: #FC4C02;    /* Strava's own orange, on its export button */
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  --blue: #007AFF;
  --orange: #FF9500;
  --red: #FF3B30;
  --green: #34C759;
  /* Power zones, from recovery (Z1) to sprint (Z7) */
  --z1: #AEAEB2;
  --z2: #32ADE6;
  --z3: #34C759;
  --z4: #FFE03D;
  --z5: #FF9500;
  --z6: #FF3B30;
  --z7: #AF52DE;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
/* Read by screen readers, not shown: the units already say what each number is */
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* The app fills the window, and only scrolls when the window is lower than the cards at their
   smallest readable size */
body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 26px 44px 30px;
  background: var(--bg);
  color: var(--ink);
  font-family: Inter, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* Header: name, docked bubble, auto-pause, weight, FTP, connection, all wrapping as one row of
   items aligned to the right, the name pushed to the left on its line */
.top { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 10px; }
h1 { margin: 0 auto 0 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.pill { display: flex; align-items: center; gap: 8px; min-height: 38px; padding: 6px 14px; border-radius: 999px; background: var(--card); box-shadow: var(--shadow); font-size: 14px; color: var(--ink); }
.pill input { width: 4.2em; padding: 4px 8px; border: 0; border-radius: 8px; background: var(--fill); font: inherit; font-weight: 600; color: var(--ink); text-align: right; -moz-appearance: textfield; }
.pill input::placeholder { color: var(--tertiary); }
.pill input:focus::placeholder { color: transparent; }
/* An empty field glows red, softly and over and over, so it's hard to leave empty. Both fields run
   the glow from page load, filled or not, and only show it while empty: their glows stay in step. */
@property --glow { syntax: "<number>"; inherits: false; initial-value: 0; }
@keyframes glow { to { --glow: 1; } }
.pill input { animation: glow 0.8s ease-in-out infinite alternate; }
.pill input:placeholder-shown:not(:focus) { background: color-mix(in srgb, #FFC8C4 calc(var(--glow) * 100%), #FFE3E1); box-shadow: 0 0 0 calc(var(--glow) * 4px) rgba(255, 59, 48, calc(0.4 - var(--glow) * 0.25)); }
.pill input:focus { background: var(--card); }
.pill input::-webkit-outer-spin-button, .pill input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.status { font-weight: 500; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--z1); }
.dot.on { background: var(--green); }
.dot.lost { background: var(--red); }

.segmented { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: var(--fill); }
.segmented button { padding: 6px 13px; border: 0; border-radius: 999px; background: transparent; font: 500 14px Inter, sans-serif; color: var(--secondary); cursor: pointer; }
.segmented button[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); }

.switch { width: 44px; height: 26px; padding: 3px; border: 0; border-radius: 13px; background: var(--ghost); cursor: pointer; transition: background 0.2s; }
.switch span { display: block; width: 20px; height: 20px; border-radius: 50%; background: var(--card); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); transition: transform 0.2s; }
.switch[aria-checked="true"] { background: var(--green); }
.switch[aria-checked="true"] span { transform: translateX(18px); }

/* Two columns: readings and chart on the left, totals and durations on the right. Cards share the
   height of their column, and their contents scale with both sides of the card. */
main { flex: 1; display: flex; min-height: 0; }
.screen { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr) var(--column, clamp(240px, 36vw, 420px)); gap: 24px; }
.side, .column { display: flex; flex-direction: column; gap: 20px; min-width: 0; min-height: 0; }
.column { gap: 14px; }
.card { position: relative; background: var(--card); border-radius: 28px; box-shadow: var(--shadow); }
.readings, .chart, .totals, .durations, .slot { flex: 1 1 0; }
.readings, .totals, .durations { container-type: size; }
/* Each card's smallest readable size */
.readings, .chart { min-height: 100px; }
.chart.with-switch { min-height: 140px; }
#summary .readings { min-height: 150px; }
#live .totals { min-height: 56px; }
#summary .totals { min-height: 140px; }
.durations { min-height: 200px; }
/* Corner grips of the live cards: invisible, the cursor shows where to drag */
.grip { position: absolute; z-index: 2; width: 28px; height: 28px; touch-action: none; }
.grip.top { top: -6px; }
.grip.bottom { bottom: -6px; }
.grip.left { left: -6px; }
.grip.right { right: -6px; }
/* Edges, between the corners */
.grip:not(.left, .right) { left: 22px; right: 22px; width: auto; height: 12px; }
.grip:not(.top, .bottom) { top: 22px; bottom: 22px; width: 12px; height: auto; }

dl, dd { margin: 0; }
dt { font-size: 15px; color: var(--secondary); }
.unit { margin-left: 6px; font-size: max(0.26em, 10px); font-weight: 500; letter-spacing: 0; color: var(--tertiary); }

/* Digits of equal width, so numbers don't shift as they change. Only on numbers: in Inter this
   also widens the hyphen. */
.values, .power-line, .totals, .durations, .zone-legend, .pill input { font-variant-numeric: tabular-nums; }

.readings { --f: min(10.5cqw, 42cqh); padding: 24px 28px; }
.values { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: end; }
.now { display: flex; align-items: baseline; font-size: var(--f); font-weight: 600; letter-spacing: -0.055em; line-height: 0.95; }
.now .big { font-size: calc(var(--f) * 1.38); }
.value .average { margin-top: calc(var(--f) * 0.1); font-size: calc(var(--f) * 0.38); letter-spacing: -0.02em; color: var(--secondary); }
.date { margin: 0 0 10px; font-size: 15px; color: var(--secondary); }

/* Live, past a certain shape, figures stacked come out bigger than side by side, so they stack. A
   card can't restyle its own layout from its shape, only its children's, hence the rules on them. */
#live .figures { display: flex; flex-direction: column; justify-content: center; height: 100%; }
/* Stacked figures are centred, with even space above, between and below them: the gap between
   two figures matches the card's vertical padding, which adds to the space above and below */
@container (aspect-ratio < 1.62) {
  #live .figures { --f: min(28cqw, (100cqh - 72px) * 0.18); gap: 24px; justify-content: space-evenly; text-align: center; }
  #live .values { display: contents; }
  #live .now, #live .power-line { justify-content: center; }
  #live .power-line { margin-top: 0; }
}
/* The summary's figures come with their date and labels, which keep their size */
#summary .readings { --f: min(10.5cqw, (100cqh - 50px) * 0.55); display: flex; flex-direction: column; justify-content: center; }

.power-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 22px; min-height: calc(var(--f) * 0.44); margin-top: calc(var(--f) * 0.19); font-size: max(calc(var(--f) * 0.3), 15px); }
.zone { font-weight: 600; }
.power-line strong { font-weight: 600; }
.wkg .average { margin-left: 0.5em; font-size: 0.85em; color: var(--secondary); }

/* Out of the flow, so the drawing's size never feeds back into the layout */
.chart canvas { position: absolute; top: 22px; left: 20px; width: calc(100% - 44px); height: calc(100% - 40px); }
.chart.with-switch canvas { top: 66px; height: calc(100% - 84px); }
.chart .segmented { position: absolute; top: 16px; right: 18px; z-index: 1; }

/* The state of the ride, floating over the middle of the app like a notification, with a deeper
   shadow than the cards. Clicks go through it to the cards below, except on its dock button. Its
   colour tells the kind of message: info (blue), start (green), pause (orange) or error (red).
   Centred by its margins rather than a transform, which is left to the docking animation. */
.bubble { position: fixed; inset: 0; z-index: 20; display: flex; align-items: center; gap: 18px; width: max-content; height: max-content; max-width: min(90vw, 960px); margin: auto; padding: 42px 80px 42px 56px; border-radius: 38px; background: #E1EEFF; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22), 0 4px 14px rgba(0, 0, 0, 0.1); font-size: 30px; font-weight: 600; text-align: center; overflow-wrap: anywhere; transform-origin: 0 0; pointer-events: none; }
.bubble::before { content: ""; flex: none; width: 14px; height: 14px; border-radius: 50%; background: var(--blue); }
.bubble[data-kind="start"] { background: #DDF6E4; }
.bubble[data-kind="start"]::before { background: var(--green); }
.bubble[data-kind="pause"] { background: #FFEBD2; }
.bubble[data-kind="pause"]::before { background: var(--orange); }
.bubble[data-kind="error"] { background: #FFDCDA; }
.bubble[data-kind="error"]::before { background: var(--red); }
.dock { position: absolute; top: 16px; right: 16px; display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%; background: rgba(0, 0, 0, 0.07); color: var(--secondary); cursor: pointer; pointer-events: auto; }
.dock:hover { background: rgba(0, 0, 0, 0.12); }
.dock svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* Docked: a small pill in the header, left of the auto-pause switch. It counts as 120 px wide when
   the header decides to wrap, and grows into the room left, up to its text: so it shrinks first,
   cutting its text short, and the header only wraps once it's down to 120 px. */
.bubble.docked { position: static; flex: 1 1 120px; width: auto; height: auto; min-width: 120px; max-width: max-content; min-height: 38px; margin: 0; padding: 6px 14px; gap: 8px; border-radius: 19px; box-shadow: var(--shadow); font-size: 14px; font-weight: 500; text-align: left; cursor: pointer; pointer-events: auto; }
.bubble.docked::before { width: 8px; height: 8px; }
.bubble.docked span { min-width: 0; max-width: 380px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.bubble.docked .dock { display: none; }

.totals { padding: 20px 22px; }
.totals dd { font-weight: 600; letter-spacing: -0.045em; line-height: 1.05; }
.totals .unit { font-size: max(0.38em, 10px); margin-left: 5px; }
/* Live, even space around the totals: the gap between them matches the card's padding, which adds
   to the space at the edges, and their lines are trimmed to the digits so the space is the one seen */
#live .totals dl { display: flex; align-items: center; justify-content: space-evenly; gap: 20px 22px; height: 100%; }
#live .totals dd { font-size: min((100cqw - 22px) * 0.145, 100cqh); text-box: trim-both cap alphabetic; }
@container (aspect-ratio < 3.5) {
  #live .totals dl { flex-direction: column; }
  #live .totals dd { font-size: min(27cqw, (100cqh - 20px) * 0.6); }
}
/* The summary's four totals, two by two under their labels */
#summary .totals dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: center; gap: 16px; height: 100%; }
#summary .totals dd { font-size: min((100cqw - 16px) * 0.13, (100cqh - 52px) * 0.47); }

/* Rolling averages (live) and best efforts (summary): one row per duration */
.durations { display: flex; flex-direction: column; justify-content: space-between; margin: 0; padding: 20px 22px; list-style: none; }
.durations li { font-size: min(5.9cqw, 7cqh); display: grid; grid-template-columns: max(2.3em, 38px) minmax(0, 1fr) 2.3em max(1.6em, 22px); align-items: center; gap: 0.55em; }
.durations .label { font-size: max(0.6em, 10px); color: var(--secondary); }
.durations .bar { position: relative; height: 0.45em; border-radius: 0.23em; background: var(--track); overflow: hidden; }
.durations .bar span { position: absolute; top: 0; bottom: 0; left: 0; border-radius: 0.23em; }
.durations .ghost { background: var(--ghost); }
.durations .watts { font-weight: 500; letter-spacing: -0.02em; text-align: right; }
.durations .record { font-size: max(0.55em, 10px); color: var(--tertiary); text-align: right; }

.zones { display: flex; flex-direction: column; gap: 10px; padding: 20px 22px; }
.zone-bar { display: flex; gap: 2px; height: 10px; border-radius: 5px; overflow: hidden; }
.zone-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 0; padding: 0; list-style: none; font-size: 13px; color: var(--secondary); }
.zone-legend li { display: flex; align-items: center; gap: 6px; }
.zone-legend i { width: 10px; height: 10px; border-radius: 3px; }
.zone-legend strong { color: var(--ink); font-weight: 600; }

.button { display: flex; align-items: center; justify-content: center; gap: 12px; height: 56px; padding: 0 20px; border: 0; border-radius: 999px; background: var(--fill); color: var(--ink); font: 600 16px Inter, sans-serif; box-shadow: var(--shadow); cursor: pointer; transition: transform 0.15s, background 0.15s; user-select: none; -webkit-user-select: none; touch-action: none; }
.button:hover { background: #DEDEE3; }
.button:active { transform: scale(0.98); }
.button.primary { background: var(--ink); color: var(--card); }
.button.primary:hover { background: #333336; }
.button.accent { background: var(--accent); color: var(--card); }
.button.strava { background: var(--strava); color: var(--card); }
.button.strava:hover { background: #E34402; }
.button.accent:hover { background: #F04A24; }
.button:disabled { opacity: 0.4; cursor: default; }
.button:focus-visible, .segmented button:focus-visible, .switch:focus-visible, .dock:focus-visible, .pill input:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.downloads { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; }
.downloads .button { padding: 0 16px; font-size: 15px; white-space: nowrap; }

/* The auto-pause switch and the bubble only matter during a ride */
body[data-screen="summary"] .live-only { display: none; }

/* Hold-to-confirm: the ring fills over 2 seconds while the button is held */
.ring { flex: none; width: 20px; height: 20px; transform: rotate(-90deg); }
.ring circle { fill: none; stroke: currentColor; stroke-width: 2.5; }
.ring circle:first-child { opacity: 0.3; }
.ring .progress { stroke-linecap: round; stroke-dasharray: 50.3; stroke-dashoffset: 50.3; transition: stroke-dashoffset 0.25s ease-out; }
.hold.holding .progress { stroke-dashoffset: 0; transition: stroke-dashoffset 2s linear; }

/* Readings fade while the ride is paused, so a glance is enough to see it */
#live .figures, #live .totals dl { transition: opacity 0.3s; }
body[data-state="paused"] #live :is(.figures, .totals dl) { opacity: 0.45; }

/* Two columns on a large screen or a landscape one, where the live cards can be resized and moved.
   The same query is in app.js. */
@media (min-width: 901px), (orientation: landscape) {
  #live .card { user-select: none; transform-origin: 0 0; }
  #live .side > :first-child { flex-grow: 30; }
  #live .side > :nth-child(2) { flex-grow: 70; }
  #live .column > :first-child { flex-grow: 13; }
  #live .column > :nth-child(2) { flex-grow: 87; }
  #summary .side > :nth-child(2) { flex-grow: 1.2; }
  #summary .column > :nth-child(2) { flex-grow: 2.4; }
  /* A dragged card floats above the others, at the size it had in its place */
  #live .card.dragged { position: fixed; z-index: 10; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); cursor: grabbing; will-change: translate; }
}

/* A phone or a small window: tighter spacing, a smaller header and bubble. The connection pill
   only shows its dot, its text is still read out. */
@media (max-width: 900px) {
  body { gap: 12px; padding: 12px 16px; }
  .screen, .side, .column { gap: 12px; }
  .top { gap: 8px; }
  .pill { min-height: 32px; gap: 6px; padding: 4px 12px; font-size: 13px; }
  .bubble.docked { min-height: 32px; }
  .status { width: 32px; padding: 0; justify-content: center; font-size: 0; }
  .bubble { gap: 12px; padding: 24px 52px 24px 28px; border-radius: 28px; font-size: 20px; }
  .dock { top: 12px; right: 12px; }
  .readings { padding: 16px 20px; }
  .totals { padding: 12px 18px; }
  .durations { padding: 14px 18px; }
  .button { height: 48px; }
  .segmented button { padding: 4px 10px; font-size: 13px; }
  .chart .segmented { top: 12px; right: 12px; }
  .chart.with-switch canvas { top: 50px; height: calc(100% - 66px); }
}

/* Upright, one column: the cards share the height of the window, in their order */
@media (max-width: 900px) and (orientation: portrait) {
  .screen { display: flex; flex-direction: column; }
  .side, .column { display: contents; }
  .grip { display: none; }
  #live .readings { flex-grow: 24; }
  #live .chart { flex-grow: 26; }
  #live .totals { flex-grow: 10; }
  #live .durations { flex-grow: 40; }
  #summary .readings { flex-grow: 26; }
  #summary .chart { flex-grow: 22; }
  #summary .totals { flex-grow: 18; }
  #summary .durations { flex-grow: 34; }
}
@media (prefers-reduced-motion: reduce) {
  .switch, .switch span, .button { transition: none; }
  .pill input { animation: none; }
}
