/* Throughline — coach desk + public shared timeline.
   Coaching Kit house style. The palette is NOT new: it is lifted verbatim from
   ~/Desktop/formwork/public/styles.css, which in turn took it from Check-In
   Studio. Dark is the base; light arrives by media query unless `data-theme`
   forces it. There is deliberately no standalone [data-theme="dark"] block —
   dark lives on :root, so an unset preference falls through to it.

   ── The one thing this file is actually about ────────────────────────────────
   The axis is the signature element. Everything else — cards, forms, the desk
   chrome — is deliberately quiet so the axis carries the page. Two axis modes:

     .tl-axis   vertical spine. Points are nodes on the spine; a span occupies
                real VERTICAL extent (a bar as tall as its own row). Used by
                `progress` and `transform`, which read top-down into the past.

     .tl-plot   horizontal track. Points sit on the rule; a span occupies real
                HORIZONTAL extent (a bar whose width is its duration). Used by
                `roadmap`, which reads left-to-right into the future.

   ── Contract with app.js / share.js ─────────────────────────────────────────
   Position inside .tl-lane-track is driven by two unitless custom properties,
   both read as percentages of the track:

       <span class="tl-span" style="--x:12; --w:26">   left 12%, width 26%
       <span class="tl-point" style="--x:63">          centred at 63%
       <span class="tl-tick"  style="--x:50">Mar</span>

   Setting inline `left`/`width` directly also works — an inline declaration
   beats the calc() here — so either convention is safe.

   Inside .tl-row the spine glyph (.tl-node / .tl-bar) and the .mark may be
   emitted in either order; both glyphs are pinned to row 1 so grid placement
   does not depend on it. One mark per row.

   Kind and direction are read off the root: <div class="tl" data-kind="roadmap"
   data-direction="forward">. Direction flips which end of the spine carries the
   accent, because the accent end means "now".

   Class map, in the order it appears below:
     layout    .wrap .wrap-wide .top .mono .desk .desk-side .desk-main .toolbar
     type      h2 .overline .note .hint .lede .byline
     controls  .btn .btn-primary .btn-ghost .btn-danger .btn-small inputs .lbl
     objects   .card .pill .alert .empty .scroll table
     picker    .kinds .kind .kind-name .kind-blurb .kind-glyph
     axis      .tl .tl-head .tl-range .tl-axis .tl-row .tl-node .tl-bar
               .tl-plot .tl-lane .tl-lane-name .tl-lane-track .tl-span
               .tl-point .tl-tick .tl-now .tl-series .tl-key
     marks     .mark .mark-when .mark-label .mark-body .mark-meta .mark-value
*/

/* ═══════════════════════════════════════════════════════════════════ tokens */
/* Copied verbatim from formwork. Measured WCAG 2.1 contrast, computed not
   eyeballed (sRGB relative luminance), for the pairs that carry body text:

     DARK      --ink   on --paper 15.40:1   --ink   on --card 14.18:1
               --bone  on --paper 13.91:1   --bone  on --card 12.81:1
               --muted on --paper  6.40:1   --muted on --card  5.90:1
               --faint on --paper  5.25:1   --faint on --card  4.84:1
               --good  on --paper  6.62:1
     LIGHT     --ink   on --paper 15.06:1   --ink   on --card 16.43:1
               --bone  on --paper  5.15:1   --bone  on --card  5.62:1
               --muted on --paper  6.70:1   --muted on --card  7.31:1
               --faint on --paper  5.15:1   --faint on --card  5.62:1
               --good  on --paper  6.01:1

   Hover states repaint the background, so they are their own pair and are
   measured separately — a token that passes on --card can fail on the tint
   painted over it:

     .btn-danger:hover  tint = 10% accent on --card (#281e20 dark, #fae7df light)
               DARK    --accent-text on the tint      4.32:1  FAILS
                       80% accent on the tint         4.98:1  used
               LIGHT   --accent-text on the tint      6.41:1
                       80% accent on the tint         4.87:1  used
     .btn-primary:hover is covered by the --accent-hi note below (5.39:1).

   All pass 4.5:1 in both themes. Raw --accent as TEXT does not (4.49:1 dark,
   3.72:1 light on --paper) — which is exactly why --accent-text exists and why
   --accent is only ever used for fills, rules, rings and bars, never for prose.
   See the --accent-text note below for the one deliberate change.

   Known gap, inherited on purpose: --line on --paper is 1.34:1 dark / 1.54:1
   light, so the 1px border of an input or a .btn does not meet the 3:1 that
   WCAG 1.4.11 asks of a control boundary. Formwork and Check-In Studio have the
   same borders; fixing it here alone would make Throughline the odd window on
   the desk, and the palette is not this file's to redraw. Focus is unaffected —
   the focus ring is --accent at 4.49:1 dark / 3.72:1 light, comfortably over
   3:1 — so keyboard users always see where they are. Flagged, not silently
   shipped: it wants a decision across the whole Coaching Kit. */

