/* ════════════════════════════════════════════════════════════════════════
   Half Marathon Tracker — editorial sports-annual aesthetic.
   Display: Anton. Body: Karla. Data: IBM Plex Mono.

   Light is the original static page's palette, kept to the hex.
   Dark is *selected*, not inverted: its own steps, its own chart colours,
   each checked against the dark surface for contrast and colour-vision
   separation. Everything below the token block reads tokens only.
   ════════════════════════════════════════════════════════════════════════ */

:root{
  /* ── Light theme (the original) ───────────────────────────── */
  --night:#12182B;
  --night-2:#1C2440;
  --paper:#F4F6F3;
  --card:#FFFFFF;
  --river:#2E6E8E;
  --river-deep:#1F4E68;
  --gold:#C99A2E;
  /* Gold has two jobs and they pull opposite ways. As a *surface* — the race
     chip, the "Today" marker, the primary button on the masthead — it has to
     stay light enough to carry near-black ink. As *ink* on a light surface it
     has to be dark enough to be read, and at #C99A2E it was 2.51:1 on a card
     and 2.02:1 on its own pale chip. So the surface keeps --gold and the ink
     gets its own step, exactly as --river/--river-deep already do. Measured on
     the three light backgrounds gold is ever printed on: 4.99:1 on the peak
     chip, 5.76 on the read-only banner, 6.37 on a white card. */
  --gold-deep:#7A5A10;
  --slate:#6B7A8F;
  --line:#DDE3E4;
  /* A completed session's title and distance. This is a growing majority of the
     page as the plan is ticked off, so it answers to the 4.5 body floor like
     any other prose, not to a designer's sense of "finished means faded" — the
     line-through and the dimmed volume bar say "done" without help from the
     contrast. Tuned against the lightest thing it prints on, which is not the
     white card but the gold-tinted "today" row (#FBF8F0): 4.82:1 there, 4.97 on
     a week head and 5.11 on a card. The old #8FA3B0 measured 2.61 and 2.46. */
  --done:#5A7180;
  /* Destructive verbs. Only the ink splits by theme: the fill stays the same
     red in both, because white on it is 4.92:1 either way. */
  --danger:#B4543A;
  --danger-ink:#B4543A;
  --form-error-ink:#8C3A22;
  --tag-cut-ink:var(--river-deep);
  /* Dark rims the race row so it separates from the card; light does not need
     to, and a token is how "does not need to" is said in one place. */
  --race-inset:none;

  /* Semantic surfaces — components use these, never the raw names above. */
  --bg:var(--paper);
  --surface:var(--card);
  --surface-2:#FBFCFB;
  --surface-hover:#F8FAFA;
  --ink:var(--night);
  /* The two quiet inks are set as literals rather than as --slate and a tint of
     it, because both have a floor to clear that the original palette did not:
     4.5:1 against the *darkest* thing either is ever printed on, since that is
     where dark ink has least to work with. That is the page background --paper,
     not the white card, so they are tuned to it —
     5.95:1 and 5.04:1 there, 6.47 and 5.48 on a card. Both carry a little more
     than the floor asks, because the grain overlay lifts a measurement off the
     screen by four or five per cent and the floor has to hold there too. The
     old pair measured 4.02 and 1.95 on --paper, and the sentence that told a
     reader nothing was missed was the faintest thing on the page. Two steps of
     hierarchy survive: dim is still visibly ahead of faint. */
  --ink-dim:#545F70;
  --ink-faint:#5E6A7D;
  --rule:var(--line);
  --rule-soft:#F0F3F2;
  --masthead-bg:var(--night);
  --masthead-ink:var(--paper);
  /* Nav links, the fact labels, the ridge caption and the habit day names all
     read this, and the floor it has to clear is not --masthead-bg. The masthead
     carries a radial wash of accent light across its top-left, which is where
     the nav sits, and it lifts the surface under those very links to about
     #1E3050. Measured from pixels there, #7C8AA8 was 4.33:1. This is 4.83 on
     the brightest part of the wash and 5.38 clear of it. */
  --masthead-dim:#919DB6;
  --accent:var(--river);
  --accent-bright:#4E93B4;
  /* The accent is tuned for ink-on-white; on the navy masthead it is 3.14:1,
     and the .eyebrow it paints carries the cadence of a habit. This is the same
     hue lifted until it clears 4.5 against the masthead. */
  --masthead-accent:#4599C3;
  /* An empty checkbox is a UI component, so its edge answers to 1.4.11's 3:1,
     not to a designer's eye for a hairline. The old #B9C4C8 was 1.78. */
  --checkbox-border:#80949B;
  --check-ink:#FFFFFF;
  --race-bg:var(--night);
  --race-bg-hover:var(--night-2);
  --race-ink:var(--paper);
  --shadow-card:0 1px 2px rgba(18,24,43,.04);
  --shadow-lift:0 10px 30px -12px rgba(18,24,43,.22);
  --grain-opacity:.035;

  /* Chart tokens — the SVGs are generated server-side but paint from here,
     so one stylesheet drives both themes. */
  --k-easy:#4E93B4;
  --k-long:#1F4E68;
  --k-tempo:#C99A2E;
  --k-shakeout:#B4543A;
  --k-race:#12182B;
  --c-planned:#A9C4D2;
  --c-actual:#2E6E8E;
  --c-grid:#DDE3E4;
  /* The scale numbers and the W1…W8 labels are 10 px text, so they answer to
     4.5 and not to the 3:1 a gridline would. #8FA3B0 was 2.61 on the card. */
  --c-axis:#5C7382;
  --c-axis-strong:#12182B;
  --c-trail:#8FA3B0;
  --c-band:#C99A2E;
  --c-band-opacity:.14;
  --c-ring:#FFFFFF;

  --display:'Anton',Impact,sans-serif;
  --body:'Karla',system-ui,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,monospace;

  --ease-out:cubic-bezier(.22,.85,.3,1);

  /* The page gutter. The masthead reads it too, so its text lines up with the
     week cards instead of drifting by the width of its own negative margin.

     There are two, because a phone's safe area is not symmetric. Installed to
     the home screen and turned on its side, the notch is on one edge only: the
     inset is 59px on that side and 0 on the other. A single gutter reading
     safe-area-inset-left was correct in one of the two rotations and ran the
     right-hand column 23px under the notch in the other, which is measured in
     tests/test_pwa.py rather than reasoned about. */
  --gutter:max(20px,env(safe-area-inset-left));
  --gutter-r:max(20px,env(safe-area-inset-right));
}

:root[data-theme="dark"]{
  --night:#080C16;
  --night-2:#111827;
  --paper:#E8EDF2;
  --card:#161E33;
  --river:#6BB0D0;
  --river-deep:#3E86AC;
  --gold:#E0B44E;
  /* Dark's gold is already 8.12:1 on a card and 4.67 on the peak chip, so ink
     and surface are the same colour here and the split costs nothing. */
  --gold-deep:#E0B44E;
  --slate:#93A3BC;
  --line:#26304A;
  /* 4.84:1 on the gold-tinted "today" row, 5.46 on a card. The old #5F708C ran
     3.30 on a card and 2.90 on that row. */
  --done:#8695AD;
  --danger:#B4543A;
  --danger-ink:#E4917A;
  --form-error-ink:#E4917A;
  --tag-cut-ink:var(--accent-bright);
  --race-inset:inset 0 0 0 1px color-mix(in srgb,var(--gold) 30%,transparent);

  --bg:#0E1424;
  --surface:#161E33;
  --surface-2:#1A2239;
  --surface-hover:#1E273F;
  --ink:#E8EDF2;
  --ink-dim:#93A3BC;
  /* The lightest thing this is printed on is not --surface-hover. It is the
     "today" row, whose 7% gold tint over the card comes out at #242835 —
     lighter than any of the three named dark surfaces. Tuned against that:
     4.83:1 there, 4.87 on --surface-hover, 5.45 on a card. The previous
     #7D90AE was picked against --surface-hover alone and read 4.57 there but
     4.48 on the today row, which is where the pace band, the detail line and
     the ＋ all sit. Measure a token on the row it is hardest on, not on the
     surface it is named after. */
  --ink-faint:#8395B2;
  --rule:#26304A;
  --rule-soft:#1F2840;
  --masthead-bg:#080C16;
  --masthead-ink:#E8EDF2;
  --masthead-dim:#8D9BB5;
  --accent:#6BB0D0;
  --accent-bright:#7CC0DE;
  /* Dark already clears 4.5 on the masthead, so the eyebrow keeps the accent. */
  --masthead-accent:#6BB0D0;
  --checkbox-border:#5C6F99;
  --check-ink:#0B1220;
  --race-bg:#0A101F;
  --race-bg-hover:#0F1729;
  --race-ink:#F4F6F3;
  --shadow-card:0 1px 2px rgba(0,0,0,.3);
  --shadow-lift:0 14px 34px -14px rgba(0,0,0,.7);
  --grain-opacity:.05;

  --k-easy:#7CC0DE;
  --k-long:#3E86AC;
  --k-tempo:#E0B44E;
  --k-shakeout:#D97757;
  --k-race:#F4F6F3;
  --c-planned:#5A7095;
  --c-actual:#7CC0DE;
  --c-grid:#26304A;
  --c-axis:#93A3BC;
  --c-axis-strong:#E8EDF2;
  --c-trail:#5F708C;
  --c-band:#E0B44E;
  --c-band-opacity:.18;
  --c-ring:#161E33;
}

