/* ============================================================================
   HPAD VISIONS DESK
   Design principle: "Hue belongs to the workflow. State belongs to form."

   The six workflow channels (white, purple, green, orange, yellow, red) own
   colour. Red already means FROZEN. Therefore urgency, escalation and
   lateness are NEVER expressed with hue -- they are expressed with FORM:

     overdue    -> hairline outline in the text colour + inset ring + bold clock
     escalated  -> dashed border
     frozen     -> diagonal repeating-linear-gradient hatch (~62% opacity)
     unread     -> small brand-Purple signal dot before the title

   Every state above survives a greyscale conversion. That is the acceptance
   test and it is also what keeps the board readable for colour-blind artists.

   Brand Purple (--signal) is the single interactive accent: focus rings,
   active nav, primary actions, the unread dot. It is the one hue the six
   workflow channels do not claim, so it is never spent on decoration.
   ========================================================================= */


/* ---------------------------------------------------------------------------
   1. FACES  (do not change paths -- preloaded in index.html, offline build)
   ------------------------------------------------------------------------ */

@font-face {
  font-family: 'Oxygen Mono';
  src: url('./fonts/OxygenMono-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Kumbh Sans';
  src: url('./fonts/KumbhSans-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}


/* ---------------------------------------------------------------------------
   2. TOKENS
   Components are styled through tokens only. The theme blocks below redefine
   tokens and nothing else, so a [data-theme] toggle can be wired up later
   without touching a single component rule.
   ------------------------------------------------------------------------ */

:root {
  color-scheme: light;

  /* -- surfaces & ink (LIGHT).
        --ground IS brand Blue #F4F9FD. --text is a brand-tinted near-black
        rather than brand Purple itself, because Purple is spent on --signal
        and body ink must not collide with the focus accent. -- */
  --ground: #F4F9FD;          /* BRAND Blue, verbatim                        */
  --panel: #FFFFFF;
  --raise: #FFFFFF;
  --rule: #E4E2DE;            /* warm hairline                               */
  --rule-2: #787570;          /* BRAND Grey, verbatim. Borders only: it is
                                 4.33:1 on ground and FAILS AA as text.      */
  --dim: #6E6B66;             /* BRAND Grey darkened ~4% to clear 4.5:1      */
  --text: #2A2430;

  /* -- the single interactive accent. "Blue on Purple" is an approved
        brand pairing, so a filled control is literally brand-correct. -- */
  --signal: #493D51;          /* BRAND Purple, verbatim                      */
  --signal-ink: #F4F9FD;      /* BRAND Blue, verbatim                        */

  /* -- workflow channels (LIGHT). Names are read by app.js channelColor().
        Hue-faithful: every channel still reads as the colour it is named,
        pulled into the brand's desaturated register. Each sits at its own
        NATURAL luminance (yellow light, red dark) rather than a forced rank
        ladder, which buys greyscale separation for free: the worst pair is
        now 1.20:1 where it used to be 1.019:1 -- i.e. five of the six
        channels used to be the same grey, and the colour-blind claim below
        was not actually true. Every value clears 3:1 on --ground. -- */
  --ch-white: #928E87;
  --ch-purple: #5D3D64;
  --ch-green: #3A5F4C;
  --ch-orange: #905A3B;
  --ch-yellow: #8E783A;
  --ch-red: #662931;

  /* -- derived, theme-tuned -- */
  --wash: 7%;                 /* channel wash strength across a ticket body.
                                 Raised from 5%: the brand channels are lower
                                 in chroma, so 5% left the lanes invisible.  */
  --veil: rgba(42, 36, 48, 0.38);   /* modal / drawer scrim                  */
  /* Contact + near + ambient. A single flat 1px at 5% is a border pretending
     to be depth. Tinted in the brand ink rather than pure black, so the shadow
     belongs to the palette instead of greying it. Brand Blue #F4F9FD is fixed
     as --ground, so separation has to come from the panel LIFTING off it, not
     from darkening the ground. */
  /* Elevation is declared ONCE per surface: in light the shadow carries it and
     --edge is transparent, in dark the shadow barely reads so the hairline
     carries it instead. A 1px border sitting under a wide soft shadow is the
     ghost card -- two elevation systems arguing on the same element. */
  --edge: transparent;
  --lift:
    0 1px 1px rgba(42, 36, 48, 0.04),
    0 2px 6px rgba(42, 36, 48, 0.05),
    0 8px 24px -8px rgba(42, 36, 48, 0.08);
  --lift-2:
    0 2px 4px rgba(42, 36, 48, 0.06),
    0 16px 48px -12px rgba(42, 36, 48, 0.22);
  --hatch: color-mix(in srgb, var(--dim) 62%, transparent);

  /* -- faces -- */
  --sans: 'Kumbh Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'Oxygen Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* -- type scale (base 15px, ratio ~1.2) -- */
  --fs-1: 0.6875rem;   /* 11  mono caps: eyebrow, table head, tag, label.
                              Was 10px under Kumbh 650. Every consumer is now
                              Oxygen Mono 400 -- a ~250-unit weight loss -- so
                              one step up restores the optical weight.        */
  --fs-2: 0.6875rem;   /* 11  meta, nav, button               */
  --fs-3: 0.75rem;     /* 12  dense mono, ids                 */
  --fs-4: 0.8125rem;   /* 13  secondary prose, notes          */
  --fs-5: 0.9375rem;   /* 15  body  (base)                    */
  --fs-6: 1.0625rem;   /* 17  card title, lead value          */
  --fs-7: 1.3125rem;   /* 21  h1, section lead                */
  --fs-8: 1.625rem;    /* 26  big-title                       */
  --fs-9: 2rem;        /* 32  hero / sign-in wordmark         */

  /* -- tracking.
        A monospace glyph already carries ~0.1em of built-in sidebearing (the
        gap between an I and its advance). Adding another 0.1em on top DOUBLES
        the apparent letter gap and destroys word shape at 11px -- exactly the
        size where word-shape recognition matters most. Mono caps want roughly
        half the tracking Kumbh caps wanted. Wide tracking survives only where
        letterspacing is the deliberate gesture: mastheads and section rails. */
  --track-mono: 0.05em;       /* uppercase mono chrome: the default          */
  --track-mono-lead: 0.12em;  /* mastheads, rails -- air is the point        */
  --track-num: 0.02em;        /* mono numerals: tight so digits column up    */

  /* -- spacing scale (4px grid, 2px half-step) -- */
  --sp-1: 2px;
  --sp-2: 4px;
  --sp-3: 6px;
  --sp-4: 8px;
  --sp-5: 12px;
  --sp-6: 16px;
  --sp-7: 20px;
  --sp-8: 24px;
  --sp-9: 32px;
  --sp-10: 40px;
  --sp-11: 56px;

  /* -- radii: tight, instrument-like. No pill shapes. -- */
  --r-1: 2px;
  --r-2: 4px;
  --r-3: 6px;

  /* -- motion -- */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --fast: 120ms;
  --slow: 260ms;

  /* -- legacy aliases: app.js writes these inline. Do not remove. -- */
  --bg: var(--ground);
  --surface: var(--panel);
  --paper: var(--raise);
  --ink: var(--text);
  --ink-soft: var(--dim);
  --muted: var(--dim);
  --line: var(--rule);
  --line-strong: var(--rule-2);
  --border: var(--rule);
  --purple: var(--ch-purple);
  /* --maroon / --danger / --err used to alias --ch-red, which is ALSO the
     frozen channel -- so a config error and a revision-pressure marker both
     rendered in the exact hue that means "this project is frozen". The brand
     supplies a real Maroon distinct from the red channel; use it, and leave
     --ch-red meaning frozen and nothing else. */
  --maroon: #563D40;          /* BRAND Maroon, verbatim */
  --danger: var(--maroon);
  --err: var(--maroon);
  --warn: var(--ch-orange);
  --ok: var(--ch-green);
}

/* Dark token block (1 of 2) -- system preference.
   :not([data-theme='light']) lets a future toggle opt back out. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    color-scheme: dark;

    /* "Blue on Purple" is an approved brand pairing, so the dark theme is
       brand Blue ink on a ground derived from brand Purple. */
    --ground: #1E1822;
    --panel: #282130;
    --raise: #332B3C;
    --rule: #3A3142;
    --rule-2: #4E4458;
    --dim: #A79FB0;
    --text: #F4F9FD;          /* BRAND Blue, verbatim */

    --signal: #B79FC4;        /* brand Purple lifted to carry on a dark ground */
    --signal-ink: #1E1822;

    --ch-white: #D0CFCB;
    --ch-purple: #A060B1;
    --ch-green: #549474;
    --ch-orange: #CF8C66;
    --ch-yellow: #C9B16F;
    --ch-red: #C63A4C;

    --maroon: #C98D93;        /* brand Maroon lifted for a dark ground */

    --wash: 10%;
    --veil: rgba(12, 9, 15, 0.66);
    --edge: var(--rule);
    --lift: 0 1px 2px rgba(0, 0, 0, 0.4);
    --lift-2: 0 16px 40px rgba(0, 0, 0, 0.55);
  }
}

/* Dark token block (2 of 2) -- explicit override. Keep values in sync with
   the media-query block above; nothing else in this file duplicates. */
:root[data-theme='dark'] {
  color-scheme: dark;

  --ground: #1E1822;
  --panel: #282130;
  --raise: #332B3C;
  --rule: #3A3142;
  --rule-2: #4E4458;
  --dim: #A79FB0;
  --text: #F4F9FD;            /* BRAND Blue, verbatim */

  --signal: #B79FC4;
  --signal-ink: #1E1822;

  --ch-white: #D0CFCB;
  --ch-purple: #A060B1;
  --ch-green: #549474;
  --ch-orange: #CF8C66;
  --ch-yellow: #C9B16F;
  --ch-red: #C63A4C;

  --maroon: #C98D93;          /* brand Maroon lifted for a dark ground */

  --wash: 10%;
  --veil: rgba(12, 9, 15, 0.66);
  /* Must match the media-query block: on a dark ground a shadow barely reads,
     so the hairline carries elevation instead. Missing here, an elevated
     surface would have no border AND no visible shadow. */
  --edge: var(--rule);
  --lift: 0 1px 2px rgba(0, 0, 0, 0.4);
  --lift-2: 0 16px 40px rgba(0, 0, 0, 0.55);
}


/* ---------------------------------------------------------------------------
   3. CHANNEL HOOKS
   A ticket surface reads its hue from --ch. app.js can supply it three ways:
   data-channel="purple", class="ch-purple", or inline style="--ch:var(--ch-x)".
   Absent all three the surface falls back to a neutral rule, so nothing breaks.
   ------------------------------------------------------------------------ */

[data-channel='white'],  .ch-white  { --ch: var(--ch-white); }
[data-channel='purple'], .ch-purple { --ch: var(--ch-purple); }
[data-channel='green'],  .ch-green  { --ch: var(--ch-green); }
[data-channel='orange'], .ch-orange { --ch: var(--ch-orange); }
[data-channel='yellow'], .ch-yellow { --ch: var(--ch-yellow); }
[data-channel='red'],    .ch-red    { --ch: var(--ch-red); }


/* ---------------------------------------------------------------------------
   4. RESET & BASE
   ------------------------------------------------------------------------ */

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

html {
  -webkit-text-size-adjust: 100%;
  overflow-x: hidden;
}

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background-color: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-5);
  font-weight: 400;
  line-height: 1.5;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, svg, video { display: block; max-width: 100%; }

