/* FILE: ticketbook/ui/app.css
 *
 * THE QUIET DESK — a Linear-inspired redesign of ticketbook's visual world,
 * replacing the Night Register (ink-black/bone-ink ledger, zero radius, two
 * licensed colours). User-pinned 2026-09-30: Linear is the reference; see
 * docs/ticketbook-visual-directions.md and the direction contract this build
 * answers to. What survives the redesign, because the product itself did not
 * change underneath it:
 *
 *   THE ROW IS STILL THE OBJECT for a list. Tickets, trail lines, knowledge
 *   sections and changes are ruled rows, not cards you pick up and move — the
 *   data model has no "advance work by dragging it" gesture, because category
 *   IS the queue and tickets never travel between queues. Small corners are
 *   licensed on CHROME now — buttons, fields, menus, nav — not on rows: a
 *   register that rounded every row would be the "decorative fake Linear
 *   clone" the brief explicitly refuses, and it would mean reaching into
 *   tickets.js/board.js/detail.js/kb.js layout math this file does not own.
 *   See the Radius section below for the full list of what takes one.
 *
 *   STATE IS STILL A MARK, NOT A HUE. Every ticket status stays legible with
 *   all colour removed; see icons.js. Colour only ever confirms what a mark
 *   already said.
 *
 * Two colours stay licensed, and narrowly restrained — Linear's own palette
 * spends colour just as sparingly. GenPlus blue (#2D6CDF, from the Teams
 * manifest) still means LIVE and SELF — the current selection, the caller's
 * own hand, the committed action — and is unchanged: it is a brand colour, not
 * a mood, and this redesign recolours the neutrals around it rather than the
 * one colour that has to match Teams. Rubric red still means one thing only: a
 * correction to a line that was already committed — a reopen, a replaced
 * resolution. Neither is ever decoration.
 *
 * The neutrals are what changed. The Night Register was warm-neutral by
 * deliberate refusal of "the dark-SaaS default"; Linear's own world IS that
 * cool neutral gray, and a pinned reference is a pinned reference — the grounds
 * below are cut cool rather than warm, on purpose, which is the one place this
 * file knowingly contradicts guidance written for the system it replaces.
 *
 * System appearance is now the default (`prefers-color-scheme`, overridable to
 * Light or Dark and remembered in this browser) rather than dark-always: see
 * the theme picker in the top rule and the THEME section below. Dark was
 * previously the fixed default for the use scene, not a look — that reasoning
 * still holds for whichever half of the day someone's OS is already dark for;
 * it no longer overrides someone whose system is light.
 */

/* ── Tokens ──────────────────────────────────────────────────────────────── */