/* The same palette again, for a browser with JavaScript switched off.

   The head script resolves a theme onto <html data-theme> before first
   paint, which is what lets the block above be the only place dark is
   written. With no script there is no attribute at all, and the OS
   preference has to be answered by CSS or not at all: without this arm a
   reader on a dark desktop got a white page while <meta name="color-scheme">
   still told the browser dark was supported, so the form controls painted
   dark on it. The theme *switch* may reasonably need script. The OS
   preference must not.

   :not([data-theme]) and not :not([data-theme="light"]) on purpose — the
   attribute being absent is precisely the no-script case, and a reader who
   has chosen light must keep it whatever the OS says.

   The values are a copy, because a CSS selector list cannot be extended by
   a media query and there is no build step to generate one. That makes two
   copies a real hazard, so tests/test_ui.py parses both blocks out of this
   file and fails if they ever disagree. Change one, change the other. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme]){
    --night:#080C16;
    --night-2:#111827;
    --paper:#E8EDF2;
    --card:#161E33;
    --river:#6BB0D0;
    --river-deep:#3E86AC;
    --gold:#E0B44E;
    --gold-deep:#E0B44E;
    --slate:#93A3BC;
    --line:#26304A;
    --done:#8695AD;
    --danger:#B4543A;
    --danger-ink:#E4917A;
    --form-error-ink:#E4917A;
    --tag-cut-ink:var(--accent-bright);
    --race-inset:inset 0 0 0 1px color-mix(in srgb,var(--gold) 30%,transparent);
    --bg:#0E1424;
    --surface:#161E33;
    --surface-2:#1A2239;
    --surface-hover:#1E273F;
    --ink:#E8EDF2;
    --ink-dim:#93A3BC;
    --ink-faint:#8395B2;
    --rule:#26304A;
    --rule-soft:#1F2840;
    --masthead-bg:#080C16;
    --masthead-ink:#E8EDF2;
    --masthead-dim:#8D9BB5;
    --accent:#6BB0D0;
    --accent-bright:#7CC0DE;
    --masthead-accent:#6BB0D0;
    --checkbox-border:#5C6F99;
    --check-ink:#0B1220;
    --race-bg:#0A101F;
    --race-bg-hover:#0F1729;
    --race-ink:#F4F6F3;
    --shadow-card:0 1px 2px rgba(0,0,0,.3);
    --shadow-lift:0 14px 34px -14px rgba(0,0,0,.7);
    --grain-opacity:.05;
    --k-easy:#7CC0DE;
    --k-long:#3E86AC;
    --k-tempo:#E0B44E;
    --k-shakeout:#D97757;
    --k-race:#F4F6F3;
    --c-planned:#5A7095;
    --c-actual:#7CC0DE;
    --c-grid:#26304A;
    --c-axis:#93A3BC;
    --c-axis-strong:#E8EDF2;
    --c-trail:#5F708C;
    --c-band:#E0B44E;
    --c-band-opacity:.18;
    --c-ring:#161E33;
  }
}

*{box-sizing:border-box}
button,label,a,input{-webkit-tap-highlight-color:transparent;touch-action:manipulation}
html{-webkit-text-size-adjust:100%;color-scheme:light}
:root[data-theme="dark"]{color-scheme:dark}
@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark}}
/* A column the height of the viewport with the content stretching, so the
   footer sits at the bottom of a short page — 404, an invalid share link, the
   login form, an account with no habits yet — instead of floating halfway down
   it with a few hundred pixels of background underneath. svh, not vh: on a
   phone vh is the *largest* viewport and would push the bar below the fold
   while the address bar is showing. */
body{
  margin:0;
  min-height:100vh;
  min-height:100svh;
  display:flex;
  flex-direction:column;
  background:var(--bg);
  color:var(--ink);
  font-family:var(--body);
  font-size:16px;
  line-height:1.5;
  transition:background-color .35s var(--ease-out),color .35s var(--ease-out);
}
.wrap{
  flex:1 0 auto;
  max-width:820px;margin:0 auto;position:relative;width:100%;
  padding:0 var(--gutter-r) calc(80px + env(safe-area-inset-bottom)) var(--gutter);
}

/* ── Grain: a paper tooth over the whole page, never in the way ── */
body::after{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:9;
  opacity:var(--grain-opacity);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── Header ─────────────────────────────── */
.masthead{
  flex:none;
  background:var(--masthead-bg);
  color:var(--masthead-ink);
  margin:0 0 28px;
  /* Installed, there is no browser chrome, so the masthead starts at y=0 and the
     status bar is drawn on top of it. Padding by the inset alone put the brand
     flush against the bottom of the clock; the extra 14px is the gap that makes
     it read as a header rather than as text that has slid under something. In a
     browser tab the inset is 0 and this is the 22px it always was. */
  padding:max(22px,calc(env(safe-area-inset-top) + 14px)) 0 0;
  position:relative;
  overflow:hidden;
  transition:background-color .35s var(--ease-out);
}
/* A slow wash of accent light across the top-left, so the black is not flat. */
.masthead::before{
  content:"";position:absolute;top:-40%;left:-10%;width:70%;height:180%;
  background:radial-gradient(closest-side,color-mix(in srgb,var(--accent) 26%,transparent),transparent);
  pointer-events:none;
}
.masthead-inner{
  max-width:820px;margin:0 auto;position:relative;
  padding:0 var(--gutter-r) 0 var(--gutter);
}
.eyebrow{
  font-family:var(--mono);
  font-size:11px;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--masthead-accent);
  margin:0 0 10px;
  /* The masthead clips its overflow, so a long race name simply vanished off
     the right edge — 1716px of scrollWidth against 361px of box, with nothing
     to say it had been cut. An ellipsis is not a fix for the underlying length,
     but it is the difference between truncated and silently truncated. */
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* Announced, never drawn. The habit list changes its own count under the
   reader's thumb, and a screen reader has to be told. */
.sr-only{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;
}
h1{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(40px,10vw,76px);
  line-height:.92;
  letter-spacing:.01em;
  margin:0 0 18px;
  text-transform:uppercase;
  /* A plan name with no spaces in it is one long word at 76px. Breaking it
     mid-word is ugly; letting it run under the clip is worse, because the
     heading is how you know which plan you are looking at. */
  overflow-wrap:anywhere;
}
h1 em{font-style:normal;color:var(--gold)}

.facts{
  display:flex;
  flex-wrap:wrap;
  gap:26px;
  padding-bottom:22px;
}
.fact-k{
  font-family:var(--mono);
  font-size:10px;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--masthead-dim);
  margin:0 0 3px;
}
.fact-v{
  font-family:var(--mono);
  font-size:17px;
  font-weight:500;
  margin:0;
}