/* ---------------------------------------------------------------------------
   TEXT SELECTION
   ---------------------------------------------------------------------------
   Selection has to state BOTH colours or it is only correct by luck. The old
   rule set a 28% signal tint and pinned the ink to --text, which is right on a
   normal surface and wrong everywhere the surface is inverted:

     .tag.solid and .prio.p1 paint --text as their BACKGROUND, so forcing the
     ink to --text as well rendered selected text black-on-black -- invisible.
     A filled .btn is --signal-ink on --signal; the same rule dropped near-black
     ink onto brand Purple, around 2:1.

   So the pair is stated outright, and it is the brand's own approved pairing:
   Blue on Purple. Because it overrides background AND colour, it is legible on
   any surface underneath -- washed ticket cards, hatched frozen rows, channel
   chips, inputs -- without one rule per component.

   Both themes work out because the tokens flip together: light is #F4F9FD on
   #493D51 (~9.3:1), dark is #1E1822 on #B79FC4 (~8.6:1). Comfortably AA either
   way, which is the whole point -- selected text is text you are trying to
   READ, usually to copy it. */
::selection {
  background: var(--signal);
  color: var(--signal-ink);
}

/* The two inverted families, where the rule above would be legible but
   INVISIBLE -- the highlight would be the same colour as the surface it sits
   on, so nothing appears to happen when you drag across it. Each swaps to the
   opposite side of its own pair, which keeps the contrast and restores the
   highlight. */

/* Surfaces painted --signal: primary buttons, the active nav item, segmented
   and chip toggles, the sign-in CTA. */
.btn::selection,
button.on::selection,
.topnav a.active::selection,
.mobile-drawer-nav a.active::selection,
.seg button.on::selection,
.chips button.on::selection,
.signin-google::selection {
  background: var(--signal-ink);
  color: var(--signal);
}

/* Surfaces painted --text: solid tags, the P1 badge, maroon and hovered
   danger buttons. */
.tag.solid::selection,
.prio.p1::selection,
.btn.maroon::selection,
.btn.danger:hover::selection {
  background: var(--ground);
  color: var(--text);
}

/* forced-colors mode supplies its own selection pair and must win: overriding
   it there would undo the very accommodation the user asked their OS for. */
@media (forced-colors: active) {
  ::selection,
  .btn::selection,
  button.on::selection,
  .topnav a.active::selection,
  .mobile-drawer-nav a.active::selection,
  .seg button.on::selection,
  .chips button.on::selection,
  .signin-google::selection,
  .tag.solid::selection,
  .prio.p1::selection,
  .btn.maroon::selection,
  .btn.danger:hover::selection {
    background: Highlight;
    color: HighlightText;
  }
}

/* One focus language everywhere. Brand Purple, offset, never removed. */
:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
  border-radius: var(--r-1);
}
:focus:not(:focus-visible) { outline: none; }


/* ---------------------------------------------------------------------------
   5. TYPOGRAPHY  -- the brand pairing: OXYGEN CAPS over kumbh lowercase.

   Oxygen Mono uppercase is THE APP'S OWN VOICE: labels, nav, actions, enums,
   column heads. Anything a developer typed into a template literal.
   Kumbh Sans sentence case is OTHER PEOPLE'S VOICE: anything that came out of
   the database, a form field, or a person's name.
   Numerals stay mono and are NEVER uppercased -- they have no case.

   OXYGEN MONO HAS EXACTLY ONE WEIGHT (400). Setting font-weight above 400 on
   a mono element does not embolden it -- the browser fakes it by smearing the
   glyph horizontally, which on a uniform-advance face at 11px is muddy and
   unpredictably wider. Never do it. Where weight used to carry state, the
   state is now carried by border width or style, which also survives
   forced-colors mode where box-shadow does not.
   ------------------------------------------------------------------------ */

h1, h2, h3, h4 {
  font-family: var(--sans);
  color: var(--text);
  text-wrap: balance;
  margin: 0;
}

h1 {
  font-size: var(--fs-8);
  font-weight: 700;
  letter-spacing: -0.02em;   /* larger type needs tighter optical tracking */
  line-height: 1.15;
  margin-bottom: var(--sp-2);
}

h2 {
  font-size: var(--fs-6);
  font-weight: 650;
  letter-spacing: -0.006em;
  line-height: 1.3;
  margin-bottom: var(--sp-5);
}

/* h3 is a label, not a headline: section rails, kanban column heads. */
h3, .h {
  font-family: var(--mono);
  font-size: var(--fs-1);
  /* Explicit 400, NOT a deleted declaration: h3 is bold in the UA stylesheet,
     so removing font-weight here leaves browser-default bold on a font that
     has no bold, i.e. synthetic bold. Same reason for .table th and
     .wordmark b below. */
  font-weight: 400;
  letter-spacing: var(--track-mono-lead);
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: var(--sp-4);
}
.h { display: block; }

/* A section rail with a trailing count. Was an inline style block in app.js
   that reimplemented .h by hand. */
.rail-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-5);
}

h4 { font-size: var(--fs-4); font-weight: 650; margin-bottom: var(--sp-3); }

p { margin: 0 0 var(--sp-5); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

a { color: var(--signal); text-underline-offset: 2px; }
a:hover { color: var(--signal); }

code {
  font-family: var(--mono);
  font-size: 0.92em;
  background-color: var(--raise);
  border: 1px solid var(--rule);
  border-radius: var(--r-1);
  padding: 0 var(--sp-2);
}

button, input, textarea, select { font: inherit; color: inherit; }

/* Counted things. Applied by app.js to ids, points, hours, totals. */
.mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: var(--track-num);
}

.small { font-size: var(--fs-4); }
.big { font-size: var(--fs-6); }

/* Single-letter utilities emitted by app.js. Each has a component-scoped
   refinement further down (.type-card .t, .meta-grid .k, .path-row .p);
   these are the standalone fallbacks so they never render unstyled. */
.t { font-weight: 650; letter-spacing: 0.02em; }
.m { color: var(--dim); font-size: var(--fs-2); }
.p { color: var(--dim); font-family: var(--mono); font-size: var(--fs-3); overflow-wrap: anywhere; }

/* Generic active toggle. .seg button.on and .chips button.on outrank this. */
button.on {
  background-color: var(--signal);
  border-color: var(--signal);
  color: var(--signal-ink);
}

.note, .hint {
  color: var(--dim);
  font-size: var(--fs-4);
  line-height: 1.5;
  text-wrap: pretty;
}
.hint { margin-top: var(--sp-2); }
.note { margin: 0; }


/* ---------------------------------------------------------------------------
   6. TOP BAR / NAV  (mobile first: drawer trigger shows, nav hides)
   ------------------------------------------------------------------------ */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  min-height: 56px;
  padding: var(--sp-4) var(--sp-6);
  background-color: var(--panel);
  border-bottom: 1px solid var(--rule);
}

/* The lockup: primary mark + sub-product name. "HPAD" is no longer set in type
   -- the mark carries it -- so the old weight/colour split between the two
   words is gone and the separation is now mark-vs-text, which survives
   greyscale and forced-colors on its own. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.9ch;
  font-family: var(--mono);
  font-size: var(--fs-4);
  letter-spacing: var(--track-mono-lead);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--text);
}
.wordmark b {
  font-weight: 400;                 /* <b> is UA-bold; mono has no bold */
  letter-spacing: var(--track-mono-lead);
  color: var(--dim);
}
/* The mark is fixed raster artwork from Logos/, so the light/dark colourway is
   a real second file rather than a tint. <picture><source media="dark">
   handles the system preference; the two rules below cover an explicit
   data-theme toggle, which a media query cannot see. */
.wordmark .mark {
  display: block;
  flex: none;
}
.wordmark .mark img {
  display: block;
  height: 1.7em;
  width: auto;
}
:root[data-theme='dark'] .wordmark .mark img { content: url('./logo/hpad-white.png'); }
:root[data-theme='light'] .wordmark .mark img { content: url('./logo/hpad-purple.png'); }
/* Gate screens (sync, auth, sign-in) render the wordmark oversized. This was
   an inline font-size:20px in app.js that overflowed a 320px phone. */
.wordmark.lg { font-size: var(--fs-6); }

.topnav {
  display: none;
  flex: 1;
  flex-wrap: wrap;
  gap: var(--sp-1);
}
.topnav a {
  border-radius: var(--r-2);
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-2);
  letter-spacing: var(--track-mono);
  padding: var(--sp-3) var(--sp-5);
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.topnav a:hover { background-color: var(--ground); color: var(--text); }
.topnav a.active {
  background-color: var(--signal);
  color: var(--signal-ink);
}

.whoami {
  display: none;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-4);
  justify-content: flex-end;
  color: var(--dim);
  font-size: var(--fs-4);
  white-space: nowrap;
}

.mobile-menu-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r-2);
  color: var(--text);
  cursor: pointer;
  font-size: var(--fs-6);
  line-height: 1;
  height: 38px;
  width: 38px;
  padding: 0;
}
.mobile-menu-trigger:hover { background-color: var(--ground); }


/* ---------------------------------------------------------------------------
   7. MOBILE DRAWER
   ------------------------------------------------------------------------ */

.mobile-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: 100;
  display: flex;
  flex-direction: column;
  width: 300px;
  max-width: 86vw;
  background-color: var(--panel);
  border-left: 1px solid var(--rule);
  box-shadow: var(--lift-2);
  transform: translateX(100%);
  transition: transform var(--slow) var(--ease);
}
.mobile-drawer.active { transform: translateX(0); }

.mobile-drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 99;
  background-color: var(--veil);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--slow) var(--ease);
}
.mobile-drawer-backdrop.active { opacity: 1; pointer-events: auto; }

.mobile-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-6) var(--sp-7);
  border-bottom: 1px solid var(--rule);
}

.close-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-2);
  color: var(--dim);
  cursor: pointer;
  font-size: var(--fs-7);
  line-height: 1;
  height: 34px;
  width: 34px;
  padding: 0;
}
.close-btn:hover { background-color: var(--ground); color: var(--text); }

.mobile-drawer-nav {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-5);
  overflow-y: auto;
}
.mobile-drawer-nav a {
  border-radius: var(--r-2);
  color: var(--text);
  font-family: var(--mono);
  /* fs-4 -> fs-2: 13px mono caps is heavy in a 300px drawer. */
  font-size: var(--fs-2);
  letter-spacing: var(--track-mono);
  padding: var(--sp-5) var(--sp-6);
  text-decoration: none;
  text-transform: uppercase;
}
.mobile-drawer-nav a:hover:not(.active) { background-color: var(--ground); }
.mobile-drawer-nav a.active {
  background-color: var(--signal);
  color: var(--signal-ink);
}

.mobile-drawer-foot {
  padding: var(--sp-7);
  border-top: 1px solid var(--rule);
  background-color: var(--ground);
  color: var(--dim);
  font-size: var(--fs-4);
}


/* ---------------------------------------------------------------------------
   8. PAGE FRAME
   ------------------------------------------------------------------------ */

/* HPAD run 27" monitors as a floor. A fixed 1280px cap left 640px of dead
   margin on each side of every screen; the board was worse at 480px a side and
   the queue worst of all at 850px, two thirds of the glass. These are a floor
   plus a gutter now rather than a ceiling: fill the viewport, keep a consistent
   margin, and stop growing once prose lines get uncomfortably long. */
.page {
  margin: 0 auto;
  /* Explicit, not implied. Two things here would otherwise starve it: `margin:
     0 auto` stops a flex item stretching to its cross size, and the container
     queries below mean the page cannot size from its own contents either. In
     block flow this is a no-op; inside the board's flex column it is the
     difference between a full-width board and one collapsed to 216px. */
  width: 100%;
  max-width: min(1760px, calc(100vw - 96px));
  padding: var(--sp-9) var(--sp-6) var(--sp-11);
}
/* Was an inline style="max-width:940px" on the detail page, which no rule
   could override -- including the panel's. */