:root {
  /* Ruling, in two halves.
   *
   * The `*-base` values are the INTENT and belong to this file — the media
   * queries at the foot change them for a narrow window. The `--m-*` values are
   * what the layout actually uses, and rules.js rewrites them at boot and on
   * every resize and zoom with the same numbers rounded to whole device pixels.
   *
   * The `--m-*` fallbacks below are what a 1x display would be given, so a page
   * whose scripts have not run yet is already correctly ruled. Do not read a
   * `*-base` from the layout and do not write an `--m-*` from a stylesheet:
   * rules.js sets those inline and an inline style beats a media query. */
  --row-base: 28px;
  --rail-base: 236px;
  --trail-base: 428px;
  --top-base: 44px;
  --foot-base: 30px;

  --dpr: 1;
  --hair: 1px;

  /* Two rule weights, and they mean different things. This is a NAMED pair on
   * purpose: the shell shipped with one hairline and a hard-coded 2px scattered
   * through six declarations, which left the sessions building the register and
   * the trail to invent their own weight for the same job — and the register's
   * most load-bearing rule is exactly the one that must not disagree with
   * itself across two regions.
   *
   *   --w-rule  one device pixel. Separates LIKE FROM LIKE: row from row,
   *             entry from entry, region from region.
   *   --w-band  two CSS pixels. Separates UNLIKE FROM UNLIKE: a heading from
   *             what it heads, and one pile from another — the unclaimed pile
   *             above the rest of the register is this rule, not a hairline.
   *
   * A third weight needs an argument, not a value. */
  --w-rule: var(--hair);
  --w-band: 2px;

  --m-row: 28px;
  --m-rail: 236px;
  --m-trail: 428px;
  --m-top: 44px;
  --m-foot: 30px;

  /* Radius. New in this redesign, and deliberately just two steps: Linear's
   * own corners do not vary much either, and a third step invites the kind of
   * drift the ruling engine above refuses for size. `--radius` is the default
   * for anything that gets one — buttons, fields, chips, menus, the theme
   * picker; `--radius-sm` is for small dense controls (kbd, the resize
   * handle's own hit target has none, it is a line). Rows, tables and list
   * items take neither — see the file header. */
  --radius-sm: 4px;
  --radius: 6px;

  /* Grounds, unlit to lifted.
   *
   * COOL-NEUTRAL now, on purpose: this is Linear's own palette, pinned as the
   * reference for this redesign, and Linear's neutrals run cool. (The Night
   * Register's grounds were warm by deliberate refusal of exactly this
   * rendition — that reasoning applied to the system this build replaces, not
   * to the one it is asked to match.) Ink stays legible against either: bone
   * is set near-neutral rather than warm, so it does not fight the cool
   * grounds the way the old warm bone would. */
  --ink-0: #0a0a0c;
  --ink-1: #101114;
  --ink-2: #0d0e10;
  --ink-3: #1b1c20;
  --ink-4: #24262b;

  /* Rules, quiet to heavy. */
  --rule-1: #1e1f23;
  --rule-2: #26282d;
  --rule-3: #34363d;

  /* Ink. bone-3 is #868893, not the flatter #6c6e77 a straight eyeball match to
     Linear's own tertiary would land on: tertiary text is held to the 4.5:1
     floor on every surface it sits on. Measured (WCAG 2.x): 5.61:1 on --ink-0,
     5.36 on --ink-1, 5.48 on --ink-2 and 4.83 on --ink-3, the hover and
     selected-row ground. #7b7d88 was 4.16:1 on --ink-3. It stays visibly the
     quietest of the three. */
  --bone-1: #edeef0;
  --bone-2: #9a9ca5;
  --bone-3: #868893;

  /* Live and self. GenPlus blue, unchanged — it is Teams' own brand colour,
     not a mood this redesign gets to pick. */
  --live: #2d6cdf;
  --live-ink: #729bf1;
  --live-wash: rgba(45, 108, 223, 0.16);
  --live-on: #ffffff;

  /* A correction to a committed line. */
  --rubric: #d9483f;
  --rubric-ink: #ff8079;

  /* The bot's own voice in a ticket's thread (user, 2026-10-23: "distinct
     but not faded"). Teal, so it is told from IT's blue and from red by hue,
     not by being quieter. Measured: --bot-ink 9.1:1 on --bot-wash, and
     --bone-3 4.7:1 on it, so a timestamp in a bot bubble holds the floor.
     Light: 6.3:1 and 4.8:1. */
  --bot-ink: #5ed3bf;
  --bot-wash: #0d2220;
  --bot-rule: rgba(94, 211, 191, 0.22);

  --font-text: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans",
    "Noto Sans Greek", "Helvetica Neue", Arial, sans-serif;
  /* Figures, references and timestamps only — measurement, never atmosphere.
     Prose in this interface is never set in a monospace. */
  --font-figure: ui-monospace, SFMono-Regular, "SF Mono", "Cascadia Mono",
    "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;

  /* Dense, but with floors. 11px is the smallest anything functional is set at
     and 12px the smallest for anything read as prose — a desk app earns density,
     it does not earn text somebody has to lean in for, and both floors were
     found by measurement rather than chosen by eye. */
  --t-micro: 11px;
  --t-small: 12px;
  --t-base: 13px;
  --t-sub: 15px;
  --t-head: 19px;
  /* The one display step. Used once, on the gate. */
  --t-lead: 30px;

  /* Spacing, in the register's own steps. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;

  color-scheme: dark light;
}

/* THEME.
 *
 * Three settings, chosen from the picker in the top rule and remembered per
 * browser in `ticketbook_theme` (app.js): System (the default), Light, Dark.
 * Dark is still this file's base palette above — unqualified `:root` — but it
 * is no longer the desk's actual default; System is, and System is answered
 * here in CSS rather than by app.js writing colours, so the very first paint
 * (before app.js has run a line) already matches the OS. app.js sets
 * `data-theme` to the literal SETTING, not the resolved theme — "system",
 * "light" or "dark" — which is what makes the distinction below work: an
 * explicit choice gets its own selector below and always wins; "system" (or,
 * for a page whose script has not run yet, no attribute at all) matches
 * neither and falls to the media query, which is exactly the OS's answer.
 *
 * This is the CSP-compatible half of "avoid the flash": there is no
 * 'unsafe-inline' on this page to run a render-blocking script before first
 * paint, so the OS preference has to be readable from the stylesheet alone,
 * and a `prefers-color-scheme` media query is the one thing that can do that
 * with no script at all. What it cannot do is read what this browser saved
 * last time — an explicit Light choice on a Dark system still shows one frame
 * of the OS's own dark before app.js corrects it to light, same trade the old
 * fixed-dark-default made in the other direction. */
:root[data-theme="light"] {
  --ink-0: #f7f7f9;
  --ink-1: #ffffff;
  --ink-2: #f5f5f7;
  --ink-3: #efeff2;
  --ink-4: #e7e7ec;
  --rule-1: #ececef;
  --rule-2: #e3e3e8;
  --rule-3: #cbccd3;
  --bone-1: #1b1c1f;
  --bone-2: #63656d;
  /* 5.3:1 on --ink-1, 5.0:1 on --ink-0 — comfortably over the 4.5:1 floor this
     desk holds its tertiary ink to, since it carries the foot rule's standing
     notice at 11px, read all day. */
  --bone-3: #696b72;
  --live: #2d6cdf;
  --live-ink: #1d57c4;
  --live-wash: rgba(45, 108, 223, 0.09);
  --live-on: #ffffff;
  --rubric: #c4302b;
  --rubric-ink: #b42318;
  --bot-ink: #086458;
  --bot-wash: #e8f5f2;
  --bot-rule: rgba(8, 100, 88, 0.18);
  color-scheme: light;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) {
    --ink-0: #f7f7f9;
    --ink-1: #ffffff;
    --ink-2: #f5f5f7;
    --ink-3: #efeff2;
    --ink-4: #e7e7ec;
    --rule-1: #ececef;
    --rule-2: #e3e3e8;
    --rule-3: #cbccd3;
    --bone-1: #1b1c1f;
    --bone-2: #63656d;
    --bone-3: #696b72;
    --live: #2d6cdf;
    --live-ink: #1d57c4;
    --live-wash: rgba(45, 108, 223, 0.09);
    --live-on: #ffffff;
    --rubric: #c4302b;
    --rubric-ink: #b42318;
    --bot-ink: #086458;
    --bot-wash: #e8f5f2;
    --bot-rule: rgba(8, 100, 88, 0.18);
    color-scheme: light;
  }
}