/* ── Signature: volume ridge ─────────────── */
/* Full bleed: it cancels the wrap's gutters, so it has to cancel each of them
   separately now that the two sides can differ. */
.ridge{
  margin:0 calc(var(--gutter-r) * -1) 0 calc(var(--gutter) * -1);
  display:block;width:calc(100% + var(--gutter) + var(--gutter-r));
}
.ridge-label{
  display:flex;justify-content:space-between;
  font-family:var(--mono);font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--masthead-dim);
  padding:0 0 8px;
}
.ridge .ridge-line{
  stroke-dasharray:100;stroke-dashoffset:100;
  animation:draw 1.6s var(--ease-out) .15s forwards;
}
.ridge .ridge-fill{opacity:0;animation:fade .9s var(--ease-out) .5s forwards}
/* An SVG transform is measured from the user-space origin unless it is told
   otherwise, so scale(.4) started every marker at 40% of its own coordinates —
   up and to the left, further the further along the ridge it sat, and for most
   of a second two of them hung detached above the line. transform-box makes
   each circle's own bounding box the reference, and the origin its centre, so
   the marker grows where it belongs. */
.ridge circle{
  opacity:0;animation:pop .5s var(--ease-out) forwards;
  transform-box:fill-box;transform-origin:center;
}

@keyframes draw{to{stroke-dashoffset:0}}
@keyframes fade{to{opacity:1}}
@keyframes pop{from{transform:scale(.4)}to{opacity:1;transform:scale(1)}}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}

/* ── Progress ───────────────────────────── */
.progress{
  display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;
  padding:18px 0 14px;
}
.progress-big{
  font-family:var(--display);font-size:34px;line-height:1;
  font-variant-numeric:lining-nums;
}
.progress-txt{font-size:14px;color:var(--ink-dim)}
.progress-txt b{color:var(--ink);font-family:var(--mono);font-weight:500}
.reset{
  margin-left:auto;background:none;border:1px solid var(--rule);
  color:var(--ink-dim);font-family:var(--mono);font-size:11px;
  letter-spacing:.1em;text-transform:uppercase;
  padding:7px 12px;border-radius:2px;cursor:pointer;min-height:44px;
  transition:border-color .15s,color .15s,background-color .15s;
}
.reset:hover{border-color:var(--ink);color:var(--ink)}
.reset:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* The 31-run meter: one tick per workout, filled as they are logged. */
.meter{
  display:flex;gap:2px;margin-bottom:30px;
  padding-bottom:12px;border-bottom:2px solid var(--ink);
}
.meter-tick{
  flex:1;height:6px;border-radius:1px;background:var(--rule);
  transition:background-color .3s var(--ease-out);
}
.meter-tick.on{background:var(--accent)}
.meter-tick.race{background:var(--rule);border:1px solid var(--gold)}
.meter-tick.race.on{background:var(--gold);border-color:var(--gold)}

/* ── Week blocks ────────────────────────── */
.week{
  background:var(--surface);
  border:1px solid var(--rule);
  border-radius:3px;
  margin-bottom:14px;
  overflow:hidden;
  box-shadow:var(--shadow-card);
  animation:rise .5s var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 45ms);
  transition:background-color .35s var(--ease-out),border-color .35s var(--ease-out);
}
.week.is-now{border-color:color-mix(in srgb,var(--gold) 55%,var(--rule))}
/* Wrapping, so that a narrow column — a phone, or a desktop at 200% zoom, which
   is the same measurement — folds the volume onto a second line instead of
   squeezing the date range into three. */
.week-head{
  display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;
  padding:14px 18px;
  border-bottom:1px solid var(--rule);
  background:var(--surface-2);
}
.week-no{
  font-family:var(--display);
  font-size:30px;line-height:.8;
  color:var(--ink);
  min-width:34px;
}
.week.is-now .week-no{color:var(--gold-deep)}
.week-meta{flex:1 1 auto;min-width:0}
.week-dates{font-family:var(--mono);font-size:12px;color:var(--ink-dim);white-space:nowrap}
.week-tag{
  display:inline-block;font-family:var(--mono);font-size:9.5px;
  letter-spacing:.14em;text-transform:uppercase;
  padding:2px 7px;border-radius:2px;margin-left:8px;vertical-align:2px;
}
.tag-cut{background:color-mix(in srgb,var(--river) 16%,transparent);color:var(--tag-cut-ink)}
/* Gold on its own pale chip was 2.02:1 and on the card 2.51. The chip keeps
   its gold wash; the letters take the deep step. */
.tag-peak{background:color-mix(in srgb,var(--gold) 26%,transparent);color:var(--gold-deep)}
.tag-race{background:var(--gold);color:#241A02}
.tag-now{background:none;border:1px solid var(--gold-deep);color:var(--gold-deep)}
.week-km{font-family:var(--mono);font-size:13px;font-weight:500;white-space:nowrap;margin-left:auto}
.week-km span{color:var(--ink-dim);font-weight:400}

/* ── Run rows ───────────────────────────── */
.run-wrap{position:relative;border-bottom:1px solid var(--rule-soft)}
.run-wrap:last-of-type{border-bottom:none}
.run{
  display:grid;
  grid-template-columns:auto 62px 1fr 100px;
  align-items:center;
  gap:12px;
  padding:11px 44px 11px 18px;
  cursor:pointer;
  transition:background-color .12s;
  position:relative;
}
.run::before{
  content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:var(--accent);transform:scaleY(0);transform-origin:center;
  transition:transform .18s var(--ease-out);
}
@media (hover:hover){
  .run:hover{background:var(--surface-hover)}
  .run:hover::before{transform:scaleY(1)}
}
/* The tick used to be a checkbox inside the row. It is now the row itself — a
   submit button whose state lives in aria-pressed, so it works with no script.
   The checkbox rules that stood here are gone; the tick keyframes below stay,
   because the new mark still plays them. See plan.css for the replacement. */
@keyframes tick{
  0%{height:0;opacity:.4}
  60%{height:11px}
  100%{height:9px;opacity:1}
}
.run-day{
  font-family:var(--mono);font-size:11px;letter-spacing:.06em;
  color:var(--ink-dim);text-transform:uppercase;
}
.run-body{min-width:0}
.run-name{display:block;font-size:14.5px;font-weight:600;line-height:1.3}
.run-name small{font-weight:400;color:var(--ink-dim);font-size:13px}
.bar{
  display:block;
  height:5px;border-radius:3px;background:var(--river);
  margin-top:6px;opacity:.75;min-width:6px;
  transform-origin:left;
  animation:grow .7s var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 45ms + 120ms);
}
@keyframes grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.bar.long{background:var(--river-deep);opacity:.9}
.bar.tempo{background:var(--gold);opacity:.85}
.run-pace{
  font-family:var(--mono);font-size:12px;color:var(--ink-dim);
  text-align:right;line-height:1.35;
}
.run-pace b{display:block;color:var(--ink);font-weight:500;font-size:13px}

.run.done .run-name,.run.done .run-pace b{color:var(--done);text-decoration:line-through;text-decoration-thickness:1px}
.run.done .run-name small,.run.done .run-pace{color:var(--ink-faint);text-decoration:none}
.run.done .bar{opacity:.25}

/* Today / next up — one row only, the plan's cursor. */
.run-wrap.marked{background:color-mix(in srgb,var(--gold) 7%,transparent)}
.run-wrap.marked .run::before{background:var(--gold);transform:scaleY(1)}
.marker{
  position:absolute;left:18px;top:-8px;z-index:2;
  font-family:var(--mono);font-size:8.5px;letter-spacing:.18em;
  text-transform:uppercase;color:#241A02;background:var(--gold);
  padding:2px 6px;border-radius:2px;line-height:1.4;
}