.page.detail { max-width: 940px; }


/* ---------------------------------------------------------------------------
   TICKET POPUP
   A ticket opens as a card OVER the list it came from.

   It used to open BESIDE the list, as a sticky right-hand column. That read
   well on a wide monitor and had one flaw that outweighed the rest: the column
   begins at the top of the page, so opening a ticket from a card you had
   scrolled down to put the ticket's contents above the viewport, and you had to
   scroll back up to read the thing you had just clicked. On a full board that
   is most of a screen height, every single time.

   Fixed and centred, it is always where you are looking, at any scroll
   position — and it uses the same backdrop convention as every other dialog
   here (dark area to dismiss, Close to dismiss, Escape to dismiss) rather than
   being a third kind of surface with its own rules.

   What this trades away, deliberately: the list no longer narrows to sit beside
   the ticket, so you cannot click straight from one card to the next with the
   ticket still open. Close then click is one extra press, against not having to
   hunt for what you opened every time.
   ------------------------------------------------------------------------ */

/* Same scrim, blur and scroll containment as .modal-backdrop, and deliberately
   BELOW it. A ticket is a surface you act from, so every dialog it raises --
   Hold, Edit, Delete, Finish, Deliver, Revision -- has to stack on top of it.
   This sat at 55 against .modal-backdrop's 50 and swallowed all six: the modal
   rendered correctly and was then covered by the panel, so the buttons read as
   completely dead. Keep this number under .modal-backdrop's. */
.ticket-overlay {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-8) var(--sp-5) var(--sp-10);
  background-color: var(--veil);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

/* The CARD is the scroll container, not the overlay.
   Letting the overlay scroll instead looks identical until the ticket is taller
   than the screen: then the whole card slides up and takes its own Close button
   with it, and no `position: sticky` inside can help, because sticky resolves
   against the nearest SCROLLING ancestor — which in that arrangement is the
   overlay and not the card. Capping the card's height and scrolling its body is
   what lets .ticket-head stick, keeping the name and the way out on screen at
   every viewport size. */
.ticket-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Wider than .modal's 640px: this is a whole record — meta grid, heaviness
     lines, hour logs, activity — not a single question. */
  max-width: 900px;
  /* Matches the overlay's own vertical padding (sp-8 top + sp-10 bottom). */
  max-height: calc(100dvh - var(--sp-8) - var(--sp-10));
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-3);
  box-shadow: var(--lift-2);
  overflow: hidden;   /* keeps the head inside the rounded corner */
}
/* The record scrolls; the card's own header sticks to the top of it.
   `.panel-head` used to be a separate strip outside this scroller, which is how
   Close stayed put -- and why the card's coloured top edge started below a band
   of chrome. The header moved INTO the card, so it has to sticky itself or a
   long ticket scrolls its own way out. Sticky resolves against the nearest
   scrolling ancestor, which is this element, so `top: 0` is measured from here.
   Nothing between them may set `overflow`, or sticky silently stops working. */
.ticket-panel > .page {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ticket-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-5);
  flex-wrap: wrap;
}
.ticket-head-name { min-width: 0; }   /* so a long title ellipsises rather than shoving the buttons out */
.ticket-head-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: none;
  /* Never squeezed: these are the ways out of, and into, this record. */
  flex-wrap: wrap;
  justify-content: flex-end;
}
/* Inside the panel the header is sticky and bleeds to the scroller's edges, so
   the record passes UNDER it rather than beside it. The negative top margin
   cancels the card's own top padding, which is what puts the header directly
   against the coloured edge -- the card now starts at the colour. */
.ticket-panel .ticket-head {
  position: sticky;
  top: 0;
  z-index: 3;
  background-color: var(--panel);
  margin: calc(var(--sp-8) * -1) calc(var(--sp-6) * -1) var(--sp-5);
  padding: var(--sp-5) var(--sp-6);
  border-bottom: 1px solid var(--rule);
}
/* The id sat above the title with its own spacing when it was the panel strip's
   job. In the header it is a caption on the line above the name. */
.ticket-head .tid { margin-bottom: var(--sp-1); }
.ticket-head .ticket-title { margin: 0; }
/* The detail screen is reused verbatim, so its page framing is undone here
   rather than forked into a second copy that would drift. */
.ticket-panel .page,
.ticket-panel .page.detail {
  max-width: none;
  margin: 0;
  padding: var(--sp-6);
}
/* Inside the panel the card IS the panel -- a second border and shadow around
   it just draws a box within a box. */
.ticket-panel > .page > .card {
  border: 0;
  box-shadow: none;
  padding-left: 0;
  padding-right: 0;
}

/* Nothing here narrows, reflows or locks the page behind the popup.
   No background scroll lock on purpose: `overflow: hidden` on the document
   clamps the scroll offset, which would throw away the reading position on the
   board underneath — the exact complaint this change exists to fix. The overlay
   is itself scrollable and carries `overscroll-behavior: contain`, which is how
   .modal-backdrop has always handled it, and it is enough.
   The board's column count no longer needs correcting either; it was only ever
   reduced because the old side panel took 470px out of the width. */

/* On a phone the card is the screen: full width, and tall enough that the
   rounded corners stay visible so it still reads as a card you can dismiss
   rather than a page you have navigated to. */
@media (max-width: 700px) {
  .ticket-overlay { padding: var(--sp-5) var(--sp-4) var(--sp-8); }
  .ticket-panel {
    max-width: none;
    max-height: calc(100dvh - var(--sp-5) - var(--sp-8));
  }
}

/* Which card you are reading, marked on the list itself. Without it, a panel
   open beside twelve near-identical cards gives no clue which one it is. */
/* Offset outward so it reads as a separate ring from .overdue's own inset
   outline -- an overdue card that is also the open one has to show both. */
.tcard.open {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}

/* Keyboard focus wears the same ring as the open card: brand Purple is the one
   interactive accent, and a card you have focused is one keypress from being
   the card you have opened. Only :focus-visible, so a mouse click does not
   leave a ring behind on the board. */
.tcard:focus-visible,
div.card[data-action='open-ticket']:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}

/* The board used to drop to three columns while a ticket was open, then win one
   back at 1700px. Both rules are gone with the side panel: an overlay takes no
   width from the list, so the board keeps all six columns whatever is open. */
.page.narrow { max-width: min(900px, calc(100vw - 96px)); }
/* A grid of columns, not a document. It has no reading measure to protect, so
   it takes the width it is given. */
.page.wide { max-width: calc(100vw - 72px); }
/* Stats: a table that reads better at a bounded width. */
.page.mid { max-width: min(1040px, calc(100vw - 96px)); }

/* THE TITLE BLOCK.
   HPAD are architects, and the artifact that defines their working day is the
   drawing sheet -- which identifies itself with a hairline rule terminated by
   ticks. So every screen's head is one. It is the single signature in this
   stylesheet: it appears once per screen, never on a card, never on a panel,
   never twice. Everything around it is deliberately quiet.

   The brand guidelines pin four colours and two typefaces, so there is no new
   hue and no new face anywhere in this treatment -- the rule is brand Grey,
   which is the one role the guidelines give that colour (borders only, it
   fails AA as text). */
.page-head {
  position: relative;
  container-type: inline-size;   /* so the right tick can be placed in cqw */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-6);
  margin-bottom: var(--sp-9);
  padding-bottom: var(--sp-6);
}
/* The rule. */
.page-head::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--rule-2);
  opacity: 0.55;
}
/* The terminal ticks. One element, two ticks: the right-hand one is placed by a
   box-shadow offset in container query units so it lands on the far edge at any
   page width, with no extra markup. 6px, the convention on a dimension line. */
.page-head::before {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 6px;
  background-color: var(--rule-2);
  opacity: 0.55;
  box-shadow: 100cqw 0 0 0 var(--rule-2);
}
/* Where cqw is unsupported, drop the right tick rather than drawing it in the
   wrong place -- a misplaced tick is worse than one tick. */
@supports not (width: 1cqw) {
  .page-head::before { box-shadow: none; }
}
.page-head > div { min-width: 0; }
/* Stays SANS. Setting it in wide-tracked mono caps looked more like a real
   title block and read considerably worse -- a twelve-word sentence in
   uppercase at 0.12em loses word shape exactly where the reader needs it. The
   type rule is also narrower than it first appears: mono caps are for labels,
   nav, actions, enums and column heads, and a subtitle is prose. */
.page-head p {
  color: var(--dim);
  font-size: var(--fs-4);
  line-height: 1.6;
  margin: var(--sp-2) 0 0;
  max-width: 58ch;   /* short, so a wrap cannot push the rule off the title */
}

.section + .section { margin-top: var(--sp-8); }
.status-group + .status-group { margin-top: var(--sp-8); }
.status-group > h3 {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-5);
}

.grid-2, .grid-3 {
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: minmax(0, 1fr);
}
.stack { display: grid; gap: var(--sp-5); }
.split { display: grid; gap: var(--sp-6); grid-template-columns: minmax(0, 1fr); align-items: start; }

.hidden { display: none !important; }


/* ---------------------------------------------------------------------------
   9. PANELS
   background-color (longhand) is deliberate: .frozen layers a background-image
   hatch on top and a `background` shorthand here would silently erase it.
   ------------------------------------------------------------------------ */

.card {
  background-color: var(--panel);
  border: 1px solid var(--edge);
  border-radius: var(--r-3);
  box-shadow: var(--lift);
  padding: var(--sp-8);
}
.card + .card { margin-top: var(--sp-6); }

.soft-card {
  background-color: var(--raise);
  border: 1px solid var(--rule);
  border-radius: var(--r-2);
  padding: var(--sp-6);
}

.empty {
  display: flex;
  align-items: center;
  color: var(--dim);
  font-size: var(--fs-4);
  min-height: 84px;
}


/* ---------------------------------------------------------------------------
   10. BUTTONS
   ------------------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  background-color: var(--signal);
  border: 1px solid var(--signal);
  border-radius: var(--r-2);
  color: var(--signal-ink);
  cursor: pointer;
  font-family: var(--mono);
  font-size: var(--fs-2);
  letter-spacing: var(--track-mono);
  line-height: 1;
  min-height: 38px;
  padding: var(--sp-4) var(--sp-6);
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.btn:hover { background-color: color-mix(in srgb, var(--signal) 84%, var(--text)); border-color: transparent; }
.btn:active { transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled='true'] { cursor: not-allowed; opacity: 0.42; }
.btn:disabled:hover { background-color: var(--signal); }

.btn.ghost {
  background-color: transparent;
  border-color: var(--rule-2);
  color: var(--text);
}
.btn.ghost:hover { background-color: var(--ground); border-color: var(--text); }

/* Secondary solid. Ink-on-ground, no hue -- reads in greyscale. */
.btn.maroon {
  background-color: var(--text);
  border-color: var(--text);
  color: var(--ground);
}
.btn.maroon:hover { background-color: color-mix(in srgb, var(--text) 82%, var(--ground)); border-color: transparent; }

/* Destructive. Form, not colour: a doubled rule.
   font-weight:800 is GONE -- .btn is Oxygen Mono now and mono has one weight,
   so 800 only ever produced synthetic bold. The border carries it instead,
   which is strictly better: forced-colors mode discards box-shadow entirely
   but preserves border-width, so this state is now visible in High Contrast
   where the inset ring alone never was. Padding is pulled 1px to absorb the
   extra border so the button does not change size. */