:root {
  --paper: #0d0e11;
  --card: #16181c;
  --ink: #ece5d7;
  --bone: #e3dac9;
  --muted: #9a9488;
  --faint: #8a857a;
  --line: #272a30;
  --line-soft: #1e2126;
  --line-hover: #343841;
  --good: #4ea87a;
  --accent: #d9463a;
  --accent-dark: #b3352a;
  --on-accent: #150a09;

  /* --accent-text is the derived TEXT form of the accent, for links, overlines
     and inline emphasis. Formwork sets it to the raw accent in dark, which
     measures 4.49:1 on --paper — one hundredth under the 4.5:1 floor. The
     light theme already solves this by mixing toward --ink; because --ink is
     light in dark mode, the identical formula lightens instead of darkens.
     Same rule, both directions. 90% accent measures 5.15:1 on --paper and
     4.74:1 on --card, and shifts #d9463a to #dd594b — a change you cannot see
     next to a Formwork window, only in a contrast checker.
     --accent itself is untouched: buttons, nodes and bars stay exactly #d9463a. */
  --accent-text: color-mix(in oklab, var(--accent) 90%, var(--ink));

  /* Primary-button hover. Formwork darkens to --accent-dark, which drops the
     --on-accent label to 3.21:1. Brightening instead keeps the same dark label
     on a warmer red at 5.39:1, and mixes toward #fff rather than --ink so it
     brightens in BOTH themes rather than inverting in light. */
  --accent-hi: color-mix(in oklab, var(--accent) 90%, #fff);

  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.28);
  --radius: 14px;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --mono: "SF Mono", Menlo, Monaco, monospace;

  /* Axis materials. These live on :root, not on .tl, because the kind picker
     draws its glyph from the same rail and sits outside any timeline — a
     var() that resolves to nothing takes the whole declaration with it. */
  --tl-rail: color-mix(in oklab, var(--muted) 70%, var(--paper));
  --tl-grid: var(--line);
  --tl-node: 13px;
  --tl-inset: 7px;   /* keeps a node at 0% or 100% fully inside its track */

  color-scheme: dark;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --paper: #f3eee4; --card: #fbf8f2; --ink: #1b1a16; --bone: #6a6354;
    --muted: #585246; --faint: #6a6354; --line: #cec1a9; --line-soft: #e2d9c8;
    --line-hover: #6f6759; --good: #186644;
    --accent-text: color-mix(in oklab, var(--accent) 62%, var(--ink));
    --shadow: 0 1px 2px rgba(60,45,25,.10), 0 8px 24px rgba(60,45,25,.08);
    color-scheme: light;
  }
}
:root[data-theme="light"] {
  --paper: #f3eee4; --card: #fbf8f2; --ink: #1b1a16; --bone: #6a6354;
  --muted: #585246; --faint: #6a6354; --line: #cec1a9; --line-soft: #e2d9c8;
  --line-hover: #6f6759; --good: #186644;
  --accent-text: color-mix(in oklab, var(--accent) 62%, var(--ink));
  --shadow: 0 1px 2px rgba(60,45,25,.10), 0 8px 24px rgba(60,45,25,.08);
  color-scheme: light;
}

/* Selection state everywhere in this file is a 9–12% accent tint over --card.
   That lifts the surface just enough to break the quietest text on it: measured
   on the tint, --faint is 4.44:1 (9%), 4.41:1 (10%) and 4.33:1 (12%) in dark —
   all three under the 4.5:1 floor. Light is fine (4.87–5.06:1), so this is a
   dark-theme-only failure and the kind that ships unnoticed.
   Rather than retune the tint (which is doing real work — it is how "selected"
   reads), --faint is redefined locally to --muted, which measures 5.27–5.40:1
   on the same tints. Inherited, so it reaches .hint/.lbl/.byline/.mark-when
   without any of them needing to know they are inside a selected box. */
.alert,
.opt.sel,
.kind.sel,
.kind[aria-pressed="true"],
.kind:has(input:checked),
.side-item.is-active {
  --faint: var(--muted);
}

/* ═════════════════════════════════════════════════════════════════════ base */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* `hidden` has to win, and without this it does not.
   The browser's own sheet says [hidden] { display: none }, but that is a NORMAL
   author-level-beating rule only against nothing — any author `display` on a
   matching selector outranks it, and `.btn` sets `display: inline-block`. The
   masthead's Log out button ships with the `hidden` attribute and app.js only
   removes it when /api/auth/status reports both `enabled` and `authed`; with no
   coach password set that never happens, so the button stayed on screen with no
   click handler behind it — a dead control on the one desk that must look
   exactly as it always has. `!important` is the fix rather than reordering
   `.btn`, because the next component to set `display` would bring it straight
   back. */
[hidden] { display: none !important; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  padding: 0 0 6rem;
  overflow-wrap: break-word;
}
img, svg, video { max-width: 100%; }
img, video { height: auto; }

.wrap { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem; }
.wrap-wide { max-width: 76rem; }
.scroll { overflow-x: auto; }
.spacer { flex: 1; }
.stack > * + * { margin-top: .75rem; }

/* One rule, every interactive element. Nothing in this file ever sets
   `outline: none`, so this cannot be silently cancelled downstream. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
details:focus-visible,
label:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible,
[role="link"]:focus-visible,
[role="tab"]:focus-visible,
[contenteditable]:focus-visible,
.opt:focus-within,
.kind:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ═════════════════════════════════════════════════════════════════ masthead */
.top {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: 1.1rem 0 .9rem; border-bottom: 1px solid var(--line-soft);
  margin-bottom: 2rem;
}
.mono {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 8px;
  background: var(--accent); color: var(--on-accent);
  display: grid; place-items: center; font-weight: 800; font-size: .95rem;
}
.top h1 { font-size: 1rem; margin: 0; font-weight: 650; letter-spacing: -.01em; }
.top .by { color: var(--faint); font-size: .8rem; margin-left: auto; display: flex; gap: .5rem; align-items: center; }