/* ── Base, and the surfaces the browser draws ────────────────────────────── */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--ink-0);
  color: var(--bone-1);
  font-family: var(--font-text);
  font-size: var(--t-base);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Nothing is round BY DEFAULT — rows, tables, panels and grounds stay square,
   which is most of the page. Radius is opt-in, spent only on the chrome listed
   in the file header (§ Radius token comment): a later addition that reaches
   for one on anything else has to argue with that list, not with taste. */
* {
  border-radius: 0;
}

[hidden] {
  display: none !important;
}

/* The parts of the page the browser draws are part of the design too. Left at
   their defaults they belong to no design system at all, and they are the
   cheapest tell that a page was assembled rather than built. */
::selection {
  background: var(--live);
  color: var(--live-on);
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--rule-2) transparent;
}

::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--rule-2);
  border: 3px solid transparent;
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--rule-3);
  border: 3px solid transparent;
  background-clip: padding-box;
}

:focus-visible {
  outline: 2px solid var(--live-ink);
  outline-offset: 1px;
}

a {
  color: var(--live-ink);
  text-decoration-thickness: var(--hair);
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-thickness: 1px;
}

code {
  font-family: var(--font-figure);
  font-size: 0.94em;
  color: var(--bone-1);
}

kbd {
  font-family: var(--font-figure);
  font-size: var(--t-micro);
  color: var(--bone-2);
  border: var(--w-rule) solid var(--rule-2);
  border-radius: var(--radius-sm);
  padding: 0 4px;
  margin-right: var(--s-1);
}

/* Every figure in this interface is tabular. A reference, a count or a clock
   that shifts width as it changes is a column that will not hold still. */
.folio,
.qbtn__count,
.qbtn__key,
.fact__v,
.pulse,
kbd,
code {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  background: var(--live);
  color: var(--live-on);
  padding: var(--s-2) var(--s-4);
}

.skip:focus {
  left: 0;
}

/* ── The gate ────────────────────────────────────────────────────────────── */

/* The first page of the register: a masthead under a heavy rule, the entry you
 * have to make on the left, and the rest of the sheet ruled and empty on the
 * right — because it is, until you are through this page. */

.gate {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  background: var(--ink-0);
}

/* A double rule under the title, which is what a register's head actually is:
   one heavy, one hair, with a hair of paper between them. */
.gate__masthead {
  position: relative;
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: baseline;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  border-bottom: var(--w-band) solid var(--rule-3);
  margin-bottom: 4px;
}

.gate__masthead::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: var(--hair);
  background: var(--rule-2);
}

.gate__mark {
  color: var(--live-ink);
  line-height: 0;
  align-self: center;
}

.gate__mark svg {
  width: 18px;
  height: 18px;
}

.gate__head {
  font-size: var(--t-sub);
  font-weight: 600;
  letter-spacing: -0.005em;
  margin: 0;
}

.gate__org {
  justify-self: end;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--bone-3);
  white-space: nowrap;
}

.gate__body {
  display: grid;
  grid-template-columns: minmax(0, 52ch) minmax(0, 1fr);
  min-height: 0;
}

/* The leaf is the written side of the sheet, so it carries the lifted ground the
   desk's own leaf does; the ruled side stays on the desk ground. */
.gate__leaf {
  padding: var(--s-7) var(--s-5) var(--s-6);
  overflow-y: auto;
  background: var(--ink-1);
}

/* The unwritten sheet: the register's real column heads, and empty rules under
 * them. The ruling is the register's own line spacing — the same --m-row the
 * desk's rows use — drawn by the same device-pixel arithmetic, so this is
 * literally the surface the entries will sit on rather than a texture that
 * resembles one. */
.gate__sheet {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  border-left: var(--w-rule) solid var(--rule-2);
  min-height: 0;
}

.gate__heads {
  display: grid;
  grid-template-columns: 13ch minmax(0, 1fr) 16ch 12ch;
  gap: var(--s-4);
  align-items: end;
  height: var(--m-row);
  padding: 0 var(--s-5);
  border-bottom: var(--w-band) solid var(--rule-3);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--bone-3);
  font-weight: 600;
  padding-bottom: var(--s-1);
}

.gate__ruled {
  background-image: repeating-linear-gradient(
    to bottom,
    var(--rule-1) 0,
    var(--rule-1) var(--hair),
    transparent var(--hair),
    transparent var(--m-row)
  );
  background-position: 0 calc(var(--m-row) - var(--hair));
}