.btn.danger {
  background-color: transparent;
  border: 2px solid var(--text);
  box-shadow: inset 0 0 0 1px var(--text);
  color: var(--text);
  font-weight: 400;                 /* pin: outranks the later .danger helper */
  padding: calc(var(--sp-4) - 1px) calc(var(--sp-6) - 1px);
}
.btn.danger.small { padding: calc(var(--sp-2) - 1px) calc(var(--sp-4) - 1px); }
.btn.danger:hover {
  background-color: var(--text);
  color: var(--ground);
  box-shadow: none;
}

.btn.big { font-size: var(--fs-4); min-height: 48px; padding: var(--sp-5) var(--sp-8); }
.btn.small { font-size: var(--fs-1); min-height: 30px; padding: var(--sp-2) var(--sp-4); }

.btn-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-4);
}

.copy-btn {
  background-color: var(--ground);
  border: 1px solid var(--rule);
  border-radius: var(--r-1);
  color: var(--dim);
  cursor: pointer;
  flex: none;
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  padding: var(--sp-1) var(--sp-3);
  text-transform: uppercase;
}
.copy-btn:hover { border-color: var(--signal); color: var(--signal); }


/* ---------------------------------------------------------------------------
   11. FORMS
   ------------------------------------------------------------------------ */

.field { margin-bottom: var(--sp-6); }
.field > label {
  display: block;
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  margin-bottom: var(--sp-3);
  text-transform: uppercase;
}

/* This used to be an allowlist of input[type='...'] selectors, which silently
   missed every <input> written without a type attribute — an attribute
   selector matches the attribute, not the IDL default of "text". Seventeen
   inputs across the app were written that way (title, input path, output
   path, hold reason, the Zoho client id, the inline table edits), so they got
   none of this: no width, no padding, no radius, UA inset border, 26px tall
   against the 42px everything else stands at. Inverting it to an exclusion
   list fixes those and cannot regress the next one somebody writes. Anything
   that is NOT a box — the toggles, buttons and hidden fields — is listed out
   by hand. */
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']):not([type='submit']):not([type='button']):not([type='reset']):not([type='image']):not([type='hidden']),
textarea,
select {
  width: 100%;
  min-height: 42px;
  padding: var(--sp-4) var(--sp-5);
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  color: var(--text);
  outline: none;
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--dim); opacity: 0.75; }

input:focus-visible, textarea:focus-visible, select:focus-visible {
  border-color: var(--signal);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--signal) 40%, transparent);
  outline: none;
}
input:disabled, textarea:disabled, select:disabled {
  background-color: var(--raise);
  color: var(--dim);
  cursor: not-allowed;
}

textarea { min-height: 88px; resize: vertical; line-height: 1.5; }

/* Numeric / identifier inputs are counted, so they are mono. */
.mono-input {
  font-family: var(--mono);
  font-size: var(--fs-4);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
}

/* Segmented control */
.seg {
  display: flex;
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  overflow: hidden;
}
.seg button {
  flex: 1;
  background-color: var(--panel);
  border: 0;
  color: var(--dim);
  cursor: pointer;
  font-family: var(--mono);
  font-size: var(--fs-2);
  letter-spacing: var(--track-mono);
  padding: var(--sp-5) var(--sp-4);
  text-transform: uppercase;
  transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.seg button + button { border-left: 1px solid var(--rule-2); }
.seg button:hover { background-color: var(--ground); color: var(--text); }
.seg button.on {
  background-color: var(--signal);
  color: var(--signal-ink);
}
.seg button:focus-visible { outline-offset: -2px; }

/* Value chips (hours, quantities) -- counted, so mono. */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.chips button {
  min-width: 54px;
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  color: var(--text);
  cursor: pointer;
  font-family: var(--mono);
  font-size: var(--fs-4);
  font-variant-numeric: tabular-nums;
  padding: var(--sp-4) var(--sp-5);
  transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.chips button:hover { border-color: var(--text); }
.chips button.on {
  background-color: var(--signal);
  border-color: var(--signal);
  color: var(--signal-ink);
}

/* Type picker */
.type-cards {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: minmax(0, 1fr);
}
.type-card {
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  cursor: pointer;
  padding: var(--sp-5) var(--sp-5);
  text-align: left;
  transition: border-color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
.type-card:hover { border-color: var(--text); }
/* "PHOTOS" over "still renders" -- the brand pairing in miniature, and the
   clearest demonstration of the new system anywhere in the app. */
.type-card .t {
  font-family: var(--mono);
  font-size: var(--fs-2);
  font-weight: 400;                 /* pin: outranks the standalone .t helper */
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--text);
}
.type-card .m {
  color: var(--dim);
  font-size: var(--fs-2);
  margin-top: var(--sp-1);
}
.type-card.on {
  border-color: var(--signal);
  box-shadow: inset 0 0 0 1px var(--signal);
  background-color: color-mix(in srgb, var(--signal) 6%, var(--panel));
}


/* ---------------------------------------------------------------------------
   12. TAGS, DOTS, COUNTERS
   Tag variants are FORM variants. .warn borrows the dashed language of
   escalation; .danger borrows the doubled rule of overdue. No extra hues.
   ------------------------------------------------------------------------ */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-1);
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  line-height: 1.4;
  padding: var(--sp-1) var(--sp-3);
  text-transform: uppercase;
  white-space: nowrap;
}
.tag.solid {
  background-color: var(--text);
  border-color: var(--text);
  color: var(--ground);
}
.tag.ok { border-color: var(--rule-2); color: var(--text); font-weight: 400; }
.tag.warn { border-style: dashed; border-color: var(--text); color: var(--text); font-weight: 400; }
/* Doubled rule. font-weight:800 removed -- see .btn.danger for the full
   reasoning; border-width survives forced-colors, box-shadow does not.
   Padding absorbs the extra 1px so the chip does not grow. */
.tag.danger {
  border-color: var(--text);
  border-width: 2px;
  box-shadow: inset 0 0 0 1px var(--text);
  color: var(--text);
  font-weight: 400;                 /* pin: outranks the later .danger helper */
  padding: calc(var(--sp-1) - 1px) calc(var(--sp-3) - 1px);
}

/* --- the two escape hatches ------------------------------------------------
   .tag is overwhelmingly chrome (channel, status, priority, type, role), so
   the base is mono caps. But three things land in a .tag that are NOT the
   app's voice, and shouting them is wrong:
     .tag.name -> a person's name (assignee) or free text a lead typed (a hold
                  reason). Uppercasing "Priya Subramanian" or a whole sentence
                  is both ugly and, for the sentence, unreadable.
     .tag.num  -> a counted value ("40 pts, 4.0 h"). Numerals have no case.
   These MUST ship with the matching class additions in app.js. */
.tag.name {
  font-family: var(--sans);
  font-size: var(--fs-2);
  letter-spacing: 0;
  text-transform: none;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline-block;
}
.tag.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: var(--track-num);
  text-transform: none;
}

.dot {
  display: inline-block;
  flex: none;
  height: 9px;
  width: 9px;
  border-radius: 50%;
}
/* .b = the white channel; it needs a rule so it does not vanish on paper. */
.dot.b { box-shadow: inset 0 0 0 1px var(--rule-2); }

.n {
  font-family: var(--mono);
  font-size: var(--fs-3);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  margin-left: auto;
}

/* Hero count on the Completed page. Retires --fs-9, which was defined and
   never used, and replaces a hardcoded 28px in app.js. */
.stat-num {
  font-family: var(--mono);
  font-size: var(--fs-9);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
  line-height: 1.1;
  color: var(--text);
}

/* Countdown / clock. Always mono, always tabular. */
.countdown, .when {
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
  white-space: nowrap;
}


/* ---------------------------------------------------------------------------
   13. TICKET CARD
   Channel arrives as a top edge rule plus a ~5% wash of the same hue across
   the body, so a lead scanning the board sees the colour lane before reading
   a single title. No left accent bar; density over decoration.

   Selector covers both the current markup (.tcard) and a card element
   carrying the open-ticket hook (div.card[data-action="open-ticket"]).
   ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
   A CARD ADAPTS TO ITS CONTAINER, NOT TO THE VIEWPORT
   The same .tcard renders in a ~250px board lane, a ~380px queue track and a
   full-width list. Viewport media queries cannot tell those apart -- they are
   all "desktop" -- so a card in a narrow lane and a card in a wide list were
   getting identical treatment and one of them was always wrong.

   The lane, the queue and the list declare themselves containers (below), and
   the card asks how much room IT has. That is why this needs no breakpoints:
   the same rules hold on a laptop and on a 27", because neither is the
   question being asked.
   ------------------------------------------------------------------------ */
.tcard,
div.card[data-action='open-ticket'] {
  position: relative;
  display: block;
  cursor: pointer;
  /* Fluid, so there is no jump between a narrow lane and a wide one. */
  padding: clamp(var(--sp-4), 3.5cqi, var(--sp-6))
           clamp(var(--sp-4), 3.5cqi, var(--sp-6))
           clamp(var(--sp-3), 3cqi, var(--sp-5));
  border: 1px solid var(--rule);
  /* The channel moved from a top rule to a LEFT spine. Cards stack vertically
     in a column, so a left edge lines up into one continuous colour rail you
     can read straight down without looking at any card in particular -- which
     is the point, because the channel is how the client pays and therefore
     whose work leads. */
  border-left: 4px solid var(--ch, var(--rule-2));
  border-radius: var(--r-2);
  box-shadow: none;
  background-color: var(--panel);
  background-color: color-mix(in srgb, var(--ch, var(--rule)) var(--wash), var(--panel));
  transition: border-color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
.tcard + .tcard,
div.card[data-action='open-ticket'] + div.card[data-action='open-ticket'] {
  margin-top: var(--sp-4);
}

/* The three places a ticket card lives declare themselves as the thing to
   measure against. `inline-size` contains width only, so heights, sticky
   column heads and the lanes' own scrolling are untouched. */
.kcol,
.page.queue,
.page.narrow,
.page:not(.wide) { container: cardhost / inline-size; }

/* Roomy: title and the trailing meta line can breathe, and the meta row stops
   wrapping onto three lines. Reached by a queue track or a wide list, never by
   a board lane -- which is the point. */
@container cardhost (min-width: 340px) {
  .tcard .ttl { font-size: var(--fs-6); }
  .tcard-rest { gap: var(--sp-2) var(--sp-4); }
}

/* Tight: a board lane on a smaller monitor. Drop the trailing meta line to the
   essentials rather than letting it wrap three deep and push every card down
   the lane. Nothing is hidden that is not also on the ticket itself. */
@container cardhost (max-width: 250px) {
  .tcard .ri:nth-child(n + 4) { display: none; }
  .tcard-when { font-size: var(--fs-2); }
}
.tcard:hover,
div.card[data-action='open-ticket']:hover {
  border-bottom-color: var(--rule-2);
  border-right-color: var(--rule-2);
  background-color: color-mix(in srgb, var(--ch, var(--rule)) calc(var(--wash) * 2), var(--panel));
}

.tid {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
}

/* ---------------------------------------------------------------------------
   CARD ANCHORS: channel, priority, deadline
   Three questions a card must answer before it is read: whose client is this
   (channel, from how they pay), how does the studio rank it (P1-P4), and when
   is it due. Each owns a fixed position so the eye learns where to look once
   and never hunts again. Everything else is one quiet trailing line.
   ------------------------------------------------------------------------ */

.tcard-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-3);
}