/* ═══════════════════════════════════════════════════════════════ typography */
h2 { font-size: 1.6rem; line-height: 1.2; letter-spacing: -.02em; margin: 0 0 .4rem; font-weight: 680; }
h3 { font-size: 1.12rem; line-height: 1.3; letter-spacing: -.01em; margin: 0 0 .4rem; font-weight: 660; }
h4 { font-size: 1rem; margin: 0 0 .35rem; font-weight: 640; line-height: 1.35; }
p { margin: 0 0 .85rem; }
.overline {
  font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent-text); font-weight: 700; margin: 0 0 .55rem;
}
.note { color: var(--muted); font-size: .92rem; margin: .3rem 0 0; }
.hint { color: var(--faint); font-size: .86rem; margin: -.55rem 0 .8rem; }
.lede p { color: var(--bone); }
.byline { color: var(--faint); font-size: .8rem; letter-spacing: .08em; text-transform: uppercase; }
.err { color: var(--accent-text); }
.num { font-variant-numeric: tabular-nums; }
a { color: var(--accent-text); }
code { font-family: var(--mono); font-size: .85em; background: var(--card); padding: .15rem .4rem; border-radius: 5px; border: 1px solid var(--line-soft); overflow-wrap: anywhere; }
.foot { margin-top: 2.5rem; padding-top: 1.2rem; border-top: 1px solid var(--line-soft); color: var(--faint); font-size: .84rem; }

/* ══════════════════════════════════════════════════════════════════ controls */
input[type="text"], input[type="search"], input[type="number"], input[type="email"],
input[type="date"], input[type="time"], input[type="datetime-local"],
input[type="password"],
textarea, select {
  width: 100%; padding: .7rem .8rem; font: inherit; font-size: 1rem;
  color: var(--ink); background: var(--card);
  border: 1px solid var(--line); border-radius: 10px;
}
textarea { min-height: 5.5rem; resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--faint); opacity: 1; }
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

.lbl {
  display: block; font-size: .74rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint); font-weight: 650; margin: 0 0 .35rem;
}
.field { padding: 1.15rem 0; border-bottom: 1px solid var(--line-soft); }
.field:last-child { border-bottom: 0; }
.q { display: block; font-weight: 600; font-size: 1.02rem; margin: 0 0 .9rem; line-height: 1.4; }
.q .req { color: var(--accent-text); margin-left: .15rem; }
.grid2 { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr)); }
.grid2 > * { min-width: 0; }
.actions { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; margin-top: 1rem; }

.btn {
  font: inherit; font-weight: 640; padding: .78rem 1.4rem; border-radius: 11px;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
  cursor: pointer; text-decoration: none; display: inline-block; line-height: 1.25;
  transition: border-color .12s, background .12s, color .12s;
}
.btn:hover { border-color: var(--line-hover); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: var(--on-accent); }
.btn-ghost { background: transparent; }
.btn-ghost:hover { background: var(--card); }
.btn-danger { color: var(--accent-text); border-color: color-mix(in oklab, var(--accent) 40%, var(--line)); }
/* The label has to lift with the background. --accent-text is tuned against
   --card (4.74:1 dark); on the 10% accent hover tint it drops to 4.32:1, under
   the floor. 80% accent measures 4.98:1 dark / 4.87:1 light on that tint and is
   still plainly red — see the hover-state pair in the token note above. */
.btn-danger:hover {
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, var(--card));
  color: color-mix(in oklab, var(--accent) 80%, var(--ink));
}
.btn-small { padding: .38rem .8rem; font-size: .84rem; border-radius: 9px; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

.toolbar { display: flex; gap: .55rem; flex-wrap: wrap; align-items: center; margin: 0 0 1.25rem; }

/* choice rows, house pattern */
.opts { display: grid; gap: .5rem; }
.opt {
  display: flex; align-items: center; gap: .7rem; padding: .68rem .85rem;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px; cursor: pointer;
}
.opt:hover { border-color: var(--line-hover); }
.opt input { width: 1.05rem; height: 1.05rem; margin: 0; flex: 0 0 auto; }
.opt.sel { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 9%, var(--card)); }

/* ═══════════════════════════════════════════════════════════════════ objects */
.card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.25rem; box-shadow: var(--shadow);
}
.pill {
  display: inline-block; font-size: .72rem; font-weight: 600; line-height: 1.5;
  padding: .1rem .55rem; border-radius: 999px; border: 1px solid var(--line);
  color: var(--muted); vertical-align: .1em; white-space: nowrap;
}
.pill-good { color: var(--good); border-color: color-mix(in oklab, var(--good) 45%, var(--line)); }
.pill-accent { color: var(--accent-text); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); }
h3 .pill, h4 .pill { margin-left: .35rem; }