/* The one line of scale on the page a colleague meets first. It was set at the
   chrome's own 15px, which left the first viewport with nothing to read from
   across the room and 70% empty ground. */
.gate__sub {
  margin: 0;
  color: var(--bone-1);
  font-size: var(--t-lead);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.015em;
  max-width: 22ch;
  text-wrap: balance;
}

.gate__form {
  margin-top: var(--s-7);
  display: grid;
  gap: var(--s-2);
  max-width: 42ch;
}

.gate__label,
.rail__title,
.stub__sub,
.fact dt {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--bone-3);
  font-weight: 600;
}

/* A bordered field now, not a bare ruled line — the one idiom this redesign
   changes structurally rather than just recolouring, because a boxed input is
   how the pinned reference draws every field on the page and a lone ruled
   line beside boxed ones would read as unfinished rather than as a choice. */
.gate__input {
  font-family: var(--font-figure);
  font-size: var(--t-base);
  color: var(--bone-1);
  caret-color: var(--live-ink);
  background: var(--ink-0);
  border: var(--w-rule) solid var(--rule-3);
  border-radius: var(--radius);
  padding: var(--s-2) var(--s-3);
  width: 100%;
}

.gate__input::placeholder {
  color: var(--bone-3);
}

.gate__input:focus {
  border-color: var(--live);
}

.gate__input:disabled {
  color: var(--bone-3);
}

/* The committed action is a solid plate with the words knocked out — still
   the strongest statement on the page, now with the same small corner every
   other control on it takes. */
.gate__submit {
  margin-top: var(--s-3);
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--live-on);
  background: var(--live);
  border: 0;
  border-radius: var(--radius);
  padding: var(--s-3) var(--s-4);
  cursor: pointer;
  justify-self: start;
  min-width: 22ch;
}

/* Darker on hover, not lighter. Lightening the plate drops the knocked-out
   white below 4.5:1 — the hover state was measured at 4.0:1 — and pressing ink
   into paper is the right direction for a register anyway. */
.gate__submit:hover {
  background: color-mix(in srgb, var(--live) 84%, #000);
}

.gate__submit[data-busy] {
  color: color-mix(in srgb, var(--live-on) 60%, transparent);
  cursor: progress;
}

.gate__submit:disabled {
  cursor: not-allowed;
}

.gate__msg {
  margin: var(--s-4) 0 0;
  padding: var(--s-2) var(--s-3);
  border: var(--w-rule) solid var(--rubric);
  border-radius: var(--radius);
  color: var(--rubric-ink);
  max-width: 46ch;
  line-height: 1.55;
}

.gate__msg[data-tone="note"] {
  border-color: var(--rule-3);
  color: var(--bone-2);
}

/* Prose somebody actually reads, once, while holding a password manager open —
   so it is set at the body size rather than at the chrome size. */
.gate__help {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: var(--w-rule) solid var(--rule-1);
  color: var(--bone-2);
  font-size: var(--t-base);
  max-width: 62ch;
}

.gate__help p {
  margin: 0;
  line-height: 1.6;
}

.gate__help p + p {
  margin-top: var(--s-3);
}

.gate__help b {
  color: var(--bone-1);
  font-weight: 600;
}

/* Microsoft sign-in, when the server takes it. It becomes THE stamp on the
   page, and the key form beneath it steps down to a ruled outline: two solid
   plates would be two committed actions, and there is one thing most people
   here should press. The key stays for agents and anyone holding one. */
.gate__sso {
  margin-top: var(--s-7);
  display: grid;
  max-width: 42ch;
}

.gate__ms {
  margin-top: 0;
}

.gate__or {
  margin: var(--s-6) 0 0;
  padding-top: var(--s-3);
  border-top: var(--w-rule) solid var(--rule-1);
  color: var(--bone-2);
}

.gate[data-sso="true"] .gate__form {
  margin-top: var(--s-3);
}

.gate[data-sso="true"] .gate__form .gate__submit {
  color: var(--bone-1);
  background: transparent;
  border: var(--w-rule) solid var(--rule-3);
}

.gate[data-sso="true"] .gate__form .gate__submit:hover {
  background: var(--ink-3);
}

.gate__elsewhere {
  margin: var(--s-3) 0 0;
  padding-left: var(--s-3);
}

/* ── The desk ────────────────────────────────────────────────────────────── */

.desk {
  height: 100dvh;
  display: grid;
  grid-template-rows: var(--m-top) minmax(0, 1fr) var(--m-foot);
  background: var(--ink-0);
}

.topline {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s-4);
  padding: 0 var(--s-3);
  border-bottom: var(--w-band) solid var(--rule-3);
  background: var(--ink-0);
}

.topline__id {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

.topline__mark {
  color: var(--live-ink);
  line-height: 0;
}

.topline__name {
  font-weight: 600;
  letter-spacing: -0.005em;
}

.topline__state {
  justify-self: start;
  min-width: 0;
}

.pulse {
  font-family: var(--font-figure);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--bone-3);
  text-transform: uppercase;
  padding-left: var(--s-3);
  border-left: var(--w-rule) solid var(--rule-2);
  white-space: nowrap;
}

.pulse:empty {
  border-left-color: transparent;
}

.pulse[data-tone="bad"] {
  color: var(--rubric-ink);
  border-left-color: var(--rubric);
}

