/* Interface kit: tokens, type and components for MarksMate's text-mode interface.
   Three files make the kit:
     kit.css     tokens, type and components (this file)
     glyphs.css  the status marks ([+] [!] [x] …): one rule per meaning
     motion.js   the text-mode animations (decode, type, meter, spin, print)
   A specimen of everything is in kit/index.html.

   Type: Departure Mono, one weight, on its 11 px grid (11 / 16.5 / 22 / 33 / 66). No bold: hierarchy is
   size, case and colour. The system sans only for sentences you read.
   Colour: the gun-marker tape (fluorescent green, magenta) plus shot-timer amber and red, on gunmetal
   (dark) or target paper (light).
   Motion: frames step like an LCD redrawing (steps(), ~30 ms a frame). Nothing tweens softly.
     - Only transform, opacity and clip-path animate. The one exception is reverse video (k-blip,
       k-flash): a colour swap in steps(1), never interpolated, two repaints of one small element.
     - Hover motion is gated to (hover: hover) and (pointer: fine); colour-only hover may ease 150 ms.
     - Under prefers-reduced-motion every animation and transition jumps to its end state (bottom of
       this file); motion.js does the same for the scripted ones, and canvas overlays use stepped().
     - Screen readers get the final text: while motion.js scrambles or types, the real text is already
       in the element and the scramble is drawn by .k-fx from data-k, with empty alt text.
     - Keyboard-initiated changes (tabs, shortcuts) and anything that happens many times a minute get
       no motion at all. */

@font-face {
  font-family: "Departure Mono";
  src: url("https://cdn.jsdelivr.net/gh/rektdeckard/departure-mono@1.500/public/assets/DepartureMono-Regular.woff2") format("woff2");
  font-display: swap;
}

/* ---------- tokens ---------- */

/* Dark (default); .k-dark forces it, e.g. on the camera stage, which sits over video. */
:root, .k-dark {
  color-scheme: dark;
  --bg: oklch(0.17 0.006 250);
  --surface: oklch(0.21 0.007 250);
  --surface-2: oklch(0.26 0.008 250);
  --line: oklch(0.31 0.01 250);
  --line-strong: oklch(0.46 0.012 250);
  --text: oklch(0.95 0.01 110);
  --muted: oklch(0.73 0.012 250);
  --faint: oklch(0.45 0.012 250);
  --good: oklch(0.85 0.19 140);
  --ok: oklch(0.84 0.14 80);
  --work: oklch(0.72 0.17 25);
  --mark: oklch(0.74 0.2 350);
  --disabled: oklch(0.5 0.01 250);
  --scrim: oklch(0 0 0 / 0.78);        /* behind text laid over video */
  --scrim-strong: oklch(0 0 0 / 0.88); /* behind a panel laid over video */
  --stage: oklch(0 0 0);               /* letterbox behind the camera */
}
@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;
    --bg: oklch(0.955 0.014 85);
    --surface: oklch(0.985 0.008 85);
    --surface-2: oklch(0.92 0.016 85);
    --line: rgb(50 40 20 / 0.14);
    --line-strong: rgb(50 40 20 / 0.34);
    --text: oklch(0.24 0.012 70);
    --muted: oklch(0.46 0.015 70);
    --faint: oklch(0.7 0.015 70);
    --good: oklch(0.5 0.14 145);
    --ok: oklch(0.54 0.13 70);
    --work: oklch(0.52 0.19 27);
    --mark: oklch(0.52 0.22 350);
    --disabled: oklch(0.62 0.012 70);
    --scrim: oklch(0.24 0.012 70 / 0.5);
  }
}
:root {
  --mono: "Departure Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --t-xs: 11px;
  --t-s: 16.5px;
  --t-m: 22px;
  --t-l: 33px;
  --t-xl: 66px;
  --k-frame: 30ms;          /* one animation frame; keyframes below step in whole frames */
  --k-ease: cubic-bezier(0.32, 0.72, 0, 1);
  --k-fast: 150ms;
}

/* ---------- base ---------- */