.alert {
  background: color-mix(in oklab, var(--accent) 12%, var(--card));
  border: 1px solid var(--accent); border-radius: var(--radius);
  padding: 1.1rem 1.2rem; margin-bottom: 1.25rem;
}
.alert h3 { margin: 0 0 .4rem; font-size: 1.02rem; }
.alert p { margin: 0 0 .8rem; font-size: .93rem; color: var(--bone); }
.alert :last-child { margin-bottom: 0; }

table { width: 100%; border-collapse: collapse; font-size: .92rem; }
th, td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--line-soft); }
th { font-size: .74rem; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); font-weight: 650; }

/* A real empty state: says what is missing and carries the action. */
.empty {
  border: 1px dashed var(--line-hover); border-radius: var(--radius);
  padding: 2.1rem 1.25rem; text-align: center;
  background: color-mix(in oklab, var(--card) 50%, transparent);
}
.empty h3 { margin: 0 0 .35rem; font-size: 1.02rem; font-weight: 640; }
.empty p { color: var(--muted); font-size: .93rem; margin: 0 auto 1.1rem; max-width: 30rem; }
.empty .actions { justify-content: center; margin-top: 0; }

/* ═════════════════════════════════════════════════════════════════════ desk */
.desk {
  display: grid; grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
  gap: 1.75rem; align-items: start;
}
.desk-side, .desk-main { min-width: 0; }
.desk-side { position: sticky; top: 1rem; }

.side-nav { display: grid; gap: .3rem; list-style: none; margin: 0; padding: 0; }
.side-item {
  display: block; width: 100%; text-align: left; font: inherit;
  padding: .6rem .75rem; border-radius: 10px; border: 1px solid transparent;
  background: none; color: var(--ink); cursor: pointer; text-decoration: none;
  transition: border-color .12s, background .12s;
}
.side-item:hover { border-color: var(--line); background: var(--card); }
.side-item.is-active { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 10%, var(--card)); }
.side-item strong { font-weight: 620; display: block; overflow-wrap: anywhere; }
.side-sub { display: block; color: var(--muted); font-size: .8rem; }

/* ═════════════════════════════════════════════════════════════ kind picker */
.kinds { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr)); }
.kind {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .1rem .65rem;
  align-items: start; padding: .8rem .9rem; cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  transition: border-color .12s, background .12s;
}
.kind:hover { border-color: var(--line-hover); }
button.kind { width: 100%; text-align: left; font: inherit; color: var(--ink); }
/* placed explicitly, so a .kind without a glyph does not leave a phantom row */
.kind input { grid-column: 1; grid-row: 1; width: 1.05rem; height: 1.05rem; margin: .25rem 0 0; }
.kind-name { grid-column: 2; font-weight: 640; font-size: .98rem; line-height: 1.3; }
.kind-blurb { grid-column: 2; color: var(--muted); font-size: .85rem; line-height: 1.4; }
.kind:has(input:checked), .kind.sel, .kind[aria-pressed="true"] {
  border-color: var(--accent); background: color-mix(in oklab, var(--accent) 9%, var(--card));
}

/* A 14px sketch of what the kind draws: a rail with two point nodes, or a rail
   with a span bar. Pure CSS — no image, no icon font. */
.kind-glyph {
  grid-column: 2; position: relative; height: 14px; margin-top: .55rem; align-self: end;
  background: linear-gradient(to right, var(--tl-rail) 0%, color-mix(in oklab, var(--tl-rail) 20%, transparent) 100%);
  background-size: 100% 2px; background-repeat: no-repeat; background-position: 0 50%;
}
.kind-glyph::before, .kind-glyph::after {
  content: ""; position: absolute; top: 50%; transform: translateY(-50%);
  width: 9px; height: 9px; border-radius: 50%;
  border: 2px solid var(--accent); background: var(--paper);
}
.kind-glyph::before { left: 4%; }
.kind-glyph::after { left: 46%; }
/* span-dominant kinds draw a bar instead of the second node */
.kind[data-kind="roadmap"] .kind-glyph::after, .kind-glyph.is-span::after {
  left: 40%; width: 52%; height: 10px; border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 30%, var(--card));
  box-shadow: inset 3px 0 0 var(--accent);
}
.kind[data-kind="roadmap"] .kind-glyph, .kind-glyph.is-span {
  background-image: linear-gradient(to right, color-mix(in oklab, var(--tl-rail) 20%, transparent) 0%, var(--tl-rail) 100%);
}

/* ═════════════════════════════════════════════════════════════════ THE AXIS */
/* Geometry locals, so one timeline can be retuned without touching the others.
   --tl-rail (defined on :root) measures 3.47:1 on --paper in dark and 3.50:1
   in light, clearing the 3:1 floor for a meaningful graphical object in both
   themes — the spine is structure, not decoration. */
.tl {
  --tl-gutter: 2.6rem;      /* width of the spine column in .tl-axis          */
  --tl-labelw: 9rem;        /* width of the lane-name column in .tl-plot      */
  --tl-laneh: 2.5rem;       /* height of one lane track                       */
  --tl-gridstep: 12.5%;     /* spacing of the vertical gridlines              */
  position: relative;
}