.pulse[data-tone="reading"] {
  color: var(--live-ink);
}

.topline__state {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

/* Persistent, in the one rule that is always on screen, and marked in the
   colour this interface uses for "live": new code is waiting, not a fault. */
.shell-update {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding-left: var(--s-3);
  border-left: var(--w-band) solid var(--live);
  font-size: var(--t-small);
  color: var(--bone-1);
  white-space: nowrap;
}

.shell-update[hidden] {
  display: none;
}

/* The search box: a small bordered field in the top rule, the same box every
   other field on the desk now takes rather than a pill or a bare rule. */
.topline__search {
  min-width: 0;
}

.search {
  width: 18rem;
  max-width: 100%;
  font: inherit;
  font-size: var(--t-small);
  color: var(--bone-1);
  caret-color: var(--live-ink);
  background: var(--ink-2);
  border: var(--w-rule) solid var(--rule-2);
  border-radius: var(--radius);
  padding: 5px var(--s-2);
}

.search::placeholder {
  color: var(--bone-3);
}

.search:focus {
  border-color: var(--live);
  background: var(--ink-1);
}

.topline__actor {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.actor {
  font-size: var(--t-small);
  color: var(--bone-1);
  padding: 0 var(--s-2);
  border-left: var(--w-rule) solid var(--rule-2);
}

.actor[data-known="false"] {
  color: var(--bone-3);
  font-style: italic;
}

.tbtn {
  font: inherit;
  font-size: var(--t-small);
  color: var(--bone-2);
  background: transparent;
  border: var(--w-rule) solid var(--rule-2);
  border-radius: var(--radius);
  padding: 3px var(--s-2);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  line-height: 1;
}

a.tbtn {
  text-decoration: none;
}

.tbtn svg {
  width: 14px;
  height: 14px;
}

.tbtn:hover {
  color: var(--bone-1);
  border-color: var(--rule-3);
  background: var(--ink-3);
}

.tbtn:disabled {
  color: var(--bone-3);
  cursor: default;
  background: transparent;
}

/* The top rule's primary action, in the same plate idiom as the gate's: a solid
   field with the words knocked out. It shipped as a ghost button identical to
   Sign out, which made the register's primary action and its exit the same
   object — the one thing a top rule must not do. */
.tbtn--primary {
  color: var(--live-on);
  background: var(--live);
  border-color: var(--live);
}

.tbtn--primary:hover {
  color: var(--live-on);
  background: color-mix(in srgb, var(--live) 84%, #000);
  border-color: color-mix(in srgb, var(--live) 84%, #000);
}

.tbtn--primary:disabled {
  background: color-mix(in srgb, var(--live) 45%, var(--ink-0));
  border-color: transparent;
  color: color-mix(in srgb, var(--live-on) 65%, transparent);
}

.tbtn__label {
  white-space: nowrap;
}

/* The appearance picker: System / Light / Dark. A native <select> rather than
   a custom listbox — it is one control on a page with no component library
   and no bundler to build one with, it is keyboard- and screen-reader-usable
   for free, and "accessible select/radio/menu, not a binary button that
   cannot reach System" only asks for a control that can reach all three
   states in a way everyone can drive, not for a bespoke one. Drawn in the
   .tbtn idiom — same box, same border — so it reads as one of the top rule's
   own controls rather than a browser default dropped onto the page. */
.theme-picker {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--bone-2);
  background: transparent;
  border: var(--w-rule) solid var(--rule-2);
  border-radius: var(--radius);
  padding: 3px var(--s-1) 3px var(--s-2);
  cursor: pointer;
}

.theme-picker:hover {
  color: var(--bone-1);
  border-color: var(--rule-3);
  background: var(--ink-3);
}

.theme-picker:has(.theme-picker__select:focus-visible) {
  outline: 2px solid var(--live-ink);
  outline-offset: 1px;
}

.theme-picker svg {
  width: 14px;
  height: 14px;
  flex: none;
}

/* The select itself is transparent and borderless — the label around it
   already drew the box — and kept at the picker's own text size rather than
   the browser's form-control default, which on most platforms is larger than
   anything else in this top rule. */
.theme-picker__select {
  font: inherit;
  font-size: var(--t-small);
  color: inherit;
  background: transparent;
  border: 0;
  padding: 0 0 0 1px;
  cursor: pointer;
}

.theme-picker__select:focus-visible {
  outline: none;
}

/* ── The leaf: rail and register ────────────────────────────────────────── */

/* A layout grid and nothing else. It carries no ground of its own — each region
   paints its own — and no padding, because each region's own inset is what
   keeps text off the rules. There is no third column any more: an open ticket
   is a popover over the register (detail.css), so the list keeps its whole
   width — on the board, the columns that used to fall off the right-hand edge
   were colleagues whose piles nobody could see. */
.leaf {
  display: grid;
  grid-template-columns: var(--m-rail) minmax(0, 1fr);
  min-height: 0;
}

/* Rail ------------------------------------------------------------------- */

.rail {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  border-right: var(--w-rule) solid var(--rule-2);
  background: var(--ink-2);
  min-height: 0;
}

.rail__top {
  grid-row: 1;
  padding: var(--s-2);
  border-bottom: var(--w-rule) solid var(--rule-2);
}

.rail__head {
  grid-row: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3) var(--s-2);
  border-bottom: var(--w-rule) solid var(--rule-1);
}

/* The unclaimed count is the desk's own triage cue, so it is set in primary ink
   against the rail's secondary. Deliberately NOT rubric: rubric means a
   correction to a committed line, and an unclaimed ticket is not a correction —
   it is the ordinary state of work nobody has picked up yet. */
.rail__note {
  font-family: var(--font-figure);
  font-size: var(--t-micro);
  color: var(--bone-1);
  white-space: nowrap;
}

/* The rail's own destinations: quiet pills in the sidebar idiom, banded off
   from the queues above by a hairline (still unlike from unlike — a page of
   the book against a filter on this one — but the band now lives in the
   padding around each pill rather than across the full width, which is where
   Linear's own sidebar spends it). */
.rail__foot {
  grid-row: 4;
  border-top: var(--w-rule) solid var(--rule-2);
  padding: var(--s-2);
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.rail__link {
  display: flex;
  align-items: center;
  height: var(--m-row);
  padding: 0 var(--s-3);
  border-radius: var(--radius);
  color: var(--bone-2);
  font-size: var(--t-base);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rail__link:hover {
  color: var(--bone-1);
  background: var(--ink-3);
  text-decoration: none;
}

.rail__link[aria-current="page"] {
  color: var(--bone-1);
  background: var(--live-wash);
}

.rail__list {
  grid-row: 3;
  list-style: none;
  margin: 0;
  padding: var(--s-1) var(--s-2);
  display: flex;
  flex-direction: column;
  gap: 1px;
  overflow-y: auto;
  min-height: 0;
}

.qbtn {
  width: 100%;
  height: var(--m-row);
  display: grid;
  grid-template-columns: 14px 2ch minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-2) 0 var(--s-1);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--t-base);
  color: var(--bone-2);
  background: transparent;
  border: 0;
  text-align: left;
  cursor: pointer;
}

.qbtn:hover {
  background: var(--ink-3);
  color: var(--bone-1);
}

/* The selected queue is marked in the margin with a pointer, the way a reader
   marks a register — not with a coloured bar down the side of a list item. */
.qbtn__pointer {
  color: transparent;
  line-height: 0;
}

.qbtn__pointer svg {
  width: 14px;
  height: 14px;
}

.qbtn[data-selected="true"] {
  background: var(--live-wash);
  color: var(--bone-1);
}

.qbtn[data-selected="true"] .qbtn__pointer {
  color: var(--live-ink);
}

.qbtn[data-archived="true"] .qbtn__name {
  color: var(--bone-3);
  text-decoration: line-through;
  text-decoration-thickness: var(--hair);
}

.qbtn__key {
  font-family: var(--font-figure);
  font-size: var(--t-micro);
  color: var(--bone-3);
}

.qbtn__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.qbtn__count {
  font-family: var(--font-figure);
  font-size: var(--t-small);
  color: var(--bone-2);
}

.qbtn[data-selected="true"] .qbtn__count,
.qbtn[data-selected="true"] .qbtn__key {
  color: var(--live-ink);
}

/* Register ---------------------------------------------------------------- */

.register {
  overflow: auto;
  min-height: 0;
  background: var(--ink-1);
}

.register:focus {
  outline: none;
}

/* Footline ---------------------------------------------------------------- */

.footline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-4);
  padding: 0 var(--s-3);
  border-top: var(--w-rule) solid var(--rule-2);
  background: var(--ink-0);
  font-size: var(--t-micro);
}