.chan {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--text);
}
.chan .dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background-color: var(--ch, var(--rule-2));
}
/* The white channel would vanish on a white panel, so it alone is outlined. */
.chan .dot.b { box-shadow: inset 0 0 0 1px var(--rule-2); }
/* `.chan.none` lived here to dim the words "no channel" on a card. channelBadge
   returns an empty placeholder span now instead of naming the absence, so there
   is nothing left to dim. */

/* Rank as WEIGHT, never as a second hue -- the channel already owns colour,
   and two colour scales side by side is two things shouting. The ladder still
   reads in greyscale and survives forced-colors, where box-shadow is dropped
   but border and background are not. */
.prio {
  flex: none;
  min-width: 2.4ch;
  padding: 2px var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--r-1);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-num);
  text-align: center;
  color: var(--dim);
}
.prio.p1 {
  background-color: var(--text);
  border-color: var(--text);
  color: var(--ground);
}
.prio.p2 {
  border-color: var(--text);
  color: var(--text);
}
.prio.p3 { border-color: var(--rule-2); }
.prio.p4 { color: var(--dim); }
/* Dashed, because "not ranked yet" is a gap someone has to close, and a gap
   should not look like a settled state. */
.prio.none { border-style: dashed; border-color: var(--rule-2); }

.tcard-when {
  margin-top: var(--sp-3);
  font-family: var(--mono);
  font-size: var(--fs-2);
  letter-spacing: var(--track-num);
  color: var(--dim);
}

.tcard-rest {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--dim);
}
/* The separator belongs to the item that FOLLOWS it, so a wrap can never end a
   line on a dangling "·" -- which it did when the dots were their own elements
   in a flex row.

   Items are nowrap and joined by <wbr>: without an explicit break opportunity
   the whole run became one unbreakable string and shot straight out of the
   card into the next column.

   The separator is an ::after INSIDE the nowrap span, so it can only ever
   travel with the item it follows. As a ::before on the next item it simply
   moved the problem -- lines then began with a stray "·" instead of ending
   with one. */
.tcard-rest .ri { white-space: nowrap; }
.tcard-rest .ri:not(:last-child)::after {
  content: '·';
  margin: 0 var(--sp-3);
  opacity: 0.55;
}

/* The project name. Sans, because it is other people's voice -- it comes out
   of the Zoho registry, not out of a template literal.

   It gets its OWN line rather than sharing the id row. Sharing was tried: a
   kanban column is ~200px, the id eats half of it, and flex then squeezed the
   id itself until "HP-403-V01" broke across three lines. Neither field had
   room. On its own line the id stays whole and the name gets the full card
   width before it has to ellipsise.

   One line only, ellipsised, with the full string on title -- a project name
   that wrapped to three lines would out-weigh the ticket title directly below
   it, and the title is what you are choosing between. */
.tproj {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: var(--sp-1);
  color: var(--dim);
  font-family: var(--sans);
  font-size: var(--fs-4);
  line-height: 1.35;
}

.ttl {
  font-family: var(--sans);
  font-size: var(--fs-5);
  font-weight: 650;
  line-height: 1.3;
  letter-spacing: -0.004em;
  margin: var(--sp-2) 0 var(--sp-4);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

.meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

/* The ticket detail heading. This is a title a USER typed ("Parents toilet
   photos"), so it stays sans and sentence case -- it is not the app's voice.
   Was five inline overrides on the h1 in app.js, where the rule was invisible. */
.ticket-title {
  font-family: var(--sans);
  font-size: var(--fs-8);
  font-weight: 650;
  letter-spacing: -0.014em;
  line-height: 1.2;
  text-transform: none;
  overflow-wrap: anywhere;
}


/* -- STATE 1: UNREAD -------------------------------------------------------
   A small brand-Purple signal dot. The one place the accent touches content. */

span.unread,
.unread:not(.card):not(.tcard):not(.now-card) {
  display: inline-block;
  flex: none;
  height: 7px;
  width: 7px;
  border-radius: 50%;
  background-color: var(--signal);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--signal) 22%, transparent);
}
/* On a ticket card the dot now leads the title rather than trailing the id, so
   it needs the gap the old flex row used to supply. */
.ttl > span.unread { margin-right: var(--sp-4); vertical-align: middle; }

/* If unread is ever applied to the card itself, the dot leads the title. */
.tcard.unread > .ttl::before,
.card.unread > .ttl::before,
.now-card.unread .big-title::before {
  content: '';
  display: inline-block;
  height: 7px;
  width: 7px;
  margin-right: var(--sp-3);
  vertical-align: 0.15em;
  border-radius: 50%;
  background-color: var(--signal);
}


/* -- STATE 2: OVERDUE ------------------------------------------------------
   Hairline outline in the TEXT colour + inset ring + bold clock. Never red. */

.tcard.overdue,
.card.overdue,
.now-card.overdue,
div.card[data-action='open-ticket'].overdue {
  outline: 1px solid var(--text);
  outline-offset: 2px;
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--text) 14%, transparent);
}

/* The clock is mono, and mono has one weight -- so the old font-weight:700
   here was synthetic bold from the day this file was written, not something
   this retheme introduced. The border (previously only on a standalone clock)
   is promoted to the whole overdue group, and tracking does the rest: on a
   monospace numeral run the digits already sit on a fixed grid, so widening
   it is instantly visible against the on-time clocks stacked below it in a
   board column. Reads in greyscale and survives forced-colors. */
.tcard.overdue .countdown,
.card.overdue .countdown,
.now-card.overdue .countdown,
.countdown.overdue,
span.overdue,
.overdue > .mono {
  color: var(--text);
  letter-spacing: 0.08em;
  border: 1px solid currentColor;
  border-radius: var(--r-1);
  padding: 0 var(--sp-2);
}


/* -- STATE 3: ESCALATED ----------------------------------------------------
   Dashed border. The channel rule stays on top, dashed, so the lane still
   reads while the dash announces the escalation. */

.esc-banner {
  /* inline-block, not inline-flex: the banner now carries a sentence tail
     (.detail), and as a flex item that tail could not wrap with the text --
     it became its own column and broke the box. */
  display: inline-block;
  margin-bottom: var(--sp-4);
  padding: var(--sp-1) calc(var(--sp-4) - 1px);
  /* 2px dash replaces font-weight:800 -- the dash IS the escalation language
     (see .escalated below), so thickening it reinforces rather than invents. */
  border: 2px dashed var(--text);
  border-radius: var(--r-1);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono-lead);
  text-transform: uppercase;
}
/* The detail-page banner carries a sentence and an approver's NAME after the
   word "Escalated". That tail is content, not chrome -- it must not shout. */
.esc-banner .detail {
  font-family: var(--sans);
  font-size: var(--fs-4);
  letter-spacing: 0;
  text-transform: none;
}

.tcard:has(.esc-banner),
.card:has(.esc-banner),
.now-card:has(.esc-banner),
.escalated {
  border-style: dashed;
  border-top-width: 3px;
}


/* -- STATE 4: FROZEN -------------------------------------------------------
   Diagonal hatch at ~62% opacity, laid in as a background-image so it works
   on cards, rows and panels alike without a pseudo-element. */

.frozen {
  background-image: repeating-linear-gradient(
    135deg,
    transparent 0 6px,
    var(--hatch) 6px 7px
  );
  background-attachment: local;
}
.tcard.frozen .ttl,
.card.frozen .ttl { color: var(--dim); }
.tcard.frozen:hover { background-color: color-mix(in srgb, var(--ch, var(--rule)) var(--wash), var(--panel)); }


/* ---------------------------------------------------------------------------
   14. BOARD
   ------------------------------------------------------------------------ */

.board {
  display: grid;
  align-items: start;
  gap: var(--sp-5);
  /* auto-fit derives the lane count from the room available, so the board
     reflows continuously instead of stepping at 640/900/1280. min() stops the
     240px floor from overflowing a phone narrower than 240px. */
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  overflow-x: auto;
  padding-bottom: var(--sp-2);
}

/* ---------------------------------------------------------------------------
   BOARD: fills the viewport once, lanes scroll themselves
   The board used to scroll the PAGE, which meant the column heads -- "New 3",
   "In Queue 4", the counts an admin is actually reading -- left the screen as
   soon as you looked into the third lane, and six lanes all grew to the height
   of the tallest, so a busy New column dragged four empty ones down with it.
   Now the board owns the space below the bar and each lane scrolls its own
   contents with its head pinned, so all six counts stay visible at all times.

   Only where there is vertical room to justify it: a laptop at 800px tall is
   better off scrolling the page than scrolling six 500px boxes.

   No magic number for the header. .topbar's min-height says 56px but it
   MEASURES 60px once its padding and 1px border count -- so a
   `calc(100dvh - 56px)` would be 4px out and leave the page scrolling by
   exactly the amount nobody can see. #app is a flex column instead and the
   board takes whatever is left, so the bar can be any height it likes.
   ------------------------------------------------------------------------ */
/* Content-derived, not device-derived. Six lanes need 6x200 + five 12px gaps +
   the 72px page margin = 1332px, so 1340px is the width at which this layout is
   actually possible; 720px is the height below which six scrolling boxes are
   worse than scrolling the page once. The old 1500x900 was a guess, and it shut
   every 1366x768 and 1440x900 laptop in the studio out of a layout their screen
   could have carried. */
@media (min-width: 1340px) and (min-height: 720px) {
  #app {
    display: flex;
    flex-direction: column;
    height: 100dvh;
  }
  #app > .topbar { flex: 0 0 auto; }

  #app > .page.wide {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding-bottom: var(--sp-5);
    overflow: hidden;
  }
  #app > .page.wide > .page-head { flex: 0 0 auto; }
  #app > .page.wide > .board {
    flex: 1 1 auto;
    min-height: 0;        /* without this the grid refuses to shrink */
    /* `start`, not `stretch`. Stretching made every lane the full height of the
       board, so a Review column holding one ticket drew a mostly-empty box the
       size of the screen and four quiet lanes shouted as loudly as the busy
       one. A lane is now exactly as tall as its work; .kcol's max-height below
       is what keeps a FULL lane scrolling rather than overflowing. */
    align-items: start;
    overflow: hidden;
    /* Pinned to six here, unlike the fluid board elsewhere: auto-fit wrapping a
       second row inside a FIXED height would squash both rows. minmax(0, 1fr)
       lets the six shrink to fit rather than overflow. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .page.wide .kcol {
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* The cap, and the reason `align-items: start` above is safe: a lane grows
       with its cards until it reaches the bottom of the board, then stops and
       scrolls itself. Short lanes stay short, long lanes never overflow. */
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* The count is the point of the head, so it stays put while the lane scrolls
     under it. --raise matches .kcol's own background so cards do not show
     through behind the sticky row. */
  .page.wide .kcol > h3 {
    position: sticky;
    top: calc(var(--sp-4) * -1);
    z-index: 1;
    flex: 0 0 auto;
    margin-top: 0;
    background-color: var(--raise);
  }
  /* A lane with one ticket must not stretch that ticket down the screen. */
  .page.wide .kcol > .tcard { flex: 0 0 auto; }

  /* Six scrollbars would be six grey stripes across a board whose whole job is
     to be scanned. */
  .page.wide .kcol { scrollbar-width: thin; }
  .page.wide .kcol::-webkit-scrollbar { width: 8px; }
  .page.wide .kcol::-webkit-scrollbar-thumb {
    background-color: var(--rule-2);
    border-radius: 4px;
  }
  .page.wide .kcol::-webkit-scrollbar-track { background: transparent; }
}