.run.race{background:var(--race-bg);color:var(--race-ink);cursor:pointer}
@media (hover:hover){.run.race:hover{background:var(--race-bg-hover)}}
.run.race::before{background:var(--gold)}
.run.race .run-day{color:var(--gold)}
.run.race .run-name{font-family:var(--display);font-weight:400;font-size:22px;letter-spacing:.02em;text-transform:uppercase}
.run.race .run-pace b{color:var(--gold)}
.run.race .run-pace{color:#8C9BB8}
.run.race .bar{background:var(--gold);opacity:1}
.run.race input{border-color:#4A5878;background:transparent}
.run.race input:checked{background:var(--gold);border-color:var(--gold)}
.run.race input:checked::after{border-color:var(--night)}
.run.race.done .run-name{color:var(--gold);text-decoration:none}
.run.race{box-shadow:var(--race-inset)}

/* ── Gym rows ───────────────────────────── */
/* Quieter than a run: no distance bar, a slate accent instead of the river
   blue, so the eye still reads the running plan first. */
.run.gym{background:var(--surface-2)}
@media (hover:hover){.run.gym:hover{background:var(--surface-hover)}}
.run.gym::before{background:var(--ink-dim)}
.run.gym .run-name{font-weight:600;font-size:14px}
.run.gym .run-pace b{color:var(--ink-dim)}
.run.gym .run-pace{text-transform:uppercase;font-size:10px;letter-spacing:.12em}
.run.gym input:checked{background:var(--ink-dim);border-color:var(--ink-dim)}
.gym-badge{
  display:inline-block;font-family:var(--mono);font-size:9.5px;
  letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-dim);border:1px solid var(--rule);
  padding:1px 5px;border-radius:2px;margin-right:8px;vertical-align:1px;
}
/* The badge is the only thing that tells a gym session from a run, so it is not
   status and must not fade with one. Ticking the row used to drop it to 50%
   opacity — #A6ACB4 on the week head, 2.22:1 — which took the identity away at
   exactly the moment the row went quiet. It steps to the faint ink instead and
   the border carries the softening. The type went 8.5px → 9.5px with it, the
   size every other mono chip on the page is set at. */
.run.gym.done .gym-badge{color:var(--ink-faint);border-color:var(--rule-soft)}

.rest{
  padding:10px 18px;font-family:var(--mono);font-size:11px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint);
}

/* ── Notes ──────────────────────────────── */
.notes{margin-top:38px}
.notes h2{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:22px;letter-spacing:.03em;margin:0 0 14px;
  padding-bottom:8px;border-bottom:2px solid var(--ink);
}
.paces{width:100%;border-collapse:collapse;margin-bottom:30px}
.paces td{padding:9px 0;border-bottom:1px solid var(--rule);font-size:14.5px}
.paces td:last-child{text-align:right;font-family:var(--mono);font-size:13.5px;white-space:nowrap}
.rules{padding-left:20px;margin:0}
.rules li{margin-bottom:11px;font-size:14.5px}
.rules b{font-weight:700}
.foot{
  margin-top:34px;padding-top:16px;border-top:1px solid var(--rule);
  font-family:var(--mono);font-size:11px;color:var(--ink-faint);letter-spacing:.04em;
}

/* ── Nav + theme switch ─────────────────── */
.nav{
  display:flex;gap:18px;align-items:center;
  font-family:var(--mono);font-size:11px;letter-spacing:.16em;
  text-transform:uppercase;padding-bottom:18px;
}
.nav a{position:relative;color:var(--masthead-dim);text-decoration:none;padding-bottom:3px;border-bottom:1px solid transparent;transition:color .15s}
/* 11 px of ink, 44 px of target. An overlay rather than padding, because
   padding here would push the underline off the word and add a centimetre to
   every masthead on the site. Half the row gap on each side, so no two links
   ever fight over the same pixel. */
.nav a::after{content:"";position:absolute;inset:-12px -6px}
.nav a:hover{color:var(--masthead-ink)}
.nav a[aria-current="page"]{color:var(--masthead-ink);border-bottom-color:var(--gold)}
.nav a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* Offered only where it can work. The switch needs script — it writes a choice
   to localStorage and flips an attribute — and with script off it rendered as a
   button reading "Light" that did nothing when pressed. The head script marks
   the root, and the OS preference is answered in CSS above, so a reader without
   script gets the right theme and no dead control. */
.theme-toggle{
  position:relative;
  display:none;
  margin-left:auto;align-items:center;gap:7px;
  background:none;border:1px solid color-mix(in srgb,var(--masthead-dim) 40%,transparent);
  color:var(--masthead-dim);border-radius:2px;cursor:pointer;
  font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;
  padding:6px 10px;transition:color .15s,border-color .15s;
}
:root[data-js] .theme-toggle{display:flex}
/* The switch draws at 27 px and answers at 45. Same trick as the links. */
.theme-toggle::after{content:"";position:absolute;inset:-9px -2px}
.theme-toggle:hover{color:var(--masthead-ink);border-color:var(--masthead-ink)}
.theme-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.theme-toggle svg{width:13px;height:13px;display:block}
.theme-toggle .i-moon{display:none}
:root[data-theme="dark"] .theme-toggle .i-sun{display:none}
:root[data-theme="dark"] .theme-toggle .i-moon{display:block}

/* ── Log-actual affordance ──────────────── */
.log-toggle{
  position:absolute;top:9px;right:10px;
  width:24px;height:24px;padding:0;
  display:flex;align-items:center;justify-content:center;
  background:none;border:1px solid transparent;border-radius:3px;
  color:var(--ink-faint);font-family:var(--mono);font-size:14px;line-height:1;
  cursor:pointer;transition:color .12s,border-color .12s;
}
.log-toggle:hover{color:var(--accent);border-color:var(--rule)}
.log-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.log-toggle[aria-expanded="true"]{color:var(--accent);border-color:var(--rule)}
/* Its own step rather than a token: the race row is the one place on the site
   with a near-black surface in both themes. #5A6787 read 3.13:1 on it. */
.run-wrap.race .log-toggle{color:#7986A5}
.run-wrap.race .log-toggle:hover,.run-wrap.race .log-toggle[aria-expanded="true"]{
  color:var(--gold);border-color:#39456B;
}
.log-panel{
  display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;
  padding:12px 18px 14px;background:var(--surface-2);
  border-top:1px solid var(--rule-soft);
  animation:rise .25s var(--ease-out);
}
.log-field{display:flex;flex-direction:column;gap:4px}
.log-field label{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-dim);
}
.log-field input{
  font-family:var(--mono);font-size:13px;color:var(--ink);
  width:92px;padding:5px 7px;background:var(--surface);
  border:1px solid var(--rule);border-radius:2px;
}
.log-field input:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}
.log-field.wide input{width:180px}
.log-save{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  padding:7px 12px;border:1px solid var(--accent);border-radius:2px;
  background:var(--accent);color:var(--surface);cursor:pointer;
  transition:background-color .15s,border-color .15s;
}
.log-save:hover{background:var(--river-deep);border-color:var(--river-deep)}
.log-save:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.log-hint{
  font-family:var(--mono);font-size:10.5px;color:var(--ink-faint);
  flex-basis:100%;margin:0;letter-spacing:.02em;
}
.row-error{
  margin:0;padding:8px 18px;
  background:color-mix(in srgb,var(--danger) 14%,var(--surface));
  color:var(--danger-ink);
  font-family:var(--mono);font-size:11px;letter-spacing:.02em;
  border-top:1px solid color-mix(in srgb,var(--danger) 26%,transparent);
}
.row-error[hidden]{display:none}

/* ── Statistics page ────────────────────── */
.tiles{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:3px;
  margin-bottom:30px;overflow:hidden;
}
.tile-wide{grid-column:1 / -1}
.tile{
  background:var(--surface);padding:16px 18px;
  animation:rise .5s var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 55ms);
}
.tile-k{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-dim);margin:0 0 8px;
}
.tile-v{font-family:var(--display);font-size:32px;line-height:1;margin:0}
.tile-v.small{font-size:24px}
.tile-v em{font-style:normal;color:var(--gold-deep)}
.tile-sub{font-family:var(--mono);font-size:11.5px;color:var(--ink-dim);margin:6px 0 0}