:root { font-family: var(--mono); font-size: var(--t-s); line-height: 1.5; font-synthesis: none; background: var(--bg); color: var(--text); }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100dvh; background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; }
button, input, select { font: inherit; color: inherit; }
b, strong { font-weight: inherit; }
h1, h2 { margin: 0; font-weight: normal; }
:focus-visible { outline: 2px solid var(--mark); outline-offset: 2px; }
::selection { background: var(--mark); color: var(--bg); }

/* Ruled heading: "── 01 SOURCE ──────". The rule and the optional number come from the kit. */
.k-rule, h2 {
  display: flex; gap: 1ch; align-items: baseline; margin-bottom: 16px;
  font-size: var(--t-s); text-transform: uppercase; white-space: nowrap; overflow: hidden;
}
.k-num { color: var(--muted); }

/* Visually hidden, still read by screen readers. */
.srOnly { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* Prose */
.k-prose { font-family: var(--sans); text-wrap: pretty; }

/* ---------- viewfinder frame ----------
   Crop-mark corners, the one signature shape: the camera stage, the safety box, overlays and the
   kit's panels all use it. Drawn with background layers so it costs no extra elements. */
.k-frame {
  --k-c: 14px; --k-w: 2px; --k-ink: var(--line-strong); --k-fill: transparent;
  background:
    linear-gradient(var(--k-ink) 0 0) left top / var(--k-c) var(--k-w),
    linear-gradient(var(--k-ink) 0 0) left top / var(--k-w) var(--k-c),
    linear-gradient(var(--k-ink) 0 0) right top / var(--k-c) var(--k-w),
    linear-gradient(var(--k-ink) 0 0) right top / var(--k-w) var(--k-c),
    linear-gradient(var(--k-ink) 0 0) left bottom / var(--k-c) var(--k-w),
    linear-gradient(var(--k-ink) 0 0) left bottom / var(--k-w) var(--k-c),
    linear-gradient(var(--k-ink) 0 0) right bottom / var(--k-c) var(--k-w),
    linear-gradient(var(--k-ink) 0 0) right bottom / var(--k-w) var(--k-c),
    var(--k-fill);
  background-repeat: no-repeat;
}

/* ---------- controls ---------- */

/* Buttons: square, uppercase, one filled primary per view. */
button, .button {
  display: inline-flex; align-items: center; justify-content: center; gap: 1ch; text-align: center;
  min-height: 48px; padding: 12px 18px; border-radius: 0;
  border: 1px solid var(--line-strong); background: transparent; color: var(--text);
  text-transform: uppercase; text-decoration: none; cursor: pointer; touch-action: manipulation;
  transition: background-color var(--k-fast) ease-out, color var(--k-fast) ease-out, border-color var(--k-fast) ease-out;
}
.primary { background: var(--text); border-color: var(--text); color: var(--bg); }
.danger { color: var(--work); border-color: color-mix(in oklch, var(--work) 60%, transparent); }
/* A primary that starts something gets a ► that nudges forward on hover. */
.k-go::after { content: "►"; content: "►" / ""; transition: translate calc(var(--k-frame) * 2) steps(2, jump-end); }
@media (hover: hover) and (pointer: fine) {
  button:hover:not(:disabled), .button:hover { background: var(--surface-2); }
  .primary:hover:not(:disabled), .button.primary:hover { background: color-mix(in oklch, var(--text) 85%, var(--bg)); }
  .k-go:hover:not(:disabled)::after { translate: 0.5ch 0; }
}
button:active:not(:disabled), .button:active { background: var(--surface-2); }
.primary:active:not(:disabled) { background: color-mix(in oklch, var(--text) 75%, var(--bg)); }
button:disabled { background: var(--surface); border-color: var(--line); color: var(--disabled); cursor: default; }
button:disabled.k-go::after { content: "[/]"; content: "[/]" / ""; }

/* Fields: small uppercase label above a square control. */
.field { display: grid; gap: 6px; font-size: var(--t-xs); text-transform: uppercase; color: var(--muted); }
.field select, .field input[type=number], .field input[type=file], .field input[type=email], .field input[type=password] {
  width: 100%; min-height: 44px; padding: 10px 12px; border-radius: 0;
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--text);
  font-size: var(--t-s); text-transform: none;
}
.field input::placeholder { color: var(--disabled); }
@media (hover: hover) and (pointer: fine) {
  .field select:hover, .field input:hover { border-color: var(--muted); }
}