.footline__notice {
  color: var(--bone-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.footline__notice[data-tone="bad"] {
  color: var(--rubric-ink);
}

.footline__notice[data-tone="note"] {
  color: var(--bone-2);
}

.footline__keys {
  display: flex;
  gap: var(--s-3);
  color: var(--bone-3);
  white-space: nowrap;
}

.key {
  display: inline-flex;
  align-items: center;
}

/* ── Marks ───────────────────────────────────────────────────────────────── */

.mark {
  width: 14px;
  height: 14px;
  color: var(--bone-2);
  flex: none;
}

.mark--claimed,
.mark--waiting {
  color: var(--live-ink);
}

.mark--resolved,
.mark--archived {
  color: var(--bone-3);
}

.notch--urgent {
  color: var(--rubric);
}

.notch--high {
  color: var(--bone-2);
}

.glyph {
  width: 16px;
  height: 16px;
}

.glyph--big {
  width: 44px;
  height: 44px;
}

/* ── Placeholder regions ─────────────────────────────────────────────────── */

.stub {
  padding: var(--s-6) var(--s-5);
  max-width: 68ch;
}

.stub__glyph {
  color: var(--rule-3);
  line-height: 0;
  margin-bottom: var(--s-4);
}

.stub__head {
  margin: 0;
  font-size: var(--t-head);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
  text-wrap: balance;
}

.stub__title {
  margin: var(--s-2) 0 0;
  font-size: var(--t-sub);
  font-weight: 600;
  line-height: 1.3;
  text-wrap: balance;
}

.stub__body {
  margin: var(--s-3) 0 0;
  color: var(--bone-2);
  max-width: 62ch;
  line-height: 1.6;
}

.stub__sub {
  margin: var(--s-6) 0 var(--s-2);
}

/* The empty register. Set as a written note against the fold rather than as a
   greyed-out shrug: it is telling you something true about the desk. */
.stub__empty {
  margin: var(--s-5) 0 0;
  padding-left: var(--s-4);
  border-left: var(--w-band) solid var(--rule-3);
  color: var(--bone-2);
  max-width: 58ch;
  line-height: 1.6;
}

/* Ruled rows: label in the margin, value in the column, one per line.
 *
 * These were a 2x2 grid of small-label-over-large-figure, which is the
 * hero-metric template — the one page scaffold the craft floor bans outright —
 * and it mattered more here than it would elsewhere: this placeholder is the
 * only picture of "the register" the session building the real one will have
 * seen, so its shape teaches. A register states a value on a ruled line beside
 * its heading; it does not put four numbers on tiles. */
.stub__facts {
  margin: var(--s-5) 0 0;
  padding: 0;
  border-top: var(--w-rule) solid var(--rule-2);
}

.fact {
  display: grid;
  grid-template-columns: 14ch minmax(0, 1fr);
  align-items: baseline;
  gap: var(--s-3);
  min-height: var(--m-row);
  padding: var(--s-1) 0;
  border-bottom: var(--w-rule) solid var(--rule-1);
  min-width: 0;
}

.fact dt {
  margin: 0;
}

/* Prose by default. The figure face is for measurement — counts, references,
   clocks — and a queue name set in it is exactly the "monospace as a costume for
   technical" this file's own header refuses. Mark the numeric ones. */
.fact__v {
  margin: 0;
  font-family: var(--font-text);
  font-size: var(--t-base);
  color: var(--bone-1);
}

.fact__v[data-figure] {
  font-family: var(--font-figure);
  font-variant-numeric: tabular-nums;
}

.stub__cols {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--w-rule) solid var(--rule-2);
}

.stub__col {
  height: var(--m-row);
  display: flex;
  align-items: center;
  border-bottom: var(--w-rule) solid var(--rule-1);
  color: var(--bone-1);
}

.stub__col--none {
  color: var(--bone-3);
  font-style: italic;
}

.stub__folio {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: var(--w-band) solid var(--rule-3);
}

.folio {
  font-family: var(--font-figure);
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  color: var(--live-ink);
}

.stub__status {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--bone-2);
}

