
/* The site and the PDF are one product, so they share a typeface and a palette.
   Same bundled faces the renderer uses — no webfont fetch here either. */
@font-face { font-family: 'Playfair Display'; font-weight: 700; font-style: normal;
             src: url('/static/fonts/PlayfairDisplay-700.ttf') format('truetype'); font-display: swap; }
@font-face { font-family: 'Playfair Display'; font-weight: 700; font-style: italic;
             src: url('/static/fonts/PlayfairDisplay-700Italic.ttf') format('truetype'); font-display: swap; }
@font-face { font-family: 'Source Sans 3'; font-weight: 400; font-style: normal;
             src: url('/static/fonts/SourceSans3-400.ttf') format('truetype'); font-display: swap; }
@font-face { font-family: 'Source Sans 3'; font-weight: 600; font-style: normal;
             src: url('/static/fonts/SourceSans3-600.ttf') format('truetype'); font-display: swap; }
@font-face { font-family: 'Source Sans 3'; font-weight: 400; font-style: italic;
             src: url('/static/fonts/SourceSans3-400Italic.ttf') format('truetype'); font-display: swap; }

:root {
    /* Two blues, and the split is the point. The brand blue is an *accent* —
       the stripe, the step marks, the load bars, the rules. Anything carrying
       text is the deep blue, because white on #0091d9 measures 3.47:1 and the
       PDF settled this exact question the same way
       (/.wiki/architecture/the-visual-language.md). */
    --blue: #0091d9;
    --blue-deep: #0a3d62;
    --blue-lift: #0d4d7a;
    /* The PDF’s accent, darkened. Every orange on this site is small text —
       the eyebrow is 12.5px semibold with 0.34em tracking, the thinnest thing
       here — so there is no place for the brand #c1632c, which measures
       3.87:1 on the wash. One orange, and it is the one that can be read. */
    --accent: #a94f1a;

    --ink: #1d1d1d;
    --muted: #5a5a5a;
    /* Placeholder text. It has to read as "nothing typed here yet" without
       being text nobody can read: #9aa4a9 was 2.54:1, and 3.5:1 is the
       non-text threshold, which a placeholder is not. 4.59:1. */
    --faint: #6b7780;
    /* Hairlines that separate content, and edges that define a control. The
       second is not decoration — a border at 1.56:1 was the only thing making
       "Save the PDF" look like a button at all. */
    --rule: #e3e6e8;
    /* 3.45:1 on paper, 3.24 on the wash, 3.09 on --sink. All three, because a
       quiet button sits on all three across the flow and the first two passes
       each checked only the ground in front of them. */
    --edge: #7f8c95;
    --paper: #ffffff;
    --wash: #f6f8f9;
    /* The blue at a tint the page can carry behind text, and the one a home
       screen still shows. Used for the active step and the hardest day. */
    --tint: #eaf4fb;
    /* The one off-white that is not the page: a quiet button's hover, a code
       chip's ground. There were two of these, six hundredths apart. */
    --sink: #eef3f6;

    /* One scale, because there were four sizes between 14 and 16px doing
       subtly different jobs and nobody could tell them apart. */
    --t-hero: 2.6rem;
    --t-h2: 1.35rem;
    --t-lede: 1.06rem;
    --t-body: 1rem;
    --t-small: 0.9rem;
    --t-micro: 0.78rem;

    --s2: 0.7rem;
    --s3: 1.1rem;
    --s4: 1.7rem;
    --s5: 2.6rem;
    --radius: 10px;
}

* { box-sizing: border-box; }

/* On <html> as well as <body>: a meta-refresh repaints between documents, and
   the colour it repaints is the root’s. White there is a flash every five
   seconds on the one page an athlete watches for a quarter of an hour. */
html { background: var(--wash); }

body {
    margin: 0;
    font-family: 'Source Sans 3', -apple-system, 'Segoe UI', sans-serif;
    font-size: 17px;
    line-height: 1.55;
    color: var(--ink);
    background: var(--wash);
}

.stripe { height: 6px; background: var(--blue); }

/* The mark, on every page. The site had no identity of its own — the product
   name appeared once, in 12.5px grey, in the footer — while the PDF opens on a
   full-bleed cover with a roundel. It doubles as the way home from the pages
   that had none: the finished plan, and the wall. */
.brand {
    display: inline-flex; align-items: center; gap: 0.5rem;
    margin: 1.1rem 0 0; padding: 0.2rem 0; text-decoration: none;
    font-family: 'Playfair Display', Georgia, serif; font-weight: 700;
    font-size: 1.05rem; color: var(--blue-deep);
}
.brand .mark { width: 22px; height: 22px; border-radius: 5px; background-size: cover; }

/* Said to a screen reader, drawn for nobody. The rail’s states live in class
   names, which are not information. */
.vh {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
}
.wrap { max-width: 40rem; margin: 0 auto; padding: 0 1.25rem 4rem; }