/* Select: native control, kit chevron. */
.k-select { position: relative; display: block; }
.k-select select { appearance: none; -webkit-appearance: none; padding-right: 4ch; cursor: pointer; }
.k-select::after {
  content: "▼"; content: "▼" / ""; position: absolute; right: 12px; top: 50%; translate: 0 -50%;
  font-size: var(--t-xs); color: var(--muted); pointer-events: none;
}

/* Kit select (select.js): the native select stays underneath as the value; a trigger and a list
   draw it. Without select.js (or without the Popover API) nothing here applies. */
.k-select.on > select { position: absolute; inset: 0; opacity: 0; pointer-events: none; }
.k-selectBtn {
  display: flex; justify-content: flex-start; width: 100%; min-height: 44px; padding: 10px 4ch 10px 12px;
  border-color: var(--line-strong); background: var(--surface); color: var(--text);
  font-size: var(--t-s); text-transform: none; text-align: left;
}
.k-select.on { min-width: 0; }
/* width 0 + flex 1: a long value never widens the field's grid column; it ends in an ellipsis. */
.k-selectValue { flex: 1 1 0; width: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.k-select.on:has(.k-selectBtn:disabled)::after { color: var(--disabled); }
@media (hover: hover) and (pointer: fine) {
  .k-selectBtn:hover:not(:disabled) { background: var(--surface); border-color: var(--muted); }
}
.k-selectBtn:active:not(:disabled) { background: var(--surface); }
.k-select.open .k-selectBtn { border-color: var(--text); }
.field.invalid .k-selectBtn { border-color: var(--work); }

/* The list: under the trigger on desktop, a bottom sheet on phones. Rows are 44 px, no rules between
   them; the chosen one carries (*) like a segmented choice, the active one is reverse video. */
.k-list {
  position: fixed; inset: auto; margin: 0; padding: 0; overflow: hidden;
  display: none; flex-direction: column;
  border: 1px solid var(--text); background: var(--surface); color: var(--text);
  font-family: var(--mono); font-size: var(--t-s); line-height: 1.4; text-transform: none;
  overscroll-behavior: contain;
}
.k-list:popover-open { display: flex; }
.k-listRows { overflow-y: auto; overscroll-behavior: contain; }
.k-listHead { display: none; }
.k-option-row {
  display: flex; align-items: center; gap: 1ch; min-height: 44px; padding: 10px 12px;
  color: var(--muted); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent;
}
.k-option-row::before { content: "( )"; content: "( )" / ""; white-space: pre; flex: 0 0 auto; }
.k-option-row[aria-selected=true] { color: var(--text); }
.k-option-row[aria-selected=true]::before { content: "(*)"; content: "(*)" / ""; }
.k-option-row.active { background: var(--text); color: var(--bg); }
.k-option-row[aria-disabled=true] { color: var(--disabled); cursor: default; }
@media (forced-colors: active) { .k-option-row.active { outline: 2px solid Highlight; outline-offset: -2px; } }

.k-list[data-mode=sheet] {
  left: 0; right: 0; bottom: 0; width: 100%; max-width: 100%; max-height: min(70dvh, 520px);
  border: 0; border-top: 1px solid var(--line-strong); background: var(--bg);
  padding-bottom: env(safe-area-inset-bottom);
}
/* Older engines don't pass custom properties to ::backdrop: a literal first. */
.k-list[data-mode=sheet]::backdrop { background: rgb(0 0 0 / 0.6); background: var(--scrim, rgb(0 0 0 / 0.6)); }
.k-list[data-mode=sheet][data-covered] .k-listHead {
  display: block; margin: 0; padding: 14px 16px 10px; border-bottom: 1px solid var(--line);
  font-size: var(--t-xs); text-transform: uppercase; color: var(--muted);
}
.k-list[data-mode=sheet] .k-option-row { min-height: 52px; padding-inline: max(16px, env(safe-area-inset-left)) max(16px, env(safe-area-inset-right)); }

/* Opening by pointer prints the list in from the trigger side, 4 frames. Keyboard opens: no motion. */
.k-drop { animation: k-drop calc(var(--k-frame) * 4) steps(4, jump-end) both; }
.k-drop[data-side=up] { animation-name: k-drop-up; }
.k-drop[data-mode=sheet] { animation-name: k-drop-up; }
@keyframes k-drop { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }
@keyframes k-drop-up { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0); } }

