/* chopped-run — technical-docs styling. Elements are styled, not classes. */

/* Two faces. IBM Plex Mono sets the page; Bodoni Moda sets the hero type — the
   wordmark and each page's <h1> — a high-contrast didone against the mono's
   readout. Bodoni Moda is variable: the optical-size axis follows font-size on its
   own, and the weight is set per use rather than loaded per weight.

   They are declared here rather than pulled in as a Google Fonts stylesheet so
   that style-src stays 'self'; only font-src had to widen for them (see
   securityHeaders). The unicode-range on each Plex face is Google's own latin
   subset: a character outside it falls through to the system mono rather than
   rendering as tofu.

   Bodoni Moda comes from Fontsource on jsDelivr because Google Fonts hands
   each weight out as its own static file. The one file carries both axes
   (opsz, wght), so it covers any weight from 400 to 900. Its hairlines vanish
   at the lighter weights, so nothing set in it goes below 600. */

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3pQPwlBFgsAXHNk.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6pfjptAgt5VM-kVkqdyU8n1ioa1XdgregdFA.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Bodoni Moda";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url(https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/files/bodoni-moda-latin-opsz-normal.woff2) format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light dark;
  --fg: light-dark(#1a1a1a, #d8d8d8);
  --fg-muted: light-dark(#6b6b6b, #8a8a8a);
  --bg: light-dark(#ffffff, #121212);
  --rule: light-dark(#d0d0d0, #333333);
  --link: light-dark(#0a4bb5, #7aa7ff);
  --mono-fallback: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --mono: "IBM Plex Mono", var(--mono-fallback);
  --display: "Bodoni Moda", Didot, "Bodoni 72", Georgia, serif;
  --tab-bar: 3.25rem;
  --status-fade: 1.75rem;
}

/* Appearance. Every colour on the page is a light-dark() pair, so an account's
   choice of surface is one declaration: pinning color-scheme re-resolves the
   whole palette, and the form controls and scrollbars the browser draws with
   it. theme.js sets data-theme on <html> from this browser's choice before
   the first paint; with none, :root's own `light dark` stands and the
   browser decides. No swap reaches <html>, so a choice outlives every
   boosted navigation. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

html { font: 14px/1.6 var(--mono); color: var(--fg); background: var(--bg); }
/* The page is a column as tall as the viewport, so a short page — the login
   form, an empty month — still lands its footer rule on the bottom edge rather
   than halfway up with a band of background beneath it. main takes the slack,
   which is also why it needs no minimum height of its own. */
/* An app installed to a home screen draws under the notch and the home
   indicator (viewport-fit=cover in the layout), so the page's gutters, the
   header and the footer each grow by the safe-area inset on their side. In a
   browser tab every inset is zero and nothing moves. */
body { margin: 0 auto; max-width: 80ch; padding: 0 max(2ch, env(safe-area-inset-right)) 0 max(2ch, env(safe-area-inset-left)); display: flex; flex-direction: column; min-height: 100dvh; }

a { color: var(--link); text-decoration: underline; text-underline-offset: .15em; }
a:hover { text-decoration-thickness: 2px; }

/* Header and navigation.

   The wordmark, the nav and the account sit on one row. That only fits the
   80ch measure because the account shows the email's local part rather than
   the whole address (userView.ShortName) — with the full address the row wrapped
   and the header became three bands of chrome above every page.

   There is no rule beneath it. The page's own <h1> already separates the header
   from the content, so the line was doing nothing but adding weight; the footer
   keeps its rule, which is what still bounds the page. The type steps down a
   notch and the wordmark trades bold for letter-spacing, so the header reads as
   chrome and the content keeps the page's only strong ink. */
body > header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: .6rem 2.5ch; padding: calc(.75rem + env(safe-area-inset-top)) 0 .75rem; font-size: .92em; }
body > header > a:first-child { display: inline-flex; align-items: center; gap: .65ch; font-family: var(--display); font-weight: 700; color: var(--fg); text-decoration: none; font-size: 1.15em; }
body > header > a:first-child img { width: 2rem; height: 2rem; }
body > header nav { display: flex; gap: 2.5ch; }
body > header nav a { color: var(--fg-muted); text-decoration: none; }
body > header nav a:hover { color: var(--fg); text-decoration: underline; }
body > header nav a[aria-current="page"] { color: var(--fg); text-decoration: underline; }
body > header nav .home-tab { display: none; }

/* Content */
main { padding: 2rem 0 3rem; flex: 1; }
/* A page carries a heading only when it earns one: an activity is titled by
   its name, the signed-out login page has no nav to say where it is, and the
   dashboard is headed by the date rather than by its own name (.dash-head).
   The rule that muted a blurb under the heading went with the blurbs — the one
   paragraph it still reached was the login error, which should not be muted. */
main > h1 { font-family: var(--display); font-size: 1.75rem; font-weight: 600; letter-spacing: -.01em; margin: 0 0 1.2rem; }
section { margin-top: 2.4rem; }
/* A page that opens straight onto a section — the calendar carries no
   heading — would otherwise stack that margin on top of main's own
   padding and open with a band of nothing. */
main > :first-child { margin-top: 0; }
section > h2 { font-size: 1rem; font-weight: 700; margin: 0 0 .8rem; padding-bottom: .3rem; border-bottom: 1px solid var(--rule); }
section > h2 small { font-weight: 400; color: var(--fg-muted); margin-left: 1ch; }
p { margin: .6rem 0; }
p:has(> em:only-child) { color: var(--fg-muted); }
form { margin: .8rem 0; }
button { font: inherit; padding: .25rem 1ch; cursor: pointer; }

/* The dashboard's masthead. The date is the page's heading, in the display
   face like every other <h1>; the week so far sits on the same line, pushed to
   the right, and drops under the date when the row runs out. */
.dash-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: .2rem 3ch; margin-bottom: 1.6rem; }
.dash-head h1 { font-family: var(--display); font-size: 1.75rem; font-weight: 600; letter-spacing: -.01em; margin: 0; }
.dash-head .week-so-far { margin: 0; font-variant-numeric: tabular-nums; }
.dash-head .label, .dash-head .of { color: var(--fg-muted); white-space: nowrap; }
.dash-head .label { font-size: .78em; letter-spacing: .06em; text-transform: uppercase; margin-right: .5ch; }

/* Next up. The one card on the dashboard, because it is the one thing on it
   that says what to do rather than what was done. The edge carries the kind
   the way a calendar cell does: the accent for a hard day, the rule for the
   rest, so the card says which sessions matter without shouting all of them. */
.next-session {
  border: 1px solid var(--rule); border-left: 3px solid var(--fg-muted);
  padding: .8rem 2ch .9rem; margin: .8rem 0 0;
}
.next-session:is(.tempo, .intervals, .race) { border-left-color: var(--link); }
.next-session .when { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1ch; }
.next-session .when strong { text-transform: capitalize; }
.next-session .when time { color: var(--fg-muted); }
.next-session .kind {
  margin-left: auto; font-size: .78em; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-muted); border: 1px solid var(--rule); padding: 0 .5ch; border-radius: 2px;
}
.next-session:is(.tempo, .intervals, .race) .kind { color: var(--link); border-color: var(--link); }
.next-session h3 { font-size: 1.15rem; font-weight: 700; margin: .3rem 0 .4rem; }
.next-session h3 a { color: var(--fg); text-decoration: none; }
.next-session h3 a:hover { color: var(--link); text-decoration: underline; }
/* Distance, time and pace on one line, each only when the session has it:
   an easy run by feel is a distance with no clock beside it. */
.next-session .figures { margin: .2rem 0 .4rem; font-variant-numeric: tabular-nums; }
.next-session .figures span + span::before { content: "\00b7"; margin: 0 .8ch; color: var(--fg-muted); }
.next-session .steps { margin: .4rem 0; color: var(--fg); }
.next-session .detail { margin: .4rem 0; color: var(--fg-muted); }
.next-session .from { margin: .6rem 0 0; font-size: .92em; color: var(--fg-muted); }
/* The sessions after the next one: a day, a title, an amount, in three
   columns so the amounts line up down the right like a table's would. */
ol.later { list-style: none; padding: 0; margin: .6rem 0 0; }
ol.later li {
  display: grid; grid-template-columns: 11ch 1fr auto; gap: 0 1.5ch; align-items: baseline;
  padding: .3rem 0; border-bottom: 1px solid var(--rule);
}
ol.later .day { color: var(--fg-muted); text-transform: capitalize; }
ol.later .amount { color: var(--fg-muted); font-variant-numeric: tabular-nums; text-align: right; }
/* The way on from a section to the page that holds all of it. */
p.more { margin: .4rem 0 0; text-align: right; font-size: .92em; }
p.more a { color: var(--fg-muted); }
p.more a:hover { color: var(--link); }

/* Definition lists */
dl { display: grid; grid-template-columns: max-content 1fr; gap: .3rem 3ch; margin: 0; }
dt { color: var(--fg-muted); }
dd { margin: 0; }

/* Tables */
table { width: 100%; border-collapse: collapse; margin: .6rem 0; }
th, td { text-align: left; padding: .4rem 1.5ch .4rem 0; vertical-align: top; border-bottom: 1px solid var(--rule); }
th { font-weight: 400; color: var(--fg-muted); }
td:last-child, th:last-child { padding-right: 0; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
time { white-space: nowrap; }

/* Charts.

   Bars carry the data colour; every piece of text stays in the muted ink,
   including the axis. Gridlines are hairlines one step off the surface — solid,
   never dashed, and always behind the bars.

   The deviation charts use blue/red rather than the green/red of the +/- text
   above: a bar has no sign to read, so its colour has to survive colour-blind
   vision on its own. Both steps are validated against the two surfaces. */
:root {
  --chart-up: light-dark(#1f6feb, #4a90e2);
  --chart-down: light-dark(#d1242f, #d9534a);
  --chart-grid: light-dark(#e6e6e6, #262626);
  --chart-wash: light-dark(rgba(26, 26, 26, .10), rgba(216, 216, 216, .10));
}
svg[role="img"] { display: block; width: 100%; height: auto; color: var(--fg-muted); overflow: visible; }
svg[role="img"] path[data-bar] { fill: var(--fg); }
/* The open period is outlined rather than dimmed: an unfinished column on a
   printed training graph, visibly provisional without looking disabled. */
svg[role="img"] path[data-bar="open"] {
  fill: var(--chart-wash); stroke: var(--fg-muted); stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}
svg[role="img"] path[data-bar="up"] { fill: var(--chart-up); }
svg[role="img"] path[data-bar="down"] { fill: var(--chart-down); }
svg[role="img"] path[data-bar]:hover { fill: var(--link); }

/* Curves. A line is stroked, not filled, and keeps its weight whatever the
   viewBox is scaled to. Mitered joins keep the printed-plot character while a
   round cap lets a single plotted week draw as a dot rather than as nothing. */
svg[role="img"] path[data-line] {
  fill: none; stroke: var(--fg); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: miter; vector-effect: non-scaling-stroke;
}
svg[role="img"] path[data-line="fitness"] { stroke: var(--chart-up); }
svg[role="img"] path[data-line="fatigue"] { stroke: var(--chart-down); }
/* The curves page. The window is the blue the eye goes to first and the span
   before it steps back to grey; the prediction is a model rather than a
   record, so it is dashed. */
svg[role="img"] path[data-line="now"], svg[role="img"] path[data-line="aerobic"] { stroke: var(--chart-up); }
svg[role="img"] path[data-line="before"] { stroke: var(--fg-muted); }
svg[role="img"] path[data-band] { fill: var(--chart-up); fill-opacity: .15; stroke: none; }
svg[role="img"] path[data-line="drift"] { stroke: var(--chart-down); }
svg[role="img"] line { stroke: var(--chart-grid); stroke-width: 1; }
svg[role="img"] line[data-zero] { stroke: var(--fg-muted); }
svg[role="img"] text {
  fill: var(--fg-muted); font: 10px var(--mono); letter-spacing: .03em;
  text-anchor: middle; text-transform: uppercase;
}
svg[role="img"] text[data-tick] { text-anchor: end; }
/* A column's own number, drawn at the top of the bar. Every column carries one,
   so a label sits over its neighbour whenever the bar beside it is taller: the
   page background is stroked behind the glyphs, under the fill, which keeps the
   figure readable wherever it lands without a box around it. */
svg[role="img"] text[data-value] {
  fill: var(--fg); paint-order: stroke; stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round;
}

/* The stream plots. uPlot draws to a canvas, so charts.js reads these colours
   out of the stylesheet rather than inheriting them. Its legend is kept as the
   cursor read-out — with one series per plot there is no identity to legend. */
.u-legend { font: 11px var(--mono); color: var(--fg-muted); padding-top: .2rem; }
.u-legend .u-marker { display: none; }
.u-legend .u-series > * { padding: 0 .5ch; }
.u-legend .u-value { color: var(--fg); }
svg[aria-label="route outline"] { height: 320px; }
svg[aria-label="route outline"] polyline { fill: none; stroke: var(--fg); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
figure { margin: 1.2rem 0; }
figcaption { color: var(--fg-muted); margin-bottom: .3rem; }
figure > figcaption { font-size: .85em; letter-spacing: .06em; text-transform: uppercase; }
/* A curve's legend sits in its caption, keyed by the same series name the line
   carries, so a rule that colours one colours the other. */
figcaption [data-line] { color: var(--fg); margin-left: 2ch; white-space: nowrap; }
figcaption [data-line]::before { content: ""; display: inline-block; width: 2ch; height: 2px; margin-right: .6ch; vertical-align: middle; background: currentColor; }
figcaption [data-line="fitness"] { color: var(--chart-up); }
figcaption [data-line="fatigue"] { color: var(--chart-down); }
figcaption [data-line="now"] { color: var(--chart-up); }
figcaption [data-line="before"] { color: var(--fg-muted); }
figcaption [data-line="predicted"]::before { height: 1ex; background: color-mix(in srgb, var(--chart-up) 15%, transparent); }
figure p { margin: 0; }
details > summary { color: var(--fg-muted); cursor: pointer; margin-top: .6rem; }
details[open] > summary { margin-bottom: -.3rem; }
[data-chart] { min-height: 200px; width: 100%; }
[data-map] { height: 320px; width: 100%; }

/* The basemap is context, not content: desaturated until it is nearly grey,
   but not all the way — the last of the green and blue is what separates a
   park and a reservoir from the blocks around them.

   Tiles are images, so they cannot follow color-scheme the way a light-dark()
   colour does: the dark surface needs the whole raster inverted. Inverting
   alone turns water orange; the hue rotation puts the map back on its own side
   of the colour wheel. Which filter applies is the one place the chosen mode
   has to be read twice — once for an account that asked for dark, once for a
   system-dark browser the account has not overridden — so the two filters are
   named and the selectors only pick between them. */
:root {
  --tiles-light: saturate(.35);
  --tiles-dark: invert(1) hue-rotate(180deg) saturate(.35) brightness(.85);
  --tiles: var(--tiles-light);
}
:root[data-theme="dark"] { --tiles: var(--tiles-dark); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --tiles: var(--tiles-dark); }
}
.leaflet-tile-pane { filter: var(--tiles); }
.leaflet-container { background: var(--bg); }

/* The caption's keys repeat the two route markers: hollow is where the run
   started, filled is where it ended. */
figcaption .start, figcaption .finish { color: var(--fg); white-space: nowrap; }
figcaption .start::before, figcaption .finish::before {
  content: ""; display: inline-block; width: .55em; height: .55em; margin-right: .5ch;
  border: 1px solid var(--link); border-radius: 50%; vertical-align: baseline;
}
figcaption .finish::before { background: var(--link); }

/* Pager (calendar month navigation) */
nav[aria-label="Month navigation"] { display: flex; justify-content: space-between; align-items: baseline; gap: 2ch; }
nav[aria-label="Month navigation"] h2 { margin: 0; border: 0; padding: 0; }
/* The curves page's window. Its links read like the site nav's, the current
   one in full ink. */
nav[aria-label="Window"] { display: flex; flex-wrap: wrap; gap: .3rem 2.5ch; font-size: .92em; }
nav[aria-label="Window"] a { color: var(--fg-muted); text-decoration: none; padding: .2rem 0; }
nav[aria-label="Window"] a:hover { color: var(--fg); text-decoration: underline; }
nav[aria-label="Window"] a[aria-current] { color: var(--fg); text-decoration: underline; }

/* Calendar.

   A month is a fixed grid of eight equal columns — seven days and the week's
   total — so every cell is the same width whatever it holds and the rows stay
   in step down the page. At the 80ch measure that leaves about 9ch a cell,
   which is a distance and nothing else; the run's name, time and pace are in
   the link's title rather than the cell.

   Days outside the month on display are dimmed rather than emptied. Their runs
   still count towards the week beside them, and a blank cell would say they
   did not happen. */
.calendar table { table-layout: fixed; }
.calendar caption { text-align: left; color: var(--fg-muted); padding-bottom: .4rem; }
.calendar th, .calendar td { padding: .4rem .5ch .4rem 0; font-size: .92em; }
.calendar th[scope="col"] { font-size: .85em; letter-spacing: .04em; }
.calendar td.day { height: 4.2rem; }
/* inline-block, so the rule marking today hugs the number instead of running
   the width of the cell. */
.calendar .daynum { display: inline-block; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.calendar td.outside { color: var(--fg-muted); }
.calendar td.outside .daynum, .calendar td.outside .run, .calendar td.outside .planned { opacity: .55; }

/* Today is marked with a rule under its number rather than a background: the
   grid is already dense, and a filled cell would read as data. */
.calendar td.today .daynum { color: var(--fg); font-weight: 700; box-shadow: inset 0 -2px 0 var(--link); }

.calendar .run { display: block; margin-top: .15rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A run that answered a session carries the session's rule beside it, set
   upright like the run it is: the plan done, shown once rather than as a run
   and an intention side by side. */
.calendar .run.answered { border-left: 2px solid var(--chart-up); padding-left: .5ch; }
.calendar .run.answered:is(.tempo, .intervals, .race) { border-left-color: var(--link); }

/* A planned session, under the run rather than beside it: the question a
   calendar answers is what was done, and the plan is what was meant to be.
   Outlined rather than filled, so a cell holding both reads as one fact and
   one intention and never as two runs. */
.calendar .planned {
  display: block; margin-top: .15rem; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; color: var(--fg-muted); font-style: italic;
  border-left: 2px solid var(--rule); padding-left: .5ch;
}
.calendar .planned.missed { border-left-color: var(--chart-down); color: var(--chart-down); }
.calendar .planned .rest { font-style: normal; font-size: .85em; }
/* Hard days carry the accent; easy ones stay quiet. A calendar that shouted
   every session would say nothing about which ones matter. */
.calendar .planned:is(.tempo, .intervals, .race) { border-left-color: var(--link); }
/* Each figure on its own line: the column is one cell wide, and a distance
   and a run count sharing a line wrap mid-phrase at every width that matters. */
.calendar .week-total { color: var(--fg-muted); }
.calendar .week-total .distance { display: block; color: var(--fg); white-space: nowrap; }
.calendar .week-total small { display: block; font-size: .85em; white-space: nowrap; }
.calendar .week-total .rest { font-style: italic; }
/* A plan week not written yet has a target and no sessions; it is shown as
   what the week is meant to hold. */
.calendar .week-total .target { font-style: italic; color: var(--link); }

/* The unit inside a calendar cell. Thirty repetitions of " km" are what stops
   eight columns crossing a phone, and the caption above the grid already says
   the unit once for the whole month — so the phone layer hides these and the
   figures alone fit. At every other width they read as part of the number.

   The month used to keep the 80ch measure below that and scroll sideways,
   which meant a phone showed Monday to Friday and you panned for the weekend.
   Dropping the unit is the smaller loss by far. */
.calendar .unit { color: var(--fg-muted); }

/* If a cell should still overflow — a five-figure imperial distance, say — the
   grid scrolls rather than pushing the whole page sideways. */
@media (max-width: 44rem) {
  .calendar { overflow-x: auto; }
}

footer { padding: 1.2rem 0 calc(1.2rem + env(safe-area-inset-bottom)); border-top: 1px solid var(--rule); color: var(--fg-muted); }

/* Account controls in the header. The name is kept on one line: an unusually
   long local part then pushes the whole cluster onto a row of its own, which is
   the old two-row header rather than a broken one. */
body > header .account { display: flex; align-items: baseline; gap: 1.5ch; }
body > header .account a { color: var(--fg-muted); text-decoration: none; white-space: nowrap; }
body > header .account a:hover { color: var(--fg); text-decoration: underline; }
body > header .account a[aria-current="page"] { color: var(--fg); text-decoration: underline; }
/* The sign-out form is a flex item, so its display: inline is blockified and
   the page-wide form margin applies — .8rem above and below, which made the
   account cluster two lines tall and the header with it. */
body > header .account form { margin: 0; }
body > header .account button { background: none; border: 0; padding: 0; font: inherit; color: var(--fg-muted); cursor: pointer; text-decoration: underline; }
body > header .account button:hover { color: var(--fg); }

/* Forms. The appearance picker on the account page is a form row outside a
   form (it is never posted), so it takes the same rules; :not([hidden]),
   because display: flex would otherwise overrule the attribute that hides it
   until theme.js shows it. */
form p, #appearance:not([hidden]) { display: flex; align-items: baseline; flex-wrap: wrap; gap: 1ch; margin: .8rem 0; }
label { color: var(--fg-muted); min-width: 12ch; }
/* A field with no type is a text field, and the date and notes fields are
   fields too: every one of them wears the same border. */
input[type="text"], input[type="email"], input[type="password"], input[type="date"],
input:not([type]), textarea, select {
  font: inherit; color: var(--fg); background: var(--bg);
  border: 1px solid var(--rule); padding: .3rem .5rem; min-width: 28ch; max-width: 100%;
}
input:focus-visible, select:focus-visible, button:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }
input[type="checkbox"] { min-width: 0; }
label:has(input[type="checkbox"]) { min-width: 0; }
button[type="submit"] {
  font: inherit; color: var(--bg); background: var(--fg);
  border: 1px solid var(--fg); padding: .35rem 1.5ch; cursor: pointer;
}
button[type="submit"]:hover { background: var(--link); border-color: var(--link); }
/* A field's note takes a line of its own, indented to the field rather than to
   the label — the label column is 12ch and the row's gap is 1ch. Left to wrap
   on its own a note starts back at the margin, under the label, and the form
   stops reading as two columns. */
form p .hint, #appearance .hint { flex-basis: 100%; margin-left: 13ch; }

/* Changes, in the language of a diff: a signed magnitude on a faint tint,
   <ins> for what reads as an addition and <del> for a removal. The default
   underline and strikethrough are dropped — the sign and the colour already
   say it, and a struck-through number is hard to read. */
ins, del, .same {
  font-variant-numeric: tabular-nums; white-space: nowrap;
  text-decoration: none; padding: 0 .4ch; border-radius: 2px;
}
ins { color: light-dark(#1a7f37, #6fcf85); background: light-dark(#e6ffec, #12261a); }
del { color: light-dark(#b3261e, #f2b8b5); background: light-dark(#ffebe9, #2c1618); }
.same { color: var(--fg-muted); }

/* The summary's changes are a second line under each figure, smaller, so the
   figures read first and line up down their column. */
table.summary small { display: block; font-size: .8em; color: var(--fg-muted); white-space: nowrap; margin-top: .15rem; }
table.summary .change[title] { cursor: help; }
table.summary td { padding-top: .55rem; padding-bottom: .55rem; }
th[title] { text-decoration: underline dotted; text-underline-offset: .25em; cursor: help; }

/* Messages */
.error, .ok { border-left: 3px solid; padding: .4rem 1ch; margin: .8rem 0; }
.error { border-color: light-dark(#b3261e, #f2b8b5); }
.ok { border-color: light-dark(#1a7f37, #6fcf85); }
.hint { color: var(--fg-muted); font-size: .92em; }
/* A hint the server read back and refused: the error's colour, not its box,
   because it is still a note under a field rather than a message. */
.hint.refused { color: light-dark(#b3261e, #f2b8b5); }
/* A hint kept only so it can be swapped takes no line while it has nothing
   to say. */
.hint:empty { display: none; }

/* htmx's own indicator stylesheet is disabled via the htmx-config meta tag in
   the layout, because injecting a <style> element violates style-src 'self'. */
.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; transition: opacity 200ms ease-in; }

/* Phone.

   The page is a single column of monospace 80 characters wide, and 80
   characters do not cross a 375px screen. Anything laid out across one — the
   header's three clusters, a six-column table, a chart's axis — has to become
   something stacked or something smaller, because the alternative is a page
   you pan. 35rem is where the five-column tables stop fitting. */
@media (max-width: 35rem) {
  /* The wordmark and the account keep the first row; the nav takes a row of
     its own and spreads across it. Left to wrap on its own the header came out
     three bands deep, and links bunched at the left are links no thumb can
     tell apart. Every target in here takes vertical padding for the
     same reason. */
  body > header { padding: calc(.6rem + env(safe-area-inset-top)) 0 .2rem; gap: .1rem 2ch; }
  body > header .account a,
  body > header .account button { display: inline-block; padding: .5rem 0; }

  /* The nav is a tab bar pinned to the bottom edge. Installed to a home
     screen there is no browser chrome, so a nav at the top of the page is a
     nav that scrolls away and a thumb's reach from anything. Each tab is an
     equal share of the width and the whole of its cell is the target. The bar
     grows by the home indicator's inset, and the page by the bar's height, so
     the footer is never under it. */
  body { padding-bottom: calc(var(--tab-bar) + env(safe-area-inset-bottom)); }
  body > header nav {
    /* Above Leaflet's controls (z-index 1000), which scroll under the bar. */
    position: fixed; z-index: 1100; inset: auto 0 0;
    display: flex; gap: 0; padding-bottom: env(safe-area-inset-bottom);
    background: var(--bg); border-top: 1px solid var(--rule);
  }
  body > header nav a {
    flex: 1; display: flex; align-items: center; justify-content: center;
    min-height: var(--tab-bar); font-size: 1.05em;
  }
  body > header nav .home-tab { display: flex; }
  body > header nav a[aria-current="page"] { font-weight: 600; }
  /* The body already clears the home indicator. */
  footer { padding-bottom: 1.2rem; }

  main { padding: 1.4rem 0 2rem; }
  .dash-head { margin-bottom: 1.2rem; }
  .dash-head h1 { font-size: 1.25rem; }
  .next-session { padding: .7rem 1.5ch .8rem; }
  ol.later li { grid-template-columns: 9ch 1fr auto; gap: 0 1ch; }
  main > h1 { font-size: 1.25rem; }
  section { margin-top: 1.8rem; }
  details > summary { padding: .3rem 0; }

  /* One notch off the type and most of the column gutter is what keeps the
     tables that do still fit — splits, laps, the two behind the summaries —
     inside the screen when a pace runs to five characters. */
  table { font-size: .92em; }
  th, td { padding-right: 1ch; }

  /* A list of runs stacks: the name on a line of its own, then the figures it
     was tabulated against under it, in the order the columns ran. A date, a
     distance and a pace each carry their own unit, so the dropped headings
     take no information with them. The name is column two in both tables that
     carry this class — the activities page and the dashboard's recent runs. */
  table.runs, table.runs tbody, table.runs td { display: block; }
  table.runs thead { display: none; }
  table.runs tr {
    display: flex; flex-wrap: wrap; align-items: baseline; column-gap: .5ch;
    padding: .35rem 0; border-bottom: 1px solid var(--rule);
  }
  table.runs td { padding: 0; border: 0; }
  table.runs td.num { text-align: left; }
  table.runs td:nth-child(2) { order: -1; flex-basis: 100%; font-size: 1.09em; }
  /* The name is the row's one target, so it takes the whole line rather than
     the width of the words and a seventeen-pixel band down the middle of it. */
  table.runs td:nth-child(2) a { display: block; padding: .25rem 0; }
  table.runs td:first-child,
  table.runs td:not(:first-child):not(:nth-child(2)) { color: var(--fg-muted); }
  /* Half a character either side of the separator rather than a whole one:
     three of them at a character each is seventy pixels, which is the
     difference between the figures making one line on a 320px phone and the
     pace wrapping onto a second with a leading dot in front of it. */
  table.runs td:not(:first-child):not(:nth-child(2))::before { content: "\00b7"; margin-right: .5ch; }

  /* The summary is a matrix, and a matrix does not cross a phone. Each period
     becomes a block and each figure a labelled line — the shape of the
     definition list on an activity, which is the same reading job. The cells
     carry the heading they sit under, because a bare 4 is not a number of
     runs. */
  table.summary, table.summary tbody, table.summary tr, table.summary td { display: block; }
  table.summary thead { display: none; }
  table.summary tr { padding: .6rem 0; border-bottom: 1px solid var(--rule); }
  table.summary td { padding: 0; border: 0; text-align: left; }
  table.summary td:first-child { margin-bottom: .3rem; }
  /* Flex rather than a two-column grid: a figure and the change beside it are
     two items, and a grid would drop the change onto a row of its own under
     the label. Here the line wraps only when it has to, and wraps under the
     figure rather than under the label. */
  table.summary td[data-label] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1.5ch; padding: .1rem 0; }
  /* On a phone the change goes back beside its figure: the label already
     gives each line its own row, and an empty change takes no room. */
  table.summary td[data-label] small { display: inline; margin-top: 0; }
  table.summary td:first-child small { display: inline; margin-left: 1ch; }
  table.summary td[data-label]::before { content: attr(data-label); flex: 0 0 9ch; color: var(--fg-muted); }

  /* The month fits by dropping the repeated unit and stepping the grid down a
     size. The pager puts its month on a line of its own, so the two arrows can
     sit at the edges of the one below it, far apart and thumb-sized. */
  .calendar th, .calendar td { font-size: .8em; padding-right: .4ch; }
  .calendar .unit { display: none; }
  .calendar td.day { height: 3.4rem; }
  .calendar .week-total small { font-size: .8em; }
  nav[aria-label="Month navigation"] { flex-wrap: wrap; gap: 0 2ch; }
  nav[aria-label="Month navigation"] h2 { order: -1; flex-basis: 100%; }
  nav[aria-label="Month navigation"] a { padding: .5rem 0; white-space: nowrap; }

  /* An SVG's text scales with its viewBox: 10 units of a 640-unit chart drawn
     into a 286px column — a 320px phone — is four and a half pixels of axis.
     Twenty-two units puts it back around eleven, which is the smallest an axis
     can be read at arm's length, and every second x-axis label goes to make
     room for it. The labels are dropped from the right, so the most recent
     position always keeps its own.

     Two of every three labels go rather than one of two: at this size the
     survivors of an every-other thinning still touch each other. Four across a
     phone is what the width holds. The pair of selectors is a quantity query —
     the first matches only once a seventh label exists, the second picks up
     the six after it — so a chart with six labels or fewer keeps all of them.
     A three-split chart has room for its three numbers, and thinning it to one
     would be a rule applying itself where there is no crowding to relieve.

     The column labels are thinned by the same rule, counted within their own
     group: a week with no runs has no column and no number, so the two rows of
     type do not always survive on the same weeks.

     The labels at either end are anchored inwards. The last two sit at the
     last plotted x, a few units short of the viewBox edge, and type this size
     centred there hangs off the end of the chart; the first sits at the plot's
     left edge, where centred type reaches back over the y-axis labels.

     The last column's number is the exception: anchoring it inwards would hang
     it over the column before it, which is the one thing a figure sitting on a
     bar must not do. The SVG does not clip, so it keeps its centre and the few
     units it spills sit in the page's own margin. */
  svg[role="img"] text { font-size: 22px; }
  svg[role="img"] :is(g[data-marks], g[data-values]) text:nth-last-of-type(n+7):not(:nth-last-of-type(3n+1)),
  svg[role="img"] :is(g[data-marks], g[data-values]) text:nth-last-of-type(n+7) ~ text:not(:nth-last-of-type(3n+1)) { display: none; }
  svg[role="img"] :is(g[data-marks], g[data-values]) text:first-of-type { text-anchor: start; }
  svg[role="img"] g[data-marks] text:last-of-type { text-anchor: end; }
  svg[aria-label="route outline"] { height: 260px; }
  [data-map] { height: 260px; }

  /* The two columns hold: the longest term here is "elevation gain" and the
     longest value "150 avg / 170 max bpm", which together clear 375px, and a
     value that ever does run long wraps inside its own column. Stacking them
     instead doubled the length of an activity for nothing. */
  dl { gap: .25rem 2ch; }

  /* Forms. 16px is the size at or above which iOS leaves a focused field
     alone; below it the browser zooms the page in and never zooms back out.
     The label owns its line and the field under it owns the width — a field
     that stops two thirds of the way across reads as though something is
     missing beside it. */
  form p, #appearance { gap: .2rem; }
  label { min-width: 100%; }
  label:has(input[type="checkbox"]) { min-width: 0; }
  input[type="text"], input[type="email"], input[type="password"], input[type="date"],
  input:not([type]), textarea, select {
    font-size: 16px; width: 100%; min-width: 0;
  }
  input[type="checkbox"] { width: auto; }
  /* The label owns its own line here, so there is no column to indent to. */
  form p .hint, #appearance .hint { margin-left: 0; }
  button[type="submit"] { font-size: 16px; width: 100%; padding: .6rem 1.5ch; }
}


/* Plans.

   A plan is a stack of weekly tables rather than one long one: sixteen weeks
   in a single table is a scroll with no landmarks in it, and the week is the
   unit a training plan is actually read in.

   The status classes are shared with the plans list, where a plan being
   written and one that could not be are the two things worth seeing at a
   glance. */
.pending { color: var(--fg-muted); font-style: italic; }
.failed { color: var(--chart-down); }

.plan-week { margin-bottom: 1.5rem; }
.plan-week caption { text-align: left; padding-bottom: .4rem; }
.plan-week caption .total { color: var(--fg-muted); float: right; font-size: .92em; }
.plan-week.empty { color: var(--fg-muted); border-top: 1px solid var(--rule); padding-top: .4rem; }
/* A draft is the call in flight, and reads as provisional until the week it
   becomes is stored and shown in full. */
.plan-week.draft tbody, table.outline.draft tbody { color: var(--fg-muted); }

/* The plan's head: the goal as the headline, the rest as a line of facts. */
.plan-head { margin-bottom: 1rem; }
.plan-head h2 { margin-bottom: .2rem; }
.plan-goal { margin: 0; font-size: 1.05em; }
.plan-goal .plan-target { color: var(--fg-muted); }
.plan-facts, .plan-so-far { margin: .2rem 0 0; color: var(--fg-muted); font-size: .92em; }
.plan-so-far .missed { color: var(--chart-down); }
.plan-shape { margin: 0 0 1.5rem; }
.plan-shape figure { margin: 0; }

/* The coach at work: one sentence of what it is doing, the steps still to
   come, and the call in flight — in its week's card when there is one. */
.coach-at-work { margin: 0 0 1.5rem; padding: .6rem 1ch; border-left: 3px solid var(--link); }
ol.progress { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: .25rem 2ch; font-size: .92em; }
ol.progress li { color: var(--fg-muted); }
ol.progress li.active { color: var(--fg); font-weight: 600; }
ol.progress li.active::before { content: "→ "; }
ol.progress li + li::before { content: "· "; color: var(--rule); }
ol.progress li.active + li::before { content: "· "; }
.stage { margin: .5rem 0 .25rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1ch; }
.stage .elapsed { color: var(--fg-muted); font-variant-numeric: tabular-nums; margin-left: auto; }
.stage .state { font-size: .78em; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-muted); }
.stage .state.writing, .stage .state.thinking { color: var(--link); }
.stage .so-far { color: var(--fg-muted); font-size: .92em; }
.note { margin: .25rem 0; color: var(--fg-muted); font-style: italic; }
/* The model's thinking as it arrives. Only the newest lines are in view:
   the block is pinned to its bottom, and older lines fade out at the top
   instead of being cut through. Each draft replaces the whole block, but the
   text already shown is the same text in the same place, so only .fresh, the
   part new in this draft, visibly arrives. */
.reasoning {
  margin: .35rem 0; padding: 0 1ch; border-left: 2px solid var(--rule);
  color: var(--fg-muted); font-size: .88em; line-height: 1.45;
  height: 7.25em; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  mask-image: linear-gradient(to bottom, transparent, #000 3em);
}
/* Opens once, on the first draft of a call: every later one has text it
   already showed and would replay this on each swap. */
.reasoning.opening { animation: reasoning-open .35s ease-out; }
.reasoning p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
/* New text is typed out: typeOut splits it into 24 steps, each shown 40 ms
   after the one before. Until its delay is up a step is invisible (fill mode
   both), so it reads as the text arriving a few characters at a time. */
.reasoning .fresh > span { animation: reasoning-type .12s ease-out both; }
@keyframes reasoning-type { from { opacity: 0; } }
.reasoning .fresh .t0 { animation-delay: 0ms; }
.reasoning .fresh .t1 { animation-delay: 40ms; }
.reasoning .fresh .t2 { animation-delay: 80ms; }
.reasoning .fresh .t3 { animation-delay: 120ms; }
.reasoning .fresh .t4 { animation-delay: 160ms; }
.reasoning .fresh .t5 { animation-delay: 200ms; }
.reasoning .fresh .t6 { animation-delay: 240ms; }
.reasoning .fresh .t7 { animation-delay: 280ms; }
.reasoning .fresh .t8 { animation-delay: 320ms; }
.reasoning .fresh .t9 { animation-delay: 360ms; }
.reasoning .fresh .t10 { animation-delay: 400ms; }
.reasoning .fresh .t11 { animation-delay: 440ms; }
.reasoning .fresh .t12 { animation-delay: 480ms; }
.reasoning .fresh .t13 { animation-delay: 520ms; }
.reasoning .fresh .t14 { animation-delay: 560ms; }
.reasoning .fresh .t15 { animation-delay: 600ms; }
.reasoning .fresh .t16 { animation-delay: 640ms; }
.reasoning .fresh .t17 { animation-delay: 680ms; }
.reasoning .fresh .t18 { animation-delay: 720ms; }
.reasoning .fresh .t19 { animation-delay: 760ms; }
.reasoning .fresh .t20 { animation-delay: 800ms; }
.reasoning .fresh .t21 { animation-delay: 840ms; }
.reasoning .fresh .t22 { animation-delay: 880ms; }
.reasoning .fresh .t23 { animation-delay: 920ms; }
@keyframes reasoning-open { from { height: 0; opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .reasoning.opening, .reasoning .fresh > span { animation: none; } }
/* Thinking can be minutes of nothing arriving. A pulse says the silence is
   work, not a stall, without a script to drive it. */
.stage .state.thinking, .stage .state.asking { animation: thinking 1.6s ease-in-out infinite; }
@keyframes thinking { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .stage .state.thinking, .stage .state.asking { animation: none; } }

/* A week that is a target and nothing more yet: set as an annotation of the
   plan rather than as a table of days it does not have. */
.plan-week.outline { border-top: 1px solid var(--rule); padding-top: .4rem; }
/* The week being written fills in where it will stay, marked while it does. */
.plan-week.outline.writing { border-left: 3px solid var(--link); padding-left: 1ch; }
.plan-week.outline .caption { margin: 0 0 .25rem; }
.plan-week.outline .caption .total { color: var(--fg-muted); float: right; font-size: .92em; }
.plan-week.outline .focus { margin: .25rem 0; }
.plan-week.outline .due { color: var(--fg-muted); margin: .25rem 0; }
.plan-week.outline .due a { margin-left: 1ch; }
form.write, form.rewrite { display: inline; margin-left: 1ch; }
/* Small and in the line of text they act on: an action on a week, not the
   page's call to action. */
form.write button, form.rewrite button { font-size: .85em; padding: .05rem .9ch; width: auto; }

/* The phase is a reading of the plan's shape, so it is set as an annotation
   rather than as data: small caps beside the week, not a column of its own. */
.phase {
  margin-left: 1ch; font-size: .78em; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-muted); border: 1px solid var(--rule); padding: 0 .5ch; border-radius: 2px;
}
.phase-peak, .phase-race { color: var(--link); border-color: var(--link); }
.phase-recovery, .phase-taper { font-style: italic; }

.workout .detail { color: var(--fg-muted); }
.workout.rest td { color: var(--fg-muted); }
/* A missed session is marked on the row rather than filled in behind it: a
   plan is read by scanning down one column, and a filled row breaks that. */
.plan-day.missed td { color: var(--chart-down); }
/* On a double day with one run on it, the missed session is marked in its
   own actual cell; the day was run, so the row is not. */
.plan-day .missed, .meta .missed { color: var(--chart-down); }
.workout.extra td { color: var(--fg-muted); }
.plan-day.done .delta.up { color: var(--chart-up); }
.plan-day.done .delta.down { color: var(--chart-down); }
.plan-vs-run th[scope="row"] { font-weight: 400; color: var(--fg-muted); }
/* A day is a group of rows, one per session. The rule falls between days,
   not between the sessions of a double day. */
.plan-day + .plan-day { border-top: 1px solid var(--rule); }
.plan-day tr + tr td { border-top: 0; }
.adherence { color: var(--fg-muted); }
/* A session belonging to no plan says so rather than showing a blank, which
   would read as a plan whose name failed to load. */
.loose { color: var(--fg-muted); font-style: italic; }
caption .total { float: right; color: var(--fg-muted); font-weight: 400; }

/* Editing. A blank day is an invitation rather than a gap, so the link that
   fills it is quiet until the row is under the cursor. */
.workout.empty td { color: var(--fg-muted); }
.workout .add { color: var(--fg-muted); }
.workout.empty:hover .add, .workout .add:focus { color: var(--link); }
.row-actions { white-space: nowrap; text-align: right; }
.row-actions form { display: inline; }
/* Scoped past the phone rule that widens every submit button to the screen:
   a delete mark the width of the page pushed the whole table off it. */
.row-actions form button {
  border: 0; background: none; color: var(--fg-muted); cursor: pointer;
  font: inherit; padding: 0 .5ch; width: auto;
}
.row-actions form button:hover, .row-actions form button:focus { color: var(--chart-down); }

/* A session's structure, read as it would be on a whiteboard. */
.steps { color: var(--fg-muted); }
ol.steps { color: inherit; padding-left: 3ch; }
ol.steps li { margin: .25rem 0; }
fieldset.steps { border: 1px solid var(--rule); padding: .5rem 1ch; margin: 1rem 0; }
.steps-form input:not([type]) { width: 100%; min-width: 0; box-sizing: border-box; padding: .2rem .4rem; }
.steps-form td { padding: .15rem .5ch .15rem 0; }

/* A distance and its unit, and a time in its three boxes, sit together as one
   field (templates/partials/inputs.html). The selector outranks the page-wide
   field width and the steps table's, which would each stretch a box meant for
   a few digits across the row. */
.measure { display: inline-flex; align-items: baseline; gap: .5ch; flex-wrap: nowrap; }
.measure input:not([type]) { width: 9ch; min-width: 0; }
.measure select { min-width: 0; width: auto; }
input.duration:not([type]) { width: 11ch; min-width: 0; font-variant-numeric: tabular-nums; }
/* A field the browser will refuse says so where it is, once the athlete has
   typed in it and moved on — :user-invalid, not :invalid, so an empty
   required field on a fresh form is not red before anyone has touched it. */
:is(input, select, textarea):user-invalid:not(:focus) { border-color: light-dark(#b3261e, #f2b8b5); }
.steps-form .measure input:not([type]) { width: 7ch; }
.steps-form input.duration:not([type]) { width: 8ch; }
.steps-form td:has(.times) { white-space: nowrap; }
.steps-form td:has(.times) input:not([type]) { width: 5ch; }
.steps-form .times { color: var(--fg-muted); margin-left: .5ch; }
.steps-form .step-word { display: none; color: var(--fg-muted); margin-right: 1ch; }
/* On a phone the five columns do not fit. Each step becomes a block of its
   own — the label across the top, the numbers wrapping under it — and the
   header row goes, since every field says what it is to a screen reader and
   the unit beside it says so to everyone else. */
@media (max-width: 40rem) {
  .steps-form thead { display: none; }
  .steps-form, .steps-form tbody { display: block; }
  .steps-form tr { display: flex; flex-wrap: wrap; gap: .4rem 1ch; padding: .5rem 0; border-bottom: 1px solid var(--rule); }
  .steps-form td { padding: 0; }
  .steps-form td:first-child { flex-basis: 100%; }
  .steps-form .step-word { display: inline; }
}
/* A secondary action: adding a step row, deleting. It is a submit button
   because that is what works with no JavaScript, not because it is the point
   of the form. */
button.quiet { color: var(--fg-muted); background: none; border-color: var(--rule); }
button.quiet:hover, button.quiet:focus { color: var(--fg); border-color: var(--fg-muted); }

.cancel { margin-left: 2ch; color: var(--fg-muted); }
.plan-actions { margin-top: 1rem; }

/* A label that only a screen reader reads. The edit column has no heading
   worth printing, but a column with no heading at all is one a screen reader
   announces as nothing. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

@media (max-width: 40rem) {
  /* The description is the first thing to go: on a phone the day, the title
     and the distance are what a plan is read for, and a sentence per row
     turns six sessions into a page of prose. */
  .workout .detail, .workout .steps { display: none; }
  .plan-week caption .total { float: none; display: block; }
  .plan-week.outline .caption .total { float: none; display: block; }
  .stage .elapsed { margin-left: 0; }
}

/* Installed to a home screen, the page runs under the status bar, and iOS
   blurs the content beneath it with an edge effect that fades out some way
   below the safe-area inset. A header just clear of the inset sat in that
   fade, half legible. The installed app gives it a band of clear space below
   the inset instead; a browser tab has its own chrome there and is left as
   it was. Last in the file so it follows the phone rule it overrides. */
@media (display-mode: standalone) {
  body > header { padding-top: calc(env(safe-area-inset-top) + var(--status-fade)); }
}