/* ---------------------------------------------------------------------------
   MY QUEUE: a work surface, not a column
   The artist's screen is the one they sit in front of all day, and it was a
   single 860px column on a 27" monitor -- one big card, then a vertical stack
   running off the bottom. The scroll was the real cost: "what am I doing after
   this" is the question the screen exists to answer, and it was below the fold.

   "Do this now" pins left and stays there; the rest of the queue lays out as
   capped tracks beside it. The cards are REAL GRID TRACKS -- an earlier pass
   used inline-block and they sat 8px out of line with each other, because
   inline-block still answers to the line box's baseline. Grid items are
   blockified and snapped to their row.
   ------------------------------------------------------------------------ */
/* 460px now-card + 24px gap + a 300px minimum queue track + 96px page margin =
   880px. That is where this layout becomes possible, so that is the number --
   not 1500px, which was arbitrary and denied it to every laptop. */
@media (min-width: 900px) {
  .page.queue {
    display: grid;
    /* Track 1 is the now-card. The rest are queue tracks, CAPPED rather than
       1fr: a queue card holds a title, a project and one meta line, and at
       640px it is a mostly-empty band. auto-fill also means a short queue does
       not stretch to fill the monitor. */
    grid-template-columns: minmax(400px, 460px) repeat(auto-fill, minmax(300px, 380px));
    justify-content: start;
    column-gap: var(--sp-6);
    row-gap: var(--sp-5);
    align-content: start;
  }
  /* .err / .ok-box used to be listed here too, back when a flash was drawn
     into the page as a direct child of the grid and needed spanning. Toasts
     are on document.body now, so those selectors matched nothing. */
  .page.queue > .page-head,
  .page.queue > .card.empty { grid-column: 1 / -1; }

  /* 60px is .topbar's MEASURED height, not the 56px its min-height claims. */
  .page.queue > .now-card {
    grid-column: 1;
    grid-row: 2 / span 40;
    align-self: start;
    position: sticky;
    top: calc(60px + var(--sp-6));
    margin-bottom: 0;
  }
  .page.queue > h3 { grid-column: 2 / -1; align-self: end; margin: 0; }
  /* No grid-column: auto-placement drops each card into the next free cell. */
  .page.queue > .tcard { margin: 0; align-self: start; }
}

.kcol {
  min-width: 0;
  min-height: 120px;
  padding: var(--sp-4);
  background-color: var(--raise);
  border: 1px solid var(--rule);
  border-radius: var(--r-3);
}
.kcol > h3 {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-4);
  padding: var(--sp-1) var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--rule);
}
/* No .kcol .tcard background override here on purpose: it would outrank
   .tcard:hover and silently kill the hover wash inside board columns. */


/* ---------------------------------------------------------------------------
   15. NOW CARD + META GRID
   ------------------------------------------------------------------------ */

.now-card {
  position: relative;
  margin-bottom: var(--sp-8);
  padding: var(--sp-9) var(--sp-8);
  background-color: var(--panel);
  background-color: color-mix(in srgb, var(--ch, var(--rule)) var(--wash), var(--panel));
  border: 1px solid var(--edge);
  border-top: 3px solid var(--ch, var(--text));
  border-radius: var(--r-3);
  box-shadow: var(--lift);
}

.now-label {
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  /* 0.2em stays a literal: a deliberate editorial gesture on a three-word
     label, not a system value. */
  letter-spacing: 0.2em;
  margin-bottom: var(--sp-4);
  text-transform: uppercase;
}

.big-title {
  font-family: var(--sans);
  font-size: var(--fs-8);
  font-weight: 700;
  letter-spacing: -0.018em;
  line-height: 1.15;
  margin: var(--sp-2) 0 var(--sp-5);
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.meta-grid {
  display: grid;
  gap: var(--sp-6) var(--sp-8);
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.meta-grid > div { min-width: 0; }

.k {
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  margin-bottom: var(--sp-1);
  text-transform: uppercase;
}
.v {
  font-family: var(--sans);
  font-size: var(--fs-4);
  line-height: 1.45;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   PROJECT COMBOBOX
   Replaces a native <datalist>. That gave no ranking, no two-word matching,
   and filtered on the option label only in Chrome -- in Firefox it matched
   the code alone, so "search by name" silently did not exist for half the
   studio. It also could not explain an empty result, which matters here
   because the list is a Zoho export and "missing" often means "newer than the
   export" rather than "you typed it wrong".
   ------------------------------------------------------------------------ */

.combo {
  position: relative;
}
.combo-list {
  position: absolute;
  z-index: 30;
  top: calc(100% + var(--sp-1));
  left: 0;
  right: 0;
  max-height: 17rem;
  overflow-y: auto;
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  box-shadow: var(--lift-2);
}
.combo-opt {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  cursor: pointer;
  border-bottom: 1px solid var(--rule);
}
.combo-opt:last-child { border-bottom: 0; }
/* Hover and keyboard share one highlight, so there is never a second, competing
   "selected" row when the pointer drifts across an arrow-key selection. */
.combo-opt:hover,
.combo-opt.on {
  background-color: color-mix(in srgb, var(--signal) 12%, var(--panel));
}
/* The code is the app's own vocabulary -- mono, and it must not shrink, since a
   half-shown code is useless. The name is the client's words: sans, and it is
   the part that gives way when the row runs out of room.

   Spelled out rather than .c/.n: a bare .n already exists in this file as the
   count pill, carrying margin-left:auto, which silently flung every project
   name to the far right of its row. Single-letter classes in one global
   stylesheet do not stay unique. */
.combo-code {
  flex: none;
  font-family: var(--mono);
  font-size: var(--fs-3);
  letter-spacing: var(--track-num);
  color: var(--text);
}
.combo-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--sans);
  font-size: var(--fs-4);
  color: var(--dim);
}
/* Sticky, because "ahmedabad" matches 135 projects and that count is the whole
   reason to keep typing. Pinned to the bottom of the scroller it stays on
   screen; sitting after the last row it would be the one thing you never
   scrolled far enough to read. */
.combo-more {
  position: sticky;
  bottom: 0;
  padding: var(--sp-3) var(--sp-5);
  background-color: var(--ground);
  border-top: 1px solid var(--rule);
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
}

.combo-status {
  margin-top: var(--sp-3);
  color: var(--dim);
  font-family: var(--sans);
  font-size: var(--fs-4);
  line-height: 1.45;
}
.combo-status.ok {
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--fs-3);
}
.combo-status.err-text { color: var(--err); }
/* Standalone form of the same idea, for failure ink outside a .combo-status
   (the Zoho heartbeat's credential line). Brand Maroon, never --ch-red: red is
   the FROZEN channel, and "the token was rejected" must not read as "this
   project is frozen". */
.err-text { color: var(--err); }

.path-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-width: 0;
}
/* Paths are long and their meaningful end is the tail, so direction: rtl is
   what makes text-overflow clip the START and keep the tail visible.

   But rtl also REORDERS the text, and a backslash is bidi-neutral: it takes
   the direction of its surroundings. A UNC path begins with two of them, so
   \\Oxygen\Projects\HP-350 rendered as Oxygen\Projects\HP-350\\ -- the leading
   slashes jumped to the end. Every HPAD path is a UNC path, and this is a
   value people copy, so it had to go.

   The fix keeps rtl on the box (still clips from the start) and isolates the
   text inside it as ltr, so the glyphs stay in their real order. app.js wraps
   the path in <bdi> for exactly this; the pairing is load-bearing, so do not
   drop one without the other. */
.path-row .p {
  direction: rtl;
  text-align: left;
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.path-row .p > bdi {
  direction: ltr;
  unicode-bidi: isolate;
}


/* ---------------------------------------------------------------------------
   16. TABLES
   Wide content scrolls inside its own container. The page body never moves.
   ------------------------------------------------------------------------ */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  max-width: 100%;
}

.table {
  border-collapse: collapse;
  font-size: var(--fs-4);
  min-width: 560px;
  width: 100%;
}
.table th {
  border-bottom: 1px solid var(--rule-2);
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  font-weight: 400;                 /* th is UA-bold; mono has no bold */
  letter-spacing: var(--track-mono);
  padding: var(--sp-4) var(--sp-5);
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Sortable column head. A real <button> so it is tabbable and announces itself;
   it inherits the th's type rather than looking like a control dropped into a
   table. The active column is marked by an UNDERLINE and the caret, never by
   colour — hue belongs to the channel, and both survive a greyscale print.

   The active state is keyed off the th's aria-sort, NOT a class. It used to
   carry `on`, which collided head-on with the generic `button.on` toggle
   further up this file: that rule is (0,1,1) and paints --signal, while
   `.th-sort { background: none }` is only (0,1,0) and lost. The active header
   came out as a filled purple block, and because `.th-sort.on` (0,2,0) DID win
   the colour, the label stayed near-black on it — unreadable, and the one
   header you were most likely to be looking at. Keying off the ARIA state
   removes the collision instead of out-specifying it, and there is now one
   source of truth for "this column is sorted" rather than a class and an
   attribute that could disagree. */
.table th:has(.th-sort) { padding: 0; }
.th-sort {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font: inherit;
  letter-spacing: inherit;
  min-height: 0;
  padding: var(--sp-4) var(--sp-5);
  text-transform: inherit;
  width: 100%;
}
.th-sort:hover { color: var(--text); }
th[aria-sort='ascending'] .th-sort,
th[aria-sort='descending'] .th-sort {
  background: none;
  border-bottom-color: var(--text);
  color: var(--text);
}
.th-sort:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
/* Reserve the caret's width on every sortable head so the labels do not shift
   sideways as the sorted column changes. */
.th-caret { display: inline-block; min-width: 1ch; opacity: 0.55; }
th[aria-sort='ascending'] .th-caret,
th[aria-sort='descending'] .th-caret { opacity: 1; }
.table td {
  border-bottom: 1px solid var(--rule);
  padding: var(--sp-5);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover td { background-color: var(--ground); }

/* Digits in a column align. Anything counted in a cell is mono + tabular. */
.table .mono,
.table td[data-num],
.table .num,
.table input[type='number'] {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.table .mono, .table .num { white-space: nowrap; }
/* Encodes once what was an inline font-size:12px repeated 14 times in app.js. */
.table .mono,
.table td[data-num],
.table .num { font-size: var(--fs-3); }
.table input, .table select { min-height: 34px; padding: var(--sp-3) var(--sp-4); }

.proj-row { cursor: pointer; }
.proj-row:hover td { background-color: var(--ground); }
.proj-details > td {
  background-color: var(--raise);
  border-bottom: 1px solid var(--rule-2);
  padding: var(--sp-6) var(--sp-5);
}


/* ---------------------------------------------------------------------------
   17. THREAD / TIMELINE
   ------------------------------------------------------------------------ */

.thread-item {
  position: relative;
  margin-left: var(--sp-3);
  padding: 0 0 var(--sp-7) var(--sp-7);
  border-left: 1px solid var(--rule-2);
}
.thread-item::before {
  content: '';
  position: absolute;
  left: -4px;
  top: 6px;
  height: 7px;
  width: 7px;
  border-radius: 50%;
  background-color: var(--dim);
}
/* A revision is a different KIND of event, so it gets a different SHAPE. */
.thread-item.revision::before,
.revision::before {
  border-radius: 0;
  transform: rotate(45deg);
  background-color: var(--text);
}
.thread-item:last-child { border-left-color: transparent; padding-bottom: 0; }

/* `.timeline` and its three descendants used to style the ticket's Activity
   drawer. That drawer became the Activity page, which is a `.table` like the
   Projects registry, so nothing emits `.timeline` any more and these rules were
   styling an element that no longer exists. `.when` stays — thread groups and
   the deadline countdown still wear it. */


/* ---------------------------------------------------------------------------
   18. MODAL
   ------------------------------------------------------------------------ */

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-7) var(--sp-5) var(--sp-10);
  background-color: var(--veil);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

.modal {
  width: 100%;
  max-width: 640px;
  padding: var(--sp-8) var(--sp-7);
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-3);
  box-shadow: var(--lift-2);
}
.modal.wide { max-width: 820px; }

.foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  justify-content: flex-end;
  margin-top: var(--sp-7);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
}