.masthead { text-align: center; padding: var(--s5) 0 var(--s4); }
.eyebrow {
    /* 0.78rem, not 0.7: on the preview and the status page this line is the
       date range, which is the answer to the only question the athlete has at
       that moment — did you understand which week I meant? */
    text-transform: uppercase; letter-spacing: 0.34em; font-size: var(--t-micro);
    font-weight: 600; color: var(--accent);
}
h1 {
    font-family: 'Playfair Display', Georgia, serif;
    font-weight: 700; font-size: var(--t-hero); line-height: 1.1;
    margin: 0.6rem 0 0.4rem;
}
h2 {
    font-family: 'Playfair Display', Georgia, serif;
    font-weight: 700; font-size: var(--t-h2); margin: 0 0 var(--s2);
}
.lede { color: var(--muted); font-size: var(--t-lede); margin: 0 auto; max-width: 30rem; }

.card {
    background: var(--paper); border: 1px solid var(--rule); border-radius: var(--radius);
    padding: var(--s4); margin-bottom: var(--s3);
}

label { display: block; font-weight: 600; font-size: var(--t-small); margin-bottom: 0.25rem; }
/* Inside a <label> it trails the label on the same line — the em dash joining
   them is punctuation, and only works if they are one sentence. As a <p> under
   the field it is a line of its own. */
.hint { font-weight: 400; color: var(--muted); font-size: var(--t-small); }
/* The language in its own script, beside the menu that sets it. A chip rather
   than more grey text: it is the answer to "which language", and it has to
   survive being set in a script the rest of the page is not. */
.endonym {
    display: inline-block; padding: 0.05rem 0.4rem; border-radius: 4px;
    background: var(--tint); color: var(--ink); font-weight: 600;
    font-size: var(--t-small); line-height: 1.5; vertical-align: baseline;
}
p.hint { margin: 0.35rem 0 0; }
.field { margin-bottom: var(--s3); }
input, textarea, select {
    width: 100%; padding: 0.6rem 0.7rem; font: inherit; font-size: var(--t-small);
    border: 1px solid var(--edge); border-radius: 6px; background: #fff; color: var(--ink);
}
/* The chevron, drawn once and used by both the native select and the combobox
   that replaces it — so the affordance does not change when the script lands.
   It used to be painted on input[list] on the belief that a datalist cannot
   have a native arrow; Chrome draws one anyway, so the field carried two and
   the one people clicked was a background image. */
select, .combobox-input {
    appearance: none; -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%235a5a5a' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 0.75rem center;
    background-size: 12px 8px; padding-right: 2.2rem;
}

/* The combobox the script builds. position:relative on the wrapper is what lets
   the list hang over the field below instead of pushing the form down. */
.combobox { position: relative; }
.combobox-input { cursor: text; }
/* Off the page rather than display:none: a required control the browser cannot
   focus is one it refuses to report an error on. It is out of the tab order and
   hidden from assistive tech, which reads the combobox instead. */
select.combobox-native {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    border: 0; opacity: 0; pointer-events: none; overflow: hidden;
}
.combobox-list {
    position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; right: 0;
    margin: 0; padding: 0.25rem; list-style: none; max-height: 15rem; overflow-y: auto;
    background: #fff; border: 1px solid var(--edge); border-radius: 6px;
    box-shadow: 0 6px 24px rgba(16, 34, 44, 0.16);
}
.combobox-option {
    padding: 0.4rem 0.55rem; border-radius: 4px; font-size: var(--t-small); cursor: pointer;
}
/* Selection follows the keyboard, so the highlight has to be the same whether
   it arrived by arrow key or by pointer. */
.combobox-option[aria-selected="true"] { background: var(--tint); }
.combobox-option:hover { background: var(--tint); }
.combobox-empty {
    padding: 0.4rem 0.55rem; font-size: var(--t-small); color: var(--muted);
}
/* Offset and deep, not a 2px brand-blue hairline. The old ring was #0091d9 on
   the wash — 3.26:1 — drawn hard against a #c8d0d4 border, so on the field it
   marked it was barely a change of colour. Keyboard focus has to be the most
   obvious thing on the page or it is not doing its job. */
input:focus-visible, textarea:focus-visible, select:focus-visible,
button:focus-visible, a:focus-visible {
    outline: 3px solid var(--blue-deep); outline-offset: 2px; border-color: var(--blue-deep);
}

/* Placeholders are examples, never values. Three rules keep that readable:
   a muted grey that still clears 3:1, because #9aa4a9 measured 2.54:1 and is
   not text anybody should have to squint at; and clearing it on focus, so the
   example cannot be mistaken for something already filled in. Every field
   has a real <label>, so nothing is lost when the placeholder goes. */
::placeholder { color: var(--faint); opacity: 1; }
:focus::placeholder { color: transparent; }
textarea { min-height: 4.5rem; resize: vertical; }
.narrow { max-width: 9rem; }
/* Two short controls side by side — unlike .row, which stretches its children. */
.pair { display: flex; gap: 0.6rem; }
.pair > select { flex: 0 0 5.5rem; }

/* One button, three weights, and an <a> can wear any of them — the download
   used to be a <button> nested inside an <a>, which is invalid and is announced
   inconsistently by screen readers. */