.card{
  background:var(--surface);border:1px solid var(--rule);border-radius:3px;
  padding:18px;margin-bottom:22px;box-shadow:var(--shadow-card);
  animation:rise .5s var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 55ms);
}
.card h3{
  font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink);margin:0 0 4px;font-weight:600;
}
.card p.card-sub{font-size:13px;color:var(--ink-dim);margin:0 0 14px}
.chart{display:block;width:100%;height:auto;overflow:visible}

/* ── A chart whose scale does not scroll away ──
   Both charts are 560 px of drawing inside a 359 px card on a phone, and the
   scale used to be drawn inside the scrolling SVG. Scroll to weeks 4–8 and the
   40/30/20/10/0 was simply gone, which leaves a row of bars meaning nothing.

   So the scale is a second SVG, outside the scroller, pinned over the plot's
   left-hand gutter. It carries the same viewBox and the same min-width as the
   plot, which is what makes the two line up at any width: identical viewBox and
   identical rendered width give identical rendered height, so a label at y=104
   lands on the gridline at y=104. clip-path trims it to the 54 units of gutter
   the plot leaves empty — charts.AXIS_GUTTER, and the two have to agree — so
   the bars scroll underneath it and are covered the
   way a frozen column covers them. It is aria-hidden and pointer-transparent:
   the plot keeps the accessible name, and the table under every chart repeats
   the numbers. */
.chart-frame{position:relative}
.chart-axis{
  position:absolute;left:0;top:0;z-index:2;pointer-events:none;
  clip-path:inset(0 calc(100% - 54 / 820 * 100%) 0 0);
}
/* A sideways scroller that gives no sign of it reads as a broken card: the
   device table on /account is 440 px inside 359, and it was simply cut flush at
   the edge. The two ``local`` layers are card-coloured caps that scroll away
   with the content; the two ``scroll`` layers underneath are shadows pinned to
   the frame. Uncover a shadow and there is more to the side. No script, no
   markup, and it works for every .scroll-x on the site.

   These are the element's *background*, so anything opaque laid over them wins.
   The tables have nothing over them and the cue has always worked there; the
   charts painted a full-bleed <rect class="surface"> as the SVG's first child
   and covered it, which is why weeks 6–8 did not exist as far as a phone reader
   could tell. That rect now belongs to the frozen scale panel, where being
   opaque is the point, and the plot is transparent over the shadows. */
.scroll-x{
  overflow-x:auto;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain;
  background:
    linear-gradient(to right,var(--surface) 55%,transparent) 0 0,
    linear-gradient(to left,var(--surface) 55%,transparent) 100% 0,
    radial-gradient(farthest-side at 0 50%,color-mix(in srgb,var(--ink) 30%,transparent),transparent) 0 0,
    radial-gradient(farthest-side at 100% 50%,color-mix(in srgb,var(--ink) 30%,transparent),transparent) 100% 0;
  background-repeat:no-repeat;
  background-size:40px 100%,40px 100%,18px 100%,18px 100%;
  background-attachment:local,local,scroll,scroll;
}
/* And a scrollbar that is actually there. macOS hides overlay scrollbars until
   you scroll, which is precisely when the reader has already decided the card
   is broken; styling the track opts into a permanent one that takes its own
   eight pixels of the box. `scrollbar-width` is deliberately not set — the
   standard property wins over these rules and hands the overlay back. */
/* And room for it. macOS draws overlay scrollbars, which take none of the box
   — measured, clientHeight == offsetHeight — so without this the bar is painted
   across the W1…W8 labels on the one gesture that would have revealed it. */
.scroll-x{padding-bottom:10px}
.scroll-x::-webkit-scrollbar{height:8px}
.scroll-x::-webkit-scrollbar-track{background:var(--rule-soft);border-radius:4px}
.scroll-x::-webkit-scrollbar-thumb{background:var(--ink-faint);border-radius:4px}
.scroll-x::-webkit-scrollbar-thumb:hover{background:var(--ink-dim)}
.chart .ax{font-family:var(--mono);font-size:10px;fill:var(--c-axis)}
.chart .ax-strong{font-family:var(--mono);font-size:11px;fill:var(--c-axis-strong);font-weight:500}
.chart .ax-tag{font-family:var(--mono);font-size:9px;fill:var(--ink-faint);letter-spacing:.1em;text-transform:uppercase}
.chart .bar-planned{fill:var(--c-planned)}
.chart .bar-actual{fill:var(--c-actual)}
.chart .grid{stroke:var(--c-grid);stroke-width:1}
.chart .axis{stroke:var(--c-axis);stroke-width:1}
.chart .band{fill:var(--c-band);opacity:var(--c-band-opacity)}
.chart .trail{stroke:var(--c-trail);stroke-width:2;fill:none;opacity:.55}
.chart .dot{stroke:var(--c-ring);stroke-width:2}
.chart .k-easy{fill:var(--k-easy)}
.chart .k-long{fill:var(--k-long)}
.chart .k-tempo{fill:var(--k-tempo)}
.chart .k-shakeout{fill:var(--k-shakeout)}
.chart .k-race{fill:var(--k-race)}
.chart .surface{fill:var(--surface)}

.legend{
  display:flex;flex-wrap:wrap;gap:14px;margin:0 0 14px;padding:0;list-style:none;
  font-family:var(--mono);font-size:11px;color:var(--ink-dim);
}
.legend li{display:flex;align-items:center;gap:6px}
.swatch{width:10px;height:10px;border-radius:2px;flex:none}
.swatch.dot{border-radius:50%}

.grid-table{width:100%;border-collapse:collapse;font-size:14px}
.grid-table th{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-dim);text-align:right;font-weight:400;
  padding:0 0 8px;border-bottom:1px solid var(--rule);
}
.grid-table th:first-child{text-align:left}
.grid-table td{
  padding:9px 0;border-bottom:1px solid var(--rule-soft);
  font-family:var(--mono);font-size:13px;text-align:right;font-variant-numeric:tabular-nums;
}
.grid-table td:first-child{text-align:left;font-family:var(--body);font-size:14.5px}
.grid-table tr:last-child td{border-bottom:none}
.grid-table td.muted{color:var(--ink-faint)}
/* "Average pace is distance-weighted across runs where you logged both a
   distance and a time" — and then Actual km was cut mid-header and Avg pace was
   entirely off-screen. The blanket 440 px minimum below is set by the header
   row, not by the data: let those three headings wrap onto two lines and the
   whole table is 225 px of min-content, which fits a 359 px card with room to
   spare. Applied where the lede names the columns, not to the two table views
   inside <details>, whose long run titles want the width. */
/* .scroll-x in the selector only to outrank the phone rule that sets the 440,
   which lands later in the file. */
.scroll-x .grid-table.fits{min-width:0}
.grid-table.fits th{white-space:normal}
.kind-dot{
  display:inline-block;width:9px;height:9px;border-radius:50%;
  margin-right:8px;vertical-align:baseline;
}
details>summary{cursor:pointer;font-family:var(--mono);font-size:11.5px;color:var(--ink-dim)}
details>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
details[open]>summary{margin-bottom:4px}

.empty{
  background:var(--surface-2);border:1px dashed var(--rule);border-radius:3px;
  padding:26px 20px;text-align:center;
}
.empty p{margin:0 0 6px;font-size:14.5px}
.empty p.empty-lead{
  font-family:var(--display);font-size:22px;text-transform:uppercase;letter-spacing:.03em;
}
.empty a{color:var(--accent);font-weight:600}
.empty .hint{font-family:var(--mono);font-size:11.5px;color:var(--ink-dim)}

/* ── Phones ─────────────────────────────────
   Tuned against a 393 px viewport (iPhone 16). Touch targets clear 44 px,
   inputs sit at 16 px so iOS does not zoom the page on focus, and anything
   too wide to shrink legibly scrolls sideways instead. */