/* ---------------------------------------------------------------------------
   19. MESSAGES
   Errors are announced with weight and a rule, not with a colour that would
   collide with the red channel.
   ------------------------------------------------------------------------ */

.err, .ok-box {
  margin: var(--sp-5) 0;
  padding: var(--sp-5) var(--sp-6);
  border-radius: var(--r-2);
  font-size: var(--fs-4);
  line-height: 1.45;
  text-wrap: pretty;
}
.err {
  background-color: var(--raise);
  border: 1px solid var(--text);
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--text) 12%, transparent);
  color: var(--text);
  font-weight: 650;
}
.ok-box {
  background-color: var(--raise);
  border: 1px solid var(--rule-2);
  color: var(--dim);
}

/* Standalone semantic text helpers, for SANS prose outside tags and buttons.
   These are single-class (0,1,0) and appear LATER in the file than .tag, so
   when app.js writes class="tag danger" the font-weight below would win and
   silently faux-bold an Oxygen Mono chip. The .tag./.btn. compound rules above
   therefore pin font-weight: 400 explicitly. Do not remove those pins. */
.ok { color: var(--text); }
.warn { color: var(--text); font-weight: 650; }
.danger { color: var(--text); font-weight: 700; }
.maroon { color: var(--text); }


/* ---------------------------------------------------------------------------
   20. ACCEPT / LINES EDITOR
   Every column here is counted, so the whole row runs on tabular mono.
   ------------------------------------------------------------------------ */

.lines-editor { display: grid; gap: var(--sp-4); }

.lrow {
  display: grid;
  align-items: center;
  gap: var(--sp-4);
  grid-template-columns: minmax(0, 1fr) 72px;
}
.lrow input, .lrow select { min-height: 38px; }
.lrow input[type='number'],
.lrow select,
.lrow .mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.accept-line { padding: var(--sp-3) 0; border-bottom: 1px solid var(--rule); }
.accept-line:last-child { border-bottom: 0; }

.lhead {
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
}
.lhead span { white-space: nowrap; }

.line-points {
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--fs-4);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.lines-total {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--rule-2);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--fs-5);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
  text-align: right;
}


/* ---------------------------------------------------------------------------
   21. SETTINGS / STORAGE / SIGN-IN
   ------------------------------------------------------------------------ */

.storage-actions {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.who-list { display: grid; gap: var(--sp-4); }
.who-list button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-6);
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  color: var(--text);
  cursor: pointer;
  font-family: var(--sans);
  font-size: var(--fs-5);
  text-align: left;
}
.who-list button:hover { border-color: var(--signal); background-color: var(--ground); }

.sync-spinner {
  display: inline-block;
  height: 15px;
  width: 15px;
  border: 2px solid var(--rule-2);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: sync-spin 0.8s linear infinite;
  vertical-align: -2px;
}

@keyframes sync-spin { to { transform: rotate(360deg); } }


/* ---------------------------------------------------------------------------
   22. RESPONSIVE  (mobile first -- these are additive, nothing is cancelled)
   ------------------------------------------------------------------------ */

@media (min-width: 600px) {
  .page { padding: var(--sp-8) var(--sp-7) var(--sp-11); }
  .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .type-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .lrow { grid-template-columns: minmax(140px, 1fr) 68px 80px 84px 76px 34px; }
  .page-head {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--sp-7);
  }
}

@media (min-width: 900px) {
  .topbar { gap: var(--sp-7); padding: var(--sp-4) var(--sp-8); min-height: 60px; }
  .topnav { display: flex; }
  .whoami { display: flex; }
  .mobile-menu-trigger { display: none; }
  .mobile-drawer, .mobile-drawer-backdrop { display: none; }
  .grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .split { grid-template-columns: minmax(0, 1fr) 320px; }
  .page { padding: var(--sp-9) var(--sp-8) var(--sp-11); }
}

/* The manager nav is 7 items. In Oxygen Mono caps that is ~700px of label
   against ~410px of topbar between the wordmark and the whoami block. .topnav
   has flex-wrap, so it would not overflow -- it would silently double the
   height of a STICKY bar on every page. Tighten it in the band where the nav
   is visible but the viewport is not yet wide. */
@media (min-width: 900px) and (max-width: 1099px) {
  .topbar { gap: var(--sp-5); padding-inline: var(--sp-6); }
  .topnav a {
    font-size: var(--fs-1);
    padding: var(--sp-3) var(--sp-3);
    letter-spacing: 0;
  }
  .whoami { gap: var(--sp-3); font-size: var(--fs-2); }
}

/* Six lanes is the workflow, and the workflow does not change because the
   monitor got bigger — so recovered width goes into WIDER lanes, not more of
   them. auto-fit already caps at six because there are six status columns, and
   at 2560 that is ~390px a lane against the old 249px: the difference between
   a two-line card title and a one-line one. No breakpoint needed. */

/* Coarse pointers get bigger hit targets without changing the look. */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  .copy-btn { padding: var(--sp-3) var(--sp-4); }
  .topnav a, .mobile-drawer-nav a { padding-block: var(--sp-5); }
}


/* ---------------------------------------------------------------------------
   23. ACCESSIBILITY & OUTPUT
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .mobile-drawer { transition: none; }
  .btn:active { transform: none; }
}

@media (prefers-contrast: more) {
  :root { --rule: var(--dim); --rule-2: var(--text); }
  .note, .hint, .k, .tag { color: var(--text); }
}

/* Forced colors discards background-color, background-image, box-shadow and
   color-mix() outright. Everything this app encodes with those disappears --
   so the states that matter get an outline (which survives) and the two
   surfaces that ARE their colour opt out of the override entirely. */
@media (forced-colors: active) {
  .tcard, .card, .now-card, .modal, .kcol { border: 1px solid CanvasText; }
  .unread, .dot { forced-color-adjust: none; }
  /* These read as "selected" only via background-color, which is dropped. */
  .tag.solid,
  .topnav a.active,
  .mobile-drawer-nav a.active,
  .seg button.on,
  .chips button.on,
  .type-card.on {
    outline: 2px solid CanvasText;
    outline-offset: -2px;
  }
  /* The frozen hatch is a background-image and would vanish, taking the only
     signal that a project is frozen with it. */
  .frozen { forced-color-adjust: none; }
}

@media print {
  .topbar, .mobile-drawer, .mobile-drawer-backdrop, .mobile-menu-trigger, .btn-row { display: none !important; }
  body { background: #fff; color: #000; }
  .card, .tcard, .now-card { box-shadow: none; break-inside: avoid; }
}

/* ---------------------------------------------------------------------------
   SIGN-IN
   The only screen every person sees, and the first impression of the tool.
   One decision wide: no form, no password, no alternative to weigh. The domain
   restriction is stated at the door rather than enforced after someone has
   already chosen an account.
   ------------------------------------------------------------------------ */

.signin-shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-6);
  padding: var(--sp-7);
  text-align: center;
}

.signin-card {
  width: 100%;
  max-width: 27rem;
  padding: var(--sp-10) var(--sp-8) var(--sp-8);
  border: 1px solid var(--rule);
  border-radius: var(--r-3);
  background-color: var(--panel);
  /* --shadow-2 was never a token in this file, so this always fell through to
     a hardcoded shadow tuned for a light ground and stayed that way in dark
     mode. --lift-2 is the elevated-surface token and it is theme-tuned. */
  box-shadow: var(--lift-2);
}

/* .signin-glyph is retired. It was a gradient square standing in for a logo;
   the wordmark now renders the real primary mark, so the placeholder became a
   competing shape directly above the actual identity. The rule is kept as a
   no-op guard in case an old cached app.js still emits the element. */
.signin-glyph { display: none; }

.signin-card .wordmark { justify-content: center; margin-bottom: var(--sp-6); }

.signin-title {
  margin: 0 0 var(--sp-3);
  font-family: var(--sans);
  font-size: var(--fs-7);
  font-weight: 700;
  letter-spacing: -0.02em;
  text-transform: none;
  color: var(--text);
  text-wrap: balance;
}

.signin-sub {
  margin: 0 auto var(--sp-8);
  max-width: 32ch;
  color: var(--dim);
  font-size: var(--fs-4);
  line-height: 1.5;
}

/* The single most prominent control in the product, so it has to be the brand
   and not a generic near-black CTA. --signal on --signal-ink is brand Purple
   carrying brand Blue, i.e. the approved "Blue on Purple" pairing, and it is
   the same paint every other primary control (.btn) already wears.

   The label stays Kumbh sentence case rather than the app's Oxygen caps: by
   this file's own rule, mono caps are the APP's voice, and "Continue with
   Google" is a third-party brand string we are required to reproduce
   faithfully -- not a word the app gets to shout. */
.signin-google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  width: 100%;
  min-height: 2.75rem;
  padding: var(--sp-4) var(--sp-7);
  border: 1px solid var(--signal);
  border-radius: var(--r-2);
  background-color: var(--signal);
  color: var(--signal-ink);
  font-family: var(--sans);
  font-size: var(--fs-4);
  font-weight: 700;
  cursor: pointer;
  transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease), transform var(--fast) var(--ease);
}
/* The G is four fixed Google colours and none of them are ours. On brand
   Purple its blue lobe drops to ~1.4:1, so the mark sits on its own white
   tile -- which is also how Google asks for it on a coloured button. */
.signin-google svg {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  box-sizing: content-box;
  padding: 3px;
  border-radius: var(--r-1);
  background-color: #FFFFFF;
}
.signin-google:hover {
  background-color: color-mix(in srgb, var(--signal) 84%, var(--text));
  border-color: transparent;
}
.signin-google:active { transform: translateY(1px); }

.signin-busy {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  min-height: 2.75rem;
  color: var(--dim);
  font-size: var(--fs-4);
  font-weight: 600;
}

.signin-fine {
  margin: var(--sp-6) 0 0;
  font-family: var(--mono);
  font-size: var(--fs-1);
  letter-spacing: var(--track-mono-lead);
  text-transform: uppercase;
  color: var(--dim);
}

.signin-foot {
  margin: 0;
  max-width: 32ch;
  color: var(--dim);
  font-size: var(--fs-3);
}

@media (max-width: 600px) {
  .signin-card { padding: var(--sp-8) var(--sp-6) var(--sp-7); }
}


/* Hold is the one button in the row that STOPS things. Everything beside it
   moves the ticket on — Accept, Deliver, Request revision, Close — and it wore
   the same ghost outline as Close, which made the two hardest to tell apart at
   a glance the two that do opposite things.
   Drawn as form rather than hue, per the stylesheet's founding rule: a dashed
   edge reads as provisional, which is what a hold is. No new colour is spent,
   and it survives greyscale like every other state here. The dash is on a
   button, so it does not collide with the dashed border that means "escalated"
   on a card. */
.btn.ghost.pause {
  border-style: dashed;
  border-color: var(--rule-2);
}
.btn.ghost.pause:hover {
  border-style: dashed;
  border-color: var(--text);
}

/* Reference images on a revision — in the modal before sending, and in the
   thread afterwards. Small on purpose: this is "which tile did you mean",
   answered at a glance, not a gallery. Click opens the full size. */