.stub--detail {
  padding: var(--s-5) var(--s-5) var(--s-6);
  max-width: none;
}

.stub__back {
  display: inline-block;
  margin-top: var(--s-4);
}

/* ── noscript ────────────────────────────────────────────────────────────── */

.stall {
  max-width: 60ch;
  margin: var(--s-7) auto;
  padding: 0 var(--s-4);
}

.stall__head {
  font-size: var(--t-head);
  font-weight: 600;
  margin: 0 0 var(--s-3);
}

.stall__body {
  color: var(--bone-2);
  line-height: 1.6;
  margin: 0;
}

/* ── Motion ──────────────────────────────────────────────────────────────── */
/*
 * One authored moment: opening a ticket lifts its popover in over the list, and
 * closing it sets it back down. Nothing else in the interface animates — a poll
 * lands every thirty seconds and a page that flickered each time would be
 * unusable by the afternoon.
 *
 * `startViewTransition` is feature-guarded in router.js and additionally skipped
 * under prefers-reduced-motion there. This block is the second half of that
 * promise, for a browser that starts a transition some other way.
 */

/* The popover rises a few pixels into place over the list, and sinks back
   when it closes. A view transition, so it runs only where router.js starts
   one — never under prefers-reduced-motion. */
@keyframes ticket-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.985);
  }
}

@keyframes ticket-out {
  to {
    opacity: 0;
    transform: translateY(4px) scale(0.99);
  }
}

::view-transition-old(ticket) {
  animation: ticket-out 120ms cubic-bezier(0.4, 0, 1, 1) both;
}

::view-transition-new(ticket) {
  animation: ticket-in 200ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }

  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ── Paper ───────────────────────────────────────────────────────────────── */
/*
 * A register that cannot be printed is a strange register, and paper is the one
 * device this direction has a literal claim on. What prints is the entry and its
 * trail — the thing somebody takes into a meeting, attaches to a change request,
 * or keeps when a ticket becomes a dispute about what was done and when.
 *
 * The chrome does not print. A queue rail, a keyboard legend and a sign-out
 * button are all instruments for working the desk, and none of them is evidence.
 */

@media print {
  :root {
    --ink-0: #ffffff;
    --ink-1: #ffffff;
    --ink-2: #ffffff;
    --ink-3: #ffffff;
    --ink-4: #ffffff;
    --rule-1: #999999;
    --rule-2: #555555;
    --rule-3: #000000;
    --bone-1: #000000;
    --bone-2: #222222;
    --bone-3: #444444;
    --live-ink: #000000;
    --rubric-ink: #000000;
    --bot-ink: #000000;
    --bot-wash: #ffffff;
    --bot-rule: #999999;
    /* Toner has no device pixels to be honest about. */
    --hair: 1px;
  }

  .topline,
  .rail,
  .footline,
  .skip,
  .gate {
    display: none !important;
  }

  .desk {
    height: auto;
    display: block;
  }

  .leaf {
    display: block;
  }

  .register {
    overflow: visible;
    border: 0;
  }

  /* An open ticket IS the document: on paper it prints alone, and the list it
     sat over does not print at all. On /app with nothing open the register
     prints alone. */
  .desk:has(.popover[open]) .leaf {
    display: none;
  }

  .stub__glyph {
    display: none;
  }

  a[href]::after {
    content: " (" attr(href) ")";
    font-size: var(--t-micro);
    color: var(--bone-3);
  }
}