@media (max-width:560px){
  :root{
    --gutter:max(16px,env(safe-area-inset-left));
    --gutter-r:max(16px,env(safe-area-inset-right));
  }
  .masthead{margin-bottom:22px}

  .facts{gap:16px 22px}
  /* The brand, four links and the theme switch do not fit on one 393 px line.
     Without wrapping, the switch is pushed past the right edge and there is no
     way to change theme on a phone at all.

     `margin-left:auto` is dropped with the wrap. Pinned right, the switch left
     a 300 px hole beside a stranded "Account" on the second row at 320 px,
     which reads as a layout that broke rather than one that folded. Following
     the last link, the nav reads as one run of items that happens to need two
     lines. */
  .nav{gap:14px;padding-bottom:14px;flex-wrap:wrap;row-gap:12px}
  .theme-toggle{margin-left:0}

  .run{grid-template-columns:auto 46px 1fr;gap:10px;row-gap:2px;
       padding:13px 46px 13px 14px}
  .run-pace{grid-column:2 / -1;text-align:left;display:flex;gap:8px;align-items:baseline}
  .run-pace b{display:inline}
  .run.gym .run-pace{gap:6px}
  .run-name{font-size:15px}
  .run.race .run-name{font-size:19px}
  .week-head{padding:12px 14px;gap:10px}
  .rest,.log-panel,.row-error{padding-left:14px;padding-right:14px}
  .scroll-x{margin:0 -14px;padding:0 14px 10px}

  /* A 24 px glyph with a 44 px hit area around it, level with the title.

     It used to be top:50% of .run-wrap — and .run-wrap is the whole row
     *including* the log panel, so opening the panel walked the collapse button
     down into the middle of it, level with the "Note" field and reading as a
     control of that field. Measuring against the row would need the button to
     be inside it, and it is a sibling. So it anchors to the top instead, which
     is what the desktop rule has always done: 11 px puts the 24 px glyph on the
     centre line of the first line of type, and it cannot move because nothing
     below it is in the sum. */
  .log-toggle{top:11px;right:6px;transform:none;width:24px;height:24px}
  .log-toggle::after{content:"";position:absolute;inset:-10px;border-radius:6px}

  /* 16 px or larger, or Safari zooms the whole page when the field takes focus. */
  .log-field input{width:100%;font-size:16px;padding:9px 10px}
  .log-field{flex:1 1 128px}
  .log-field.wide{flex:1 1 100%}
  .log-field.wide input{width:100%}
  .log-save{width:100%;padding:12px;font-size:12px}
  .log-panel{gap:12px}

  .tiles{grid-template-columns:repeat(2,1fr)}
  .tile{padding:14px}
  .tile-v{font-size:26px}
  .tile-v.small{font-size:20px}
  .card{padding:14px}

  /* Charts keep their own scale and scroll under the thumb. */
  .scroll-x{margin:0 -14px;padding:0 14px 10px}
  /* The pinned scale is not inside the scroller, so it needs the same minimum
     said again — identical width is what keeps the two in register. */
  .scroll-x .chart,.chart-frame .chart-axis{min-width:560px}
  /* And the chart's scroller gives up the full-bleed. Every other .scroll-x
     wants those 14 px of card padding; a chart with a frozen scale cannot have
     them, because the scale is positioned from the frame and anything to the
     left of the frame is a strip the scale cannot reach — bars slid into it and
     sat outside the panel that is supposed to cover them. */
  .chart-frame .scroll-x{margin:0;padding:0 0 10px}
  .scroll-x .grid-table{min-width:440px}

  .progress{padding:14px 0 12px}
  .progress-big{font-size:30px}
  .reset{margin-left:0;padding:10px 14px}
  .meter{gap:1px}
}

/* ── Very narrow ─────────────────────────────
   Not a device: a 393 px phone at 200% zoom is a 196 px column, and WCAG 1.4.4
   asks for that to stay usable rather than merely unclipped. Below 340 px the
   run grid's four fixed columns leave the title about twenty pixels, which
   printed a gym session one word per line and 354 px tall. The row stacks
   instead: the tick and the day on one line, the title and the volume under
   them, each with the whole column to itself. */
@media (max-width:340px){
  .run{
    grid-template-columns:auto 1fr;
    gap:8px;row-gap:4px;
    padding:12px 42px 12px 12px;
  }
  .run-day{grid-column:2;justify-self:start}
  .run-body{grid-column:1 / -1}
  .run-pace{grid-column:1 / -1;flex-wrap:wrap}
  .run.race .run-name{font-size:17px}
  .week-head{padding:10px 12px}
  .week-no{font-size:24px;min-width:0}
  .facts{gap:12px 16px}
  .marker{left:12px}
  .rest,.log-panel,.row-error{padding-left:12px;padding-right:12px}
  .progress-tools{margin-left:0;width:100%}
  .progress-tools .button,.progress-tools .reset{flex:1 1 auto}
  /* The pace and gym tables are two columns of prose and a number. Their
     min-content is 207 px, which is wider than the whole column and the only
     thing on the page that still forced a sideways scroll here. A description
     over its value reads the same and fits. */
  .paces,.paces tbody,.paces tr,.paces td{display:block;width:100%}
  .paces tr{border-bottom:1px solid var(--rule);padding:8px 0}
  .paces td{border-bottom:none;padding:0}
  .paces td:last-child{text-align:left;color:var(--ink-dim);margin-top:2px}
}

/* Landscape on a phone: the notch moves to the side. */
@media (max-width:900px) and (orientation:landscape){
  .masthead{padding-top:max(14px,calc(env(safe-area-inset-top) + 10px))}
}
@media (prefers-reduced-motion:reduce){
  *{transition:none!important;animation:none!important}
  .ridge .ridge-line{stroke-dashoffset:0}
  .ridge .ridge-fill,.ridge circle{opacity:1}
  .bar{transform:none}
}


/* ════════════════════════════════════════════════════════════════════
   Application chrome: forms, buttons, cards, the planner grid.
   Same tokens, same editorial voice — mono for labels and data, Karla
   for prose, Anton only where a number is the headline.
   ════════════════════════════════════════════════════════════════════ */

.masthead.compact h1{font-size:clamp(34px,7vw,54px);margin-bottom:14px}
.masthead.compact{padding-bottom:22px}
.masthead.compact .facts{padding-bottom:14px}

.brand{
  font-family:var(--display);font-size:15px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--masthead-ink)!important;border-bottom:none!important;margin-right:6px;
}
.brand span{color:var(--gold)}

.hero-actions{display:flex;flex-wrap:wrap;gap:10px;padding-bottom:24px}

/* ── Buttons ────────────────────────────── */
.button{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
  padding:10px 14px;border-radius:2px;cursor:pointer;text-decoration:none;
  border:1px solid var(--rule);background:var(--surface);color:var(--ink);
  transition:background-color .15s,border-color .15s,color .15s;
  /* 44, not 40. .button.tiny already carried 44 because the smallest type on
     the site sat under the most destructive verb; there is no reason the
     ordinary button should be the one that misses. */
  min-height:44px;
}
.button:hover{border-color:var(--ink)}
.button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.button.primary{background:var(--accent);border-color:var(--accent);color:var(--surface)}
.button.primary:hover{background:var(--river-deep);border-color:var(--river-deep)}
.button.ghost{background:none}
/* The red is the same in both themes as a fill, but as ink on the dark card it
   was 3.37:1, so the border and the letters take the theme's own step. */