.rev-previews {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
}
.rev-previews img {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  background-color: var(--raise);
}
.rev-thumb { cursor: zoom-in; }

/* The x sits on the thumbnail rather than beside it: the images wrap, and a
   button in the flow would end up next to the wrong picture on a narrow panel. */
.rev-thumb-cell { position: relative; display: inline-flex; line-height: 0; }
.rev-thumb-x {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  min-height: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1;
  color: var(--signal-ink);
  background-color: var(--maroon);
  border: 1px solid var(--panel);
  border-radius: 50%;
  cursor: pointer;
}
.rev-thumb-x:hover { background-color: var(--text); }
.rev-thumb-x:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }

/* Bug report. Bottom-right, above the page and under every dialog: it must be
   reachable from any screen without ever covering the thing being reported. */
.bug-fab {
  position: fixed;
  right: var(--sp-6);
  bottom: var(--sp-6);
  z-index: 40;
  padding: var(--sp-4) var(--sp-6);
  font-family: var(--mono);
  font-size: var(--fs-2);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--signal-ink);
  background-color: var(--signal);
  border: 0;
  border-radius: var(--r-2);
  box-shadow: var(--lift-2);
  cursor: pointer;
}
.bug-fab:hover { background-color: var(--text); }
.bug-fab:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }

.bug-panel {
  position: fixed;
  right: var(--sp-6);
  bottom: var(--sp-6);
  z-index: 40;
  width: min(340px, calc(100vw - var(--sp-7)));
  padding: var(--sp-6);
  background-color: var(--panel);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-3);
  box-shadow: var(--lift-2);
}
.bug-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-4);
}
.bug-panel textarea { min-height: 84px; }

/* ---------------------------------------------------------------------------
   A COLLAPSED SECTION
   Used by the three folds on a ticket: Project details, Thread, Activity.

   `.legend` used to share these rules, for the channel disclosure that has been
   replaced by the always-visible .chan-bar below. Its own `.legend` block
   carried a 62ch measure, which was right for a paragraph of prose and wrong for
   the folds -- they hold a meta grid, a revision history and a timeline, all
   built to use the panel's full width -- so the measure left with it.
--------------------------------------------------------------------------- */
.fold > summary {
  font-family: var(--mono);
  font-size: var(--fs-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--dim);
  cursor: pointer;
  padding: var(--sp-2) 0;
  /* The marker is the only affordance saying this opens, so it stays. */
  list-style-position: inside;
}
.fold > summary:hover { color: var(--text); }
.fold > summary:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}
.fold[open] > summary { color: var(--text); }

/* Thread and Activity are h2-sized sections that now open rather than always
   being there, so the summary carries the weight the h2 used to. Explicit 400:
   summary is not UA-bold, but the count inside it is a .n and the two traps in
   CLAUDE.md both bite where a nested element inherits a heavier face than the
   mono has to give -- Oxygen Mono ships one weight, and asking for more smears
   the glyph. */
.fold > summary {
  font-weight: 400;
  font-size: var(--fs-2);
  color: var(--text);
}
.fold > summary .n { margin-left: var(--sp-2); }
.fold[open] > summary { margin-bottom: var(--sp-3); }

/* `.more-actions` and its five rules styled a disclosure that briefly held Hold,
   Edit and Delete. Those are buttons in the card's header now: a fold in front
   of three controls an admin uses daily charged a click for tidiness. */

/* ---------------------------------------------------------------------------
   THE NEW TICKET FORM, IN TWO COLUMNS
   Eleven stacked fields ran past the fold on a 900px-tall screen, so raising a
   ticket meant scrolling to find the button that raises it. Split by what you
   are answering rather than by height: the request on the left, the logistics on
   the right, and the submit full width beneath both.

   `align-items: start` so the two columns do not stretch each other -- without
   it the shorter column's last field grows to match, which puts a tall empty box
   under Type.
--------------------------------------------------------------------------- */
.form-2col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-6) var(--sp-8);
  align-items: start;
}
/* One column below 1000px. Two columns of a UNC path is narrower than the path,
   and this form's whole job is carrying one. */
@media (max-width: 1000px) {
  .form-2col { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   THE CHANNEL BAR
   ---------------------------------------------------------------------------
   Six pills across the top of the board and the queue, replacing a collapsed
   "what do the colours mean?" disclosure. Collapsed, it charged a click to
   answer something you were looking straight at, and then closed again.

   Each pill wears the SAME idiom a ticket card wears: a left spine in --ch plus
   the standard --wash. That is deliberate reuse, not coincidence -- a pill and
   the edge of a card are the same fact, so they must not be two colour
   languages. Setting --ch stays the only thing either of them does.

   `.legend-list` and its three descendants used to style the old disclosure and
   were deleted with it. `.legend`'s summary rules are gone too; the shared
   selector list they lived in survives as `.fold > summary`, which the ticket
   drawers still use.
--------------------------------------------------------------------------- */
.chan-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  /* Pulled up under the page head's rule so it reads as part of the header
     rather than as the first item of the list below it. */
  margin: 0;
}

/* The bar lives INSIDE .page-head, and that is load-bearing rather than tidy.
   As a sibling it became a grid item of `.page.queue`, whose `.now-card`
   explicitly claims `grid-row: 2 / span 40` so the up-next lanes can flow beside
   it -- leaving a full-width item nowhere to go but row 42, under the entire
   queue. Inside the head it is not a grid item at all, and it is also where a
   header bar belongs.

   .page-head is a nowrap flex row carrying the title and the New Ticket action,
   so it has to wrap for the bar to get its own line. Scoped with :has() to the
   two pages that have one, so no other page-head changes behaviour. */
.page-head:has(> .chan-bar) { flex-wrap: wrap; }
.page-head > .chan-bar {
  flex-basis: 100%;
  margin-top: var(--sp-2);
}
.chan-pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--ch, var(--rule-2));
  border-radius: var(--r-2);
  background-color: color-mix(in srgb, var(--ch, var(--rule)) var(--wash), var(--panel));
  /* Never shrink a pill into an ellipsis: a legend that has to be widened to be
     read is not a legend. It wraps to a second line instead. */
  flex: none;
}
/* The channel name is an enum, so it is the app's own voice: mono, caps. The
   meaning beside it is prose and stays in the sans face. Getting this backwards
   is what makes the board shout. */
.chan-pill .k {
  font-family: var(--mono);
  font-size: var(--fs-3);
  text-transform: uppercase;
  letter-spacing: var(--track-mono);
  color: var(--text);
  margin-bottom: 0;
}
.chan-pill .m {
  font-size: var(--fs-3);
  color: var(--dim);
  line-height: 1.3;
}
/* The swatch already carries the hue; inside a pill that is also washed in it,
   the border is what keeps white and yellow from dissolving into their own
   background. */
.chan-pill .ch-swatch { margin-right: 0; }

/* Narrow screens: the meanings are the first thing to go, not the colours. A
   phone still needs to know which hue is which; it does not need six sentences
   competing with the queue underneath. The full sentence stays on the title. */
@media (max-width: 640px) {
  .chan-pill .m { display: none; }
}

/* The swatch, shared with the read-only channel row in Accept. A square, not a
   dot: it is the same shape as the edge rule a card wears, so the two read as
   the same fact. Bordered, because white-on-white is otherwise invisible. */
.ch-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: var(--r-1);
  background: var(--ch, var(--line-strong));
  border: 1px solid var(--line-strong);
  /* baseline-aligned with the text it labels rather than sitting on it */
  transform: translateY(1px);
  margin-right: var(--sp-3);
}

/* ---------------------------------------------------------------------------
   READ-ONLY VALUE IN A MODAL
   Project, Client and Channel in Accept are facts, not inputs -- their editor
   is the Projects tab. Sized like the inputs beside them so the row still lines
   up, but with no box, because a box that cannot be typed in is a lie.
--------------------------------------------------------------------------- */
.v.ro {
  min-height: 40px;
  display: flex;
  align-items: center;
  gap: 0;
}
.muted { color: var(--dim); }

/* ---------------------------------------------------------------------------
   TOASTS
   Top centre, over everything, on document.body so a re-render cannot take one
   away mid-sentence. This replaced a box drawn into the page by whichever view
   claimed it first, which meant the message moved depending on which screen
   you were on -- there was no one place to look.

   Top centre and not a corner: this app is used on 27" monitors, where a
   bottom-right toast is a foot away from where the work is and gets missed.
--------------------------------------------------------------------------- */
.toasts {
  position: fixed;
  top: var(--sp-6);
  left: 50%;
  transform: translateX(-50%);
  z-index: 90;               /* over the ticket overlay (45) and modals (50) */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
  width: max-content;
  max-width: min(560px, calc(100vw - var(--sp-8)));
  /* The layer never eats a click; the toasts themselves opt back in. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  cursor: pointer;
  padding: var(--sp-5) var(--sp-7);
  border-radius: var(--r-2);
  font-family: var(--sans);
  font-size: var(--fs-4);
  line-height: 1.45;
  text-align: center;
  text-wrap: pretty;
  background-color: var(--raise);
  color: var(--text);
  border: 1px solid var(--rule-2);
  /* The one place in this app that earns a real drop shadow: it is the only
     element with nothing behind it to sit on, and it has to read as floating
     over whatever it covers. */
  box-shadow: 0 8px 28px -12px color-mix(in srgb, var(--text) 45%, transparent),
              0 2px 6px -2px color-mix(in srgb, var(--text) 25%, transparent);
  animation: toast-in 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}

/* State as form, not another hue -- an error is a heavier edge and brand
   Maroon ink, never --ch-red, because red is the FROZEN channel and a failed
   save must not read as "this project is frozen". */
.toast.err {
  border: 1px solid var(--err);
  box-shadow: 0 8px 28px -12px color-mix(in srgb, var(--err) 55%, transparent),
              inset 0 0 0 2px color-mix(in srgb, var(--err) 14%, transparent);
  color: var(--err);
  font-weight: 650;
}

.toast.out { animation: toast-out 180ms ease-in both; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-10px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes toast-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-6px) scale(0.98); }
}

/* Motion is the decoration here, not the message. Taken away, the toast still
   appears, still waits, still leaves -- dismissToast uses a timer rather than
   transitionend precisely so this branch cannot strand one on screen. */
@media (prefers-reduced-motion: reduce) {
  .toast, .toast.out { animation-duration: 1ms; }
}

@media (max-width: 640px) {
  .toasts {
    top: var(--sp-4);
    width: calc(100vw - var(--sp-6));
    max-width: none;
  }
  .toast { width: 100%; }
}

/* ---------------------------------------------------------------------------
   REQUIRED MARKER
   Driven off the `required` attribute rather than written into each label, so
   the star and the validation can never disagree. Add or drop `required` on
   the control and the mark follows; there is nothing to keep in step by hand.

   `label.req` is the escape hatch for the three controls that cannot carry the
   attribute meaningfully -- the hour chips and the type/half segments are a
   hidden input behind buttons, and `required` on a hidden input is ignored by
   every browser.

   Maroon, the brand's attention ink. Not a channel colour, so it cannot be
   misread as "this project is frozen"; not --dim, because a mark nobody
   notices is a mark that is not there.
--------------------------------------------------------------------------- */
/* Descendant, not direct child: the project field wraps its input in .combo,
   and that is the single most required field in the app. */
.field:has(:is(input, select, textarea):required) > label::after,
label.req::after {
  content: ' *';
  color: var(--err);
  /* Mono: it is a mark this app is making, not part of the label's word. */
  font-family: var(--mono);
}
/* The same mark, quoted inside prose that explains it. */
.req-key { color: var(--err); font-family: var(--mono); }
