/* One palette, defined once.
 *
 * Every colour in every stylesheet is a token from this block, so the dark
 * override below cannot miss a surface: adding a literal colour anywhere else
 * fails `theme.test.ts`, which is the only thing standing between a new panel
 * and unreadable white-on-white in dark mode.
 *
 * `--signal` is the one accent the whole page is built from. Its tint, glow
 * and focus ring are not tokens of their own: they are `color-mix()`ed from
 * `--signal` at the point of use, so re-tinting the accent re-tints them too.
 * A token has to resolve to a literal colour in both themes (the guard proves
 * it), which a `var()`-carrying mix cannot — hence only the hover shade, which
 * mixes toward black and would otherwise smuggle a literal in, is a token.
 */

:root {
  --source-a: #3977b8;
  --source-b: #a76124;
  --source-c: #32866b;
  --source-d: #9263b6;
  --source-e: #bd546a;
  --source-f: #368b99;
  --source-g: #8a7b29;
  --source-h: #7477c1;
  /* Stats chart series: validated categorical slots 1-3 (light). Not Source
     colours, which name a Source wherever they appear. */
  --chart-a: #2a78d6;
  --chart-b: #eb6834;
  --chart-c: #1baf7a;

  --page: #f7f7f5;
  --paper: #ffffff;
  --paper-veil: rgb(247 247 245 / 82%);
  --wash: #f7f7f5;
  --ink: #1d1d1f;
  --ink-soft: #3a3a3c;
  --ink-muted: #6e6e73;
  --ink-faint: #86868b;
  --ink-ghost: #a1a1a6;
  --border: rgb(0 0 0 / 6%);
  --border-firm: rgb(0 0 0 / 12%);
  --border-soft: rgb(0 0 0 / 5%);
  --border-pill: rgb(0 0 0 / 10%);
  --chip: rgb(0 0 0 / 4.5%);
  --signal: #1d1d1f;
  --signal-hover: #19191a;
  --on-signal: #fffef9;
  --danger: #9a2e2e;
  --danger-edge: rgb(154 46 46 / 25%);
  --danger-wash: rgb(154 46 46 / 9%);
  --warn-edge: #c9822b;
  --warn-ink: #8a4b12;
  --warn-wash: rgb(201 130 43 / 10%);
  --shadow-hair: rgb(0 0 0 / 3%);
  --shadow-card: rgb(0 0 0 / 5%);
  --shadow-raise: rgb(0 0 0 / 12%);
  --scrim: rgb(0 0 0 / 28%);
  color: var(--ink);
  background: var(--page);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "Segoe UI", sans-serif;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* Dark is a re-tint of the same roles, never a second set of rules. */
@media (prefers-color-scheme: dark) {
  :root {
    --source-a: #72aaf0;
    --source-b: #e8a25c;
    --source-c: #6ec3a0;
    --source-d: #ba93df;
    --source-e: #ed8fa5;
    --source-f: #75c5d2;
    --source-g: #c8b863;
    --source-h: #a5a7ed;
    --chart-a: #3987e5;
    --chart-b: #d95926;
    --chart-c: #199e70;

    --page: #161617;
    --paper: #1f1f21;
    --paper-veil: rgb(22 22 23 / 82%);
    --wash: #161617;
    --ink: #f2f2f4;
    --ink-soft: #c9c9ce;
    --ink-muted: #98989e;
    --ink-faint: #78787e;
    --ink-ghost: #636369;
    --border: rgb(255 255 255 / 8%);
    --border-firm: rgb(255 255 255 / 14%);
    --border-soft: rgb(255 255 255 / 7%);
    --border-pill: rgb(255 255 255 / 12%);
    --chip: rgb(0 0 0 / 25%);
    --signal: #f2f2f4;
    --signal-hover: #cdcdcf;
    --on-signal: #161617;
    --danger: #f08a8a;
    --danger-edge: rgb(240 138 138 / 30%);
    --danger-wash: rgb(240 138 138 / 14%);
    --warn-edge: #d8a05a;
    --warn-ink: #e8bd86;
    --warn-wash: rgb(216 160 90 / 14%);
    --shadow-hair: rgb(0 0 0 / 40%);
    --shadow-card: rgb(0 0 0 / 45%);
    --shadow-raise: rgb(0 0 0 / 55%);
    --scrim: rgb(0 0 0 / 55%);
  }
}

* {
  box-sizing: border-box;
}

body {
  /* One leading for every clamped transcript line and for the bubble that
     covers them. They must agree: a mismatch offsets the reveal's first line
     by half the difference, which is the error that actually shows. Read by
     `.table-frame td:last-child`, `.turn-row` and `.text-reveal` — and it
     lives here rather than on `:root`, which is the palette and nothing else. */
  --transcript-leading: 1.6;
  margin: 0;
  min-width: 20rem;
  min-height: 100vh;
  background: var(--page);
}

button,
input,
select {
  font: inherit;
}

a {
  color: var(--signal);
  text-decoration: none;
}

a:hover {
  color: var(--signal-hover);
  text-decoration: underline;
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--signal) 25%, transparent);
  outline-offset: 2px;
}