.tl-head { margin: 0 0 1.4rem; }
.tl-title { font-size: 1.6rem; line-height: 1.2; letter-spacing: -.02em; margin: 0 0 .2rem; font-weight: 680; overflow-wrap: anywhere; }
.tl-sub { color: var(--bone); margin: 0; }
.tl-range {
  display: flex; flex-wrap: wrap; gap: .35rem .9rem; align-items: baseline;
  margin: .55rem 0 0; color: var(--faint); font-size: .8rem;
  letter-spacing: .09em; text-transform: uppercase; font-variant-numeric: tabular-nums;
}

/* ─────────────────────────────────────────── vertical spine (points/progress) */
.tl-axis { position: relative; list-style: none; margin: 0; padding: 0; }

/* The spine. Accent at the "now" end, cooling to the rail and fading out into
   the far end — a timeline that literally runs out of ink as it goes. */
.tl-axis::before {
  content: ""; position: absolute; z-index: 0;
  left: calc(var(--tl-gutter) / 2 - 1px); width: 2px; top: .55rem; bottom: .55rem;
  border-radius: 2px;
  background: linear-gradient(to bottom,
    var(--accent) 0, var(--accent) 1.6rem,
    var(--tl-rail) 7rem,
    color-mix(in oklab, var(--tl-rail) 22%, transparent) 100%);
}
/* forward-reading vertical lists put "now" at the bottom instead */
.tl[data-direction="forward"] .tl-axis::before {
  background: linear-gradient(to top,
    var(--accent) 0, var(--accent) 1.6rem,
    var(--tl-rail) 7rem,
    color-mix(in oklab, var(--tl-rail) 22%, transparent) 100%);
}

.tl-row {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: var(--tl-gutter) minmax(0, 1fr);
  padding: 0 0 .9rem;
}
.tl-row:last-child { padding-bottom: 0; }

/* Both spine glyphs are pinned to row 1, not just column 1. With only a column
   given, grid auto-placement walks a cursor in DOM order and a row written as
   <mark> then <node> drops the node onto a second row — the axis silently comes
   apart. Row + column makes it definite, so it is placed before anything
   auto-flows and the mark lands beside it whichever order app.js emits. */
.tl-node {
  grid-column: 1; grid-row: 1; justify-self: center; align-self: start;
  width: var(--tl-node); height: var(--tl-node); margin-top: .55rem;
  border-radius: 50%; background: var(--paper);
  border: 2px solid var(--tl-rail);
  transition: border-color .12s, box-shadow .12s;
}
.tl-node.has-value, .tl-row.has-value .tl-node { border-color: var(--accent); }
/* The head of the timeline: the newest mark on a backward axis, the next one
   on a forward axis. Filled, ringed, unmistakable. The first row only inherits
   this when no row claims .is-now, so an explicit marker always wins. */
.tl-row.is-now .tl-node,
.tl-axis:not(:has(.is-now)) > .tl-row:first-child .tl-node {
  background: var(--accent); border-color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent);
}
.tl-row:hover .tl-node { border-color: var(--accent); }

/* A span on a vertical axis owns vertical extent: the bar is as tall as its
   own row, so a twelve-week block visibly outweighs a single weigh-in. */
.tl-bar {
  grid-column: 1; grid-row: 1; justify-self: center; align-self: stretch;
  width: 11px; margin: .55rem 0 .3rem; border-radius: 999px;
  background: linear-gradient(to bottom,
    color-mix(in oklab, var(--accent) 34%, var(--card)),
    color-mix(in oklab, var(--accent) 14%, var(--card)));
  border: 1px solid var(--accent);
  box-shadow: inset 0 3px 0 var(--accent);
}

/* ────────────────────────────────────────── horizontal track (spans/roadmap) */
/* Everything on the plot is absolutely positioned from --x/--w, which means a
   mark at 0% or 100% would otherwise hang half its own width outside the track
   and put a scrollbar on the whole page. The percentage is therefore mapped
   into an inset band — 0% lands at --tl-inset, 100% at (100% - --tl-inset) —
   so a node at either extreme sits fully inside. .tl-plot clips as a backstop. */
.tl-plot { position: relative; overflow: hidden; }
.tl-lanes { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }

.tl-lane {
  display: grid; grid-template-columns: minmax(0, var(--tl-labelw)) minmax(0, 1fr);
  gap: .25rem .75rem; align-items: center;
}
.tl-lane-name {
  min-width: 0; font-size: .88rem; font-weight: 600; line-height: 1.3;
  overflow-wrap: anywhere; color: var(--ink);
}
.tl-lane-sub { display: block; color: var(--faint); font-size: .76rem; font-weight: 400; }

.tl-lane-track {
  position: relative; min-width: 0; height: var(--tl-laneh);
  background-image: repeating-linear-gradient(to right,
    var(--tl-grid) 0 1px, transparent 1px var(--tl-gridstep));
}

/* The ruler lane carries the horizontal spine along its bottom edge. Accent at
   the "now" end again — left for a forward roadmap, right for a backward one. */