/* Stepper: [-] value [+] (motion.js steppers() builds it around each number input). */
.k-step { display: grid; grid-template-columns: 44px 1fr 44px; }
.k-step input { text-align: center; appearance: textfield; -moz-appearance: textfield; border-inline: 0 !important; min-width: 0; }
.k-step input::-webkit-inner-spin-button, .k-step input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.k-stepBtn { min-height: 44px; padding: 0; border-color: var(--line-strong); background: var(--surface); font-size: var(--t-m); }

/* Segmented choice: (*) / ( ) marks, reverse video when chosen. */
.seg { display: flex; border: 1px solid var(--line-strong); margin-bottom: 16px; }
.seg label { flex: 1; min-width: 0; display: flex; }
.seg label > span { flex: 1; }
.seg label + label { border-left: 1px solid var(--line-strong); }
.seg input { position: absolute; opacity: 0; pointer-events: none; }
.seg span { display: block; min-height: 48px; padding: 12px 8px; color: var(--muted); cursor: pointer; text-align: center; }
.seg input:checked + span { background: var(--text); color: var(--bg); }
.seg input:focus-visible + span { outline: 2px solid var(--mark); outline-offset: 2px; }

/* Checkbox: the label draws [x] / [ ]; the real input stays for keyboard and screen readers. */
.check { position: relative; display: flex; gap: 1ch; align-items: baseline; padding: 10px 0; min-height: 44px; cursor: pointer; line-height: 1.4; }
.check::before { flex: 0 0 auto; }
.check input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.check:has(input:focus-visible) { outline: 2px solid var(--mark); outline-offset: 2px; }

/* Bracketed tag: [EXPERIMENTAL], [ESTIMATED], [ADVANCED]. */
.k-tag { font-size: var(--t-xs); text-transform: uppercase; color: var(--ok); white-space: nowrap; }
.k-tag::before { content: "["; }
.k-tag::after { content: "]"; }
.k-tag:empty { display: none; }

/* Text meter (motion.js meter()): ██████░░░░ in a status colour. */
.k-meter { white-space: pre; letter-spacing: 0.2ch; color: var(--text); } /* the gap makes cells, like a shot timer's battery bar */
.k-meter.good { color: var(--good); }
.k-meter.ok { color: var(--ok); }
.k-meter.work { color: var(--work); }

/* Ticks: one cell per rep in a set. ■ done, □ to go, the next one blinks. */
.k-ticks { display: flex; flex-wrap: wrap; gap: 0 0.5ch; color: var(--faint); letter-spacing: 0; }
.k-ticks i { font-style: normal; }
.k-ticks i.on { color: var(--good); }
.k-ticks i.flag { color: var(--work); }
.k-ticks i.next { color: var(--text); animation: k-blink 1s steps(1) infinite; }

/* Resting cursor after a status line. */
.k-cursor::after { content: "▌"; content: "▌" / ""; margin-left: 0.25ch; animation: k-blink 1s steps(1) infinite; }
/* Text effects (motion.js decode / type). The element already holds its final text, so screen readers
   and live regions read that once; the scramble or the typed frame is drawn over it from data-k with
   empty alt text. While typing, the overlay replaces the resting cursor (it draws its own █). Not for
   elements whose ::after is already a mark (.k-tag): print those in instead. */
.k-fx { position: relative; -webkit-text-fill-color: transparent; }
.k-fx::before { -webkit-text-fill-color: currentColor; }
.k-fx::after {
  content: attr(data-k); content: attr(data-k) / "";
  position: absolute; inset: 0; margin: 0; animation: none; visibility: visible;
  -webkit-text-fill-color: currentColor; white-space: pre; overflow: hidden; pointer-events: none;
}

/* ---------- status line marks: see glyphs.css (.dot.ready / .go / .good / .work / .info / .pause) ---------- */