[hidden] {
  display: none !important;
}

/* A label the page still owes a screen reader but no longer draws. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A date wraps as one token: Safari otherwise breaks `2026-09-28` at a hyphen
   (`dateToken` in src/dom.ts). */
.date-token {
  white-space: nowrap;
}

/* ---------------------------------------------------------------- header */

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  border-bottom: 1px solid var(--border);
  background: var(--paper-veil);
  backdrop-filter: blur(20px) saturate(1.8);
}

.header-row {
  display: flex;
  gap: 0.5rem 1rem;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  width: min(70rem, calc(100% - 3rem));
  min-height: 3.5rem;
  margin: 0 auto;
  padding: 0.5rem 0;
}

.brand {
  display: inline-flex;
  gap: 0.6rem;
  align-items: center;
  color: var(--ink);
  font-size: 0.95rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  text-decoration: none;
}

.brand:hover {
  color: var(--ink);
  text-decoration: none;
}

.brand-mark {
  display: grid;
  width: 1.7rem;
  height: 1.7rem;
  place-items: center;
  border-radius: 0.55rem;
  color: var(--on-signal);
  background: var(--signal);
  font-size: 0.8rem;
  font-weight: 700;
}

/* The six views read as one control, so the current one is a raised pill
   inside the track rather than a link that happens to be darker. */
.view-nav {
  display: flex;
  gap: 0.15rem;
  max-width: 100%;
  padding: 0.2rem;
  overflow-x: auto;
  border-radius: 999px;
  background: var(--chip);
}

.nav-link {
  padding: 0.35rem 0.9rem;
  border: 0;
  border-radius: 999px;
  color: var(--ink-muted);
  background: transparent;
  font-size: 0.82rem;
  font-weight: 500;
  white-space: nowrap;
}

.nav-link:hover {
  color: var(--ink);
}

.nav-link-current {
  color: var(--ink);
  background: var(--paper);
  font-weight: 600;
  box-shadow: 0 1px 3px var(--shadow-raise);
}

main {
  width: min(70rem, calc(100% - 3rem));
  margin: 0 auto;
  padding: 2.75rem 0 6rem;
}

/* ------------------------------------------------------------ typography */

h1 {
  margin: 0;
  font-size: clamp(1.9rem, 4vw, 2.6rem);
  font-weight: 650;
  letter-spacing: -0.03em;
  line-height: 1.05;
}

h2 {
  margin: 0;
  font-size: 1.55rem;
  font-weight: 650;
  letter-spacing: -0.02em;
}

h3 {
  margin: 0;
  font-size: 1.15rem;
  font-weight: 650;
  letter-spacing: -0.015em;
}