.tl-lane-ruler { margin-bottom: .5rem; }
/* tick text is free-length, so the ruler clips rather than pushing the page */
.tl-lane-ruler .tl-lane-track { height: 1.7rem; background-image: none; overflow: hidden; }
.tl-lane-ruler .tl-lane-track::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  border-radius: 2px;
  background: linear-gradient(to right,
    var(--accent) 0, var(--accent) 2.2rem,
    var(--tl-rail) 40%,
    color-mix(in oklab, var(--tl-rail) 22%, transparent) 100%);
}
.tl[data-direction="back"] .tl-lane-ruler .tl-lane-track::after {
  background: linear-gradient(to left,
    var(--accent) 0, var(--accent) 2.2rem,
    var(--tl-rail) 40%,
    color-mix(in oklab, var(--tl-rail) 22%, transparent) 100%);
}

.tl-tick {
  position: absolute; bottom: .35rem;
  left: calc(var(--tl-inset) + (100% - var(--tl-inset) * 2) * var(--x, 0) / 100);
  transform: translateX(-50%); white-space: nowrap;
  font-size: .7rem; letter-spacing: .06em; color: var(--faint);
  font-variant-numeric: tabular-nums;
}
/* keep the end labels inside the box rather than half-hanging off it */
.tl-tick:first-child { transform: none; }
.tl-tick:last-child { transform: translateX(-100%); }

.tl-span {
  position: absolute; top: 50%; transform: translateY(-50%);
  left: calc(var(--tl-inset) + (100% - var(--tl-inset) * 2) * var(--x, 0) / 100);
  width: calc((100% - var(--tl-inset) * 2) * var(--w, 100) / 100);
  min-width: 8px; height: 1.55rem;
  display: flex; align-items: center; overflow: hidden;
  padding: 0 .5rem; border-radius: 999px;
  border: 1px solid var(--accent);
  background: linear-gradient(to right,
    color-mix(in oklab, var(--accent) 34%, var(--card)),
    color-mix(in oklab, var(--accent) 14%, var(--card)));
  box-shadow: inset 3px 0 0 var(--accent);
  color: var(--ink); font-size: .8rem; font-weight: 600; line-height: 1;
  white-space: nowrap; text-overflow: ellipsis; text-decoration: none;
  font-family: inherit; cursor: default;
  transition: border-color .12s, box-shadow .12s;
}
button.tl-span, a.tl-span { cursor: pointer; }
button.tl-span:hover, a.tl-span:hover { box-shadow: inset 5px 0 0 var(--accent); }

.tl-point {
  position: absolute; top: 50%;
  left: calc(var(--tl-inset) + (100% - var(--tl-inset) * 2) * var(--x, 0) / 100);
  transform: translate(-50%, -50%);
  width: var(--tl-node); height: var(--tl-node); padding: 0;
  border-radius: 50%; background: var(--paper); border: 2px solid var(--accent);
}
button.tl-point, a.tl-point { cursor: pointer; }
button.tl-point:hover, a.tl-point:hover { box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent); }

/* "today" — an accent rule the height of the lane it sits in. To draw a rule
   that crosses the whole plot, put a .tl-now in every lane at the same --x;
   give data-label to exactly ONE of them (the ruler lane is the natural home)
   or the word prints once per lane. Without data-label the rule draws bare.
   The label sits to the right of the rule rather than centred on it, so it
   cannot hang off the left edge when today is the start of a forward roadmap. */
.tl-now {
  position: absolute; top: 0; bottom: 0;
  left: calc(var(--tl-inset) + (100% - var(--tl-inset) * 2) * var(--x, 0) / 100);
  width: 2px; background: var(--accent); pointer-events: none;
}
.tl-now::after {
  content: attr(data-label); position: absolute; top: -1.15rem; left: .3rem;
  white-space: nowrap;
  font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent-text); font-weight: 700;
}

/* Order only gets numbered where order genuinely means something — a roadmap's
   blocks run in sequence, a stream of weigh-ins does not. Opt in with
   .is-numbered; never applied by default. */
.is-numbered { counter-reset: tl-step; }
.is-numbered > .tl-lane:not(.tl-lane-ruler) .tl-lane-name::before,
.is-numbered > .tl-row .mark-when::before {
  counter-increment: tl-step;
  content: counter(tl-step, decimal-leading-zero) " ";
  color: var(--accent-text); font-weight: 700;
  font-variant-numeric: tabular-nums; margin-right: .3rem;
}

/* ────────────────────────────────────────────────────── series as a polyline */
/* Draw with a viewBox and preserveAspectRatio="none"; non-scaling-stroke keeps
   the line an even 2px however far the box is stretched. */