.button.danger{border-color:var(--danger-ink);color:var(--danger-ink)}
.button.danger:hover{background:var(--danger);color:#fff}
.button.wide{width:100%}
/* "Tiny" is about weight, not about being hard to hit: 10 px type, 44 px box.
   The manage screen's "Delete for good" was 34 px tall — the most destructive
   verb on the site under the smallest thumb target on it. */
.button.tiny{padding:7px 12px;font-size:10px;min-height:44px}
.button-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.hero-actions .button{background:transparent;border-color:color-mix(in srgb,var(--masthead-dim) 50%,transparent);color:var(--masthead-ink)}
.hero-actions .button:hover{border-color:var(--masthead-ink)}
.hero-actions .button.primary{background:var(--gold);border-color:var(--gold);color:#241A02}
.hero-actions .button.primary:hover{background:#E0B44E;border-color:#E0B44E}

/* ── Flash and banners ──────────────────── */
.flash{
  margin:18px 0 0;padding:11px 14px;border-radius:2px;
  background:color-mix(in srgb,var(--accent) 12%,var(--surface));
  border:1px solid color-mix(in srgb,var(--accent) 34%,transparent);
  font-family:var(--mono);font-size:12px;color:var(--ink);
}
.readonly-banner{
  display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between;
  margin:22px 0 0;padding:14px 16px;border-radius:3px;
  background:color-mix(in srgb,var(--gold) 12%,var(--surface));
  border:1px solid color-mix(in srgb,var(--gold) 40%,transparent);
}
/* The only thing telling a share-link visitor that ticking is off, and it was
   2.33:1 on its own banner. */
.readonly-lead{
  margin:0 0 2px;font-family:var(--mono);font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--gold-deep);
}
.readonly-banner p{margin:0;font-size:14px}

/* ── Forms ──────────────────────────────── */
.form-card{
  background:var(--surface);border:1px solid var(--rule);border-radius:3px;
  padding:22px;margin:26px auto 0;max-width:460px;box-shadow:var(--shadow-card);
}
.form-card.wide-card{max-width:100%}
.field{display:flex;flex-direction:column;gap:5px;margin-bottom:14px}
.field>span{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-dim);
}
.field input,.field select,.field textarea{
  font-family:var(--mono);font-size:14px;color:var(--ink);
  padding:10px 11px;background:var(--surface);
  border:1px solid var(--rule);border-radius:2px;width:100%;
}
.field textarea{font-family:var(--body);resize:vertical}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:2px solid var(--accent);outline-offset:-1px
}
.field small{font-family:var(--mono);font-size:10.5px;color:var(--ink-faint)}
.field-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:0 16px}
.row-form{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end}
.row-form .field{margin-bottom:0;flex:1 1 180px}
.form-error{
  margin:0 0 16px;padding:11px 13px;border-radius:2px;
  background:color-mix(in srgb,var(--danger) 12%,var(--surface));
  border:1px solid color-mix(in srgb,var(--danger) 40%,transparent);
  color:var(--form-error-ink);font-family:var(--mono);font-size:12px;
}
.form-aside{font-size:13.5px;color:var(--ink-dim);margin:16px 0 0}
.form-aside a{color:var(--accent)}

/* ── Landing ────────────────────────────── */
.pitch{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:3px;
  margin:28px 0 30px;overflow:hidden;
}
.pitch-item{background:var(--surface);padding:20px}
.pitch-item h3{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:18px;letter-spacing:.03em;margin:0 0 8px;
}
.pitch-item p{margin:0;font-size:14.5px;color:var(--ink-dim)}

.template-card{
  display:flex;flex-wrap:wrap;gap:16px;align-items:center;justify-content:space-between;
  background:var(--surface);border:1px solid var(--rule);border-radius:3px;
  padding:18px;margin-bottom:14px;
}
.template-card h3{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:19px;letter-spacing:.03em;margin:0 0 6px;
}
.template-card p{margin:0 0 6px;font-size:14px;color:var(--ink-dim);max-width:52ch}
.template-meta{font-family:var(--mono);font-size:11.5px;color:var(--ink-faint)}
.template-form{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap}
.template-form .field{margin-bottom:0}

/* ── Plan list ──────────────────────────── */
.plan-list{margin-top:26px}
.plan-card{
  display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between;
  background:var(--surface);border:1px solid var(--rule);border-left:3px solid var(--rule);
  border-radius:3px;padding:16px 18px;margin-bottom:12px;
}
.plan-card.active{border-left-color:var(--accent)}
.plan-card h3{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:21px;letter-spacing:.03em;margin:0 0 5px;
}
.plan-card h3 a{color:var(--ink);text-decoration:none}
.plan-card h3 a:hover{color:var(--accent)}
.plan-card-meta{margin:0;font-family:var(--mono);font-size:11.5px;color:var(--ink-dim)}
.plan-card-actions{display:flex;flex-wrap:wrap;gap:8px}

/* ── Planner ────────────────────────────── */
.planner-hint{
  font-family:var(--mono);font-size:11.5px;color:var(--ink-dim);
  margin:20px 0 18px;line-height:1.6;
}
.planner-week{scroll-margin-top:16px}
.planner-rows{padding:4px 0}
.planner-row{
  display:flex;flex-wrap:wrap;gap:6px;align-items:center;
  padding:8px 14px;border-bottom:1px solid var(--rule-soft);
}
.planner-row:last-child{border-bottom:none}
.planner-row.gym{background:var(--surface-2)}
.planner-row input,.planner-row select{
  font-family:var(--mono);font-size:12.5px;color:var(--ink);
  padding:7px 8px;background:var(--surface);border:1px solid var(--rule);border-radius:2px;
  /* The planner is a form of forty small fields and the temptation is to shrink
     them to fit more week on screen. A date, a kind and a distance are still
     tapped with a thumb. */
  min-height:44px;
}
.planner-row input:focus-visible,.planner-row select:focus-visible{
  outline:2px solid var(--accent);outline-offset:-1px
}
.cell-date{width:132px;flex:none}
.cell-kind{width:94px;flex:none}
.cell-title{width:112px;flex:none}
.cell-detail{flex:1 1 90px;min-width:0}
.cell-num{width:64px;flex:none}
.cell-pace{width:58px;flex:none}
/* Wide enough for a whole row on one line: anything narrower wraps, which is
   what the phone wants anyway. */
@media (min-width:760px){
  .planner-row{flex-wrap:nowrap}
  .planner-row .button.tiny{white-space:nowrap}
}
.cell-unit{font-family:var(--mono);font-size:11px;color:var(--ink-dim)}
.planner-add-label{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-dim);width:46px;
}
.planner-empty{
  margin:0;padding:12px 14px;font-family:var(--mono);font-size:11.5px;color:var(--ink-faint);
}
.planner-add{border-top:1px solid var(--rule);background:var(--surface-2)}
/* A summary is a control, and this one is the only way to add a session to a
   week, so it gets the same 44 as a button rather than the height its 11px of
   type happens to need. */
.planner-add>summary{
  padding:10px 14px;font-family:var(--mono);font-size:11px;color:var(--ink-dim);
  min-height:44px;display:flex;align-items:center;
}
.planner-add-forms{padding-bottom:8px}
.planner-add-forms .planner-row{background:none}
.planner-add-forms form:last-child{padding:10px 14px}
.planner-append{margin-top:18px}

.share-url{
  margin:0 0 8px;padding:11px 12px;border-radius:2px;overflow-wrap:anywhere;
  background:var(--surface-2);border:1px dashed var(--rule);
}
.share-url code{font-family:var(--mono);font-size:12.5px;color:var(--ink)}
.danger-zone{border-color:color-mix(in srgb,var(--danger) 40%,var(--rule))}

.progress-tools{display:flex;gap:8px;align-items:center;margin-left:auto}
.progress-tools .reset{margin-left:0}

/* ── Footer ─────────────────────────────── */
.site-foot{border-top:1px solid var(--rule);background:var(--surface-2);flex:none}
.site-foot-inner{
  max-width:820px;margin:0 auto;
  padding:16px var(--gutter-r) calc(16px + env(safe-area-inset-bottom)) var(--gutter);
  display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-faint);
}
.site-foot-inner a{color:var(--ink-dim)}

@media (max-width:560px){
  .planner-row{gap:5px;padding:10px 12px}
  /* 16px text needs more room than the desktop widths allow. */
  .cell-date{flex:1 1 158px;width:auto;min-width:158px}
  .cell-kind{flex:1 1 100px;width:auto}
  .cell-num,.cell-pace{flex:0 0 auto;width:66px}
  .cell-title,.cell-detail{flex:1 1 100%;min-width:0}
  .planner-row input,.planner-row select{font-size:16px}
  .field input,.field select,.field textarea{font-size:16px}
  .plan-card-actions{width:100%}
  .plan-card-actions .button{flex:1 1 auto}
  .template-card,.plan-card{padding:14px}
  .form-card{padding:16px}
}