/* ── Adaptation ──────────────────────────────────────────────────────────── */
/*
 * Desktop-primary by decision: this is used at a desk on a laptop, and mobile is
 * explicitly not designed for (PRODUCT.md). What follows keeps the desk usable
 * on a narrow window rather than pretending to be a phone app — the rail gives
 * up width first, then folds to a strip along the top, and an open ticket
 * becomes a full-screen sheet (detail.css).
 */

@media (max-width: 1000px) {
  :root {
    --rail-base: 200px;
  }
}

@media (max-width: 900px) {
  /* The unwritten sheet is the first thing to go: on a narrow window it would
     be a stripe rather than a page. */
  .gate__body {
    grid-template-columns: minmax(0, 1fr);
  }

  .gate__sheet {
    display: none;
  }

  .gate__leaf {
    padding-top: var(--s-6);
  }
}

@media (max-width: 820px) {
  .leaf {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }

  .rail {
    grid-template-rows: auto;
    /* Both queue and destination strips need room to scroll on a phone;
       the destination links' intrinsic width must not erase the queues. */
    grid-template-columns: auto auto minmax(0, 1fr);
    border-right: 0;
    border-bottom: var(--w-rule) solid var(--rule-2);
  }

  /* The knowledge link joins the strip at its right-hand end, banded off from
     the queues on the left instead of from above. */
  .rail__foot {
    display: flex;
    flex-direction: row;
    min-width: 0;
    overflow-x: auto;
    border-top: 0;
    border-left: var(--w-band) solid var(--rule-3);
    grid-column: 3;
    grid-row: 1;
  }

  .rail__top {
    grid-column: 2;
    grid-row: 1;
    border-bottom: 0;
  }

  .rail__top .rail__link {
    flex: none;
  }

  .rail__link {
    padding: 0 var(--s-3);
  }

  .rail__head {
    display: none;
  }

  .rail__list {
    display: flex;
    flex-direction: row;
    min-width: 0;
    max-width: calc(100vw - 270px);
    overflow-x: auto;
    grid-column: 1;
    grid-row: 1;
  }

  .rail__foot .rail__link {
    flex: none;
  }

  /* Without this the items shrink to fit and the names — which are set to
     ellipsis — collapse to nothing, leaving a strip of bare numbers. The strip
     scrolls; it does not compress. */
  .rail__item {
    flex: none;
  }

  .qbtn {
    width: auto;
    grid-template-columns: 14px auto auto;
    padding-right: var(--s-3);
  }

  .qbtn__key {
    display: none;
  }

  /* The top rule has room for the name, the actions and nothing else here. The
     pulse goes because the page already says when it was last read the moment
     anything on it changes. */
  .topline {
    gap: var(--s-2);
    padding: 0 var(--s-2);
  }

  .topline__state,
  .actor {
    display: none;
  }

  .search {
    width: 100%;
  }

  /* The pulse can go; the notice that new code is waiting cannot, or a phone
     would never be told. */
  .topline__state:has(> .shell-update:not([hidden])) {
    display: flex;
  }

  .topline__state .pulse {
    display: none;
  }

  /* Re-read has a glyph and can lose its word. Sign out does not, and a button
     with nothing in it is not a smaller button. */
  #refresh-now .tbtn__label {
    display: none;
  }

  .footline__keys {
    display: none;
  }
}

/* Touch-width phones: the topline's auto/auto columns for id and actions
   leave the search field a sliver (~50px) once "Sign out" and the theme
   picker are squeezed in beside it, even though it is the one control meant
   to take a full query. Give it its own row instead of fighting the others
   for the same one. */
@media (max-width: 640px) {
  .desk {
    grid-template-rows: auto minmax(0, 1fr) var(--m-foot);
  }

  .topline {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-rows: auto auto;
    row-gap: var(--s-2);
    padding: var(--s-2);
  }

  .topline__id {
    grid-row: 1;
    grid-column: 1;
  }

  /* Collapses to nothing when hidden, so id and actor stay adjacent on the
     common case where no update is waiting. */
  .topline__state {
    grid-row: 1;
    grid-column: 2;
  }

  .topline__actor {
    grid-row: 1;
    grid-column: 3;
    justify-self: end;
  }

  .topline__search {
    grid-row: 2;
    grid-column: 1 / -1;
  }

  /* With My tickets in the header there is no room for the name on a phone:
     the mark alone stays, as the link home. */
  .topline:has(#to-my:not([hidden])) .topline__name {
    display: none;
  }

  .search {
    width: 100%;
    /* 16px stops iOS/Android zooming the page in on focus, which is its own
       way of making the field unusable for a full query. */
    font-size: 16px;
    padding: var(--s-2) var(--s-3);
  }
}