.btn {
    display: block; width: 100%; padding: 0.8rem 1rem; font: inherit;
    font-weight: 600; font-size: var(--t-body); line-height: 1.3; text-align: center;
    text-decoration: none; border: 0; border-radius: 6px; cursor: pointer;
    color: #fff; background: var(--blue-deep);
}
.btn:hover { background: var(--blue-lift); }
button { font: inherit; }
/* Secondary actions are not the same size as the thing they are beside. "Back"
   was a full-width block in a box of its own, identical in footprint to
   "Generate my plan" — the page offered two equal choices where one is the
   whole point of being on it. */
.btn.quiet {
    display: inline-block; width: auto; padding: 0.45rem 0.9rem;
    font-size: var(--t-small); font-weight: 600;
    color: var(--blue-deep); background: transparent; border: 1px solid var(--edge);
}
.btn.quiet:hover { background: var(--sink); }
.actions { display: flex; gap: 0.7rem; align-items: center; flex-wrap: wrap; margin-top: var(--s3); }
.actions.mid { justify-content: center; }
.actions form { margin: 0; }

/* A grid, not wrapping flex. Three boxes that wrap put the third alone on a
   row at double width, so on a phone "Hardest day" — the longest label of the
   three — got emphasis nobody gave it. Equal columns shrink together instead,
   and at 288px each still holds its label on one line. */
.stats {
    display: grid; gap: 0.7rem; margin: 0 0 var(--s3);
    /* minmax(0, 1fr), not 1fr: a bare 1fr floors at min-content, so on the
       delivery page "Hardest day" took 2.1x the width of "Est. load" below
       440px — the same orphaning, in a different shape. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
}
/* Two, on the delivery page: the third box used to hold a day name our own sort
   had chosen, and the plan's answer now sits in the sentence below instead. A
   third empty cell in a three-column grid leaves the pair at two-thirds width
   with a hole beside it. */
.stats.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* The plan's own account of which days it fuelled hardest, under the finished
   plan. The verdict line names the days and the table carries the sessions —
   the same two columns the preview's week table uses, so the athlete reads the
   plan's answer in the shape they read the question. Note no backticks in this
   comment: the stylesheet is a template literal, so one closes it. */
.hardest { margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--rule); }
.hardest .verdict { margin-top: 0; padding-top: 0; border-top: none; }
.hardest table.week { margin: 0; }
/* The row was the card's first child until the delivery page put the button
   above it, and a 6px navy block flush against an 8px card row reads as one
   fused control — the focus ring overlapped it too. */
.btn + .stats { margin-top: var(--s3); }
.stat {
    text-align: center; padding: 0.7rem 0.4rem;
    background: var(--wash); border: 1px solid var(--rule); border-radius: 8px;
}
.stat .k {
    display: block; text-transform: uppercase; letter-spacing: 0.1em;
    font-size: var(--t-micro); font-weight: 600; color: var(--accent);
}
.stat .v { font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 1.35rem; }

/* The week as seven boxes, so "three days" has a shape as well as a
   number. Two states, in/out, and neither is named after anything with
   "planned" in it: tr.unplanned below means *the coach has not written this
   day*, which is a different question this page answers in prose, and two greys
   meaning two things on one screen is how a reader learns neither. Nothing here
   may mark a day harder or easier — that is the plan's to say, and saying it
   before the plan exists is what 0053 deleted the verdict line for.

   There was a .window block here, for a preview that stated its window and
   drew a strip it did not own. The strip is a drawing again and the control is
   the pair of arrows beside it, so those rules went with the markup. (No backticks in this comment: the stylesheet is
   a template literal, so one closes it.) */
/* Both weeks are drawn and one is shown at a time. Both cards are in the page,
   so switching weeks costs no request and cannot be stale; what a reader sees is
   decided by the same checked radio that draws the strip. */
.weekcard, .wq { display: none; }
.eyewin span { display: none; }
/* **The default state is unguarded, and the toggling is what asks for support.**
   These three things are shown with :has(), which reaches backwards out of the
   form to the cards, the questions and the range at the top of the page.

   It used to be the other way round — the fallback sat inside a negative
   @supports on the same selector — and that fails in the one direction it
   must not. A browser that does not recognise selector() at all evaluates the
   condition as unknown, so the fallback does not apply; the :has() rules are
   dropped as invalid selectors at the same time, and the preview renders with no
   week table, no figures, no questions and an empty eyebrow. Stating the default
   first and hiding it only where the real rules work cannot fail that way: the
   worst case is the window the server rendered, which is the right degraded
   answer. */