.tl-series { display: block; width: 100%; height: 7rem; overflow: visible; }
.tl-series polyline, .tl-series path, .tl-series line {
  fill: none; stroke: var(--accent); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.tl-series .tl-series-area { fill: color-mix(in oklab, var(--accent) 16%, transparent); stroke: none; }
.tl-series .tl-series-grid { stroke: var(--tl-grid); stroke-width: 1; }
.tl-series circle { fill: var(--paper); stroke: var(--accent); stroke-width: 2; vector-effect: non-scaling-stroke; }
.tl-series text { fill: var(--muted); font-family: var(--sans); font-size: 11px; }

.tl-key { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin: .6rem 0 0; padding: 0; list-style: none; }
.tl-key li { display: flex; align-items: center; gap: .4rem; font-size: .82rem; color: var(--muted); }
.tl-key-swatch { width: 14px; height: 3px; border-radius: 2px; background: var(--accent); flex: 0 0 14px; }

/* ══════════════════════════════════════════════════════════════════ marks */
.mark {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  padding: .8rem .95rem; min-width: 0;
  transition: border-color .12s;
}
.mark:hover { border-color: var(--line-hover); }
.mark-when {
  font-size: .73rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint); font-variant-numeric: tabular-nums;
}
.mark-label { font-weight: 640; font-size: 1.02rem; line-height: 1.35; margin: .12rem 0 0; overflow-wrap: anywhere; }
.mark-body { color: var(--bone); font-size: .93rem; margin: .35rem 0 0; overflow-wrap: anywhere; }
.mark-meta { display: flex; flex-wrap: wrap; gap: .4rem .55rem; align-items: center; margin: .55rem 0 0; }
.mark-value { font-family: var(--mono); font-size: .9rem; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.mark-value .unit { color: var(--muted); font-weight: 400; margin-left: .12rem; }
.mark-media { display: block; margin: .6rem 0 0; border-radius: 8px; border: 1px solid var(--line-soft); }
.mark.is-span { border-left-color: var(--accent); border-left-width: 3px; }
.mark.is-empty { color: var(--faint); }

/* ═════════════════════════════════════════════════════════════════ photos */
/* The picker is a real <input type="file"> with a real <label> — styled, not
   replaced. A div dressed as a button is not in the tab order, does not open
   the picker on Enter, and is announced as nothing; the focus ring on this one
   comes from the single input:focus-visible rule above, unchanged.
   ::file-selector-button styles the browser's own button inside it, so the
   control keeps its native keyboard behaviour and still looks like the house. */
.file-input {
  display: block; width: 100%; max-width: 100%;
  /* --ink, not --muted: the browser paints the chosen file name in this
     colour and the name of the photo the coach picked is not fine print. */
  font: inherit; font-size: .95rem; color: var(--ink);
  background: var(--card); border: 1px dashed var(--line);
  border-radius: 10px; padding: .6rem .7rem;
  cursor: pointer;
}
.file-input:hover { border-color: var(--line-hover); }
.file-input::file-selector-button {
  font: inherit; font-size: .84rem; font-weight: 600;
  color: var(--ink); background: var(--paper);
  border: 1px solid var(--line); border-radius: 9px;
  padding: .38rem .8rem; margin: 0 .7rem 0 0;
  cursor: pointer;
}
.file-input::file-selector-button:hover { border-color: var(--line-hover); }

/* "Uploading the photo…" while it is in flight, and nothing at all otherwise:
   an empty <p> still carries its top margin and would open a gap for nothing. */
.media-status:empty { display: none; }

/* The chosen photo, beside the button that takes it away again. */
.media-pick {
  display: flex; flex-wrap: wrap; gap: .75rem;
  align-items: flex-start; margin: .75rem 0 0;
}
.media-pick:empty { margin: 0; }
.media-thumb {
  display: block; width: 9rem; max-width: 100%; height: auto;
  border-radius: 10px; border: 1px solid var(--line); background: var(--card);
}

/* ═══════════════════════════════════════════════════════════════════ login */
/* One column, one field, one button, and nothing else on the page. The width is
   the whole design decision: 22rem is about as wide as a password field wants
   to be, and letting it run the full 44rem of .wrap would make a two-line page
   look like a form that lost its other questions.

   Nothing here sets a fixed pixel width, so at 360px the card is the column and
   the page still does not scroll sideways. */
.login { max-width: 22rem; margin: 2.5rem auto 0; }
.login h2 { font-size: 1.38rem; }
.login-card { margin-top: 1.25rem; }
/* The one .field on the page carries no divider — there is nothing below it to
   be divided from — and no top padding, because the card already has some. */
.login-card .field { padding: 0; border-bottom: 0; }
.login-card .actions { margin-top: 1.15rem; }
.login-card .btn-primary { width: 100%; text-align: center; }

/* `.login-err` is NOT in this rule, and that is the whole point of the line.
   It carries role="alert", and `display: none` takes a node out of the
   accessibility tree — so an empty-and-hidden region is not registered as a
   live region at page load, and the script hid it again on every submit before
   the new error arrived. The alert then only ever appeared via a
   none -> block flip, which is the documented way to make a live region not
   announce, and VoiceOver in Safari is the one that drops it most reliably.
   That is a wrong password producing no audible feedback at all.

   Left rendered it costs nothing: an empty `.login-err` has no height, no
   padding and no margin of its own — the margin below lives on the injected
   `<p>` — so the gap the old rule was protecting against does not exist. The
   region is now in the tree from first paint and an error is a plain content
   change to a region that is already live.

   `.login-notice` keeps the rule, because it really does carry `margin: .9rem`
   (below) and really would reserve a gap while empty. It is role="status", and
   its one message is set once, before anything else has been announced. */
.login-notice:empty { display: none; }
.login-err p { margin: .5rem 0 0; font-size: .92rem; font-weight: 600; }
/* A wrong password is a state of the field, not just a sentence near it. */
.login input[aria-invalid="true"] { border-color: var(--accent); }
.login-notice { margin: .9rem 0 0; color: var(--bone); }

/* ══════════════════════════════════════════════════════════ small screens */
/* Nothing here may introduce horizontal overflow at 360px: every grid child
   carries minmax(0,·) or min-width:0, long strings wrap, and the only element
   allowed to scroll sideways is .scroll. */
@media (max-width: 900px) {
  .desk { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .desk-side { position: static; }
  .tl-lane { grid-template-columns: minmax(0, 1fr); }
  .tl-lane-name { grid-column: 1; }
  .tl-lane-track { grid-column: 1; }
  /* Stacked, the lane name moves into the band the now-label was borrowing, and
     "Today" prints straight over the lane's sub-label. Give the label its own
     band. Scoped to [data-label] so a bare rule does not open a pointless gap.
     This query is unprefixed, so it governs the printed page too — where the
     page box is 703px and the lanes stack for the same reason.
     The label occupies exactly the 1.15rem above the track, so reserving only
     1.15rem clears the sub-label by a hair; 1.5rem leaves a readable gap. */
  .tl-lane:has(.tl-now[data-label]) .tl-lane-track { margin-top: 1.5rem; }
}
@media (max-width: 520px) {
  body { font-size: 16px; }
  h2, .tl-title { font-size: 1.38rem; }
  .wrap { padding: 0 1rem; }
  .tl { --tl-gutter: 2.1rem; --tl-gridstep: 25%; }
  .btn { padding: .7rem 1.1rem; }
  .kinds { grid-template-columns: minmax(0, 1fr); }
  .tl-range { font-size: .74rem; }
  .empty { padding: 1.5rem 1rem; }
}

/* ═════════════════════════════════════════════════════════ reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }
}

/* ═════════════════════════════════════════════════════════════════ print */
/* This is the PNG substitute, so it has to genuinely work. A printed timeline
   is a client deliverable: paper-white, ink-dark, desk chrome gone, one page.
   The token override repeats the [data-theme] selectors on purpose — a bare
   :root here would lose to :root[data-theme="light"] above on specificity and
   a coach printing in light mode would get the screen palette on paper. */
@media print {
  @page { margin: 12mm; }

  :root, :root[data-theme="light"], :root[data-theme="dark"] {
    --paper: #fff; --card: #fff; --ink: #111; --bone: #333;
    --muted: #444; --faint: #555;
    --line: #bbb; --line-soft: #ddd; --line-hover: #999;
    --accent: #b3352a; --accent-text: #8c392d; --accent-hi: #b3352a;
    --good: #186644; --on-accent: #fff;
    --shadow: none;
    --tl-rail: #777; --tl-grid: #e0e0e0;
    color-scheme: light;
  }

  html, body {
    background: #fff; color: #111;
    font-size: 10.5pt; padding: 0;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  /* backgrounds are dropped by default; the axis is meaningless without them */
  .tl, .tl *, .mark, .pill, .tl-span, .tl-point, .tl-node, .tl-bar {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  .wrap, .wrap-wide { max-width: none; padding: 0; }
  .desk { display: block; }
  .top { margin-bottom: 1rem; padding-top: 0; }

  /* chrome that means nothing on paper */
  .no-print, .toolbar, .desk-side, .top .by, .kinds, .actions,
  .btn, form, .side-nav, nav, .field, .opts, .alert {
    display: none !important;
  }

  /* one page: tighten everything the axis does not need */
  .tl-head { margin-bottom: .8rem; }
  .tl-title { font-size: 15pt; }
  .tl-range { font-size: 7.5pt; margin-top: .3rem; }
  .tl { --tl-laneh: 1.9rem; --tl-node: 11px; }
  .tl-row { padding-bottom: .35rem; }
  .tl-lanes { gap: .15rem; }
  .mark { padding: .35rem .55rem; border-radius: 6px; box-shadow: none; }
  .mark-when { font-size: 7pt; }
  .mark-label { font-size: 10pt; margin-top: 0; }
  .mark-body { font-size: 9pt; margin-top: .15rem; }
  /* Print is the documented path for a photo the SVG will not embed — the
     export draws "Photo not embedded — too large. Use Print." on the card and
     the README repeats it — so a printed photo has to be big enough to look
     at. 84mm is a little under half the 186mm text column inside the 12mm
     @page margin, so a before/after pair still sits side by side. One size for
     both surfaces: the share page used to carry its own copy of this rule and
     the desk inherited a 24mm thumbnail cap that nothing on the desk wanted
     (its form thumbnail is .media-thumb, and forms do not print at all). */
  .mark-media { max-width: 84mm; max-height: 96mm; width: auto; background: none; }
  /* A lane on the horizontal plot is a row on a scale, not a card. */
  .tl-lane-name .mark-media.is-thumb { max-width: 30mm; max-height: 30mm; }
  .tl-series { height: 4.5rem; }
  .foot { margin-top: 1rem; padding-top: .5rem; }

  /* never split a mark, a lane or the head of the axis across a page break */
  .tl-row, .tl-lane, .mark, .tl-head, .tl-key { break-inside: avoid; page-break-inside: avoid; }
  h2, h3, .tl-title { break-after: avoid; page-break-after: avoid; }

  /* the deliverable signs itself */
  .tl::after {
    content: "Coached by Camacho";
    display: block; margin-top: .9rem; padding-top: .45rem;
    border-top: 1px solid var(--line-soft);
    font-size: 7.5pt; letter-spacing: .12em; text-transform: uppercase; color: var(--faint);
  }
  a { color: #111; text-decoration: none; }
}