/* ---------- shot-timer readout ----------
   One big number that behaves like a shot timer's LCD. data-state:
     idle     0.00 in faint ink, nothing armed
     standby  armed, waiting for a random beep. Never shows a countdown: a random start must not leak.
     go       the beep: reverse video in red (k-flash), 6 frames
     running  counting up from the beep in 30 ms steps (motion.js clock), magenta for the firing side
     hold     on target: the number freezes, the hold meter fills (k-gauge)
     result   the estimate decodes in; a par tag says hit or miss
     void     early start, no draw, not scored: -.-- and a tag that says why */
.k-timer { display: grid; gap: 6px; min-width: 0; }
.k-read { font-size: var(--t-xl); line-height: 1; font-variant-numeric: tabular-nums; white-space: nowrap; }
.k-read small { font-size: var(--t-s); color: var(--muted); }
.k-timer[data-state=idle] .k-read, .k-timer[data-state=void] .k-read { color: var(--faint); }
.k-timer[data-state=running] .k-read { color: var(--mark); }
.k-timer[data-state=hold] .k-read { color: var(--good); }
.k-timer .k-sub { display: flex; align-items: baseline; gap: 1ch; min-height: 1.5em; font-size: var(--t-xs); text-transform: uppercase; color: var(--muted); }
.k-timer .k-sub .glyph { margin-right: 0; }
/* Par: the second beep. A hit is green, a miss red, and the tag always says which in words. */
.k-par { font-size: var(--t-xs); text-transform: uppercase; white-space: nowrap; }
.k-par.hit { color: var(--good); }
.k-par.miss { color: var(--work); }
.k-par:empty { display: none; }

/* Gauge: label, a text meter and a value on one line (hold, calibration, rest). */
/* An output that changes more than once a second is aria-hidden; the status line carries the words. */
.k-gauge { display: flex; align-items: baseline; gap: 1ch; font-size: var(--t-xs); text-transform: uppercase; color: var(--muted); white-space: nowrap; min-width: 0; }
.k-gauge .k-meter { font-size: var(--t-s); overflow: hidden; }
.k-gauge output { color: var(--text); font-variant-numeric: tabular-nums; }
/* A gauge that comes and goes keeps its row, so nothing under it jumps mid-rep. */
.k-gauge.off { visibility: hidden; }

/* ---------- steps ----------
   A short progress list: [+] done, [▖] working (motion.js spin), [>] waiting on you, [ ] to do.
   The marks are the glyph kit's; the words in .srOnly say the same for screen readers. */
.k-steps { display: flex; flex-wrap: wrap; gap: 4px 2ch; margin: 0; padding: 0; list-style: none; font-family: var(--mono); font-size: var(--t-xs); text-transform: uppercase; color: var(--muted); max-width: none; }
.k-steps li { display: inline-flex; align-items: baseline; white-space: nowrap; }
.k-steps li::before { margin-right: 0.5ch; }
.k-steps li.g-good { color: var(--good); }
.k-steps li.g-busy, .k-steps li.g-current { color: var(--text); }

/* ---------- notices ----------
   Short lines that report something without taking over the status line: an ignored movement, a
   changed camera angle, a hand mismatch. Printed in (8 frames), printed out faster (4 frames), at most
   two at a time; a repeat refreshes the one already showing instead of stacking. */
.k-notices { display: grid; gap: 4px; justify-items: center; margin: 0; padding: 0; list-style: none; pointer-events: none; }
.k-notice {
  display: flex; align-items: baseline; gap: 1ch; max-width: 100%; padding: 6px 12px;
  background: var(--scrim); font-family: var(--sans); font-size: 14px; line-height: 1.4; text-wrap: pretty;
}
.k-notice::before { font-family: var(--mono); font-size: var(--t-xs); }
.k-unprint { animation: k-unprint calc(var(--k-frame) * 4) steps(4, jump-end) both; }
@keyframes k-unprint { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 0 100%); } }

/* ---------- state block: empty, loading, error, permission ----------
   A mark, a title, a sentence, and the action that gets you out. Used inside a .k-frame. */
.k-state { display: grid; gap: 10px; justify-items: start; align-content: center; padding: 22px; min-width: 0; }
.k-state > .glyph { font-size: var(--t-l); line-height: 1; margin: 0; }
.k-state h3 { margin: 0; font-weight: normal; font-size: var(--t-m); line-height: 1.2; text-transform: uppercase; }
.k-state p, .k-state li:not(.k-steps li) { margin: 0; font-family: var(--sans); color: var(--muted); max-width: 60ch; text-wrap: pretty; }
.k-state ol:not(.k-steps) { margin: 0; padding-left: 3ch; display: grid; gap: 4px; }
.k-state .k-acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }

/* ---------- confirm dialog ----------
   A native <dialog>: Esc cancels, focus is trapped and returned. The safe choice comes first and is
   focused; the destructive one sits apart, at the far end, in red. Opens with a print, closes at once. */
dialog.k-dialog { border: 0; padding: 0; margin: auto; width: min(480px, calc(100% - 32px)); background: transparent; color: var(--text); overflow: visible; }
dialog.k-dialog::backdrop { background: rgb(0 0 0 / 0.7); background: var(--scrim-strong, rgb(0 0 0 / 0.7)); }
.k-dialog .k-box { --k-ink: var(--text); --k-fill: var(--surface); display: grid; gap: 14px; padding: 22px; }
.k-dialog h3 { margin: 0; font-weight: normal; font-size: var(--t-m); text-transform: uppercase; }
.k-dialog p { margin: 0; font-family: var(--sans); color: var(--muted); text-wrap: pretty; }
.k-dialog .k-acts { display: flex; flex-wrap: wrap; gap: 10px 22px; justify-content: space-between; margin-top: 8px; }
.k-dialog .k-acts > * { flex: 1 1 160px; }

/* ---------- tabs ----------
   Reverse video for the open tab, like the segmented choice. Arrow keys move between tabs (motion.js
   tabs()); switching never animates, it is a keyboard action and happens often. */
.k-tabs { display: flex; border-bottom: 1px solid var(--line-strong); overflow-x: auto; scrollbar-width: none; }
.k-tabs [role=tab] { flex: 0 0 auto; min-height: 44px; padding: 10px 14px; border: 0; color: var(--muted); transition: none; }
@media (max-width: 480px) { .k-tabs [role=tab] { flex: 1 1 auto; padding-inline: 8px; font-size: var(--t-xs); } }
.k-tabs [role=tab][aria-selected=true] { background: var(--text); color: var(--bg); }
.k-tabs [role=tab] .k-num { color: inherit; opacity: 0.6; }
@media (hover: hover) and (pointer: fine) {
  .k-tabs [role=tab][aria-selected=true]:hover { background: var(--text); }
}
[role=tabpanel] { padding-top: 22px; }
[role=tabpanel]:focus-visible { outline-offset: 6px; }

/* ---------- sortable table ----------
   Header buttons cycle the sort; ↕ unsorted, ▲ ascending, ▼ descending (aria-sort carries it too). */