.weekcard.wdefault, .wq.wdefault { display: block; }
.eyewin .wdefault { display: inline; }
@supports selector(:has(*)) {
    .weekcard.wdefault, .wq.wdefault { display: none; }
    .eyewin .wdefault { display: none; }
    .wrap:has(#win0:checked) .weekcard.w0,
.wrap:has(#win1:checked) .weekcard.w1 { display: block; }
    .wrap:has(#win0:checked) .wq.q0,
.wrap:has(#win1:checked) .wq.q1 { display: block; }
    .wrap:has(#win0:checked) .eyewin .ey0,
.wrap:has(#win1:checked) .eyewin .ey1 { display: inline; }
}
/* Two answers, one on screen, an arrow either side. The sibling combinator
   reaches forward from the radios, so the strip and the arrows go on working
   where :has() is missing — and the cards and the eyebrow above the form do not,
   which is a partial toggle and is deliberate. A control that moves nothing at
   all is worse: the form would still post the other window, and the athlete
   would have no way to tell. The caption travels with the strip, so what is
   selected is always stated by something that followed the press.
   Both rows are placed in the same grid cell deliberately: exactly one is ever
   displayed. */
.weeks {
    display: grid; grid-template-columns: auto 1fr auto; align-items: center;
    column-gap: 0.4rem; margin-top: 0.5rem;
}
.weeks > .arrow.prev { grid-column: 1; grid-row: 1; }
.weeks > .week-row { grid-column: 2; grid-row: 1; display: none; }
.weeks > .arrow.next { grid-column: 3; grid-row: 1; }
#win0:checked ~ .weeks .week-row.r0,
#win1:checked ~ .weeks .week-row.r1 { display: block; }
/* One window, nothing to press, and no radio to check it — the refusal page. */
.weeks > .week-row.only { display: block; }
/* At least 24x24, per WCAG 2.5.8 Target Size (Minimum) — the glyph alone is
   about half that, and padding is not a target. */
.arrow {
    min-width: 24px; min-height: 24px; display: flex; align-items: center;
    justify-content: center; border-radius: 6px; color: var(--muted);
    cursor: pointer; user-select: none;
    /* The global label rule sets a bottom margin, which inside a centred grid
       row lifts both glyphs off the optical centre. */
    margin: 0; border: 1px solid transparent;
}
/* **Never selection by hue alone.** The fill that says *checked* is 1.12:1
   against the card and the glyph 1.64:1 against an unchecked one, which is under
   the 3:1 this project holds a graphic to — so the border carries it, the same
   way the strip's in/out states are a fill *and* a strike rather than a colour. */
#win0:checked ~ .weeks .arrow.a0,
#win1:checked ~ .weeks .arrow.a1 {
    color: var(--blue-deep); background: var(--tint); border-color: var(--blue);
}
#win0:focus-visible ~ .weeks .arrow.a0,
#win1:focus-visible ~ .weeks .arrow.a1 { outline: 3px solid var(--blue-deep); outline-offset: 2px; }
/* **The arrows come off the row before the row overflows.** Flanking them costs
   the strip about 61px — two 24px targets and two gaps — and seven boxes cannot
   go below about 207px, so inside a 246px card at 320px the grid resolves wider
   than the page and the document scrolls sideways. Break-even is around 342px,
   so this fires well above it. Under here the strip spans the full width and the
   arrows sit beneath it, one at each end, which is also the ordinary shape for a
   pager on a phone. */
@media (max-width: 24rem) {
    .weeks { grid-template-columns: 1fr 1fr; row-gap: 0.4rem; }
    .weeks > .week-row { grid-column: 1 / -1; grid-row: 1; }
    .weeks > .arrow.prev { grid-column: 1; grid-row: 2; justify-self: start; }
    .weeks > .arrow.next { grid-column: 2; grid-row: 2; justify-self: end; }
}
.week-row .wk {
    display: block; font-size: var(--t-micro); font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent);
    margin-bottom: 0.2rem;
}
.daystrip {
    display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.25rem;
    margin: 0.6rem 0 0; padding: 0;
}
.daystrip > * {
    text-align: center; padding: 0.35rem 0; border-radius: 6px;
    font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.02em;
}
/* **Flex, so a stray whitespace node cannot add a line.** The rule is kept on
   its evidence: Sunday was once drawn rather than offered, so it was a span
   where the other six were labels — and the markup left a newline after its last
   child, which against a block-level number below it made an empty line box and
   stood that one box 4px taller than its row. Every box is a span now, which is
   what that evidence asks for: no two different elements measure the same. A
   whitespace-only child is still not a flex item, so this keeps the shape
   independent of how the JSX is wrapped. Found at 320px, where it shows most. */
.daystrip > * {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.daystrip .n { display: block; font-weight: 400; opacity: 0.75; }
/* Off the page but still in the tab order and still focusable: a radio group is
   the right control here, and display:none would take the keyboard with it.
   Its own class rather than a descendant selector, because the inputs sit
   *before* the strip — being siblings of it is what lets the rules above reach
   it at all. */
.winpick { position: absolute; width: 1px; height: 1px; opacity: 0; }
/* **Never colour alone.** The strike is the mark and the grey reinforces it, so
   the two states survive greyscale, a colour-blind reader and a photocopy — the
   same rule the PDF's day-card bar follows.

   line-through on the label rather than a diagonal across the box. The
   diagonal was built first and looked at: a linear-gradient band on a box this
   wide runs perpendicular to the box's own diagonal, so it cuts one corner and
   reads as a torn flap rather than as a strike. Striking the text is the mark
   everybody already knows, and it needs no second element and no script. */
/* The default look, overridden below for the days this window covers — so a box
   is "out" until the window puts it in, which is the state a strip with no
   window drawn should start in. */
.daystrip > * {
    background: var(--sink); color: var(--muted); border: 1px dashed var(--edge);
    text-decoration: line-through; text-decoration-thickness: 1px;
}
/* The days this window covers, marked server-side rather than selected.

   **It cannot be a selector any more.** The offset into the row is
   weekdayIndex(window.start), a function of the clock, and CSS here is a
   module-level template literal hashed into a static asset — so an nth-child
   rule would be computed once at import and then be wrong on six days in seven,
   silently. Which boxes are covered is a server-side fact per strip, which is
   what this class was before the strip briefly became the control.

   Taking the strike off is not tidiness: every box starts struck, so the rule
   that puts one in has to take the strike off again, or the covered days stay
   struck while turning blue. Measured in a browser, where exactly that happened.

   (0,2,0) against the default's (0,1,0), so it wins wherever it sits. What *is*
   decided by source order is this against .daystrip .n above, also (0,2,0) —
   they set nothing in common today, and that is the thing to check before adding
   a property to either. */
.daystrip .on {
    background: var(--tint); color: var(--blue-deep); border: 1px solid var(--blue);
    text-decoration: none;
}

/* line-height is stated, not inherited: inside a table it computes to "normal"
   whatever the body says, and the load marks are centred against a line box
   whose height has to be a number this stylesheet knows. */
table.week { width: 100%; border-collapse: collapse; font-size: var(--t-small); line-height: 1.5; }
table.week td { padding: 0.4rem 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
/* Every day cell carries the indent, not just the marked one: putting it on
   the peak row alone shifted that one day half a rem out of the column. */
table.week td.day { font-weight: 600; width: 6rem; padding-left: 0.5rem; }
table.week td.load { text-align: right; color: var(--muted); white-space: nowrap; width: 5.2rem; }
/* Days of the shown week the plan does not cover. Greyed rather than removed:
   the week is what the plan sits inside, and a table that got shorter with the
   later window would read as a fetch that came back short. Server-marked, for
   the reason .daystrip .on is. On the cell to match the two rules beside it,
   which are on the cell because colour is inherited and loses to them; opacity
   is not inherited and would work on the row, so this is consistency rather
   than necessity. A row can carry outside and unplanned at once — they are
   (0,2,3) apiece and set different properties, so both apply. */
table.week tr.outside td { opacity: 0.4; }
table.week tr.rest td { color: var(--muted); }
table.week tr.unplanned td { color: var(--accent); font-style: italic; }
/* Applied to the child rather than inherited, so it wins over the row colour and
   the italic an unplanned row sets on its cells — these are quieter than either
   reading, because they are what we could not read. */
table.week td .entries { display: block; margin-top: 0.15rem; color: var(--muted); font-style: normal; font-size: 0.92em; }
/* The hardest day, marked in the table as well as named in the sentence under
   it. The product’s whole claim is that the food follows this column; showing
   the column as seven numbers and then asserting a winner in prose asks the
   reader to do the comparison we already did. */
table.week tr:last-child td { border-bottom: 0; }
table.week tr.peak td { background: var(--tint); }
table.week tr.peak td.day { box-shadow: inset 3px 0 0 var(--blue); }

/* The week’s shape, drawn. Same numbers as the load column, as a bar per day,
   so an athlete can see at a glance which day the plan is built around.

   The selector is qualified with the table, because the cell rule above it —
   "table.week td" — is the more specific one, so every padding this column
   declared was silently discarded. That is why the mark sat hard against the
   top of the row and hard against the session text beside it.

   Top, not middle: middle would centre the mark in the whole row, and on a day
   whose session runs to two lines that slides it half a line down. The cell
   aligns to the first line; the mark is centred inside a box exactly one line
   tall, which is arithmetic rather than a nudge and survives a change of type. */
table.week td.shape {
    width: 4rem; padding: 0.4rem 0.7rem 0.4rem 0; vertical-align: top;
}
.shape .rule { display: flex; align-items: center; height: 1.5em; }
/* 3px of radius, not a pill. At 9px tall a fully rounded end turns the
   shortest bar — an easy day against a five-hour Saturday — into a dot, which
   reads as a marker rather than as a quantity. */
.shape .bar {
    height: 9px; border-radius: 3px; background: var(--blue); min-width: 6px;
}
/* A rest day is not a full-width dash. It was drawn at the column’s full
   width, which made Friday’s nothing exactly as long as Saturday’s 268 — in
   the one column whose whole job is showing which day is biggest. It is the
   same geometry as a bar, so the two sit on one line: a short stub, thinner
   and grey, which reads as "nothing here" rather than "a very small day". */
.shape .none { width: 10px; height: 3px; border-radius: 2px; background: var(--edge); }

/* The plan's own account of which days it fuelled hardest, given the weight of
   one — it used to be the smallest grey line on the page, indistinguishable from
   a footnote. **The finished plan's, and only that**: the preview had a verdict
   line here too and it is gone, because a serif conclusion under three figures
   and seven rows of sessions is a fourth thing to read where the athlete has
   finished reading. */
.verdict {
    margin: var(--s3) 0 0; padding-top: var(--s3); border-top: 1px solid var(--rule);
    font-family: 'Playfair Display', Georgia, serif; font-size: 1.2rem; line-height: 1.35;
}
.verdict strong { color: var(--blue-deep); }
.verdict .why { display: block; font-size: var(--t-small);
    font-family: 'Source Sans 3', -apple-system, sans-serif; color: var(--muted); margin-top: 0.3rem; }

.note { padding: 0.9rem 1rem; border-radius: 8px; font-size: var(--t-small); margin-bottom: var(--s3); }
.note.warn { background: #fdf3ea; border: 1px solid #f0d3b8; color: #8a4a17; }
.note.stop { background: #fdecec; border: 1px solid #f2c4c4; color: #8f2b2b; }

/* ------------------------------------------------------------- the wait ---
   The page an athlete has open for a quarter of an hour. Three things had to change:
   the live message was the smallest text on it while being the only thing that
   moved; the step list drew a *filled* mark below the current one, because the
   furthest step legitimately runs ahead of the current one, and as a bulleted
   list that reads as a rendering fault; and nothing at all moved between the
   five-second refreshes, which is what a hang looks like. */
/* Outlined, not just tinted. The track’s job is to show what is *left*, and
   #e6ecef on white is 1.19:1 — at 96% the bar looked full. The outline is the
   boundary that carries the shape; the interior stays light so the fill reads
   against it. */
.meter {
    height: 10px; border-radius: 5px; background: #dde5ea; overflow: hidden;
    border: 1px solid var(--edge); margin: 0 0 0.5rem;
}
.meter i {
    display: block; height: 100%; border-radius: 4px; position: relative;
    overflow: hidden;
    background: linear-gradient(90deg, var(--blue), var(--blue-deep));
}
/* A highlight running along the filled part. This is the one thing on the page
   that moves *between* refreshes, and it has to be the honest kind of movement:
   the bar’s length is measured, and only the sheen is decorative. A bar that
   crept forward on its own would be inventing progress. */
.meter i::after {
    content: ''; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.45), transparent);
    transform: translateX(-100%); animation: sweep 2.5s ease-in-out infinite;
}
@keyframes sweep { to { transform: translateX(100%) } }
/* Not uppercase and not tracked. The right-hand slot is a clock, and
   "28M 52S SO FAR" at 0.12em is a puzzle rather than a reading. */
.meter-label {
    display: flex; justify-content: space-between; gap: 1rem;
    font-size: var(--t-micro); font-weight: 600; color: var(--muted);
    margin: 0 0 var(--s3);
}
.meter-label span:last-child { font-weight: 400; }

.track { list-style: none; padding: 0; margin: 0; }
.track li {
    position: relative; padding: 0 0 var(--s3) 1.75rem; color: var(--muted);
    font-size: var(--t-small);
}
.track li:last-child { padding-bottom: 0; }
/* The dot and the rail are placed in pixels, not ems: the current row is set a
   step larger than the rest, so an em offset would put its dot on a different
   line from everybody else’s and leave a kink in the rail. */
.track li::before {
    content: ''; position: absolute; left: 0; top: 6px;
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--paper); border: 2px solid var(--edge); box-sizing: border-box;
}
/* The rail between this dot and the next one. Drawn per item rather than as one
   bar behind the list, so the filled length is exact whatever the rows measure —
   and so a filled rail can run *past* the current ring, which is the honest
   picture: the ring is where the work is, the fill is how far it has been.
   The negative bottom carries it into the next row, where that row’s dot is
   drawn over it. */
.track li::after {
    content: ''; position: absolute; left: 5px; top: 20px; bottom: -6px;
    /* 1.48:1 was a track you could not see, so "reached" and "not reached"
       rested on a distinction one of whose states was invisible. */
    width: 2px; background: var(--edge);
}
.track li:last-child::after { display: none; }
.track li.done::before { background: var(--blue); border-color: var(--blue); }
.track li.linked::after { background: var(--blue); }
.track li.now { color: var(--ink); font-weight: 600; font-size: var(--t-body); }
/* The heaviest mark on the page, not the lightest. As a white ring it shared
   its *shape* with the steps that have not happened, so on a job that has
   doubled back — filled, filled, ring, filled — it read as a hole punched in
   the rail rather than as the head of it. A filled disc inside a white gap
   inside a blue ring cannot be mistaken for an empty one. */
.track li.now::before {
    background: var(--blue); border: 3px solid var(--paper);
    box-shadow: 0 0 0 2px var(--blue);
}

/* What the model is doing, right now, in the athlete’s words — the only thing
   on this page that changes between refreshes, so it gets the room. */
/* Body size and full ink. At 14.4px grey it was pixel-identical to the four
   inert rows around it — the one line that changes every few seconds looked
   exactly like the ones that never do. */
.live {
    display: block; font-weight: 400; font-size: var(--t-body); color: var(--ink);
    margin-top: 0.2rem;
}
/* Every keyframe restates the 2px ring, because the animation owns the whole
   box-shadow property and would otherwise drop it for the length of the run. */
@keyframes halo {
    0%   { box-shadow: 0 0 0 2px var(--blue), 0 0 0 2px rgba(0, 145, 217, 0.45) }
    70%  { box-shadow: 0 0 0 2px var(--blue), 0 0 0 9px rgba(0, 145, 217, 0) }
    100% { box-shadow: 0 0 0 2px var(--blue), 0 0 0 2px rgba(0, 145, 217, 0) }
}
/* 2.5s, not 2.4s: the document reloads every 5 seconds and restarts both
   animations wherever they happen to be. 5 ÷ 2.4 lands mid-sweep, so the one
   element on the page whose job is to prove it is alive visibly snapped back
   twelve times a minute. 5 ÷ 2.5 and 60 ÷ 2.5 are both whole. */
.track li.now::before { animation: halo 2.5s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
    .track li.now::before { animation: none }
    .meter i::after { animation: none; display: none }
}

/* Three lines under the landing form. Not a feature list — the athlete cannot
   evaluate features before seeing the artifact — but the *shape of the next few
   minutes*, which they can. Pasting a calendar credential into a stranger’s
   form is easier when the next screen is known to be a read-only preview, and a
   twenty-minute wait is a different experience when it was announced. */
.steps { counter-reset: step; list-style: none; margin: 0; padding: 0; }
.steps li {
    position: relative; padding: 0 0 var(--s2) 2.1rem;
    font-size: var(--t-small); color: var(--muted);
}
.steps li:last-child { padding-bottom: 0; }
.steps li::before {
    counter-increment: step; content: counter(step);
    position: absolute; left: 0; top: 1px;
    width: 1.45rem; height: 1.45rem; border-radius: 50%;
    background: var(--tint); color: var(--blue-deep);
    font-size: var(--t-micro); font-weight: 600; line-height: 1.45rem; text-align: center;
}
.steps strong { color: var(--ink); font-weight: 600; }

.centred { margin: var(--s3) 0 0; text-align: center; color: var(--muted); font-size: var(--t-small); }
.centred.micro { margin-top: 0.3rem; }
.quiet { color: var(--muted); font-size: var(--t-small); }
.micro { color: var(--muted); font-size: var(--t-micro); }
footer { text-align: center; color: var(--muted); font-size: var(--t-micro); padding: var(--s3) 0 0; }
/* Not a bare ".wrap a": that rule is more specific than ".btn", so an <a>
   styled as the primary button had navy text on a navy ground — the label was
   there, and the button read as an empty box. */
footer a, .wrap a:not(.btn) { color: var(--blue-deep); }
/* The wash on paper is 1.05:1, so inside a card the chip was invisible and
   "webcal://" — the one string the athlete has to recognise literally — had
   no chip at all. */
code { background: var(--sink); border: 1px solid var(--edge); padding: 0.05em 0.3em;
    border-radius: 4px; font-size: 0.85em; }

/* A key, and a command with a key in it. Scrolled rather than wrapped, because
   a wrapped credential is one somebody copies with a newline in the middle of
   it — and select-all makes the whole block one click. */
pre.key { background: var(--sink); border: 1px solid var(--edge); border-radius: 6px;
    padding: 0.7rem 0.8rem; margin: 0 0 0.9rem; overflow-x: auto; font-size: 0.82rem;
    line-height: 1.5; white-space: pre; }
pre.key { user-select: all; -webkit-user-select: all; }

/* A list that is prose rather than a sequence — .steps numbers itself, and
   these are things that are true at the same time rather than in an order. */
ul.plain { margin: 0; padding-left: 1.1rem; }
ul.plain li { margin-bottom: 0.5rem; }
ul.plain li:last-child { margin-bottom: 0; }

/* 36rem, not 30rem. The table’s fixed columns are 96 + 64 + 83px, so between
   480 and 576px the session description was down to 160px and Sunday ran to
   four lines. The shape is the garnish; the session is the content. */
@media (max-width: 36rem) {
    td.shape { display: none; }
    /* 5rem leaves "Wednesday" 0.6px clear of the session text, so the padding
       is what actually keeps them apart. */
    table.week td.day { width: 5rem; padding-right: 0.6rem; }
}
@media (max-width: 30rem) {
    :root { --t-hero: 2.1rem; --s4: 1.25rem; --s5: 2rem; }
    .wrap { padding: 0 1rem 3rem; }
}

/* ---- the dashboard ----
   Only the owner sees this, so it is the one screen in the product that may be
   dense. A table you can read row by row is worth more here than anything
   aggregated: at this scale the rows *are* the finding. */
.admin { font-size: var(--t-small); }
.admin h2 { margin: var(--s4) 0 var(--s2); font-size: 1rem; letter-spacing: 0.02em; }
.admin .big { display: flex; flex-wrap: wrap; gap: var(--s3); margin-bottom: var(--s3); }
.admin .big div { min-width: 8rem; }
.admin .big .v {
  font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 1.6rem;
  line-height: 1.1;
}
.admin .big .k { color: var(--muted); font-size: var(--t-small); }
.admin .rows { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.admin .rows th {
  text-align: left; font-weight: 600; color: var(--muted); font-size: var(--t-small);
  padding: 0.3rem 0.5rem 0.3rem 0; border-bottom: 1px solid var(--rule); white-space: nowrap;
}
.admin .rows td {
  padding: 0.3rem 0.5rem 0.3rem 0; border-bottom: 1px solid var(--rule); vertical-align: top;
}
.admin .rows td.n, .admin .rows th.n { text-align: right; padding-right: 0.9rem; }
/* The base rule for a code element gives it a background and a border, which
   is right for a token quoted inside a sentence and wrong for a whole column
   of them — forty bordered boxes read as forty input fields. Note: no
   backticks in here, they would close the template literal. */
.admin .rows code {
  font-size: 0.82em; color: var(--muted);
  background: none; border: 0; padding: 0;
}
.admin .bar { display: inline-block; height: 0.55rem; background: var(--blue); border-radius: 2px; }
/* A destructive control on a row of forty. Small enough not to dominate the
   table, large enough to still be a 32px tap target on a phone. */
.btn.small { padding: 0.3rem 0.7rem; font-size: var(--t-small); min-height: 2rem; }
.admin .note-text { color: var(--muted); font-style: italic; }
/* ---- text somebody else wrote ----
   Anything an athlete typed can be one unbroken token: a pasted URL, or a fist
   on the keyboard. The CSS default refuses to break inside a word, so such a
   token does not overflow its own box quietly — it widens every ancestor, and
   the WHOLE PAGE gains a horizontal scrollbar. Measured on the panel before
   this rule: one 2000-character message made the document 13,475px wide in a
   1,512px viewport.

   anywhere, not break-word: it also lets the box shrink below the longest word
   when the layout is measuring min-content, which is what a table cell does and
   what a flex item does, and both hold this text.
   No backticks in here — they would close the template literal. */
.said li, .verbatim li, .note-text { overflow-wrap: anywhere; }
/* Wide on a phone is the normal case for a table like this; it scrolls itself
   rather than making the page scroll. */
.admin .scroll { overflow-x: auto; }
/* The band, above everything, answering only what is true right now. It sits
   before the masthead because the operator opening this page at eleven at night
   is asking whether something is stuck, not what the cohort did. */
.band {
  display: flex; flex-wrap: wrap; gap: 0 var(--s3);
  padding: 0.55rem 0.8rem; margin-bottom: var(--s3);
  border: 1px solid var(--rule); border-radius: 6px;
  font-size: var(--t-small); font-variant-numeric: tabular-nums;
}
.band b { font-weight: 600; }
.band .ok { color: var(--muted); }
.band .bad { color: var(--accent); font-weight: 600; }
/* Which window a heading is measured over, said on the heading rather than at
   the foot of the page. Two sections here are lifetime facts and two are not,
   and a control a screen away from both cannot say which it governs. */
.admin h2 .stamp {
  font-family: 'Source Sans 3', -apple-system, 'Segoe UI', sans-serif;
  font-weight: 400; font-size: var(--t-small); color: var(--muted); letter-spacing: 0;
}
.admin h2 .stamp a { color: var(--muted); }
.admin .verbatim { margin: 0 0 var(--s2); padding-left: 0.9rem; border-left: 2px solid var(--rule); }
.admin .verbatim li { color: var(--ink); margin-bottom: 0.35rem; }
/* Words somebody wrote, given room to be read rather than scanned. A
   comfortable measure even though the panel is otherwise dense — this is the
   one part of the page that is prose. max-width bounds the line; the
   overflow-wrap rule above is what bounds a word. */
.admin .said li { margin-bottom: 0.8rem; max-width: 46rem; }
.admin .said .k { white-space: nowrap; }
/* A machine's own words under the row they explain. The verbatim rule above
   draws the left rule and the overflow-wrap rule bounds a long token; these two
   keep the line breaks the thrower wrote, because a trace folded onto one line
   is a trace nobody reads, and drop the bullet, which is wanted on the two
   lists of prose and not on this. No backticks in here — they would close the
   template literal. */
.admin .verbatim.raw { list-style: none; }
.admin .verbatim.raw li { white-space: pre-wrap; margin-bottom: 0; }
/* The message belongs to the row above it, so the rule between the two goes:
   left in, one failure reads as two. */
.admin .rows tr.explained td { border-bottom: 0; padding-bottom: 0.15rem; }
.admin .rows tr.why td { padding-top: 0; }
/* A select and a submit on one row of forty. Inline so the column stays a
   column; the button is the same small destructive-scale control Remove uses,
   which is small enough not to dominate and still a 32px tap target. */
.admin .tag { display: flex; gap: 0.3rem; margin: 0; }
/* Two small controls side by side in one cell. The actions class on its own
   stacks them full-width, which is right under a card and wrong inside a table
   row. No backticks in here — they would close the template literal. */
.admin .actions.row { display: flex; gap: 0.3rem; margin: 0; flex-wrap: nowrap; }
.admin .actions.row .btn { width: auto; white-space: nowrap; }
/* The hash lookup, sitting on the same line as the view links rather than
   above or below them: it is one more way of asking the same question. */
.admin .find { display: inline-flex; gap: 0.3rem; align-items: center; margin: 0 0 0 0.4rem; }
.admin .find label { color: var(--muted); }
.admin .find input {
  font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: 0.15rem 0.35rem; border: 1px solid var(--edge); border-radius: 4px;
  background: var(--paper); color: var(--ink); width: 7rem;
}
.admin .tag select {
  font: inherit; padding: 0.15rem 0.3rem; border: 1px solid var(--edge);
  border-radius: 4px; background: var(--paper); color: var(--ink);
  /* Wide enough for the longest value. Sized to its content, a select whose
     current option is an em-dash collapses to a bare chevron — the column then
     shows a control with no visible state, which is worse than no column. */
  min-width: 6.5rem;
}
/* Tabular data that must not wrap: a timestamp broken over two lines makes
   every row in the table twice as tall for no gain. */
.admin .rows td.when { white-space: nowrap; }