.eyebrow {
  margin: 0 0 0.5rem;
  color: var(--ink-faint);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.lede,
.status,
.claim-hint,
.source-empty,
.access-keys-blurb,
.skill-install-blurb {
  color: var(--ink-muted);
}

.lede {
  max-width: 29rem;
  margin: 0.9rem auto 2.5rem;
  font-size: 0.95rem;
  line-height: 1.6;
}

.status {
  min-height: 1.25rem;
  margin: 0 0 0.8rem;
  font-size: 0.8rem;
}

.error {
  min-height: 1.5rem;
  margin: 0 0 1rem;
  color: var(--danger);
  font-size: 0.85rem;
  font-weight: 600;
}

.error:empty {
  display: none;
}

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

label {
  display: block;
  margin-bottom: 0.5rem;
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 600;
}

label span {
  color: var(--ink-ghost);
  font-weight: 450;
}

input,
select {
  width: 100%;
  height: 2.75rem;
  padding: 0 0.9rem;
  border: 1px solid var(--border-firm);
  border-radius: 0.7rem;
  color: inherit;
  background: var(--paper);
  font-size: 0.9rem;
}

select {
  height: 2.6rem;
  padding: 0 2rem 0 0.8rem;
  font-size: 0.85rem;
}

input::placeholder {
  color: var(--ink-ghost);
}

input[type="checkbox"] {
  width: 0.9rem;
  height: 0.9rem;
  padding: 0;
  accent-color: var(--signal);
}

button {
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
  font-weight: 600;
}

button:disabled {
  cursor: default;
  opacity: 0.45;
}

.primary-button {
  height: 2.6rem;
  padding: 0 1.3rem;
  color: var(--on-signal);
  background: var(--signal);
  font-size: 0.88rem;
}

.primary-button:hover:not(:disabled) {
  background: var(--signal-hover);
}

.secondary-button {
  height: 2.4rem;
  padding: 0 1rem;
  border-color: var(--border-pill);
  color: var(--ink);
  background: var(--paper);
  font-size: 0.82rem;
}

/* The small pill a row carries: Retry, Show Spans, Play from first turn. */
.quiet-button {
  height: 2rem;
  padding: 0 0.85rem;
  border: 1px solid var(--border-pill);
  border-radius: 999px;
  color: var(--ink);
  background: var(--paper);
  font-size: 0.75rem;
  white-space: nowrap;
}

.secondary-button:hover:not(:disabled),
.quiet-button:hover:not(:disabled),
.text-button:hover:not(:disabled) {
  background: color-mix(in srgb, var(--ink) 3%, var(--paper));
}

/* Borderless text, for an action that must not compete: Sign out. */
.text-button {
  padding: 0.4rem 0.2rem;
  border: 0;
  color: var(--ink-muted);
  background: transparent;
  font-size: 0.82rem;
  font-weight: 500;
}

.text-button:hover:not(:disabled) {
  color: var(--ink);
}

/* -------------------------------------------------------------- surfaces */

.pill-signal,
.pill-neutral,
.pill-danger {
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 600;
}

.pill-signal {
  color: var(--signal);
  background: color-mix(in srgb, var(--signal) 9%, transparent);
}

.pill-neutral {
  color: var(--ink-muted);
  background: var(--chip);
}

.pill-danger {
  color: var(--danger);
  background: var(--danger-wash);
}

/* -------------------------------------------------------------- headings */

.panel-heading,
.timeline-heading,
.separation-heading {
  display: flex;
  gap: 1.5rem;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  margin-bottom: 1.75rem;
}

.timeline-heading {
  margin-bottom: 2.25rem;
}


/* One Device, one day: the page's two selections sit together, above
 * everything they scope (adr/20260907-today-is-checked-a-day-is-read). */
.day-controls {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
}

.day-source-label {
  color: var(--ink-muted);
  font-size: 0.78rem;
  font-weight: 600;
}

.day-controls select {
  height: 2.4rem;
  max-width: 14rem;
  font-size: 0.85rem;
}

.day-navigation {
  display: flex;
  gap: 0.4rem;
}

.day-step {
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border: 1px solid var(--border-pill);
  border-radius: 999px;
  color: var(--ink);
  background: var(--paper);
  font-size: 0.95rem;
  font-weight: 500;
}

.day-step:hover:not(:disabled) {
  background: color-mix(in srgb, var(--ink) 3%, var(--paper));
}

/* ------------------------------------------------------------------ auth */

.auth-view {
  width: min(26rem, 100%);
  margin: 10vh auto 0;
  text-align: center;
}

.auth-mark {
  display: grid;
  width: 3rem;
  height: 3rem;
  place-items: center;
  margin: 0 auto 1.5rem;
  border-radius: 0.9rem;
  color: var(--on-signal);
  background: var(--signal);
  font-size: 1.3rem;
  font-weight: 700;
}

.auth-view h1 {
  font-size: 2rem;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.auth-card {
  padding: 1.75rem;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
  text-align: left;
}

.auth-actions {
  display: grid;
  gap: 0.6rem;
  margin-top: 1.25rem;
}

.auth-actions button {
  width: 100%;
  height: 2.75rem;
  padding: 0;
  font-size: 0.9rem;
}

.auth-note {
  margin: 1.5rem 0 0;
  color: var(--ink-ghost);
  font-size: 0.78rem;
}

/* ------------------------------------------------------------- transcript */

.transcript-link { display: block; margin-top: 2rem; padding-block: 1rem; border-top: 1px solid var(--border); }
.day-back { display: inline-block; margin-bottom: 1.5rem; }

/* ------------------------------------------------- one line of transcript */

/* Shared by the Utterance table and the Span's Voice transcript: both are
   hundreds of mostly-short lines with the occasional monologue, and a row that
   wraps pushes the conversation off the screen. Nothing here ever opens in
   place - the rest of the line is read in `.text-reveal` below, so a row's
   height never changes under the pointer. The text stays a button so Tab
   reaches it. See `src/expandable.ts`. */
.expandable-text {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  overflow: hidden;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  line-height: inherit;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The bubble that shows a clamped line in full. It lies *over* the line, first
   glyph on first glyph, so the text unfurls where it already was. Below the
   line instead — where a tooltip goes — its opening words would restate the
   clipped line still visible above it, and a reveal that adds nothing but the
   tail has no business repeating the head.

   The top layer is the only thing that escapes the two clips in its way:
   `.table-frame` scrolls sideways, and `overflow-x: auto` makes
   `overflow-y: visible` compute to `auto`, so it clips vertically too; and the
   cell doing the truncation clips its own positioned children. The browser
   keeps the bubble on its anchor through both scrolls - measured pixel-exact
   in Chrome 152 and Safari 26.6 - so no JavaScript moves it. Escape and
   click-away come free with `popover="auto"`. See `src/text_reveal.ts`. */
.text-reveal {
  /* Integer pixels, not rem: they keep the border one device pixel, and the
     row's own fractional line box is enough sub-pixel already. */
  --reveal-pad-x: 12px;
  --reveal-pad-y: 8px;
  --reveal-border: 1px;

  position-anchor: --rlcdv-text-reveal;
  /* The UA gives a popover `inset: 0; margin: auto`. Both have to go first:
     leaving `bottom: 0` with `top` set stretches the bubble to the viewport
     floor. */
  margin: 0;
  inset: auto;
  top: anchor(top);
  left: anchor(left);
  /* Pull the bubble up and left by its own padding AND border. `anchor(top)`
     places the *border box*, but the text starts a border and a padding inside
     it, so cancelling the padding alone leaves the first line low and right by
     exactly the border width - measured 1.00px off, 0.00px with this. */
  margin-top: calc(-1 * (var(--reveal-pad-y) + var(--reveal-border)));
  margin-left: calc(-1 * (var(--reveal-pad-x) + var(--reveal-border)));

  /* At least as wide as the line it covers, or the clipped text peeks out to
     its right. The `+ 2 x (padding + border)` is not optional: `min-width`
     sizes the border box, and the text lives that much further in. */
  min-width: calc(anchor-size(width) + 2 * (var(--reveal-pad-x) + var(--reveal-border)));
  max-width: min(38rem, 92vw);
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--reveal-pad-y) var(--reveal-pad-x);
  border: var(--reveal-border) solid var(--border-firm);
  border-radius: 0.5rem;
  color: var(--ink);
  /* Opaque, always: this sits over live text, and any translucency shows the
     row's own words through the reveal's. */
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
  /* Must match the row's size AND leading. A `line-height` mismatch costs
     (bubble - row) / 2 px of vertical error, which is what actually shows. */
  font-size: 0.88rem;
  line-height: var(--transcript-leading);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  position-try-fallbacks: --rlcdv-reveal-up;
}

/* Near the foot of the viewport it unfurls upward instead, landing its *last*
   line on the row's line - so the compensation mirrors too. */
@position-try --rlcdv-reveal-up {
  top: auto;
  bottom: anchor(bottom);
  margin-top: 0;
  margin-bottom: calc(-1 * (var(--reveal-pad-y) + var(--reveal-border)));
}

.table-frame {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

/* Every table on the page is one card with a hairline between rows; only the
   column widths differ, and those stay with the view that owns them. */
table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  padding: 0.85rem 1rem;
  text-align: left;
  vertical-align: top;
}

th {
  padding: 0.9rem 1rem;
  color: var(--ink-faint);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

tbody tr {
  border-top: 1px solid var(--border-soft);
}

/* Fixed layout is what makes a one-line row possible: with the automatic
   algorithm a long Utterance simply widens its column and the frame scrolls
   sideways, so the text never reaches the ellipsis. */
.table-frame table {
  min-width: 0;
  table-layout: fixed;
}

/* Every transcript row is one line, so the cells sit on one middle. */
.table-frame td,
.table-frame th {
  padding: 0.55rem 1rem;
  vertical-align: middle;
}

/* Time: the start clock and the duration on one line, never wrapped. An
   uncertain time is marked in place (`≈`, `Received`) and its sentence is
   the cell's `title` and accessible name. */
.timeline-time,
.timeline-time-head {
  width: 9.5rem;
}

.timeline-time {
  color: var(--ink-faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  white-space: nowrap;
}

.timeline-duration {
  display: inline-block;
  margin-left: 0.5rem;
  color: var(--ink-muted);
}

/* Source: dot and name in a column of their own, clipped rather than wrapped.
   Under a filter the rows carry no cell and the header hides its own. */
.timeline-source,
.timeline-source-head {
  width: 8rem;
}

.timeline-source {
  color: var(--ink-soft);
  font-size: 0.8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timeline-filtered .timeline-source-head {
  display: none;
}

.table-frame td:last-child {
  color: var(--ink-soft);
  font-size: 0.88rem;
  line-height: var(--transcript-leading);
}

/* The line being read. `:hover` alone is not enough: the bubble covers the
   line and so takes the pointer off it, and both engines switch `tr:hover`
   back off at that moment - measured - leaving the row you are reading the
   only unlit one. `is-revealing` is set from `text_reveal.ts`, which is the
   only thing that knows. */
@media (hover: hover) {
  .table-frame tbody tr:hover {
    background: var(--wash);
  }
}

.table-frame tbody tr.is-revealing {
  background: var(--wash);
}

/* ---------------------------------------------------------------- recaps */

.recaps-list {
  display: grid;
  gap: 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One civil day: its date, then one row per Source that has a Recap for it. */
.recap-day {
  display: grid;
  gap: 0.6rem;
}

.recap-day-heading {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.8rem;
  font-weight: 650;
  letter-spacing: 0.02em;
}

.recap-day-rows {
  display: grid;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.recap-row {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  gap: 0.5rem 0.6rem;
  align-items: center;
  padding: 1.25rem 1.4rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair);
}

.recap-link {
  grid-area: 1 / 1;
  padding: 0;
  border: 0;
  color: var(--ink);
  background: transparent;
  font-size: 0.95rem;
  font-weight: 650;
  text-align: left;
}

.recap-link:hover {
  color: var(--signal-hover);
}

.recap-row-state {
  grid-area: 1 / 2;
  justify-self: start;
}

.recap-retry {
  grid-area: 1 / 3;
  justify-self: end;
}

.recap-headline {
  grid-area: 2 / 1 / 2 / -1;
  margin: 0;
  color: var(--ink);
  font-weight: 650;
  overflow-wrap: anywhere;
}

.recap-row:has(.recap-headline) .recap-snippet {
  grid-area: 3 / 1 / 3 / -1;
}

.recap-snippet {
  display: -webkit-box;
  grid-area: 2 / 1 / 2 / -1;
  margin: 0;
  overflow: hidden;
  color: var(--ink-muted);
  font-size: 0.85rem;
  line-height: 1.55;
  overflow-wrap: anywhere;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

#recaps-more {
  margin-top: 1.25rem;
}

/* --------------------------------------------------------------- devices */

#source-status {
  margin: 0.6rem 0 0;
  font-size: 0.85rem;
}

.source-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
  /* A card is as tall as what it says: one opened Details must not stretch its row. */
  align-items: start;
  gap: 0.9rem;
  margin-bottom: 2.5rem;
}

.source-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  /* Cards in one grid row share its height; the spare goes below the rows,
     never between them, so a pendant's shorter card keeps a board's header. */
  align-content: start;
  gap: 1.1rem 0.6rem;
  align-items: center;
  padding: 1.4rem;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

/* A card with a problem row reads as one at a glance, before a row is read. */
.source-trouble {
  border-color: var(--danger-edge);
  border-left: 3px solid var(--danger);
}

.source-id {
  grid-area: 1 / 1;
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.88rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* Liveness is the first thing the card says, so it is drawn rather than
   spelled: a lit dot for a Device the core is hearing from right now. */
.source-id::before {
  content: "";
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  margin-right: 0.6rem;
  border-radius: 999px;
  background: var(--ink-ghost);
  vertical-align: middle;
}

.source-online::before {
  background: var(--signal);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--signal) 11%, transparent);
}

.source-phase {
  grid-area: 1 / 2;
  justify-self: end;
}

/* Below the header every part of the face spans the card, one per row:
   summary, problems, the bundle on offer, Details. */
.source-summary,
.source-problems,
.source-offer,
.source-details {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}

/* The normal state in one line: machine text, the Device's own readings. */
.source-summary {
  color: var(--ink-soft);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Trouble, visible without a tap. */
.source-problems {
  display: grid;
  gap: 0.3rem;
  padding: 0;
  color: var(--danger);
  list-style: none;
}

.source-problems li {
  padding: 0.35rem 0.6rem;
  border-radius: 0.5rem;
  background: var(--danger-wash);
}

/* A board heard on another version than the bundle on offer: not a fault, a
   sentence pointing at /start#update. */
.source-offer {
  color: var(--ink-muted);
}

.source-details summary {
  width: fit-content;
  color: var(--ink-muted);
  font-size: 0.78rem;
  cursor: pointer;
}

.source-details dl {
  display: grid;
  gap: 0.5rem;
  margin: 0.7rem 0 0;
}

.source-details dl div {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: 0.6rem;
  align-items: baseline;
}

.source-details dt {
  color: var(--ink-faint);
  font-size: 0.75rem;
}

/* Every reading is machine text in full, wrapping and never truncated; State
   is the one sentence. */
.source-details dd {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow-wrap: anywhere;
}

.source-details .source-sentence dd {
  font-family: inherit;
}

/* A card's rare actions sit behind one ellipsis (`src/popover_menu.ts`). */
.menu-toggle {
  width: 2rem;
  height: 2rem;
  padding: 0;
  border-color: var(--border-pill);
  color: var(--ink-muted);
  background: var(--paper);
  font-size: 1rem;
  line-height: 1;
}

.menu-toggle:hover {
  background: color-mix(in srgb, var(--ink) 3%, var(--paper));
}

/* The one control on a Device card's face. */
.source-menu-toggle {
  grid-area: 1 / 3;
}

/* Top layer, placed under the ellipsis and right-aligned to it; near the foot
   of the viewport it flips above. The UA's `inset: 0; margin: auto` has to go
   first, as for the transcript reveal. */
.popover-menu {
  margin: 0.35rem 0 0;
  inset: auto;
  top: anchor(bottom);
  right: anchor(right);
  position-try-fallbacks: flip-block;
  width: max-content;
  max-width: min(18rem, calc(100vw - 2rem));
  padding: 0.35rem;
  border: 1px solid var(--border-firm);
  border-radius: 0.75rem;
  color: var(--ink);
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

.popover-menu-item {
  display: block;
  width: 100%;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  color: var(--ink);
  background: transparent;
  font-size: 0.85rem;
  font-weight: 500;
  text-align: left;
}

.popover-menu-item:hover:not(:disabled),
.popover-menu-item:focus-visible {
  background: color-mix(in srgb, var(--ink) 6%, var(--paper));
}

/* Why an Online board's Release is off. Not danger - nothing is at stake until
   the board is offline. */
.source-menu-note {
  margin: 0.1rem 0.75rem 0.4rem;
  color: var(--ink-muted);
  font-size: 0.78rem;
}

/* A dialog is in the top layer but still inherits from where it is written:
   a Key's sits in a right-aligned table cell. */
.source-dialog {
  width: min(26rem, calc(100vw - 2rem));
  padding: 1.4rem;
  text-align: start;
  border: 1px solid var(--border-firm);
  border-radius: 1.25rem;
  color: var(--ink);
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

.source-dialog::backdrop {
  background: var(--scrim);
}

.source-dialog h2 {
  margin: 0 0 0.8rem;
  font-size: 1.05rem;
  overflow-wrap: anywhere;
}

.source-dialog .source-name-input {
  width: 100%;
}

.source-dialog-hint {
  margin: 0.5rem 0 0;
  color: var(--ink-muted);
  font-size: 0.8rem;
}

/* Set place: one choice per named Place, then None. */
.source-place-choices {
  display: grid;
  gap: 0.2rem;
  margin: 0;
  padding: 0;
  border: 0;
}

.source-place-choices legend {
  margin-bottom: 0.4rem;
  padding: 0;
  color: var(--ink-muted);
  font-size: 0.8rem;
}

.source-place-choice {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  min-height: 2.75rem;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 500;
  overflow-wrap: anywhere;
  cursor: pointer;
}

.source-place-choice span {
  color: var(--ink);
  font-weight: inherit;
}

.source-place-choice input {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  margin: 0;
  padding: 0;
  accent-color: var(--signal);
}

.source-place-dialog .error,
.source-place-dialog .source-dialog-hint {
  margin: 0.5rem 0 0;
}

.source-place-dialog .source-dialog-hint:empty {
  display: none;
}

.source-dialog-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  align-items: center;
  margin-top: 1.1rem;
}

.source-confirm {
  margin: 0;
  color: var(--danger);
  font-size: 0.85rem;
}

.source-release-button {
  border-color: var(--danger-edge);
  color: var(--danger);
}

.claim-form {
  max-width: 30rem;
}

.claim-form h2 {
  margin-bottom: 0.35rem;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
}

.claim-hint {
  margin: 0 0 1rem;
  font-size: 0.82rem;
}

.source-empty {
  margin: 0 0 1.4rem;
  font-size: 0.9rem;
}

.claim-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
}

.claim-row input {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95rem;
  letter-spacing: 0.25em;
}

.claim-row .primary-button {
  height: 2.75rem;
}

/* -------------------------------------------------------------- settings */

.settings-form {
  display: grid;
  gap: 1.1rem;
  max-width: 34rem;
  margin-bottom: 2.5rem;
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

.field-row {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr);
  gap: 1rem;
  align-items: center;
}

.field-row label {
  margin: 0;
  font-size: 0.85rem;
}

.field-row input {
  height: 2.6rem;
  padding: 0 0.8rem;
  font-size: 0.85rem;
}

#save-settings {
  height: 2.5rem;
  justify-self: start;
}

.access-keys .panel-heading {
  margin-bottom: 1.25rem;
}

.access-keys-blurb,
.skill-install-blurb {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
}

.key-create {
  display: flex;
  gap: 0.6rem;
  max-width: 30rem;
  margin-bottom: 1.5rem;
}

.key-create input {
  flex: 1;
  min-width: 0;
  font-size: 0.88rem;
}

.key-create .primary-button {
  height: 2.75rem;
}

.key-secret {
  max-width: 34rem;
  margin-bottom: 1.5rem;
  padding: 1.25rem 1.4rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair);
}

.key-secret p {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
}

.key-secret code {
  display: block;
  margin: 0.75rem 0;
  padding: 0.6rem 0.8rem;
  border-radius: 0.7rem;
  background: var(--wash);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  overflow-wrap: anywhere;
}

.access-keys-list:empty {
  display: none;
}

.access-keys-list {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

/* One aligned table, not a card per key: the columns are the whole point —
   a key's last use and request count are read against its neighbours'. */
.key-table {
  width: 100%;
  min-width: 39rem;
  border-collapse: collapse;
}

.key-table td {
  vertical-align: middle;
}

.key-name {
  display: block;
  font-size: 0.85rem;
  font-weight: 650;
}

.key-date {
  width: 9rem;
  color: var(--ink-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  white-space: nowrap;
}

.key-count {
  width: 6rem;
  font-size: 0.82rem;
  white-space: nowrap;
}

.key-action {
  width: 3rem;
  text-align: right;
}

/* --------------------------------------------------------- skill install */

.skill-install {
  margin-top: 2.5rem;
}

.skill-install .panel-heading {
  margin-bottom: 1.25rem;
}

.skill-install-commands {
  display: grid;
  gap: 1.25rem;
  max-width: 44rem;
  margin: 0 0 1rem;
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  background: var(--paper);
  box-shadow: 0 1px 2px var(--shadow-hair), 0 16px 40px var(--shadow-card);
}

.skill-install-commands dt {
  font-size: 0.85rem;
  font-weight: 600;
}

.skill-install-commands dd {
  margin: 0.5rem 0 0;
}

/* A command is copied by hand, so it wraps rather than scrolls. */
.skill-install-commands code {
  display: block;
  padding: 0.6rem 0.8rem;
  border-radius: 0.7rem;
  background: var(--wash);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  overflow-wrap: anywhere;
}

.skill-install-key {
  max-width: 44rem;
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-muted);
}

.skill-install-key code {
  padding: 0.1em 0.35em;
  border-radius: 0.35rem;
  background: var(--wash);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- footer */

.site-footer {
  padding: 2rem 0 3rem;
  text-align: center;
}

.site-footer a,
.legal-page a {
  color: var(--ink-faint);
  font-size: 0.78rem;
}

.legal-page {
  max-width: 48rem;
  margin: 0 auto;
  padding-bottom: 4rem;
}

.legal-page h1 {
  margin-bottom: 0.5rem;
}

.legal-page h2 {
  margin-top: 2rem;
}

.legal-page p,
.legal-page li {
  color: var(--ink-soft);
  font-size: 0.92rem;
  line-height: 1.7;
}

.legal-page a {
  font-size: inherit;
}

@media (max-width: 46rem) {
  main {
    padding-top: 2rem;
  }

  /* All six views on one row from a 360 px phone up, measured in Chrome 152 and
     Safari 26.6 on 2026-09-13. The buttons stretch to fill the track, so the
     gap goes to their labels instead. Narrower than that, the track scrolls
     inside itself as before and never widens the page. */
  .view-nav {
    order: 3;
    width: 100%;
    justify-content: space-between;
    gap: 0;
  }

  .nav-link {
    flex: 1 1 auto;
    padding: 0.4rem 0.2rem;
    font-size: 0.76rem;
  }

  .field-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.4rem;
  }

  .field-row label {
    margin-bottom: 0.1rem;
  }
}

/* Stats: one row per civil day, and a table that scrolls inside its own port
   rather than pushing the page sideways on a phone. */
.source-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.source-chip { border: 1px solid currentColor; border-radius: 999px; padding: .3rem .7rem; background: transparent; color: inherit; cursor: pointer; }
/* The name carries the Source's colour; pressed, the chip fills with it and
   the label inverts. All Sources has no Source, so it fills with the ink. */
.source-chip[aria-pressed="true"] { background: var(--source-color); border-color: var(--source-color); color: var(--paper); }
.source-chip[aria-pressed="true"] .source-name { color: inherit; }
.source-chip-all[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); }
.source-name { color: var(--source-color); font-weight: 600; }

/* The server selects the index; the names beside each colour tolerate collisions. */
.source-color-0 { --source-color: var(--source-a); }
.source-color-1 { --source-color: var(--source-b); }
.source-color-2 { --source-color: var(--source-c); }
.source-color-3 { --source-color: var(--source-d); }
.source-color-4 { --source-color: var(--source-e); }
.source-color-5 { --source-color: var(--source-f); }
.source-color-6 { --source-color: var(--source-g); }
.source-color-7 { --source-color: var(--source-h); }
.source-mark { display: inline-flex; align-items: baseline; gap: .35rem; font-family: inherit; }
.source-dot { display: inline-block; flex: 0 0 .5rem; width: .5rem; height: .5rem; border-radius: 50%; background: var(--source-color); }

/* A phone keeps the dot: the name and the column's header stay in the markup
   for a screen reader, clipped to nothing on screen. The common Time cell
   (`17:20:43  19s`) still fits on one line; only the rare `Received` row may
   break at its spaces. */
@media (max-width: 40rem) {
  .timeline-time, .timeline-time-head { width: 8rem; }
  .timeline-time { white-space: normal; overflow-wrap: normal; }
  .timeline-source, .timeline-source-head { width: 1.5rem; }
  .timeline-source-head > span,
  .timeline-source .source-mark > span:not(.source-dot) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .table-frame td, .table-frame th { padding: .55rem .5rem; }
}

.timeline-transcript { display: flex; align-items: baseline; gap: .25rem; }
.timeline-transcript .expandable-text { min-width: 0; flex: 1; }