.k-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.k-table th, .k-table td { padding: 8px 1.5ch 8px 0; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
.k-table th { font-weight: normal; padding-block: 0; }
.k-table td.num, .k-table th.num { text-align: right; }
.k-sort {
  min-height: 44px; width: 100%; padding: 0; border: 0; gap: 0.5ch; justify-content: flex-start;
  font-size: var(--t-xs); color: var(--muted); transition: none;
}
th.num .k-sort { justify-content: flex-end; }
.k-sort::after { content: "↕"; content: "↕" / ""; color: var(--faint); }
[aria-sort=ascending] .k-sort { color: var(--text); }
[aria-sort=ascending] .k-sort::after { content: "▲"; content: "▲" / ""; color: var(--text); }
[aria-sort=descending] .k-sort { color: var(--text); }
[aria-sort=descending] .k-sort::after { content: "▼"; content: "▼" / ""; color: var(--text); }
.k-table tr.flag td { color: var(--work); }
.k-table tr.flag td:first-child, .k-table tr.flag td.num { color: var(--muted); }

/* ---------- sideways scroll ----------
   Wraps a table wider than its column. The inner box scrolls; the wrapper says so in words at its
   bottom-right (◁ MORE ▷), from data-edge, which motion.js scrollHint() keeps current. */
.k-scroll { position: relative; padding-bottom: 22px; min-width: 0; max-width: 100%; }
.k-scrollInner { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; }
.k-scroll::after {
  position: absolute; right: 0; bottom: 0; font-size: var(--t-xs); text-transform: uppercase;
  color: var(--muted); letter-spacing: 0.1ch; white-space: pre;
}
.k-scroll[data-edge=right]::after { content: "MORE ▷"; content: "MORE ▷" / "More columns to the right"; }
.k-scroll[data-edge=left]::after  { content: "◁ MORE"; content: "◁ MORE" / "More columns to the left"; }
.k-scroll[data-edge=both]::after  { content: "◁ MORE ▷"; content: "◁ MORE ▷" / "More columns both ways"; }
.k-scroll[data-edge=none] { padding-bottom: 0; }
.k-scroll[data-edge=none]::after { content: none; }

/* ---------- sparkline and bars ----------
   ▁▂▃▄▅▆▇█ for a row of values (motion.js sparkText), ▏▎▍▌▋▊▉█ for a horizontal bar to an eighth of a
   cell (barText). Both are in Departure Mono 1.5. A missing value is a ·. */
.k-spark { white-space: pre; letter-spacing: 0.1ch; line-height: 1; font-size: var(--t-m); color: var(--text); }
.k-spark i { font-style: normal; }
.k-spark i.flag { color: var(--work); }
.k-spark i.best { color: var(--good); }
.k-hist { display: grid; grid-template-columns: auto 1fr auto; gap: 2px 1.5ch; align-items: baseline; font-variant-numeric: tabular-nums; }
.k-hist span:nth-child(3n+1) { color: var(--muted); font-size: var(--t-xs); }
.k-hist .k-bar { white-space: pre; overflow: hidden; color: var(--text); }
.k-hist span:nth-child(3n) { color: var(--muted); text-align: right; }

/* Heat map (heatmap.js): a grid of text cells, how long something spent in each one drawn as a shade
   · ░ ▒ ▓ █ in amber. Marks on top keep their kit meaning: > start, + hold (good), • one rep's path
   (magenta, the tape colour). Shade carries the value, so colour is never the only signal. One cell
   is one character at line-height 1, so the grid stays square to the 11 px type grid. */
.k-heat { font-family: var(--mono); font-size: var(--t-xs); line-height: 1; letter-spacing: 0; white-space: pre; margin: 0; padding: 8px; background: var(--surface); border: 1px solid var(--line); overflow: hidden; width: max-content; max-width: 100%; }
.k-heat i, .k-heatRamp i { font-style: normal; }
.hm-body { color: var(--faint); }
.hm-1 { color: color-mix(in srgb, var(--ok) 40%, var(--surface)); }
.hm-2 { color: color-mix(in srgb, var(--ok) 65%, var(--surface)); }
.hm-3 { color: var(--ok); }
.hm-4 { color: color-mix(in srgb, var(--ok) 65%, var(--text)); }
.hm-ghost { color: var(--mark); }
.hm-start { color: var(--text); }
.hm-hold { color: var(--good); }
/* A photo of the shooter under the cells (heat-ui.js): grey, dim and soft-edged, so the heat and
   the magenta path stay on top. It sits above the plate's background and below the glyphs. */
.k-heat.hasPhoto { position: relative; isolation: isolate; }
.hm-photo { position: absolute; left: 8px; top: 8px; z-index: -1; overflow: hidden; pointer-events: none;
  --fade-x: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
  --fade-y: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
  -webkit-mask-image: var(--fade-x), var(--fade-y); -webkit-mask-composite: source-in;
  mask-image: var(--fade-x), var(--fade-y); mask-composite: intersect; }
.hm-photo img { position: absolute; max-width: none; filter: grayscale(1) blur(0.6px) contrast(0.85); opacity: 0.3; }
.noRepPhotos .hm-photo { display: none; }
@media (prefers-color-scheme: light) { .hm-photo img { opacity: 0.2; mix-blend-mode: multiply; } }

/* Split meter: one line of cells shared by result, largest remainder (motion.js splitCells). */
.k-splitRow { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 2ch; align-items: baseline; }
.k-split { white-space: pre; letter-spacing: 0.2ch; overflow: hidden; }
.k-split i { font-style: normal; }
.k-split .good { color: var(--good); }
.k-split .ok { color: var(--ok); }
.k-split .work { color: var(--work); }
.k-split .off { color: var(--faint); }

/* ---------- disclosure ----------
   <details class="k-disc">: ► shut, ▼ open. Replaces hover-only title tooltips, so help works on touch
   and from the keyboard. The help prints in when it opens and goes at once when it shuts. */
.k-disc > summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: 1ch; min-height: 44px; padding-block: 6px; }
.k-disc > summary::-webkit-details-marker { display: none; }
.k-disc > summary::after { content: "►"; content: "►" / ""; margin-left: auto; padding-left: 1ch; font-size: var(--t-xs); color: var(--faint); }
.k-disc[open] > summary::after { content: "▼"; content: "▼" / ""; color: var(--muted); }
.k-disc > summary:focus-visible { outline: 2px solid var(--mark); outline-offset: 2px; }
.k-disc > :not(summary) { margin: 0 0 8px; font-family: var(--sans); font-size: 14px; line-height: 1.45; color: var(--muted); text-transform: none; text-wrap: pretty; }
@media (hover: hover) and (pointer: fine) {
  .k-disc > summary:hover::after { color: var(--text); }
}