/* ════════════════════════════════════════════════════════════════════
   Habits. The same editorial furniture as the plan — hairline cards,
   mono labels, Anton only where a number is the headline — with one new
   component: the tick, which is the whole point of the front page and so
   is the largest, closest thing on it.

   The heatmap tokens sit with the rest of the palette rather than in the
   SVG, so the theme switch repaints twelve weeks with no redraw.
   ════════════════════════════════════════════════════════════════════ */

:root{
  --hm-met:#2E6E8E;
  --hm-due:#DDE3E4;
  --hm-off:#F0F3F2;
}
:root[data-theme="dark"]{
  --hm-met:#6BB0D0;
  --hm-due:#26304A;
  --hm-off:#1A2239;
}
/* Copied for the same reason as the palette above, and held to it by the same
   test. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme]){
    --hm-met:#6BB0D0;
    --hm-due:#26304A;
    --hm-off:#1A2239;
  }
}

/* ── Groups ─────────────────────────────── */
.habit-group{margin-top:26px}
.habit-group-head{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:20px;letter-spacing:.03em;margin:0 0 4px;
  padding-bottom:8px;border-bottom:2px solid var(--ink);
}
.habit-group-sub,.habit-none{
  font-family:var(--mono);font-size:11.5px;color:var(--ink-dim);margin:10px 0 0;
}
.habit-none{margin:14px 0 0}

/* ── One habit, one tap ─────────────────── */
.habit{
  display:flex;gap:14px;align-items:flex-start;
  background:var(--surface);border:1px solid var(--rule);border-radius:3px;
  padding:14px;margin-top:12px;box-shadow:var(--shadow-card);
  transition:border-color .2s var(--ease-out),background-color .2s var(--ease-out);
}
.habit.is-done{border-color:color-mix(in srgb,var(--accent) 45%,var(--rule))}
/* Not due today: quiet, and never crossed through. Nothing here is missed. */
.habit.is-resting{background:var(--surface-2);border-style:dashed;opacity:.82}

.habit-tick{flex:none;margin:0}
.habit-mark{
  width:46px;height:46px;padding:0;display:flex;align-items:center;justify-content:center;
  border:1.5px solid var(--checkbox-border);border-radius:8px;
  background:var(--surface);color:transparent;cursor:pointer;
  transition:background-color .18s var(--ease-out),border-color .18s var(--ease-out),
             color .18s var(--ease-out),transform .12s var(--ease-out);
}
.habit-mark svg{width:24px;height:24px;display:block}
.habit-mark:hover{border-color:var(--accent);color:color-mix(in srgb,var(--accent) 40%,transparent)}
.habit-mark:active{transform:scale(.94)}
.habit-mark:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.habit-mark[aria-pressed="true"]{
  background:var(--accent);border-color:var(--accent);color:var(--check-ink);
}
.habit.is-resting .habit-mark[aria-pressed="true"]{
  background:var(--ink-dim);border-color:var(--ink-dim);
}

.habit-body{flex:1;min-width:0}
.habit-name{
  display:inline-block;font-size:16px;font-weight:700;line-height:1.25;
  color:var(--ink);text-decoration:none;
}
.habit-name:hover{color:var(--accent)}
.habit-name:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.habit-sub{margin:3px 0 0;font-family:var(--mono);font-size:11px;color:var(--ink-dim)}
.habit-recent{margin:5px 0 0;font-family:var(--mono);font-size:12px;color:var(--ink)}
.habit.is-resting .habit-recent{color:var(--ink-dim)}
.habit-dot{padding:0 6px;color:var(--ink-faint)}
.habit-flag{
  display:inline-block;margin-left:8px;padding:1px 6px;border-radius:2px;
  font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-dim);border:1px solid var(--rule);
}
.habit-note{margin:0;font-size:14.5px;color:var(--ink-dim);max-width:60ch}

.habit-amount{margin-top:8px}
.habit-amount>summary{font-size:11px;letter-spacing:.1em;text-transform:uppercase}
.habit-amount-form{display:flex;gap:10px;align-items:flex-end;margin-top:8px}
.habit-amount-form .field{margin-bottom:0;width:120px}
.habit-today .habit-amount-form{margin-top:0}

/* ── The seven-day strip ────────────────── */
.strip{
  display:grid;grid-template-columns:repeat(7,1fr);gap:6px;
  list-style:none;margin:0;padding:0;
}
.strip-day{display:flex;flex-direction:column;align-items:center;gap:5px}
.strip-bar{
  position:relative;width:100%;height:54px;border-radius:2px;
  background:var(--hm-off);border:1px solid var(--rule);overflow:hidden;
}
.strip-fill{
  position:absolute;left:0;right:0;bottom:0;height:var(--fill,0%);
  background:var(--accent);
  transition:height .4s var(--ease-out);
}
.strip-day.is-today .strip-bar{border-color:var(--gold)}
.strip-num{
  font-family:var(--mono);font-size:10.5px;color:var(--ink-dim);
  font-variant-numeric:tabular-nums;
}
.strip-lab{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink-faint);
}
.strip-day.is-today .strip-lab{color:var(--gold-deep)}

/* ── The twelve-week calendar ───────────── */
.heatmap{display:block;width:100%;max-width:420px;height:auto}
.heatmap .hm-ax{
  font-family:var(--mono);font-size:9px;fill:var(--ink-faint);
  letter-spacing:.1em;text-transform:uppercase;
}
.heatmap .hm-cell{stroke:var(--surface);stroke-width:1}

/* ── Manage ─────────────────────────────── */
.manage-row{
  background:var(--surface);border:1px solid var(--rule);border-radius:3px;
  padding:14px 16px;margin-top:12px;
}
.manage-row.is-archived{background:var(--surface-2);border-style:dashed}
.manage-head{display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:space-between}
.manage-main{min-width:0}
.manage-row h3{
  font-family:var(--display);font-weight:400;text-transform:uppercase;
  font-size:19px;letter-spacing:.03em;margin:0 0 4px;
}
.manage-row h3 a{color:var(--ink);text-decoration:none}
.manage-row h3 a:hover{color:var(--accent)}
.manage-meta{margin:0;font-family:var(--mono);font-size:11px;color:var(--ink-dim)}
.manage-note{margin:10px 0 0;font-size:13.5px;color:var(--ink-dim);max-width:58ch}
.manage-actions{display:flex;flex-wrap:wrap;gap:8px}
.manage-actions form{margin:0}
.manage-edit{margin-top:10px;border-top:1px solid var(--rule-soft);padding-top:10px}
.manage-edit>summary{font-size:11px;letter-spacing:.1em;text-transform:uppercase}

.habit-form{margin-top:14px}
.weekday-set{
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  border:1px solid var(--rule);border-radius:2px;padding:10px 12px;margin:0 0 14px;
}
.weekday-set legend{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-dim);padding:0 6px;
}
.weekday{
  display:flex;align-items:center;gap:5px;
  font-family:var(--mono);font-size:11px;color:var(--ink-dim);cursor:pointer;
}
.weekday input{width:16px;height:16px;accent-color:var(--accent);margin:0}

@media (max-width:560px){
  /* The tick keeps its 46 px at every width — it is the one control that has
     to be hittable with a thumb while holding a water bottle. */
  .habit{padding:12px;gap:12px}
  .habit-name{font-size:15.5px}
  .habit-amount-form{flex-wrap:wrap}
  .habit-amount-form .field{width:100%}
  .habit-amount-form .button{width:100%}
  .strip{gap:4px}
  .strip-bar{height:46px}
  .strip-num{font-size:9.5px}
  .manage-actions{width:100%}
  .manage-actions form{flex:1 1 auto}
  .manage-actions .button{width:100%}
  .heatmap{max-width:100%}
}
