
:root {
  --ink:#0E1319; --surface:#171E27; --raised:#202A36; --line:#2C3846;
  --text:#DDE4EA; --muted:#8798A6; --signal:#E0A356;
  --good:#5FB08A; --bad:#D2735F; --band:#2C3846;
}
* { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html { -webkit-text-size-adjust:100%; }
body {
  margin:0; background:var(--ink); color:var(--text);
  font-family:"Source Sans 3",system-ui,-apple-system,"Segoe UI",sans-serif;
  font-size:15px; line-height:1.5;
  padding-bottom:env(safe-area-inset-bottom);
}
a { color:var(--signal); text-decoration:none; }

header {
  position:sticky; top:0; z-index:10; background:var(--ink);
  border-bottom:1px solid var(--line);
  padding:calc(.75rem + env(safe-area-inset-top)) 1rem .75rem;
  display:flex; align-items:center; gap:.75rem;
}
/* The same lockup as the landing page: mark, then the word, flush left.
   The h1 is both the flex item that pushes the meta links right and the row
   holding the two together. */
header h1 { font-size:1rem; font-weight:600; margin:0; letter-spacing:-.01em; flex:1; }

/* The lockup is a button because on an installed PWA there is no address bar and
   no pull-to-refresh, so there was no way to reload short of closing the app.
   Styled to look exactly as it did — this is a control that had to already be
   somewhere obvious, not a new piece of furniture. */
.brand { display:flex; align-items:center; gap:.5rem; background:none; border:0; padding:0;
         margin:0; font:inherit; color:inherit; letter-spacing:inherit; cursor:pointer; }
.brand:active { opacity:.55; }

/* Paint from the tokens, so the mark cannot drift from the palette the way a
   second copy of two hex values would. flex:none because an SVG with no
   intrinsic size is otherwise squashed by a long neighbour.

   1.5rem against the landing page's 1.75rem, and .5rem of gap against its .6rem
   — the one place symmetry is traded deliberately. That bar is seen once at the
   top of a page; this one is fixed to the top of a phone, and matching it exactly
   costs about 12px of height on every screen. */
.mark { width:1.5rem; height:1.5rem; display:block; flex:none; }
.mark .tile { fill:var(--surface); }
.mark .wave { fill:none; stroke:var(--signal); }
header .meta { color:var(--muted); font-size:.8125rem; padding:.25rem .1rem; white-space:nowrap; }
header .meta.on { color:var(--signal); font-weight:600; }

nav { display:flex; gap:.25rem; overflow-x:auto; padding:.5rem 1rem; border-bottom:1px solid var(--line);
      scrollbar-width:none; }
nav::-webkit-scrollbar { display:none; }
nav a {
  padding:.4rem .75rem; border-radius:999px; font-size:.8125rem; white-space:nowrap;
  color:var(--muted); background:var(--surface);
}
nav a.on { background:var(--signal); color:var(--ink); font-weight:600; }
/* Groups the four data screens apart from the daily action and from Notes. */
nav .nav-sep { flex:0 0 1px; align-self:center; height:1.1rem; background:var(--line); margin:0 .4rem; }

main { padding:1rem; max-width:56rem; margin:0 auto; }
.card { background:var(--surface); border-radius:12px; padding:1rem; margin-bottom:.875rem; }
.card h2 { font-size:.9375rem; margin:0 0 .125rem; font-weight:600; }
.card .sub { color:var(--muted); font-size:.8125rem; margin:0 0 .75rem; }
.card .note { color:var(--muted); font-size:.75rem; margin-top:.5rem; line-height:1.45; }

.pill { display:inline-block; padding:.1rem .45rem; border-radius:999px; font-size:.6875rem;
        font-weight:600; vertical-align:middle; }
.pill.away { background:rgba(210,115,95,.18); color:var(--bad); }
.pill.toward { background:rgba(95,176,138,.18); color:var(--good); }
.pill.within { background:var(--raised); color:var(--muted); }

/* The movers, named under the count that totals them. Chips rather than a
   sentence: they are targets, and a comma-separated list of forty-character
   marker names is not something to hit with a thumb. */
.movers { display:flex; flex-wrap:wrap; gap:.375rem; margin:.625rem 0 0; }
.movers button {
  background:var(--raised); color:var(--signal); border:0; border-radius:999px;
  padding:.3rem .7rem; font-size:.75rem; font-family:inherit; font-weight:600;
  cursor:pointer; text-align:left;
}
.movers button:active { opacity:.6; }
/* Jumping to a card puts its title under the sticky header without this. */
.card { scroll-margin-top:4rem; }

/* ------------------------------------------------------------ importing */
/* The real file input, kept in the DOM and reachable but out of the way: the
   visible control is a button, and display:none on an input is a control some
   browsers will not open. */
.file-hidden { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
.imp { border-top:1px solid var(--line); padding:.6rem 0; }
.imp.first { border-top:0; padding-top:.25rem; }
.impk { font-weight:600; font-size:.875rem; }
.impv { color:var(--muted); font-size:.8125rem; }
.imp details { margin-top:.35rem; }
.imp summary { color:var(--signal); font-size:.75rem; cursor:pointer; }
.impval { display:flex; justify-content:space-between; align-items:baseline; gap:.75rem;
          font-size:.75rem; color:var(--muted); padding:.15rem 0; }
.impval b { color:var(--text); font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* A value the lab itself flagged. The colour is the lab's judgement, not ours. */
.impval.abn b { color:var(--bad); }
.pwrow { display:flex; gap:.5rem; align-items:center; margin:.25rem 0 .5rem; }
.pwrow input {
  flex:1; min-width:0; padding:.55rem .7rem; border:1px solid var(--line); border-radius:8px;
  background:var(--ink); color:var(--text); font-size:1rem; font-family:inherit;
}
.row-actions { display:flex; gap:.5rem; align-items:stretch; margin-top:.875rem; }
.row-actions button.primary { margin-top:0; flex:1; width:auto; }
button.quiet {
  background:var(--raised); color:var(--muted); border:0; border-radius:8px;
  padding:.7rem .9rem; font-family:inherit; font-size:.875rem; cursor:pointer;
}

.ranges { display:flex; gap:.25rem; margin-bottom:.75rem; flex-wrap:wrap; }
.ranges button {
  background:var(--raised); color:var(--muted); border:0; border-radius:999px;
  padding:.3rem .7rem; font-size:.75rem; font-family:inherit; cursor:pointer;
}
.ranges button.on { background:var(--signal); color:var(--ink); font-weight:600; }

.chart { width:100%; touch-action:pan-y; display:block; }
.chart .band { fill:var(--band); opacity:.5; }
.chart .grid { stroke:var(--line); stroke-width:1; }
.chart .axis { fill:var(--muted); font-size:10px; }
.chart .ln { fill:none; stroke:var(--signal); stroke-width:2; stroke-linejoin:round; stroke-linecap:round; }
.chart .dot { fill:var(--signal); }
/*
 * Hidden by class, not by an inline style attribute.
 *
 * The markup carried style="display:none" and the CSP is style-src 'self',
 * which blocks inline style attributes — so it never applied, and a dashed
 * vertical line has been sitting at x=0 on every chart since this was written.
 * Subtle enough to read as part of the axis, which is why it survived.
 */
.chart .cursor { stroke:var(--muted); stroke-width:1; stroke-dasharray:3 3; display:none; }
.chart .cursor.on { display:block; }
.chart .hit { fill:transparent; }

.readout { display:flex; justify-content:space-between; align-items:baseline; gap:.5rem;
           font-size:.8125rem; color:var(--muted); min-height:1.25rem; margin-top:.375rem; }
.readout b { color:var(--text); font-size:1rem; font-weight:600; }

.grid2 { display:grid; grid-template-columns:repeat(auto-fit,minmax(8rem,1fr)); gap:.5rem; }
.stat { background:var(--raised); border-radius:8px; padding:.625rem .75rem; }
/* Direction of travel under a stat. Muted by default: most of these numbers have
   no agreed good direction, and colouring them would assert one. */
.stat .t { font-size:.75rem; margin-top:.15rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.stat .t.good { color:var(--good); }
.stat .t.bad { color:var(--bad); }
.stat .k { color:var(--muted); font-size:.6875rem; text-transform:uppercase; letter-spacing:.04em; }
.stat .v { font-size:1.25rem; font-weight:600; margin-top:.125rem; }
.stat .d { color:var(--muted); font-size:.75rem; }

/* A day of stress, or a night of sleep, as one bar of its own parts.

   The comparison these exist for is between the segments, not against a scale:
   how much of the day was rest against how much was high, how much of the night
   was deep. A row of four numbers makes that arithmetic; a bar makes it a shape,
   which is the whole reason this screen is worth having over Garmin's own.

   Colour carries meaning here where it does not elsewhere in the app: the
   segments are ordered from calm to hard and shaded that way, so the bar reads
   left to right without a legend being the only key. Amber is deliberately
   absent — it is reserved for the day being logged and for the button that
   writes, and a chart segment borrowing it would read as something actionable.

   Named segbar and NOT stack, which is what it was called first. A rule for
   .stat.stack already existed and means the opposite thing — a note row whose
   children are stacked vertically — so a bare .stack rule matched every one of
   those too and squashed each note, blood reminder and draw conflict into a 12px
   flex strip with its text clipped mid-glyph. Three screens broke from one word,
   and nothing could fail: the type checker sees a string and the screen being
   worked on looked perfect. The guard is in client-assets.test.ts. */
.segbar { display:flex; width:100%; height:.75rem; border-radius:999px; overflow:hidden;
          background:var(--raised); margin:.15rem 0 .5rem; }
.segbar span { display:block; height:100%; min-width:1px; }
.sg-rest { background:#4E6F8C; }
.sg-low { background:#6E8FA8; }
.sg-med { background:#B58A63; }
.sg-high { background:var(--bad); }
.sg-act { background:#7E93C8; }
.sg-other { background:var(--line); }
.sg-deep { background:#41638A; }
.sg-light { background:#7E93C8; }
.sg-rem { background:#9C86C0; }
.sg-awake { background:var(--line); }
.keys { display:flex; flex-wrap:wrap; gap:.5rem .9rem; margin:.1rem 0 0; }
.key { display:inline-flex; align-items:center; gap:.3rem; color:var(--muted); font-size:.6875rem; }
.key i { width:.55rem; height:.55rem; border-radius:2px; display:inline-block; }

/* The session on the calendar, above the check-in that asks about it.

   Every class here is prefixed, and the step list is .wo-steps rather than
   .steps, after a bare .stack rule once restyled every note on three screens.

   ZONE COLOUR IS THE ONE PLACE THE PALETTE IS NOT OURS. It arrives in the
   workout doc from intervals.icu — green through red through purple, saturated
   in a way nothing else in this app is, and Z3 and Z4 land close to the amber
   that is otherwise reserved for the day being logged and the button that
   writes. That is a real cost and it is paid on purpose: this card exists so he
   does not have to open intervals.icu at check-in time, and a card whose zones
   are a different colour from the ones he has been reading for two years is a
   card he checks against the original, which is the whole thing it was meant to
   save. It is confined to a labelled chart and its key.

   None of those colours are written here, because they come from the data.
   They are assigned through the CSSOM for the same reason the segment widths
   are — style-src is 'self', so an inline style attribute never applies. */
.wo-meta { display:flex; flex-wrap:wrap; gap:.2rem .55rem; color:var(--muted);
           font-size:.75rem; margin:.1rem 0 .7rem; }
.wo-meta b { color:var(--text); font-weight:600; font-variant-numeric:tabular-nums; }
.wo-meta .done { color:var(--good); }
.wochart { width:100%; height:auto; display:block; margin:.2rem 0 .55rem; }
/* Hairline in the card colour so touching blocks read as separate steps. */
.wo-blk { stroke:var(--surface); stroke-width:1; }
.wo-ftp { stroke:var(--muted); stroke-width:1; stroke-dasharray:3 3; }
.wo-base { stroke:var(--line); stroke-width:1; }
.wo-ax { fill:var(--muted); font-size:11px; }
.wo-steps { list-style:none; margin:.65rem 0 0; padding:0; }
.wo-steps li { display:flex; gap:.5rem; padding:.4rem 0; border-top:1px solid var(--line); }
.wo-steps li:first-child { border-top:0; }
.wo-sw { flex:none; width:.25rem; border-radius:2px; background:var(--line); }
.wo-body { flex:1; min-width:0; font-size:.8125rem; line-height:1.45; }
.wo-dur { font-weight:600; font-variant-numeric:tabular-nums; }
.wo-tgt { color:var(--text); }
.wo-sub { color:var(--muted); }
.wo-say { color:var(--muted); font-size:.75rem; display:block; }
/* One round of a repeat, indented under the count rather than printed ten times. */
.wo-reps { list-style:none; margin:.3rem 0 0; padding:0 0 0 .6rem; border-left:1px solid var(--line); }
.wo-reps li { border-top:0; padding:.15rem 0; }
/* The coaching, not the recipe. Preserves the paragraph breaks it was written with. */
.wo-prose { white-space:pre-wrap; font-size:.8125rem; line-height:1.5; margin:0; color:var(--text); }
.an-text { white-space:pre-line; font-size:.875rem; line-height:1.55; margin:0 0 6px; color:var(--text); }
/* The advanced analysis: a summary paragraph, then headed sections of short lines. */
.an-h { font-size:.75rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); margin:14px 0 4px; }
.an-list { margin:0 0 6px; padding-left:1.1rem; font-size:.875rem; line-height:1.5; color:var(--text); }
.an-list li { margin:0 0 3px; }
.an-inline { margin-top:12px; padding-top:10px; border-top:1px solid var(--line); }
.wo-why { margin-top:.85rem; }

/* The week so far, above the day's session. Time and load against the week's
   plan, with a tick where the plan expects him by the end of the day being
   logged. No percentage on purpose — progress against the shape, not a score.
   The fill stops at the end of the track when a week runs over; the figures
   beside it still say by how much. Widths are set through the CSSOM because
   style-src is 'self'. */
.wk-row { display:grid; grid-template-columns:2.6rem 1fr auto; gap:.6rem; align-items:center;
          margin:.35rem 0; font-size:.8125rem; }
.wk-lab { color:var(--muted); }
.wk-track { position:relative; height:.75rem; border-radius:999px; background:var(--raised); }
.wk-fill { position:absolute; left:0; top:0; bottom:0; border-radius:999px; background:#7E93C8; }
.wk-tick { position:absolute; top:-.2rem; bottom:-.2rem; width:2px; margin-left:-1px;
           border-radius:1px; background:var(--text); }
.wk-val { color:var(--text); font-variant-numeric:tabular-nums; white-space:nowrap; }
.wk-val b { font-weight:600; }

/* Plan: the week, one card per day. Prefixed pl- like the session card's wo-,
   so a bare class here cannot restyle another screen. */
.pl-head { display:flex; align-items:center; gap:.5rem; }
.pl-head > div { flex:1; text-align:center; }
.pl-head h2 { margin:0; }
.pl-head .sub { margin:0; }
.pl-nav { background:var(--raised); color:var(--text); border:0; border-radius:999px;
          width:2.25rem; height:2.25rem; font-size:1.1rem; cursor:pointer; }
.pl-tot { display:flex; flex-wrap:wrap; gap:.25rem 1rem; justify-content:center; margin-top:.6rem;
          font-size:.8125rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.pl-tot b { color:var(--text); font-weight:600; }
.pl-jump { display:block; margin:.6rem auto 0; }
.pl-day.past { opacity:.72; }
.pl-day.today { box-shadow:inset 3px 0 0 var(--signal); }
.pl-dh { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; }
.pl-dh h2 { margin:0; }
.pl-dh .sub { margin:0; }
.pl-dh .pl-now { color:var(--signal); font-size:.75rem; font-weight:600; }
.pl-wx { margin:.3rem 0 0; font-size:.75rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.pl-wx .pl-good { color:var(--good); font-weight:600; margin-left:.25rem; }
.pl-wx.notable { color:var(--text); }
.pl-tags { display:flex; flex-wrap:wrap; gap:.3rem; margin-top:.4rem; }
.pl-tag { font-size:.6875rem; font-weight:600; padding:.12rem .5rem; border-radius:999px;
          background:var(--raised); color:var(--muted); }
.pl-tag.tx { background:rgba(210,115,95,.18); color:var(--bad); }
.pl-tag.med { background:rgba(224,163,86,.16); color:var(--signal); }
.pl-rows { list-style:none; margin:.5rem 0 0; padding:0; }
.pl-rows li { border-top:1px solid var(--line); }
.pl-rows li:first-child { border-top:0; }
.pl-row { display:flex; gap:.5rem; align-items:baseline; width:100%; padding:.5rem 0; background:none;
          border:0; color:var(--text); font:inherit; text-align:left; cursor:pointer; }
.pl-row .pl-name { flex:1; font-size:.875rem; }
.pl-row .pl-fig { color:var(--muted); font-size:.75rem; white-space:nowrap; font-variant-numeric:tabular-nums; }
.pl-row .pl-ok { color:var(--good); font-size:.75rem; white-space:nowrap; }
.pl-row.static { cursor:default; }
.pl-empty { color:var(--muted); font-size:.8125rem; margin:.4rem 0 0; }
.pl-mode { justify-content:center; margin:.75rem 0 0; }
main.wide { max-width:84rem; }
/* The month grid: seven days and the week's totals per row. */
.pm-grid { display:grid; grid-template-columns:repeat(7, minmax(0, 1fr)) 7.5rem; gap:4px; padding:.75rem; }
.pm-h { font-size:.6875rem; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; padding:0 .3rem .25rem; }
.pm-cell { display:flex; flex-direction:column; gap:.2rem; align-items:stretch; min-height:7.5rem; padding:.4rem;
           border-radius:8px; border:1px solid transparent; background:var(--raised); color:var(--text);
           font:inherit; text-align:left; cursor:pointer; overflow:hidden; }
.pm-cell:hover { border-color:var(--line); }
.pm-cell.out { opacity:.45; }
.pm-cell.past { background:var(--surface); }
.pm-cell.tx { box-shadow:inset 0 3px 0 var(--bad); }
.pm-cell.today { border-color:var(--signal); }
.pm-cell.sel { border-color:var(--text); }
.pm-top { display:flex; justify-content:space-between; align-items:baseline; }
.pm-d { font-size:.8125rem; font-weight:600; }
.pm-cell.today .pm-d { color:var(--signal); }
.pm-wx { font-size:.6875rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.pm-tags { display:flex; flex-wrap:wrap; gap:.2rem; }
.pm-tag { font-size:.625rem; font-weight:600; padding:.05rem .35rem; border-radius:999px; background:var(--surface); color:var(--muted);
          white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
.pm-tag.tx { background:rgba(210,115,95,.18); color:var(--bad); }
.pm-tag.med { background:rgba(224,163,86,.16); color:var(--signal); }
.pm-s { font-size:.75rem; line-height:1.25; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pm-s.done { color:var(--good); }
.pm-f { font-size:.6875rem; color:var(--muted); font-variant-numeric:tabular-nums; margin-top:-.15rem; }
.pm-n { font-size:.6875rem; color:var(--muted); font-style:italic; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pm-wk { display:flex; flex-direction:column; justify-content:center; gap:.1rem; padding:.4rem .5rem; border-radius:8px;
         font-size:.75rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.pm-wk b { color:var(--text); font-weight:600; }
.pm-wk .pm-done { color:var(--good); }
.pm-wk .pm-done b { color:var(--good); }
.pm-wk.cur { background:var(--raised); }
/* The day under the grid keeps the list's column width; at full desktop width
   the workout profile stretched into a banner. */
.pm-detail { max-width:56rem; margin:0 auto; }
.pl-detail > .card { background:transparent; padding:0 0 .5rem; margin:0; }
.pl-edit { border-top:1px solid var(--line); padding:.75rem 0 .5rem; }
/* The confirmation sheet: over everything, centred, the card style it sits in. */
.cf-wrap { position:fixed; inset:0; z-index:50; background:rgba(0,0,0,.55); display:flex;
           align-items:center; justify-content:center; padding:1rem; }
/* A column with one gap between everything, rather than each piece's own
   margins: those were written for other screens, and here they left the code
   box, the Confirm and the Cancel touching on a phone. */
.cf-sheet { width:100%; max-width:22rem; margin:0; display:flex; flex-direction:column; gap:.75rem; }
.cf-sheet h2, .cf-sheet .sub, .cf-sheet .note, .cf-sheet button.primary { margin:0; }
.cf-sheet .cf-in { font-size:1.125rem; letter-spacing:.08em; padding:.7rem; }
.cf-sheet .cf-no { width:100%; padding:.7rem; background:var(--raised); color:var(--text); font-size:.9375rem; }
/* .chip capitalises one-word labels; these are short sentences. */
.pl-edit .chip { text-transform:none; }
.pl-edit .chips + .chips, .pl-len { margin-top:.5rem; }
.pl-q { font-size:.875rem; margin:.25rem 0 .5rem; }
.pl-diff { margin:.25rem 0 .5rem; padding-left:1.1rem; font-size:.8125rem; color:var(--text); }
.pl-diff li { margin:.15rem 0; }
.pl-keep { display:flex; gap:.5rem; align-items:center; font-size:.8125rem; color:var(--muted); margin-top:.75rem; }
.pl-warn { color:var(--signal); }
.pl-edit .note { color:var(--muted); font-size:.75rem; line-height:1.45; }
.pl-edit .btn-inline { margin-top:.5rem; }
.pl-edit .pl-why { margin-top:.5rem; }

/* Today's session on the Today screen, with a button into the phone's share
   sheet. Outlined rather than amber: amber is for the day being logged and the
   button that writes, and sharing writes nothing here. */
.share-text { white-space:pre-line; font-size:.875rem; line-height:1.45; margin:.1rem 0 .7rem; }
.share-row { display:flex; align-items:center; gap:.6rem; }
.share-btn {
  background:transparent; color:var(--text); border:1px solid var(--line);
  border-radius:999px; padding:.4rem .95rem; font-size:.8125rem; font-family:inherit; cursor:pointer;
}
.share-btn:hover { border-color:var(--muted); }
.share-said { color:var(--muted); font-size:.75rem; }

/* The ride summary on the post-ride form. Reuses the session card's meta line
   and zone bar on purpose — the two screens describe the same session either
   side of it, and a plan and its ride reading differently is a lookup. */
.rs-plan { font-size:.8125rem; color:var(--muted); margin:0 0 .55rem; }
.rs-plan b { color:var(--text); font-weight:600; }
/* Marked, not hidden: a match made from the date is a guess, and an unpaired
   upload is common enough that refusing to show the target would be worse. */
.rs-guess { color:var(--muted); font-size:.6875rem; }
.rs-grid { margin-top:.7rem; }

/* The coverage line. Not an error and not styled as one: a day read at 15:38 is
   a true measurement of fifteen hours, and the only wrong thing would be to
   present it as a whole day without saying so. */
.partial { color:var(--muted); font-size:.75rem; margin:.15rem 0 0; }
.partial b { color:var(--signal); font-weight:600; }

/* Labels in the sparkline strip. Tight to the chart they name — the point of
   the strip is reading DOWN the column for the day everything moved at once,
   and a label floating between two charts belongs to neither.

   Text-coloured and ruled off above, because muted and unruled they were not
   tight to anything: eight charts in a column with a faint word between each
   pair reads as one long block, and the eye has to count to work out which
   chart "Stress average" is naming. The rule closes the previous series and the
   weight opens the next, so each label is visibly the top of its own block
   rather than something adrift between two. */
.striplab { color:var(--text); font-size:.6875rem; font-weight:600; text-transform:uppercase;
            letter-spacing:.06em; margin:1rem 0 -.25rem; padding-top:.75rem;
            border-top:1px solid var(--line); }
.striplab:first-child { margin-top:0; padding-top:0; border-top:0; }

/* Today's weather, under the session it bears on.
   Three equal blocks so the eye compares them without reading the labels, and a
   passed block is dimmed rather than removed - a row that reflows through the
   day is one he has to re-read each time, and the morning he did not ride in is
   still the context for the afternoon. */
.wx-row { display:flex; gap:.375rem; margin-top:.875rem; }
.wx-part {
  flex:1 1 0; min-width:0; padding:.5rem .4rem; border-radius:8px;
  background:var(--raised); text-align:center;
}
.wx-part.past { opacity:.45; }
.wx-part .k { font-size:.6875rem; }
.wx-temp { font-size:1.125rem; font-weight:600; margin:.125rem 0; }
.wx-part .d { font-size:.6875rem; color:var(--muted); }

.chips { display:flex; flex-wrap:wrap; gap:.375rem; }
.chip {
  flex:1 1 auto; min-width:4.5rem; padding:.55rem .5rem; border-radius:8px; border:1px solid var(--line);
  background:var(--raised); color:var(--text); font-size:.8125rem; font-family:inherit;
  cursor:pointer; text-transform:capitalize; line-height:1.2;
}
.chip.on { background:var(--signal); color:var(--ink); border-color:var(--signal); font-weight:600; }
/* A chip carrying his own sentence, not a one-word label.
   .chip capitalises, which suits "nausea" and "fatigue" and turns "yard work,
   helping my daughter with her learning to ride" into a headline he did not
   write. These chips exist precisely so the entry is in his words, so the
   words are left exactly as he typed them, and the text is allowed to wrap
   rather than being squeezed into a label-sized box. */
.chip.phrase { text-transform:none; text-align:left; white-space:normal; flex:1 1 100%; }
/* A typed answer sitting directly under the tappable ones.
   The niggle fields offer his own past phrases as chips with a free-text box
   under them for a new one, and with no gap the box read as part of the last
   chip row - one control, not two. The rule is on the pair rather than on the
   input, so the input keeps its spacing wherever it appears alone. Adjacent
   rather than inline, because inline style attributes are blocked by the CSP
   and fail silently. */
.chips + .note-input, .chips + .num-input { margin-top:.5rem; }
.ae-pick { margin-bottom:.75rem; }
.ae-pick .chip { text-transform:none; }
.note-input {
  width:100%; padding:.6rem .7rem; border-radius:8px; border:1px solid var(--line);
  background:var(--ink); color:var(--text); font-size:.9375rem; font-family:inherit; resize:vertical;
}
/* A disclosure, not an action.
   It sat directly above the day tabs in the same solid amber as the selected
   one, so a control that only opens a panel looked exactly like the choice of
   which day to log — two different kinds of thing reading as one group. Amber is
   reserved for the day being logged and for the button that writes. The body's
   top margin is the other half of it: they were touching. */
.disclose {
  background:transparent; color:var(--muted); border:1px solid var(--line);
  border-radius:999px; padding:.3rem .8rem; font-size:.8125rem;
  font-family:inherit; cursor:pointer;
}
.disclose:hover { color:var(--text); border-color:var(--muted); }
.disclose-body { margin-top:1.125rem; }

/* The forecast reads as columns of numbers across a row, so it gets a grid
   rather than stat blocks: the comparison is day against day, down the page. */
.wxrow { display:grid; grid-template-columns:6.1rem 1fr 2.6rem 3.2rem 4.2rem 3.2rem;
  gap:.4rem; align-items:baseline; padding:.4rem 0; border-bottom:1px solid var(--line);
  font-size:.8125rem; }
.wxrow:last-child { border-bottom:0; }
.wxhead { color:var(--muted); font-size:.6875rem; letter-spacing:.06em; text-transform:uppercase; }
.wxday { color:var(--muted); white-space:nowrap; }
.wxdow { color:var(--muted); }
/* The one distinction this column exists to make, so it does not need reading. */
.wxend { color:var(--text); font-weight:600; }
.wxtemp { color:var(--text); }
.wxcell { color:var(--muted); text-align:right; }
.wxfeel { color:var(--muted); }
/* The percentile chart under the ranks table. Bar is the watts percentile,
   tick is W/kg — the gap between them is what the caption describes. */
.rkchart { width:100%; height:auto; display:block; margin:.9rem 0 .25rem; }
.rk-track { fill:var(--raised); }
.rk-bar { fill:#7E93C8; }
.rk-tick { stroke:var(--text); stroke-width:2; stroke-linecap:round; }
.rk-mid { stroke:var(--line); stroke-width:1; stroke-dasharray:2 3; }
.rk-lab { fill:var(--muted); font-size:12px; }
.rk-val { fill:var(--text); font-size:12px; font-weight:600; }
.rk-ax { fill:var(--muted); font-size:10px; }
.rk-sw-bar { display:inline-block; width:12px; height:6px; border-radius:3px; background:#7E93C8; }
.rk-sw-tick { display:inline-block; width:2px; height:12px; background:var(--text); }

/* The power profile radar.

   ONE series, in the same blue the ranks bar uses, because it is the same
   quantity — percentile by watts — and colour follows the entity rather than the
   chart. The W/kg percentile is text in the tooltip and the table beside it, not
   a second outline: two overlapping polygons on nine axes muddy each other, and
   every second hue tried against this blue failed the normal-vision separation
   floor. This palette is deliberately low-chroma and it has no room for a second
   categorical hue that is still legible next to the first.

   A dot that is hollow rather than filled means the figure is the best AVERAGE
   over that span, not an effort at it — see power-profile.ts. */
.prchart { width:100%; height:auto; display:block; margin:.9rem 0 .25rem; }
.pr-web { fill:none; stroke:var(--line); stroke-width:1; }
.pr-web.mid { stroke:var(--muted); stroke-dasharray:3 3; }
.pr-spoke { stroke:var(--line); stroke-width:1; }
.pr-area { fill:#7E93C8; fill-opacity:.2; stroke:#7E93C8; stroke-width:2; stroke-linejoin:round; }
.pr-dot { fill:#7E93C8; }
.pr-dot.thin { fill:var(--surface); stroke:#7E93C8; stroke-width:2; }
.pr-lab { fill:var(--text); font-size:12px; font-weight:600; }
.pr-val { fill:var(--muted); font-size:11px; }
.pr-ring { fill:var(--muted); font-size:10px; }
.pr-best { fill:var(--muted); font-size:10px; opacity:.8; }
/* Marks sit in a strip below the plot, never over the line they explain.
   Amber is deliberately not used: it is reserved for the day being logged and
   for the button that writes, and a chart annotation borrowing it would read
   as something actionable. */
.mk { opacity:.85; }
.mk-treatment { fill:#8AA0D8; }
.mk-illness { fill:var(--bad); }
.mk-travel, .mk-work, .mk-family, .mk-other { fill:var(--muted); }
.legend { display:flex; flex-wrap:wrap; gap:.75rem; margin:.4rem 0 0; }
.lg { display:inline-flex; align-items:center; gap:.35rem; color:var(--muted); font-size:.75rem; }
.mk-sw { display:inline-block; width:12px; height:4px; border-radius:1px; }
.mk-sw.mk-treatment { background:#8AA0D8; }
.mk-sw.mk-illness { background:var(--bad); }
.mk-sw.mk-travel, .mk-sw.mk-work, .mk-sw.mk-family, .mk-sw.mk-other { background:var(--muted); }
/* Stroked, never filled: a filled glyph at 14px turns into a blob, and these
   have to read at a glance beside a number rather than be looked at. */
.wxi { width:14px; height:14px; vertical-align:-2px; fill:none; stroke:currentColor;
  stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.wxhead .wxcell, .wxhead .wxtemp { color:var(--muted); }
/* A left edge, not a fill: the row is worth a second look, it is not a warning. */
.wx-notable { box-shadow:inset 2px 0 0 0 var(--muted); padding-left:.4rem; }
/* The counterpart mark: calm, dry and temperate for here. Green because it is
   the one row on the screen that is genuinely an opportunity rather than a
   caveat, and the palette already carries --good for exactly that reading. */
.wx-good { box-shadow:inset 2px 0 0 0 var(--good); padding-left:.4rem; }
/* Today. A fill and an amber date rather than another left edge, so it stacks
   with the grey or green mark instead of replacing it. Amber because it is the
   colour this app already gives the day being lived. */
.wx-today { background:var(--raised); padding-left:.4rem; }
.wxtoday, .wxtoday .wxdow { color:var(--signal); font-weight:600; }

/* What a blood test is, opened on its own card.

   Lists rather than prose, and that is the whole design of it. Every one of
   these fields is a set of categories — things that raise a marker, things that
   lower it — and running them together into a sentence would read as a claim
   about him instead of a list of what moves the number. Bullets keep them
   visibly separate and visibly plural.

   The heading is small caps rather than a real heading because these sit inside
   a card that already has one, and a second h2 would fight it. */
.refwhat { color:var(--text); font-size:.8125rem; line-height:1.55; margin:0 0 1rem; }
.refhead {
  color:var(--muted); font-size:.6875rem; letter-spacing:.06em; text-transform:uppercase;
  margin:0 0 .3rem;
}
.reflist { margin:0 0 1rem; padding-left:1.05rem; }
.reflist li { color:var(--muted); font-size:.8125rem; line-height:1.5; margin:.2rem 0; }
.refcaution {
  color:var(--muted); font-size:.8125rem; line-height:1.5;
  border-left:2px solid var(--line); padding-left:.7rem; margin:0;
}

/* Percentiles read as columns of numbers, so they get a table rather than the
   stat blocks: the comparison is across the row, not between cards. */
.ranks { width:100%; border-collapse:collapse; font-size:.8125rem; margin-top:.25rem; }
.ranks th {
  text-align:right; color:var(--muted); font-weight:600; font-size:.6875rem;
  text-transform:uppercase; letter-spacing:.04em; padding:.25rem .35rem;
}
.ranks th:first-child, .ranks td:first-child { text-align:left; color:var(--muted); }
.ranks td {
  text-align:right; padding:.3rem .35rem; font-variant-numeric:tabular-nums;
  border-top:1px solid var(--line);
}
.ranks td:nth-child(2) { color:var(--text); font-weight:600; }

/* Two series on one set of axes. Green for the slow line and red for the fast
   one, matching the convention every training platform uses — a reader arrives
   already knowing which is which, and inventing a palette here would cost that
   for nothing. */
.overlay .ln.ctl { stroke:var(--good); }
.overlay .ln.atl { stroke:var(--bad); }
.overlay .ln.tsb { stroke:var(--text); stroke-width:1.75; }
/* Forecast, not record. Dashed and dimmed so the eye separates the two without
   having to read a caption — a projection drawn like measured data is the one
   way this chart could mislead. */
.overlay .ln.forecast { stroke-dasharray:5 4; opacity:.65; }
.overlay .today { stroke:var(--muted); stroke-width:1; stroke-dasharray:2 3; opacity:.7; }
.legend { display:flex; flex-wrap:wrap; gap:.25rem .9rem; margin:0 0 .5rem; font-size:.75rem; color:var(--muted); }
.legend span { display:inline-flex; align-items:center; gap:.35rem; }
.legend i { width:.85rem; height:0; border-top:2px solid currentColor; display:inline-block; }
.legend .ctl { color:var(--good); }
.legend .atl { color:var(--bad); }

/* TSB bands. Deliberately faint — they are a conventional reading, not this
   system's verdict, and a saturated block would argue rather than orient. */
.overlay .zone { opacity:.16; }
.overlay .zone.fresh { fill:var(--signal); }
.overlay .zone.race { fill:#4F7FB0; }
.overlay .zone.grey { fill:var(--muted); }
.overlay .zone.build { fill:var(--good); }
.overlay .zone.risk { fill:var(--bad); }

button.primary {
  width:100%; margin-top:.875rem; padding:.7rem; border:0; border-radius:8px;
  background:var(--signal); color:var(--ink); font-weight:600; font-size:.9375rem;
  font-family:inherit; cursor:pointer;
}
button.primary:disabled { opacity:.6; cursor:default; }
.empty { color:var(--muted); font-size:.875rem; padding:1.5rem 0; text-align:center; }
.err { color:var(--bad); font-size:.875rem; }
.skel { height:120px; background:linear-gradient(90deg,var(--raised),var(--surface),var(--raised));
        background-size:200% 100%; animation:sh 1.2s linear infinite; border-radius:8px; }
@keyframes sh { to { background-position:-200% 0; } }
footer { color:var(--muted); font-size:.75rem; text-align:center; padding:1rem 0 2rem; }

/* ------------------------------------------------------- expanded charts */
/*
 * A card chart is 720x240 squeezed into a phone's width, which is enough to see
 * a shape and not enough to read one. Expanding gives it the whole screen, more
 * gridlines, dated x labels and the summary figures — the same series, read
 * properly.
 */
.chart-wrap { position:relative; }
.expand {
  position:absolute; top:-.35rem; right:-.15rem; z-index:2;
  background:var(--raised); color:var(--muted); border:1px solid var(--line);
  border-radius:8px; width:2rem; height:2rem; padding:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center; line-height:1;
}
.expand:hover { color:var(--text); }
.expand svg { width:.9rem; height:.9rem; display:block; }

.sheet {
  position:fixed; inset:0; z-index:60; background:var(--ink);
  display:flex; flex-direction:column;
  padding:calc(.75rem + env(safe-area-inset-top)) .75rem calc(.75rem + env(safe-area-inset-bottom));
  opacity:0;
}
.sheet.in { opacity:1; }
@media (prefers-reduced-motion:no-preference) { .sheet { transition:opacity .15s; } }
.sheet-head { display:flex; align-items:flex-start; gap:.75rem; margin-bottom:.5rem; }
.sheet-head h2 { font-size:1rem; font-weight:600; margin:0; flex:1; line-height:1.3; }
.sheet-head .sub { color:var(--muted); font-size:.8125rem; margin:.15rem 0 0; }
.sheet-close {
  background:var(--raised); border:1px solid var(--line); color:var(--muted);
  border-radius:8px; width:2.25rem; height:2.25rem; font-size:1.25rem; line-height:1;
  cursor:pointer; flex:none;
}
.sheet-close:hover { color:var(--text); }
/*
 * The chart takes whatever is left after the header and the figures.
 *
 * height:100% rather than auto, because the SVG carries preserveAspectRatio
 * "none" and would otherwise size itself from its 720x420 viewBox — on a
 * portrait phone that is about 210px of chart in a full-screen view, which is
 * not an expanded chart at all.
 */
.sheet-body { flex:1; min-height:0; display:flex; flex-direction:column; }
.sheet-body .chart { flex:1; width:100%; height:100%; min-height:0; }
.sheet-body .readout { flex:none; }
.sheet-stats {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(4.5rem,1fr)); gap:.4rem;
  margin-top:.6rem;
}
.sheet-stats .stat { padding:.45rem .5rem; }
.sheet-stats .v { font-size:1rem; }
.sheet-hint { color:var(--muted); font-size:.75rem; text-align:center; margin:.5rem 0 0; }

/* ------------------------------------------------------- notes and ahead */
/*
 * These were inline style attributes and therefore never applied: the CSP is
 * style-src 'self'. The Notes screen and the Ahead card have been rendering
 * unstyled since they were written — no weight on the titles, no spacing
 * between rows, no strikethrough on completed notes, and no warning border on
 * a draw conflict. It read as plain rather than as broken, which is why it
 * went unnoticed.
 */
.stat.stack { margin-bottom:.5rem; }
.stat.tight { margin-bottom:.375rem; }
.row-title { font-size:.9375rem; font-weight:600; line-height:1.4; }
.row-title.done { opacity:.55; text-decoration:line-through; }
.row-note { margin-top:.25rem; }
.row-note.wrap { line-height:1.45; }
/* A conflict is the one row on this screen that wants the eye. */
.stat.alert { border-left:3px solid var(--bad); padding-left:.625rem; }

/* ------------------------------------------------------ the Today alerts */
/* The one card on Today that is meant to interrupt, so it is the one card that
   is not the plain surface colour. Signal, not bad: a thing to do, not a fault. */
.alert-card { border:1px solid var(--signal); box-shadow:inset 4px 0 0 var(--signal); }
.alert-card h2 { color:var(--signal); }
/* The pre-ride check. Signal-edged only when it suggests easing; on an ordinary
   morning it is a plain card, so the edge keeps meaning something. */
.pr-card.pr-flag { box-shadow:inset 4px 0 0 var(--signal); }
.pr-text { margin:.35rem 0; line-height:1.45; }
.pr-read summary { color:var(--signal); font-size:.75rem; cursor:pointer; }
.pr-read ul { margin:.4rem 0 0; padding-left:1.1rem; }
.pr-read li { margin:.2rem 0; color:var(--muted); }
.pr-read li.pr-off { color:var(--text); }
.al-item { border-top:1px solid var(--line); padding:.7rem 0 .2rem; }
.al-item.first { border-top:0; padding-top:.1rem; }
.al-item details, .al-why details { margin-top:.35rem; }
.al-item summary, .al-why summary { color:var(--signal); font-size:.75rem; cursor:pointer; }
.al-actions { display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.55rem; }
.al-actions button {
  background:var(--raised); color:var(--text); border:0; border-radius:999px;
  padding:.4rem .85rem; font-size:.8125rem; font-family:inherit; cursor:pointer;
}
.al-actions button.go { background:var(--signal); color:var(--ink); font-weight:600; }
.al-actions button:disabled { opacity:.5; }
.al-said { color:var(--muted); font-size:.8125rem; }
/* The words for the lab form. Big enough to read out at the counter. */
.ask-row { display:flex; align-items:center; justify-content:space-between; gap:.6rem;
           padding:.45rem 0; border-top:1px solid var(--line); }
.ask-name { font-weight:600; font-size:1rem; }
.ask-row.done .ask-name { text-decoration:line-through; opacity:.55; }
.ask-row .al-actions { margin-top:0; flex:none; }

/* ---------------------------------------------------------------- strength */
/* Stick-figure diagrams. Strokes from the palette so they follow the theme. */
.ex { border-top:1px solid var(--line); padding:.8rem 0 .4rem; }
.ex-figs { display:flex; align-items:center; gap:.35rem; margin-bottom:.4rem; }
.fig-svg { width:46%; max-width:11rem; height:auto; background:var(--raised); border-radius:8px; }
.ex-arrow { color:var(--muted); font-size:1.1rem; }
.fig { stroke:var(--text); stroke-width:2.6; stroke-linecap:round; stroke-linejoin:round; fill:none; }
.fig2 { stroke:var(--muted); stroke-width:2; }
.fig-floor { stroke:var(--line); stroke-width:1.2; }
.fig-prop { stroke:var(--muted); stroke-width:1.2; fill:none; }
.fig-band { stroke:var(--signal); stroke-width:1.4; stroke-dasharray:3 2; }
.ex-cues { margin:.35rem 0 .35rem 1.1rem; padding:0; font-size:.8125rem; line-height:1.45; }
.ex-cues li { margin-bottom:.2rem; }
.field-row { margin-bottom:.875rem; }
.num-input {
  width:100%; padding:.55rem .7rem; border-radius:8px; border:1px solid var(--line);
  background:var(--ink); color:var(--text); font-size:1rem; font-family:inherit;
}
.why { color:var(--muted); font-size:.75rem; margin:.15rem 0 .35rem; line-height:1.4; }
/* Used on both a button and an anchor, hence the two properties a button gets
   for free: an inline anchor ignores vertical padding and arrives underlined. */
.btn-inline {
  display:inline-block; text-decoration:none;
  padding:.5rem .9rem; border:0; border-radius:8px; background:var(--signal);
  color:var(--ink); font-weight:600; font-family:inherit; font-size:.875rem; cursor:pointer;
}
.report-link { margin-top:.75rem; }

/* ----------------------------------------------------------------- notice */
/* Sits above the view rather than replacing it, so what he tapped through to
   see is behind it and one tap away. */
.notice-wrap {
  position:fixed; inset:0; z-index:50;
  display:flex; align-items:flex-start; justify-content:center;
  padding:calc(1rem + env(safe-area-inset-top)) 1rem 1rem;
  background:rgba(14,19,25,.72); opacity:0;
}
.notice-wrap.in { opacity:1; }
.notice-wrap.going { opacity:0; }
@media (prefers-reduced-motion:no-preference) {
  .notice-wrap { transition:opacity .18s; }
  .notice { transition:transform .18s; }
  .notice-wrap:not(.in) .notice { transform:translateY(-.5rem); }
}
.notice {
  position:relative; width:100%; max-width:30rem;
  background:var(--surface); border:1px solid var(--line); border-radius:14px;
  padding:1.1rem 1.1rem 1rem; box-shadow:0 12px 40px rgba(0,0,0,.45);
}
.notice h2 { font-size:1rem; margin:0 0 .5rem; padding-right:2rem; font-weight:600; }
.notice-body {
  /* A briefing is four short lines; reflowing it loses the shape. */
  white-space:pre-line; margin:0; color:var(--text); font-size:.9375rem; line-height:1.55;
}
.notice-at { color:var(--muted); font-size:.75rem; margin:.75rem 0 0; }
.notice-x {
  position:absolute; top:.4rem; right:.5rem;
  background:none; border:0; color:var(--muted);
  font-size:1.5rem; line-height:1; cursor:pointer;
  /* Comfortably tappable without making the glyph huge. */
  width:2.5rem; height:2.5rem; border-radius:8px;
}
.notice-x:hover { color:var(--text); }

/* ------------------------------------------------------------------ login */
/*
 * Classes rather than inline styles, because the CSP is style-src 'self' and
 * inline style attributes are blocked by it. The login page used them, so every
 * input and button on it rendered completely unstyled — a real fault that only
 * looked like untidiness.
 *
 * dvh over vh: on mobile, 100vh is the viewport *without* the browser chrome, so
 * a centred card sits too low and the button can end up under the toolbar.
 */
.login {
  min-height:100vh; min-height:100dvh;
  display:grid; place-items:center;
  padding:calc(1.5rem + env(safe-area-inset-top)) 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
}
.login-inner { width:100%; max-width:21rem; }
.login-mark { display:block; width:3rem; height:3rem; margin:0 auto 1rem; }
.login h1 { font-size:1.25rem; font-weight:600; margin:0 0 .25rem; text-align:center; letter-spacing:-.01em; }
.login .sub { text-align:center; margin:0 0 1.5rem; }

.field { margin-bottom:1rem; }
.field label { display:block; font-size:.75rem; color:var(--muted); margin-bottom:.4rem;
               text-transform:uppercase; letter-spacing:.05em; }
.field input {
  width:100%; padding:.8rem .85rem; border-radius:10px;
  border:1px solid var(--line); background:var(--ink); color:var(--text);
  font-size:1rem; font-family:inherit; line-height:1.2;
  /* 16px minimum, or iOS zooms the page on focus and the layout jumps. */
  -webkit-appearance:none; appearance:none;
}
.field input:focus-visible {
  outline:none; border-color:var(--signal);
  box-shadow:0 0 0 3px rgba(224,163,86,.18);
}
.field input::placeholder { color:var(--muted); opacity:.6; }

/* Six digits read as digits, not prose. */
.field input.code {
  letter-spacing:.4em; text-indent:.4em; text-align:center;
  font-size:1.35rem; font-variant-numeric:tabular-nums; padding-block:.7rem;
}

.btn-primary {
  width:100%; padding:.85rem; border:0; border-radius:10px;
  background:var(--signal); color:var(--ink);
  font-weight:600; font-size:1rem; font-family:inherit; cursor:pointer;
  /* Comfortably above the 44px touch target minimum. */
  min-height:3rem;
}
.btn-primary:active { transform:translateY(1px); }
.btn-primary:focus-visible { outline:2px solid var(--text); outline-offset:2px; }
@media (prefers-reduced-motion: no-preference) { .btn-primary { transition:opacity .15s, transform .05s; } }
.btn-primary:hover { opacity:.93; }

.login .err {
  background:rgba(210,115,95,.14); border-radius:10px;
  padding:.7rem .85rem; margin:0 0 1rem; line-height:1.45;
}
.login .hint { color:var(--muted); font-size:.75rem; text-align:center; margin:1.25rem 0 0; line-height:1.5; }

/* A short landscape phone should scroll rather than crush the card. */
@media (max-height: 26rem) {
  .login { place-items:start center; }
  .login-mark { width:2.25rem; height:2.25rem; margin-bottom:.6rem; }
  .login .sub { margin-bottom:1rem; }
}