/* ---------- keycaps ----------
   Shown next to the control they trigger, only where there is a keyboard-and-mouse kind of pointer. */
.k-key { display: inline-block; padding: 0 0.6ch; border: 1px solid currentColor; font-size: var(--t-xs); line-height: 1.6; text-transform: uppercase; opacity: 0.7; white-space: nowrap; }
@media not ((hover: hover) and (pointer: fine)) { .k-key, .k-keys { display: none !important; } }
.k-keys { display: flex; flex-wrap: wrap; gap: 4px 2ch; margin: 0; padding: 0; list-style: none; font-size: var(--t-xs); text-transform: uppercase; color: var(--muted); }
.k-keys li { display: flex; gap: 1ch; align-items: baseline; }

/* ---------- file picker ----------
   The native input stays (keyboard, screen readers); a kit button and a [+] / [ ] line show its state. */
.k-file { position: relative; display: grid; gap: 8px; justify-items: start; }
.k-file input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.k-file .button { pointer-events: none; }
.k-file:has(input:focus-visible) .button { outline: 2px solid var(--mark); outline-offset: 2px; }
.k-file .k-fileName { display: flex; align-items: baseline; font-size: var(--t-s); color: var(--muted); text-transform: none; overflow-wrap: anywhere; }
.k-file .k-fileName.on { color: var(--text); }
@media (hover: hover) and (pointer: fine) {
  .k-file:hover .button { background: var(--surface-2); }
}

/* ---------- inline field error and a disabled control's reason ---------- */
/* A mark and a sentence on one line, the mark never wrapping away from it. k-err and k-why are
   k-lines in a colour. */
.k-line, .k-err, .k-why { display: flex; align-items: baseline; margin: 0; }
.k-line > .glyph, .k-line > .gunIcon { flex: 0 0 auto; }

.field.invalid input, .field.invalid select { border-color: var(--work); }
.field.invalid .k-stepBtn { border-color: var(--work); }
.k-err, .k-why { font-family: var(--sans); font-size: 13px; line-height: 1.4; text-transform: none; color: var(--work); }
.k-why { color: var(--muted); font-size: 14px; }
.k-err:empty, .k-why:empty { display: none; }

/* A full-width option button inside a list (camera hints): sentence case, left aligned. */
.k-option { width: 100%; justify-content: flex-start; text-align: left; text-transform: none; }

/* ---------- one-shot animations (motion.js play() replays them) ---------- */

/* print: a new line is drawn left to right in 8 frames. */
.k-print { animation: k-print calc(var(--k-frame) * 8) steps(8, jump-end) both; }
@keyframes k-print { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
/* blip: two frames of reverse video when a value changes. */
.k-blip { animation: k-blip calc(var(--k-frame) * 4) steps(1) both; }
@keyframes k-blip { 0%, 49% { background: var(--text); color: var(--bg); } 50%, 100% { background: transparent; } }
/* flash: the shot-timer GO, reverse video in red for 6 frames. */
.k-flash { animation: k-flash calc(var(--k-frame) * 6) steps(1) both; }
@keyframes k-flash { 0%, 99% { background: var(--work); color: var(--bg); } }
@keyframes k-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

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