/* ==========================================================================
   DNM Service Desk — one stylesheet.

   The interface is woven, not stacked. There are no cards and no shadow below
   modal level; structure is a continuous warp of vertical alignment lines that
   every table column, form field and panel edge snaps to, crossed by weft rules
   that separate rows. Hierarchy is carried by rule *weight* — 2px structural,
   1px functional, and a near-invisible band every fifth row — the way fabric
   hierarchy is thread weight.

   Indigo is the only chrome colour. Selvedge red is "the edge": it appears on a
   boundary (selection, validation, SLA breach, destructive action) and never as
   a fill or a chart colour.

   Two rules this file must keep obeying:

   1. Class names are load-bearing. Fifty Razor views reference them and none of
      them may be renamed here — only redefined.
   2. No physical direction properties. Arabic is a first-class direction, not a
      retrofit, so everything is logical: margin-inline-start, inset-inline-end,
      border-inline-start, text-align:start. The four things CSS cannot express
      logically live in one [dir="rtl"] block at the very end, with reasons.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;450;500;600;700&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&display=swap');

/* ═══════════════════════════════════════════════════════ TOKEN LAYER ═══════
   Six scales, role-named rather than hue-named, so a plant-floor theme is a
   nine-variable override. Rule weights are tokens because in this system they
   are what carries hierarchy.

   Nothing below this block should introduce a raw hex or a raw duration. Where
   one appears anyway it is a one-off tint that has no role name — those are
   commented at the point of use.
   ═════════════════════════════════════════════════════════════════════════ */
:root {
  /* — INDIGO: the dye-vat ramp. The only chrome colour. ------------------- */
  --dnm-ink-900: #0f172f;  /* app chrome, headers, primary text              */
  --dnm-ink-800: #16223f;  /* nav hover, dark panel fill                     */
  --dnm-ink-700: #1e2f55;  /* headings, active nav, links                    */
  --dnm-ink-600: #2a3f6e;  /* secondary text on ecru — AA at 12.5px          */
  --dnm-ink-500: #3b5390;  /* focus ring, primary button                     */
  --dnm-ink-400: #5d7cc4;  /* icon rest state, gauge fill                    */
  --dnm-ink-300: #93a8d8;  /* text on ink — AA at 11px over ink-900          */
  --dnm-ink-200: #c3cfe9;  /* borders on dark                                */
  --dnm-ink-100: #e7ecf7;  /* selected-row wash, gauge track                 */

  /* — SELVEDGE: the edge. Boundary only, never a fill, never a chart. ----- */
  --dnm-selv: #c0392f;     /* selection edge, breach, validation, danger     */
  --dnm-selv-700: #93271e; /* selvedge as text on ecru — AA at 12.5px        */
  --dnm-selv-100: #f8e7e3; /* error field ground                            */

  /* — SIGNALS: three, and no more. --------------------------------------- */
  --dnm-ochre: #b8791b;    /* warning / at risk                              */
  --dnm-moss: #3d7a58;     /* met / resolved / success                       */
  --dnm-slate: #5a6478;    /* muted metadata — AA at 12.5px on ecru          */
  /* Text steps for the two signals. The base tones are edge and glyph tones;
     neither passes AA as body text, so prose uses these instead. */
  --dnm-ochre-700: #8a5a0f;
  --dnm-moss-700: #2c5a41;

  /* — GROUND & RULES ------------------------------------------------------ */
  --dnm-ecru: #f4f1ea;     /* the page. Never #fff: glare under fluorescent  */
  --dnm-surface: #fffdf8;  /* data surfaces and inputs                       */
  --dnm-warp: #e4ddcb;     /* L2 — column rules, field boundaries, edges     */
  --dnm-weft: #efebe1;     /* L3 — row separators. Present, never counted    */
  --dnm-band: #faf7f0;     /* L3b — every fifth row. 1.4% luminance step     */
  --dnm-rule-1: 2px solid var(--dnm-ink-900);
  --dnm-rule-2: 1px solid var(--dnm-warp);
  --dnm-rule-3: 1px solid var(--dnm-weft);
  /* One border colour for every editable boundary, and its hover step. Named
     separately from --dnm-warp because they are the same weight but a
     different job: warp divides, this one contains. */
  --dnm-field-line: #ddd6c6;
  --dnm-field-line-hover: #b3aa96;
  --dnm-field-line-off: #d5cebd;   /* dashed = system-owned, not merely off  */
  --dnm-body-ink: #1c2438;         /* 13.9:1 on surface                      */
  --dnm-prose-ink: #2b3348;        /* long-form secondary                    */
  --dnm-off-ink: #a9a293;          /* disabled label and value               */
  --dnm-placeholder: #9b9484;
  --dnm-row-hover: #f6f2ea;        /* ecru wash. No lift, no border change   */
  --dnm-guide: #c9c2b1;            /* dotted leaders, tree guides            */
  /* Rules drawn *on* ink-900 chrome: warp and column-group weights again,
     expressed as light-on-dark so the header band reads as the same fabric. */
  --dnm-warp-on-ink: rgba(231, 236, 247, .14);
  --dnm-group-on-ink: rgba(231, 236, 247, .3);
  --dnm-group-rule: rgba(15, 23, 47, .16); /* 2px column-group rule on ecru  */
  --dnm-header-ink: #e2e8f6;

  /* — TYPE ---------------------------------------------------------------- */
  /* Plex Sans has no Arabic. Plex Sans Arabic is listed directly after it in
     every stack rather than left to the browser, because the substitute the
     browser picks (Noto) sits on a different vertical rhythm and a mixed EN/AR
     queue then has two baselines and 34px rows that no longer hold. */
  --dnm-font-display: 'Archivo', 'IBM Plex Sans Arabic', system-ui, sans-serif;
  --dnm-font-body: 'IBM Plex Sans', 'IBM Plex Sans Arabic', system-ui, sans-serif;
  --dnm-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
  --dnm-font-arabic: 'IBM Plex Sans Arabic', 'IBM Plex Sans', sans-serif;

  --dnm-t-44: 44px; --dnm-t-30: 30px; --dnm-t-22: 22px; --dnm-t-18: 18px;
  --dnm-t-15: 15px; --dnm-t-13: 13px; --dnm-t-cell: 12.5px;
  --dnm-t-mono: 12px; --dnm-t-label: 11px; --dnm-t-micro: 10px;
  --dnm-lh-tight: 1.15; --dnm-lh-cell: 1.25; --dnm-lh-prose: 1.6;

  /* Applied through variables rather than repeated, so :lang(ar) can switch
     both off in one place: Arabic has no case, and tracking breaks the joins. */
  --dnm-track-label: .09em;
  --dnm-case-label: uppercase;

  /* — SPACE: 4px base. --space-1 is for optical nudges only. --------------- */
  --dnm-space-1: 2px;  --dnm-space-2: 4px;  --dnm-space-3: 6px;
  --dnm-space-4: 8px;  --dnm-space-5: 12px; --dnm-space-6: 16px;
  --dnm-space-7: 24px; --dnm-space-8: 32px; --dnm-space-9: 48px;

  /* — DENSITY -------------------------------------------------------------
     The delivered token block sets 34/30/168 in :root and the density table
     calls Compact the default. Those disagree. Resolved in favour of the token
     block — Standard is the document default — because the app's admin and
     portal forms are the Standard case, and the one screen that must hold 25
     rows at 768px (the agent queue) reaches compact through .tbl-compact,
     which the queue markup already carries. [data-density] on <body> overrides
     all of it, which is how the design intends density to be a user setting. */
  --dnm-row-h: 34px;
  --dnm-field-h: 30px;
  --dnm-label-col: 168px;
  --dnm-cell-pad: 9px;        /* inline padding inside a grid cell          */
  --dnm-head-h: 31px;         /* the ink beam the fabric runs under         */

  /* — RADII: structure is square so warp and weft can cross cleanly. A 6px
     corner cannot meet a 1px rule; the gap shows at every intersection. --- */
  --dnm-radius-0: 0;
  --dnm-radius-1: 2px;        /* controls only                              */
  --dnm-radius-2: 3px;        /* the maximum in the system                  */

  /* — ELEVATION: two. Data surfaces never lift. --------------------------- */
  --dnm-shadow-menu: 0 6px 20px rgba(15, 23, 47, .16);
  --dnm-shadow-modal: 0 24px 64px rgba(15, 23, 47, .28);

  /* — MOTION: short enough to read as mechanical, not animated. ------------ */
  --dnm-t-fast: 90ms;
  --dnm-t-base: 140ms;
  --dnm-ease: cubic-bezier(.2, 0, .2, 1);

  /* — THE LEADING-EDGE THREAD ---------------------------------------------
     Selected rows, private notes, toasts, callouts and error field rows all
     carry a 3px thread on their leading edge. Where the element is a table row
     or must not take part in layout it has to be an inset box-shadow, and CSS
     has no logical box-shadow. Rather than write a [dir="rtl"] override per
     component, the offset itself is a token: one rule at the bottom of this
     file negates it and every thread in the system mirrors at once.

     Two tokens and not one, because the same 3px is also used as a *width* —
     the selvedge nav strip, the toast's border-inline-start. A negative width
     is meaningless, so --dnm-edge stays positive and only the offset flips. */
  --dnm-edge: 3px;    /* the thread's width. Never flips.                    */
  --dnm-edge-x: 3px;  /* the thread's inset-shadow offset. Flips in RTL.     */
  --dnm-edge-2: 2px;  /* focus ring and column-group weight. Never flips.    */

  /* ── Legacy aliases ─────────────────────────────────────────────────────
     Roughly twenty views hard-code var(--ink), var(--surface), var(--r) and
     friends in inline styles. Those views are ported in a later pass, so the
     old names stay and point at the new values. Deleting one here breaks a
     screen silently — the inline style just computes to nothing. */
  --indigo-950: var(--dnm-ink-900);
  --indigo-900: var(--dnm-ink-800);
  --indigo-800: var(--dnm-ink-700);
  --indigo-700: var(--dnm-ink-700);
  --indigo-600: var(--dnm-ink-600);
  --indigo-500: var(--dnm-ink-500);
  --indigo-400: var(--dnm-ink-400);
  --indigo-200: var(--dnm-ink-200);
  --indigo-100: var(--dnm-ink-100);
  --indigo-050: var(--dnm-ink-100);
  --selvedge: var(--dnm-selv);
  --selvedge-dark: var(--dnm-selv-700);
  --selvedge-soft: var(--dnm-selv-100);
  --ochre: var(--dnm-ochre);
  --ochre-soft: #fdf4e4;
  --moss: var(--dnm-moss);
  --moss-soft: #eaf3ee;
  --slate: var(--dnm-slate);
  --canvas: var(--dnm-ecru);
  --canvas-deep: #e9e4d9;
  --surface: var(--dnm-surface);
  --surface-alt: var(--dnm-band);
  --ink: var(--dnm-body-ink);
  --ink-2: var(--dnm-prose-ink);
  --ink-3: var(--dnm-slate);
  --ink-4: var(--dnm-off-ink);
  --rule: var(--dnm-warp);
  --rule-soft: var(--dnm-weft);
  --rule-strong: var(--dnm-field-line);
  --r-sm: var(--dnm-radius-1);
  --r: var(--dnm-radius-1);
  --r-lg: var(--dnm-radius-1);
  --shadow-1: none;
  --shadow-2: none;
  --shadow-3: var(--dnm-shadow-menu);
  --shadow-pop: var(--dnm-shadow-modal);
  --f-display: var(--dnm-font-display);
  --f-body: var(--dnm-font-body);
  --f-mono: var(--dnm-font-mono);
  --ease: var(--dnm-ease);

  --sidebar-w: 232px;
  --topbar-h: 40px;
}

/* Density changes four measures and nothing else. Type size is never a density
   control — shrinking type is not density, it is a legibility bug. */
[data-density="compact"] {
  --dnm-row-h: 26px; --dnm-field-h: 26px; --dnm-label-col: 150px;
  --dnm-cell-pad: 8px; --dnm-head-h: 27px;
}
[data-density="standard"] {
  --dnm-row-h: 34px; --dnm-field-h: 30px; --dnm-label-col: 168px;
  --dnm-cell-pad: 9px; --dnm-head-h: 31px;
}
[data-density="comfortable"] {
  --dnm-row-h: 40px; --dnm-field-h: 36px; --dnm-label-col: 190px;
  --dnm-cell-pad: 12px; --dnm-head-h: 36px;
}

/* Arabic is a face swap and a tracking reset, not a fallback. Archivo has no
   Arabic at all, so display type has to change face outright; Plex Arabic
   shares Plex Sans's x-height and metrics, so the baseline grid survives. */
:lang(ar) {
  --dnm-font-display: var(--dnm-font-arabic);
  --dnm-font-body: var(--dnm-font-arabic);
  --dnm-track-label: 0;
  --dnm-case-label: none;
  /* Declared, not merely tokenised. font-family resolves once on the element
     that declares it — <body> — so redefining the variable further down the
     tree changes nothing for text that inherited its face from above. An
     Arabic block inside an English page needs the declaration too. */
  font-family: var(--dnm-font-arabic);
}

@media (prefers-reduced-motion: reduce) {
  :root { --dnm-t-fast: 0ms; --dnm-t-base: 0ms; }
}

/* ═══════════════════════════════════════════════════════════════ RESET ════ */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--dnm-font-body);
  font-size: var(--dnm-t-cell);
  line-height: 1.5;
  color: var(--dnm-body-ink);
  background: var(--dnm-ecru);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, h4, h5 {
  font-family: var(--dnm-font-display);
  font-weight: 700;
  letter-spacing: -.02em;
  margin: 0;
  color: var(--dnm-ink-900);
  text-transform: var(--dnm-case-label);
}
h1 { font-size: var(--dnm-t-22); line-height: var(--dnm-lh-tight); }
h2 { font-size: var(--dnm-t-18); line-height: var(--dnm-lh-tight); }
h3 { font-size: var(--dnm-t-15); line-height: 1.25; }
/* h4 is the section-header object shared by rails, cards and form sections:
   Archivo, tracked, uppercase, small. Same object as a column header, one
   weight down. */
h4 {
  font-size: var(--dnm-t-label); line-height: 1.35;
  letter-spacing: var(--dnm-track-label);
  color: var(--dnm-ink-900);
}
p { margin: 0; margin-block-end: var(--dnm-space-4); }
a { color: var(--dnm-ink-700); text-decoration: none; }
a:hover { color: var(--dnm-selv-700); text-decoration: underline; text-underline-offset: 2px; }
button, input, select, textarea { font: inherit; color: inherit; }
hr { border: 0; border-block-start: var(--dnm-rule-2); margin-block: var(--dnm-space-6); }
::selection { background: var(--dnm-ink-100); }

/* One focus treatment for the whole system: a 2px indigo ring outside the
   control plus, where the control has a border, a weight change on its bottom
   edge. The ring alone is not enough — it gets clipped by sticky headers and
   by overflow:hidden table wrappers, and then focus disappears entirely. */
:focus-visible {
  outline: var(--dnm-edge-2) solid var(--dnm-ink-500);
  outline-offset: 1px;
}

/* Scrollbars: thin, and in the fabric's colours rather than the platform's. */
* { scrollbar-width: thin; scrollbar-color: var(--dnm-field-line-hover) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--dnm-field-line-hover); border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--dnm-slate); background-clip: content-box; }

/* The shuttle: one red thread crossing the work. Used by every loading state,
   which is why it is declared once, up here, rather than per component. */
@keyframes dnm-shuttle { from { transform: translateX(-100%) } to { transform: translateX(300%) } }

/* ══════════════════════════════════════════════════════════ UTILITIES ════ */
.mono { font-family: var(--dnm-font-mono); font-variant-ligatures: none; font-variant-numeric: tabular-nums; }
.tnum { font-variant-numeric: tabular-nums; }
.muted { color: var(--dnm-slate); }
.dim { color: var(--dnm-off-ink); }
.tiny { font-size: var(--dnm-t-label); }
.sm { font-size: var(--dnm-t-mono); }
.strong { font-weight: 600; }
.center { text-align: center; }
.right { text-align: end; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hidden { display: none !important; }
.grow { flex: 1 1 auto; min-width: 0; }
.row { display: flex; align-items: center; gap: var(--dnm-space-4); }
.row-t { display: flex; gap: var(--dnm-space-4); }
.col { display: flex; flex-direction: column; gap: var(--dnm-space-4); }
.wrap { flex-wrap: wrap; }
.between { justify-content: space-between; }
.gap-4 { gap: 4px } .gap-6 { gap: 6px } .gap-8 { gap: 8px } .gap-10 { gap: 10px }
.gap-12 { gap: 12px } .gap-16 { gap: 16px } .gap-24 { gap: 24px }
.mt-4{margin-block-start:4px}.mt-8{margin-block-start:8px}.mt-12{margin-block-start:12px}
.mt-16{margin-block-start:16px}.mt-24{margin-block-start:24px}
.mb-4{margin-block-end:4px}.mb-8{margin-block-end:8px}.mb-12{margin-block-end:12px}
.mb-16{margin-block-end:16px}.mb-24{margin-block-end:24px}

/* The system's smallest label: mono, heavily tracked, uppercase. It marks a
   category rather than naming a thing, so it is mono and not Archivo. */
.eyebrow {
  font-family: var(--dnm-font-mono);
  font-size: var(--dnm-t-micro);
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
}

/* The woven texture image is retired. It was noise behind 12.5px type; the
   rule system replaced it and the five-row band replaced its function as a
   scanning aid. The class stays because the layout and nav still carry it —
   removing the name would break both — but it now draws nothing. */
.weave { background-image: none; }

/* ══════════════════════════════════════════════════════════════ SHELL ════ */
.app { display: flex; min-height: 100vh; }

/* ----- sidebar: the dye vat, with the selvedge on its edge ----- */
.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--dnm-ink-900);
  color: var(--dnm-ink-200);
  display: flex; flex-direction: column;
  position: sticky; inset-block-start: 0; height: 100vh;
  z-index: 40;
}
/* The literal selvedge: the woven edge of the cloth, dashed the way a real one
   is. This is the one place the accent is allowed to run at length, because it
   is an edge and nothing else. */
.sidebar::after {
  content: ''; position: absolute; inset-inline-end: 0; inset-block: 0; width: var(--dnm-edge);
  background: repeating-linear-gradient(180deg, var(--dnm-selv) 0 5px, transparent 5px 11px);
}
.brand {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-inline: var(--dnm-space-5);
  border-block-end: 1px solid var(--dnm-warp-on-ink);
}
.brand-mark {
  width: 26px; height: 26px; flex: none;
  background: var(--dnm-surface); color: var(--dnm-ink-900);
  display: grid; place-items: center;
  font-family: var(--dnm-font-display); font-weight: 800; font-size: var(--dnm-t-label);
  letter-spacing: -.03em;
}
.brand-name {
  font-family: var(--dnm-font-display); font-weight: 700; font-size: var(--dnm-t-13);
  color: var(--dnm-surface); letter-spacing: .02em; line-height: 1.1;
  text-transform: var(--dnm-case-label);
}
.brand-sub {
  font-family: var(--dnm-font-mono); font-size: 9.5px; letter-spacing: .16em;
  text-transform: var(--dnm-case-label); color: var(--dnm-ink-300);
}

.nav { flex: 1; overflow-y: auto; padding-block: var(--dnm-space-4) var(--dnm-space-6); }
.nav-group { margin-block-end: var(--dnm-space-1); }
.nav-label {
  font-family: var(--dnm-font-mono); font-size: 9.5px; font-weight: 600;
  letter-spacing: .16em; text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-300);
  padding-block: var(--dnm-space-5) var(--dnm-space-3);
  padding-inline: var(--dnm-space-5);
}
.nav-item {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  color: var(--dnm-ink-200); font-size: var(--dnm-t-cell); font-weight: 500;
  position: relative;
  transition: background var(--dnm-t-fast) var(--dnm-ease), color var(--dnm-t-fast) var(--dnm-ease);
}
.nav-item:hover { background: var(--dnm-ink-800); color: var(--dnm-surface); text-decoration: none; }
.nav-item.active {
  background: var(--dnm-ink-700); color: var(--dnm-surface); font-weight: 600;
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv);
}
.nav-item .ico { width: 15px; flex: 0 0 15px; opacity: .8; }
.nav-item.active .ico { opacity: 1; }
.nav-count {
  margin-inline-start: auto;
  font-family: var(--dnm-font-mono); font-size: 10.5px; font-weight: 500;
  color: var(--dnm-ink-300); font-variant-numeric: tabular-nums;
}
.nav-item.active .nav-count { color: var(--dnm-ink-200); }
/* A count that has crossed a threshold. Not a fill — the selvedge is an edge,
   so the number is boxed by it rather than painted with it. */
.nav-count.hot {
  color: var(--dnm-surface);
  border: 1px solid var(--dnm-selv);
  padding-inline: 4px;
}

.sidebar-foot { border-block-start: 1px solid var(--dnm-warp-on-ink); }
.user-chip {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  cursor: pointer; transition: background var(--dnm-t-fast) var(--dnm-ease);
}
.user-chip:hover { background: var(--dnm-ink-800); }
.user-chip .nm {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-mono); font-weight: 700;
  color: var(--dnm-surface); line-height: 1.25;
}
.user-chip .rl {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro);
  letter-spacing: .1em; text-transform: var(--dnm-case-label); color: var(--dnm-ink-300);
}

/* ----- main and the view bar ----- */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* The design's view bar is ink; this one is ecru closed by a 2px structural
   rule, which is the same object in the admin console. Ecru was chosen because
   the bar carries a search field, buttons and an icon button that are all
   styled for light ground — inverting them here would fork every control. */
.topbar {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  background: var(--dnm-ecru); border-block-end: var(--dnm-rule-1);
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-inline: var(--dnm-space-5);
  position: sticky; inset-block-start: 0; z-index: 30;
}
.search { position: relative; max-width: 420px; flex: 1; }
.search input {
  /* padding-inline, not the four-value shorthand: the leading 30px reserves
     room for the search glyph, which sits on the leading edge in both
     directions. As "0 10px 0 30px" the gap stayed left in Arabic and the icon
     landed on top of the placeholder. */
  width: 100%; height: 26px;
  padding-block: 0; padding-inline: 30px var(--dnm-space-4);
  background: var(--dnm-surface); border: 1px solid var(--dnm-field-line);
  border-radius: var(--dnm-radius-1); font-size: var(--dnm-t-mono);
  transition: border-color var(--dnm-t-base) var(--dnm-ease);
}
.search input:hover { border-color: var(--dnm-field-line-hover); }
.search input:focus {
  outline: var(--dnm-edge-2) solid rgba(59, 83, 144, .28); outline-offset: 1px;
  border-color: var(--dnm-ink-500);
  box-shadow: inset 0 -1px 0 var(--dnm-ink-500);
  caret-color: var(--dnm-selv);
}
.search .s-ico {
  position: absolute; inset-inline-start: var(--dnm-space-4); inset-block-start: 50%;
  transform: translateY(-50%); color: var(--dnm-slate); pointer-events: none;
}
.search kbd {
  position: absolute; inset-inline-end: var(--dnm-space-3); inset-block-start: 50%;
  transform: translateY(-50%);
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro); color: var(--dnm-slate);
  border: 1px solid var(--dnm-warp); padding-block: 0; padding-inline: 4px;
  background: var(--dnm-ecru);
}
.icon-btn {
  width: 26px; height: 26px; display: grid; place-items: center;
  border: 1px solid transparent; background: transparent;
  border-radius: var(--dnm-radius-1);
  color: var(--dnm-ink-700); cursor: pointer; position: relative;
  transition: background var(--dnm-t-fast) var(--dnm-ease), border-color var(--dnm-t-fast) var(--dnm-ease);
}
.icon-btn:hover { background: var(--dnm-ink-100); border-color: var(--dnm-warp); }
.dot-badge {
  position: absolute; inset-block-start: 3px; inset-inline-end: 3px;
  width: 6px; height: 6px; background: var(--dnm-selv);
  box-shadow: 0 0 0 1.5px var(--dnm-ecru);
}

/* ----- the page ----- */
.page { flex: 1; padding-block: var(--dnm-space-6) var(--dnm-space-9); padding-inline: var(--dnm-space-7); min-width: 0; }
.page.flush { padding: 0; display: flex; flex-direction: column; }
.page-head { display: flex; align-items: flex-start; gap: var(--dnm-space-6); margin-block-end: var(--dnm-space-6); }
.page-head .grow { min-width: 0; }
.page-head h1 { font-size: var(--dnm-t-22); }
.page-head .sub { color: var(--dnm-slate); font-size: var(--dnm-t-cell); margin-block-start: var(--dnm-space-1); }

.crumbs {
  display: flex; align-items: center; gap: var(--dnm-space-3);
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label);
  color: var(--dnm-slate); margin-block-end: var(--dnm-space-3);
}
.crumbs a { color: var(--dnm-slate); }
.crumbs a:hover { color: var(--dnm-ink-700); }
.crumbs .sep { color: var(--dnm-guide); }

/* ══════════════════════════════════════════════════════════════ ATOMS ════ */
/* Buttons are Archivo, tracked, uppercase, flush-start. Height is fixed and
   width never is; the design specifies a 120px minimum on the primary action
   of a form, which is deliberately *not* applied globally here because the
   same class carries one-word toolbar controls ("Filter", "Clear") and a
   120px floor would double the width of the queue filter strip. Views that
   want the floor can set it locally. */
.btn {
  display: inline-flex; align-items: center; justify-content: flex-start; gap: var(--dnm-space-3);
  height: var(--dnm-field-h); padding-inline: 11px;
  border: 1px solid var(--dnm-ink-700); border-radius: var(--dnm-radius-1);
  background: transparent; color: var(--dnm-ink-700);
  font-family: var(--dnm-font-display); font-size: 11.5px; font-weight: 700;
  letter-spacing: .06em; text-transform: var(--dnm-case-label); text-align: start;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dnm-t-fast) var(--dnm-ease), border-color var(--dnm-t-fast) var(--dnm-ease), color var(--dnm-t-fast) var(--dnm-ease);
}
.btn:hover { background: var(--dnm-ink-100); color: var(--dnm-ink-900); text-decoration: none; }
.btn:focus-visible { outline: var(--dnm-edge-2) solid var(--dnm-ink-500); outline-offset: 1px; }
.btn-primary { background: var(--dnm-ink-900); border-color: var(--dnm-ink-900); color: var(--dnm-surface); }
/* #0a1226 is one step past ink-900 and exists only as a pressed/hover state.
   It is not a token because nothing else in the system may use it. */
.btn-primary:hover { background: #0a1226; border-color: #0a1226; color: var(--dnm-surface); }
/* Destructive is one of the four places selvedge is allowed. */
.btn-danger { background: var(--dnm-selv); border-color: var(--dnm-selv); color: #fff; }
.btn-danger:hover { background: var(--dnm-selv-700); border-color: var(--dnm-selv-700); color: #fff; }
.btn-ghost { border-color: transparent; color: var(--dnm-ink-700); }
.btn-ghost:hover { background: var(--dnm-ink-100); border-color: transparent; }
.btn-sm { height: 26px; padding-inline: var(--dnm-space-4); font-size: 10.5px; }
.btn-lg { height: 36px; padding-inline: var(--dnm-space-6); font-size: var(--dnm-t-mono); }
.btn-block { width: 100%; justify-content: center; }
.btn:disabled, .btn[disabled] {
  background: var(--dnm-ecru); border-color: var(--dnm-field-line);
  color: var(--dnm-off-ink); cursor: not-allowed; pointer-events: none;
}

/* Segmented control. Buttons butt against each other on a shared 1px line —
   no rounding on the inner edges, because a rounded inner edge cannot meet
   the next button's rule. */
.btn-group { display: inline-flex; }
.btn-group .btn { border-radius: 0; margin-inline-start: -1px; }
.btn-group .btn:first-child { margin-inline-start: 0; }
.btn-group .btn.on {
  background: var(--dnm-ink-900); border-color: var(--dnm-ink-900);
  color: var(--dnm-surface); z-index: 1;
}

/* Filter chip: a mono statement of an applied filter, not a button. Square,
   ink-100 ground, no radius. */
.chip {
  display: inline-flex; align-items: center; gap: var(--dnm-space-2);
  height: 22px; padding-inline: var(--dnm-space-3);
  border: 1px solid transparent; background: var(--dnm-ink-100);
  border-radius: var(--dnm-radius-0);
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label); font-weight: 500;
  color: var(--dnm-ink-800); cursor: pointer;
  transition: background var(--dnm-t-fast) var(--dnm-ease), border-color var(--dnm-t-fast) var(--dnm-ease);
}
.chip:hover { border-color: var(--dnm-ink-400); }
.chip.on { background: var(--dnm-ink-900); color: var(--dnm-surface); border-color: var(--dnm-ink-900); }
.chip .n { color: var(--dnm-slate); font-variant-numeric: tabular-nums; }
.chip.on .n { color: var(--dnm-ink-300); }

.tag {
  display: inline-flex; align-items: center; gap: var(--dnm-space-2);
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label);
  padding-block: 1px; padding-inline: var(--dnm-space-3);
  background: transparent; color: var(--dnm-ink-700); border: 1px solid var(--dnm-warp);
  border-radius: var(--dnm-radius-0);
}
.tag .x { cursor: pointer; color: var(--dnm-slate); line-height: 1; }
.tag .x:hover { color: var(--dnm-selv-700); }

/* References, IDs, clock readouts. Mono with tabular figures so that a column
   of them forms its own vertical warp. Always LTR in content, which the views
   assert with dir="ltr" on the cell. */
.ref {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-mono); font-weight: 500;
  color: var(--dnm-ink-700); font-variant-numeric: tabular-nums;
}
a.ref:hover { color: var(--dnm-selv-700); text-decoration: none; }

/* ----- avatar: square, mono, initials only ----- */
/* Round avatars were the last soft corner in the system. Square keeps the
   32px grid and lets a stack sit on the same warp as everything else. */
.av {
  width: 26px; height: 26px; flex: 0 0 26px;
  border-radius: var(--dnm-radius-0);
  display: grid; place-items: center;
  font-family: var(--dnm-font-mono); font-size: 10.5px; font-weight: 700;
  color: #fff; letter-spacing: .02em; user-select: none;
}
.av-xs { width: 20px; height: 20px; flex-basis: 20px; font-size: 9px; }
.av-sm { width: 24px; height: 24px; flex-basis: 24px; font-size: 9.5px; }
.av-lg { width: 40px; height: 40px; flex-basis: 40px; font-size: var(--dnm-t-15); }
/* Eight identity slots, taken only from the system palette. Selvedge is
   deliberately absent: an avatar is not an edge, and one person out of eight
   would otherwise appear to be permanently in breach. */
.av-c0 { background: var(--dnm-ink-900) } .av-c1 { background: var(--dnm-ink-700) }
.av-c2 { background: var(--dnm-ink-600) } .av-c3 { background: var(--dnm-ink-500) }
.av-c4 { background: var(--dnm-slate)   } .av-c5 { background: var(--dnm-moss)    }
.av-c6 { background: var(--dnm-ochre)   } .av-c7 { background: var(--dnm-ink-400) }
.av-stack { display: flex; }
.av-stack .av { margin-inline-start: -6px; box-shadow: 0 0 0 1px var(--dnm-surface); }
.av-stack .av:first-child { margin-inline-start: 0; }

/* ══════════════════════════════════════════════════════════ THE FIELD ════
   Loom Sheet. The label band *is* a warp column: every label in a form ends on
   the same vertical rule, which is the same measure as the queue's first
   column rule, so a form opened beside a queue lines up.

   The markup is <div class=field><label class=label>…</label><control>…, and
   a hint or an error may follow the control. So the label is placed in column
   one and everything else is forced into column two, where auto-placement
   stacks it. The warp rule is drawn by ::before rather than as a border on the
   label, because the label occupies only the first row and the rule has to run
   the full height of the field. inset-inline-start makes it mirror for free.  */
.field {
  display: grid;
  grid-template-columns: var(--dnm-label-col) minmax(0, 1fr);
  column-gap: 11px;
  row-gap: var(--dnm-space-2);
  align-items: start;
  position: relative;
  padding-block: 5px;
  border-block-end: var(--dnm-rule-3);
}
.field::before {
  content: ''; position: absolute; inset-block: 0;
  inset-inline-start: var(--dnm-label-col); width: 1px;
  background: var(--dnm-warp);
}
.field > .label { grid-column: 1; }
.field > *:not(.label) { grid-column: 2; }
/* A field with no label of its own — a bare checkbox row, a button row — has
   nothing to put in the label column, and an empty warp column reads as a
   missing label rather than as an absent one. It collapses instead. */
.field:not(:has(> .label)) { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
.field:not(:has(> .label))::before { display: none; }
.field:not(:has(> .label)) > * { grid-column: 1; }

.label {
  display: block;
  font-family: var(--dnm-font-display); font-size: 10.5px; font-weight: 600;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
  padding-block: var(--dnm-space-3);
  padding-inline-end: var(--dnm-space-5);
  line-height: 1.35;
}
/* Required is a 2px selvedge tick before the label, not an asterisk after it:
   at 10.5px uppercase an asterisk reads as a footnote marker. The views emit
   a <span class="req">*</span>, so the glyph is replaced, not the markup. */
.label .req {
  display: inline-block; width: 2px; height: 9px;
  background: var(--dnm-selv);
  margin-inline-end: var(--dnm-space-3);
  vertical-align: -1px;
  overflow: hidden; text-indent: -99em; /* hides the asterisk the view writes */
}
.hint { font-size: 11.5px; color: var(--dnm-slate); line-height: 1.5; }
.err { font-size: 11.5px; color: var(--dnm-selv-700); font-weight: 500; line-height: 1.5; }
/* Validation carries a glyph as well as a colour, so the state survives
   greyscale printing and a badly calibrated plant monitor. */
.err::before { content: "\25B2\00a0"; }

.input, .select, .textarea {
  width: 100%;
  padding-block: 0; padding-inline: var(--dnm-space-4);
  background: var(--dnm-surface); border: 1px solid var(--dnm-field-line);
  border-radius: var(--dnm-radius-1);
  font-family: inherit; font-size: var(--dnm-t-13); color: var(--dnm-body-ink);
  transition: border-color var(--dnm-t-base) var(--dnm-ease);
}
.input, .select { height: var(--dnm-field-h); }
.select {
  appearance: none; padding-inline-end: 26px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' viewBox='0 0 24 24' fill='none' stroke='%235a6478' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}
.textarea { min-height: 88px; resize: vertical; padding-block: var(--dnm-space-3); line-height: 1.55; }
.input:hover, .select:hover, .textarea:hover { border-color: var(--dnm-field-line-hover); }
/* Focus: the indigo thread along the bottom edge plus a soft halo, and a red
   caret. The thread is an inset shadow rather than border-bottom-width so that
   gaining focus does not move the text by a pixel. */
.input:focus, .select:focus, .textarea:focus,
.input:focus-visible, .select:focus-visible, .textarea:focus-visible {
  outline: var(--dnm-edge-2) solid rgba(59, 83, 144, .28); outline-offset: 1px;
  border-color: var(--dnm-ink-500);
  box-shadow: inset 0 -1px 0 var(--dnm-ink-500);
  caret-color: var(--dnm-selv);
}
.input.invalid, .select.invalid, .textarea.invalid {
  border-color: var(--dnm-selv); background: var(--dnm-selv-100); color: #5c1c15;
}
.input::placeholder, .textarea::placeholder { color: var(--dnm-placeholder); }
/* Dashed, not greyed. Dashed says the system owns this value, which is the
   actual reason nearly every disabled field in this app is disabled. */
.input:disabled, .select:disabled, .textarea:disabled {
  border-style: dashed; border-color: var(--dnm-field-line-off);
  background: var(--dnm-ecru); color: var(--dnm-off-ink); cursor: not-allowed;
}

/* A field row in error takes the leading-edge thread, so the whole row —
   label included — is marked, not just the control. */
.field:has(.invalid) { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.field:has(.invalid) > .label { color: var(--dnm-selv-700); }

/* Two- and three-up field grids. Inside them the label warp is shortened
   rather than dropped: at 1366px a half-width column with a 168px label leaves
   under 180px of field, which is not an input. Each column stays internally
   consistent, which is what the eye actually tracks. */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--dnm-space-6); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 var(--dnm-space-6); }
.grid-2 .field, .grid-3 .field { --dnm-label-col: 110px; }
@media (max-width: 820px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
/* Below tablet width the warp cannot survive; the field stacks and the rule
   goes with it rather than leaving a rule with nothing on either side. */
@media (max-width: 900px) {
  .field { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
  .field::before { display: none; }
  .field > *:not(.label) { grid-column: 1; }
  .label { padding-block-end: var(--dnm-space-1); }
}

.check { display: flex; align-items: flex-start; gap: var(--dnm-space-4); cursor: pointer; padding-block: var(--dnm-space-1); }
.check input { margin: 0; margin-block-start: 2px; accent-color: var(--dnm-ink-700); width: 13px; height: 13px; cursor: pointer; }
.check span { font-size: var(--dnm-t-cell); }

/* No switch appears in the delivered design; this app has thirty of them.
   Kept, squared to 2px and moved onto the indigo ramp so it belongs here. */
.switch { position: relative; display: inline-block; width: 32px; height: 18px; flex: 0 0 32px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .track {
  position: absolute; inset: 0; background: var(--dnm-field-line);
  border-radius: var(--dnm-radius-1); cursor: pointer;
  transition: background var(--dnm-t-base) var(--dnm-ease);
}
.switch .track::before {
  content: ''; position: absolute; inset-inline-start: 2px; inset-block-start: 2px;
  width: 14px; height: 14px; background: var(--dnm-surface);
  transition: transform var(--dnm-t-base) var(--dnm-ease);
}
.switch input:checked + .track { background: var(--dnm-ink-700); }
.switch input:checked + .track::before { transform: translateX(14px); }
.switch input:focus-visible + .track { outline: var(--dnm-edge-2) solid var(--dnm-ink-500); outline-offset: 1px; }

/* Radio cards — the priority and impact pickers. Square, ruled, no lift. */
.opt-cards { display: grid; gap: 0; border: var(--dnm-rule-2); }
.opt-card {
  display: flex; align-items: flex-start; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  border-block-end: var(--dnm-rule-3); background: var(--dnm-surface);
  cursor: pointer; position: relative;
  transition: background var(--dnm-t-base) var(--dnm-ease);
}
.opt-cards > .opt-card:last-child { border-block-end: 0; }
.opt-card:hover { background: var(--dnm-row-hover); }
.opt-card input { margin: 0; margin-block-start: 2px; accent-color: var(--dnm-ink-700); }
.opt-card.sel { background: var(--dnm-ink-100); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.opt-card .t {
  font-family: var(--dnm-font-display); font-size: 11.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: var(--dnm-case-label);
}
.opt-card .d { font-size: var(--dnm-t-label); color: var(--dnm-slate); line-height: 1.4; }

/* ═════════════════════════════════════════════════════════════ PANELS ════
   There are no cards. A .card is now a ruled sheet: 1px warp on the outside,
   a section band across the top closed by the 2px structural rule, and no
   radius and no shadow at all. The class name stays because fifty views use
   it; what it means has changed. */
.card {
  background: var(--dnm-surface);
  border: var(--dnm-rule-2);
  border-radius: var(--dnm-radius-0);
}
.card-head {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  background: var(--dnm-ecru);
  border-block-end: var(--dnm-rule-1);
}
/* A card heading is the same object as a form section header and a column
   header: Archivo, small, tracked, uppercase. Three weights of one thing. */
.card-head h3 {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-label); font-weight: 700;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-900);
}
.card-body { padding-block: var(--dnm-space-5); padding-inline: var(--dnm-space-5); }
.card-foot {
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  border-block-start: var(--dnm-rule-1); background: var(--dnm-ecru);
}
.card.flush .card-body { padding: 0; }

/* Callouts. The 3px leading thread is the same device as a selected row and a
   private note — one treatment, four meanings, told apart by the thread's
   colour *and* by the glyph the view already puts inside them. */
.note {
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  font-size: var(--dnm-t-cell); line-height: 1.55;
  border: var(--dnm-rule-2); background: var(--dnm-surface);
  color: var(--dnm-prose-ink);
  display: flex; gap: var(--dnm-space-4); align-items: flex-start;
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-slate);
}
.note-info { background: var(--dnm-ink-100); border-color: var(--dnm-ink-200); color: var(--dnm-ink-800); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ink-500); }
.note-warn { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ochre); }
.note-danger { background: var(--dnm-selv-100); border-color: var(--dnm-selv); color: var(--dnm-selv-700); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.note-ok { background: var(--moss-soft); border-color: #a9cbb9; color: var(--dnm-moss-700); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-moss); }

/* Deflection: the knowledge-base suggestion that appears while a request is
   being typed. Indigo thread, no animation beyond a single fade — it arrives
   under the user's hands and must not move the field they are typing into. */
.deflect {
  border: var(--dnm-rule-2); background: var(--dnm-surface);
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ink-500);
  animation: dnm-fade var(--dnm-t-base) var(--dnm-ease);
}
@keyframes dnm-fade { from { opacity: 0 } }

.kb-card {
  display: block;
  padding-block: var(--dnm-space-4) var(--dnm-space-5);
  padding-inline: var(--dnm-space-5);
  border-block-end: var(--dnm-rule-3);
  background: var(--dnm-surface);
  transition: background var(--dnm-t-base) var(--dnm-ease);
}
.kb-card:hover { background: var(--dnm-row-hover); text-decoration: none; box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ink-400); }
.kb-card h3 { font-size: 14.5px; margin-block-end: var(--dnm-space-2); text-transform: none; }
.kb-card p { font-size: var(--dnm-t-cell); color: var(--dnm-prose-ink); margin: 0; line-height: 1.55; }

/* Email preview. #fff here and nowhere else: this is a rendering of what the
   recipient's mail client will show, and their client is white. */
.envelope { border: var(--dnm-rule-1); background: #fff; }
.envelope-head {
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  background: var(--dnm-ecru); border-block-end: var(--dnm-rule-2);
  font-size: var(--dnm-t-mono);
}
.envelope-head .hrow { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: var(--dnm-space-4); padding-block: 1.5px; }
.envelope-head .hk {
  font-family: var(--dnm-font-mono); font-size: 9px; letter-spacing: .14em;
  text-transform: var(--dnm-case-label); color: var(--dnm-slate);
}
.envelope-body { padding: 0; }

/* Empty states name the filters that caused the emptiness. No illustration:
   the fix is a button, not a mood. The 34px ink rule is the only ornament. */
.empty {
  text-align: start; padding-block: var(--dnm-space-7);
  padding-inline: var(--dnm-space-6); color: var(--dnm-slate);
  background: var(--dnm-surface);
}
.empty .ico { display: none; }
.empty h3 { color: var(--dnm-ink-900); margin-block-end: var(--dnm-space-2); font-size: 14px; }
.empty h3::before {
  content: ''; display: block; width: 34px; height: 2px;
  background: var(--dnm-ink-900); margin-block-end: var(--dnm-space-5);
}
.empty p { font-size: var(--dnm-t-cell); max-width: 60ch; margin: 0; }

/* ════════════════════════════════════════════════ STATUS AND PRIORITY ════
   No state is signalled by colour alone. Every status and every priority
   carries a mono glyph and a word, so the encoding survives greyscale
   printing, colour-blindness and a plant monitor nobody has ever calibrated.

   Status views already emit <i class="pip"></i>, so the glyph is hung off
   that. Priority and SLA badges have no such element, so those use ::before.
   The asymmetry is markup, not intent; the view port can regularise it. */
.badge {
  display: inline-flex; align-items: center; gap: var(--dnm-space-2);
  height: 20px; padding-inline: var(--dnm-space-3);
  border-radius: var(--dnm-radius-0);
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-micro); font-weight: 600;
  letter-spacing: .06em; text-transform: var(--dnm-case-label);
  line-height: 1; white-space: nowrap;
  border: 1px solid var(--dnm-warp); background: var(--dnm-ecru); color: var(--dnm-ink-800);
}
/* The glyph carriers are given an explicit line-height and box. None of the
   state glyphs exist in Plex Mono, so the browser substitutes a font for them,
   and some substitutes have line metrics half again as tall as Plex - enough
   to push a 26px queue row to 32px and lose five rows off the bottom of the
   screen. An explicit line-height pins the inline box to the glyph size and
   makes the row height a decision rather than an accident. */
.badge { vertical-align: middle; }
.badge .pip {
  width: auto; height: auto; background: none; border-radius: 0; flex: none;
  display: inline-block;
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label); font-style: normal;
  line-height: 1;
}
.badge-sq { border-radius: var(--dnm-radius-0); }

/* status — glyph set from the design: open ◇, active ▶, hold ‖, approval ⊙,
   resolved ✓, not-actionable ⊘. Closed and cancelled are not in the delivered
   set; ⊘ (the "merged / not actionable" glyph) is reused for closed and ✕ for
   cancelled, on the grounds that both mean the same thing to an agent. */
.st-new       { background: var(--dnm-ink-100); border-color: var(--dnm-ink-200); color: var(--dnm-ink-800); }
.st-new .pip::before      { content: "\25C7"; }
.st-open      { background: var(--dnm-ink-500); border-color: var(--dnm-ink-500); color: #fff; }
.st-open .pip::before     { content: "\25B6"; }
.st-pending   { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); }
.st-pending .pip::before  { content: "\2299"; }
.st-hold      { background: var(--dnm-ecru); border-color: #ccc5b4; color: var(--dnm-slate); }
.st-hold .pip::before     { content: "\2016"; }
.st-resolved  { background: var(--moss-soft); border-color: #a9cbb9; color: var(--dnm-moss-700); }
.st-resolved .pip::before { content: "\2713"; }
.st-closed    { background: var(--dnm-ecru); border-color: var(--dnm-warp); color: var(--dnm-slate); }
.st-closed .pip::before   { content: "\2298"; }
.st-cancelled { background: var(--dnm-ecru); border-color: var(--dnm-warp); color: var(--dnm-off-ink); }
.st-cancelled .pip::before{ content: "\2715"; }

/* priority — the four-block gauge. Filled blocks are the count, so P1 reads
   at a glance from across a queue without reading the label at all. The gauge
   is mono, tightly tracked, and only P1 takes selvedge. */
.pr-p1, .pr-p2, .pr-p3, .pr-p4 {
  background: transparent; border-color: transparent;
  color: var(--dnm-ink-700); padding-inline: 0; gap: var(--dnm-space-2);
  font-family: var(--dnm-font-mono);
}
.pr-p1::before, .pr-p2::before, .pr-p3::before, .pr-p4::before {
  display: inline-block; line-height: 1;
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label);
  font-weight: 500; letter-spacing: -.04em;
  margin-inline-end: var(--dnm-space-2);
}
.pr-p1::before { content: "\25B0\25B0\25B0\25B0"; color: var(--dnm-selv-700); }
.pr-p2::before { content: "\25B0\25B0\25B0\25B1"; color: var(--dnm-ink-600); }
.pr-p3::before { content: "\25B0\25B0\25B1\25B1"; color: var(--dnm-ink-600); }
.pr-p4::before { content: "\25B0\25B1\25B1\25B1"; color: var(--dnm-ink-600); }

.badge-sla-ok     { background: var(--moss-soft); border-color: #a9cbb9; color: var(--dnm-moss-700); }
.badge-sla-ok::before     { content: "\2713\00a0"; font-family: var(--dnm-font-mono); display: inline-block; line-height: 1; }
.badge-sla-warn   { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); }
.badge-sla-warn::before   { content: "\25ED\00a0"; font-family: var(--dnm-font-mono); display: inline-block; line-height: 1; }
/* Breach is an edge, so it is drawn as one: selvedge border and selvedge
   text on the error ground, never a red fill behind white type. */
.badge-sla-breach { background: var(--dnm-selv-100); border-color: var(--dnm-selv); color: var(--dnm-selv-700); }
.badge-sla-breach::before { content: "\25B2\00a0"; font-family: var(--dnm-font-mono); display: inline-block; line-height: 1; }
.badge-sla-paused { background: var(--dnm-ecru); border-color: #ccc5b4; color: var(--dnm-slate); }
.badge-sla-paused::before { content: "\2016\00a0"; font-family: var(--dnm-font-mono); display: inline-block; line-height: 1; }
.badge-neutral    { background: var(--dnm-ecru); border-color: var(--dnm-warp); color: var(--dnm-slate); }
.badge-indigo     { background: var(--dnm-ink-100); border-color: var(--dnm-ink-200); color: var(--dnm-ink-800); }

/* Waiting on a PERSON, not on the clock: unassigned, awaiting a decision, a required
   field with no value yet.

   It exists because the queue was using .badge-sla-warn for "Unassigned". Same ochre, so
   it looked right - but the glyph on that class is the SLA at-risk mark, so every
   unassigned ticket in the grid reported an SLA problem it did not have. That is exactly
   the failure the glyph system was added to prevent: a state carried by colour alone is
   ambiguous, and two unrelated states sharing one glyph is worse than either. The open
   circle reads as an empty slot rather than as a warning. */
.badge-attention  { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); }
.badge-attention::before  { content: "\25CB\00a0"; font-family: var(--dnm-font-mono); display: inline-block; line-height: 1; }

/* NOT VISIBLE TO EVERYONE: an internal note, a staff-only article. Third meaning that had
   been loaded onto .badge-sla-warn, and the most consequential of the three - a note the
   requester must never see was marked with the same badge as a ticket about to breach.

   Deliberately no ::before glyph. Every one of these already renders a lock icon, and two
   glyphs on one badge is a badge nobody reads. The lock IS the colour-independent mark,
   which is the rule the glyph system is enforcing. */
.badge-restricted { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); }

/* ══════════════════════════════════════════════════════════ LOOM GRID ════
   Every column carries a 1px warp rule; rows are separated by the weft rule
   and every fifth row carries the band tint. The header is an ink beam that
   the fabric runs under, not a shaded row.

   border-collapse stays "separate" with zero spacing. Collapsed borders would
   be cheaper, but a collapsed table cannot render the inset box-shadow that
   draws the selection and priority edge, and cannot hold a sticky header with
   a visible bottom rule.                                                     */
.tbl-wrap { overflow-x: auto; background: var(--dnm-surface); border: var(--dnm-rule-2); }
table.tbl {
  width: 100%; border-collapse: separate; border-spacing: 0;
  font-size: var(--dnm-t-cell);
}
table.tbl thead th {
  position: sticky; inset-block-start: 0; z-index: 2;
  height: var(--dnm-head-h);
  background: var(--dnm-ink-900); color: var(--dnm-header-ink);
  text-align: start;
  font-family: var(--dnm-font-display); font-weight: 600; font-size: 10.5px;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  padding-block: 0; padding-inline: var(--dnm-cell-pad);
  border-inline-end: 1px solid var(--dnm-warp-on-ink);
  white-space: nowrap; user-select: none;
}
table.tbl thead th:last-child { border-inline-end: 0; }
table.tbl thead th.sortable { cursor: pointer; }
table.tbl thead th.sortable:hover { color: #fff; }
table.tbl thead th .sort-i { opacity: .5; margin-inline-start: var(--dnm-space-2); }
/* The sorted column is marked by a 2px selvedge rule on its trailing edge, not
   a coloured fill: a rule survives any zoom level and any monitor. */
table.tbl thead th.sorted { border-inline-end: var(--dnm-edge-2) solid var(--dnm-selv); }
table.tbl thead th.sorted .sort-i { opacity: 1; color: var(--dnm-selv); }

table.tbl tbody tr { height: var(--dnm-row-h); }
table.tbl tbody td {
  padding-block: 0; padding-inline: var(--dnm-cell-pad);
  border-inline-end: var(--dnm-rule-2);
  border-block-end: var(--dnm-rule-3);
  vertical-align: middle; line-height: var(--dnm-lh-cell);
}
table.tbl tbody td:last-child { border-inline-end: 0; }
/* The band rhythm. A 1.4% luminance step: found, not seen. It is what lets an
   agent return to the right line after looking away at a machine. */
table.tbl tbody tr:nth-child(5n) { background: var(--dnm-band); }
table.tbl tbody tr:hover { background: var(--dnm-row-hover); }
table.tbl tbody tr.sel {
  background: var(--dnm-ink-100);
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv);
}
table.tbl tbody tr.clickable { cursor: pointer; }
table.tbl tbody tr:focus-visible { outline: var(--dnm-edge-2) solid var(--dnm-ink-500); outline-offset: -2px; }
table.tbl td.tight, table.tbl th.tight { width: 1%; white-space: nowrap; }
/* The row-level urgency edge. The design gives this to SLA breach; the queue
   markup has no breach class yet, and P1 is the nearest thing it does carry,
   so P1 takes the edge for now. When the port adds a breach class, move it —
   selvedge should mark the deadline, not the priority. */
table.tbl tbody tr.p1 { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
table.tbl tbody tr.unread td:nth-child(2) { font-weight: 600; }

/* Compact. This is the density the queue runs at, and the whole point of it:
   26px rows with 12.5px type work because the weft rule is the only thing
   between rows — no padding, no radius, no shadow to absorb the space. */
.tbl-compact {
  --dnm-row-h: 26px;
  --dnm-cell-pad: 8px;
  --dnm-head-h: 27px;
}
/* At compact density a cell that wraps is not a slightly taller cell — it is a
   row that has left the grid. One wrapped reference costs 6px and six of them
   cost a screenful, which is exactly how the 25-row proof fails without anyone
   noticing they broke it. Every cell clips instead. */
.tbl-compact tbody td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* At 26px a two-line subject cell cannot fit. Rather than hide the metadata,
   it is laid on the same line as the subject and both truncate — the row keeps
   its information and its height. */
.tbl-compact .subject-cell { flex-direction: row; align-items: baseline; gap: var(--dnm-space-4); }

.subject-cell { display: flex; flex-direction: column; gap: 0; min-width: 0; max-width: 520px; }
.subject-cell .s {
  font-size: var(--dnm-t-cell); color: var(--dnm-body-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.subject-cell .m {
  font-size: var(--dnm-t-label); color: var(--dnm-slate);
  display: flex; gap: var(--dnm-space-3); align-items: center;
  overflow: hidden; white-space: nowrap;
}

/* ----- the strips above and below the fabric ----- */
.toolbar {
  display: flex; align-items: center; gap: var(--dnm-space-4); flex-wrap: wrap;
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  background: var(--dnm-surface); border-block-end: var(--dnm-rule-2);
}
.toolbar.sticky { position: sticky; inset-block-start: var(--topbar-h); z-index: 20; }
/* The bulk bar replaces pagination while a selection exists. Ink ground so it
   reads as chrome rather than as another row of the fabric. */
.bulkbar {
  display: flex; align-items: center; gap: var(--dnm-space-5);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  background: var(--dnm-ink-900); color: var(--dnm-surface);
}
.bulkbar .btn { background: transparent; border-color: var(--dnm-group-on-ink); color: var(--dnm-header-ink); }
.bulkbar .btn:hover { background: rgba(231, 236, 247, .1); color: #fff; }
.bulkbar .btn-primary { background: var(--dnm-ink-200); border-color: var(--dnm-ink-200); color: var(--dnm-ink-900); }
.bulkbar .btn-danger { background: var(--dnm-selv); border-color: var(--dnm-selv); color: #fff; }

/* Saved views. A flat row on ecru closed by the structural rule; the active
   view is an ink block, which is the same object as a selected grid header. */
.tabs {
  display: flex; gap: 0; background: var(--dnm-ecru);
  border-block-end: var(--dnm-rule-1);
  padding-inline: 0;
}
.tab {
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-label); font-weight: 700;
  letter-spacing: .07em; text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-700); cursor: pointer; white-space: nowrap;
  transition: background var(--dnm-t-fast) var(--dnm-ease), color var(--dnm-t-fast) var(--dnm-ease);
}
.tab:hover { background: var(--dnm-ink-100); text-decoration: none; }
.tab.active { background: var(--dnm-ink-900); color: var(--dnm-surface); }
.tab.active:hover { background: var(--dnm-ink-900); color: var(--dnm-surface); }
.tab .n { font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro); margin-inline-start: var(--dnm-space-2); opacity: .7; }

.pager {
  display: flex; align-items: center; gap: var(--dnm-space-5);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  border-block-start: var(--dnm-rule-1); background: var(--dnm-ecru);
  font-size: 11.5px; color: var(--dnm-slate);
}
.pager b { font-family: var(--dnm-font-mono); font-weight: 500; color: var(--dnm-ink-900); }

/* ═══════════════════════════════════════════ INSTRUMENTS AND MEASURES ════
   The six numbers on the dashboard are not tiles. They are cells of one ruled
   instrument row, each with a thread gauge underneath showing its share of the
   whole, and each one clicking through to the filtered queue that produced it.
   No summary number in this system leads to a dead end.                      */
.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 0;
  background: var(--dnm-surface);
  border: var(--dnm-rule-2);
  border-block-end: var(--dnm-rule-1);
}
.stat {
  background: var(--dnm-surface);
  border-inline-end: var(--dnm-rule-2);
  padding-block: var(--dnm-space-4) var(--dnm-space-5);
  padding-inline: var(--dnm-space-5);
  position: relative;
  transition: background var(--dnm-t-base) var(--dnm-ease);
}
a.stat:hover { background: var(--dnm-row-hover); text-decoration: none; }
/* The state edge. Neutral cells get none at all — an edge on everything is an
   edge on nothing. */
.stat.alert { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.stat.warn  { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ochre); }
.stat.good  { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-moss); }
.stat .k {
  font-family: var(--dnm-font-mono); font-size: 9px; font-weight: 600;
  letter-spacing: .16em; text-transform: var(--dnm-case-label); color: var(--dnm-slate);
}
.stat .v {
  font-family: var(--dnm-font-display); font-size: 28px; font-weight: 700;
  letter-spacing: -.03em; line-height: 1; margin-block-start: var(--dnm-space-2);
  color: var(--dnm-ink-900); font-variant-numeric: tabular-nums;
}
.stat.alert .v { color: var(--dnm-selv-700); }
.stat.warn .v  { color: var(--dnm-ochre-700); }
.stat.good .v  { color: var(--dnm-moss-700); }
.stat .d {
  font-size: var(--dnm-t-label); color: var(--dnm-slate);
  margin-block-start: var(--dnm-space-2);
  display: flex; align-items: center; gap: var(--dnm-space-2);
}
.delta-up { color: var(--dnm-moss-700); font-weight: 600; }
.delta-down { color: var(--dnm-selv-700); font-weight: 600; }

/* ----- the SLA clock, as a woven bar -----
   Indigo threads while running, ochre in the last quarter, and a red twill
   hatch once breached. Hatch rather than fill, so it still reads as different
   from "running" when the sheet is printed in greyscale. */
.meter { height: 6px; background: var(--dnm-ink-100); border-radius: var(--dnm-radius-0); overflow: hidden; }
.meter > i { display: block; height: 100%; background: var(--dnm-ink-500); transition: width .5s var(--dnm-ease); }
.meter.good > i { background: var(--dnm-moss); }
.meter.warn > i { background: var(--dnm-ochre); }
.meter.breach > i {
  background: repeating-linear-gradient(90deg, var(--dnm-selv) 0 3px, var(--dnm-selv-700) 3px 6px);
}

.sla-clock {
  border: var(--dnm-rule-2); background: var(--dnm-surface);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-4);
}
.sla-clock.warn { border-color: #e2c48a; background: var(--ochre-soft); }
.sla-clock.breach { border-color: var(--dnm-selv); background: var(--dnm-selv-100); }
.sla-clock.paused { border-style: dashed; }
.sla-clock .t {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-15); font-weight: 600;
  font-variant-numeric: tabular-nums; line-height: 1.1; color: var(--dnm-ink-900);
}
.sla-clock.warn .t { color: var(--dnm-ochre-700); }
.sla-clock.breach .t { color: var(--dnm-selv-700); }

/* ----- bars drawn in the grid's own rule language, not a chart library ----- */
.bars { display: flex; align-items: flex-end; gap: var(--dnm-space-3); height: 132px; padding-block-start: var(--dnm-space-4); }
.bars .b { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: var(--dnm-space-2); height: 100%; }
.bars .b i { display: block; width: 100%; background: var(--dnm-ink-500); animation: dnm-grow .4s var(--dnm-ease) backwards; }
.bars .b:hover i { background: var(--dnm-ink-700); }
.bars .b .lb { font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro); color: var(--dnm-slate); }
@keyframes dnm-grow { from { height: 0 !important } }

.hbars { display: flex; flex-direction: column; gap: 0; }
.hbar {
  display: grid; grid-template-columns: 118px minmax(0, 1fr) 56px;
  align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-3);
  border-block-end: var(--dnm-rule-3);
  font-size: var(--dnm-t-mono);
}
.hbar .lb { color: var(--dnm-body-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hbar .tr { height: 11px; background: var(--dnm-ink-100); overflow: hidden; }
.hbar .tr > i { display: block; height: 100%; background: var(--dnm-ink-500); animation: dnm-widen .5s var(--dnm-ease) backwards; }
.hbar .vl { text-align: end; font-family: var(--dnm-font-mono); font-size: 11.5px; color: var(--dnm-slate); font-variant-numeric: tabular-nums; }
@keyframes dnm-widen { from { width: 0 !important } }

/* Donut, legend and sparkline survive from the previous system. The design
   argues charts belong inside the grid, so these are kept in the palette and
   squared off but are not developed further; no delivered screen uses them. */
.donut { width: 128px; height: 128px; border-radius: 50%; position: relative; flex: 0 0 128px; }
.donut::after { content: ''; position: absolute; inset: 22px; background: var(--dnm-surface); border-radius: 50%; }
.donut .cx { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; z-index: 1; }
.donut .cx b { font-family: var(--dnm-font-display); font-size: var(--dnm-t-22); letter-spacing: -.03em; display: block; line-height: 1.1; }
.donut .cx span { font-family: var(--dnm-font-mono); font-size: 9px; color: var(--dnm-slate); text-transform: var(--dnm-case-label); letter-spacing: .14em; }
.legend { display: flex; flex-direction: column; gap: var(--dnm-space-3); font-size: var(--dnm-t-mono); }
.legend .li { display: flex; align-items: center; gap: var(--dnm-space-3); }
.legend .sw { width: 10px; height: 10px; flex: 0 0 10px; }
.spark { width: 100%; height: 44px; display: block; }

.matrix { border-collapse: collapse; font-size: var(--dnm-t-mono); }
.matrix th {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-micro); font-weight: 700;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-4);
  text-align: start;
}
.matrix td {
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  text-align: center; font-family: var(--dnm-font-mono); font-weight: 500;
  border: var(--dnm-rule-2);
}

/* ═══════════════════════════════════════════════════ THREAD AND EVENTS ════
   The ticket conversation. Each message is a row of the same fabric: a weft
   rule beneath it, no bubble, no card, no avatar circle. */
.thread { display: flex; flex-direction: column; }
.entry {
  display: flex; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-4) var(--dnm-space-5);
  padding-inline: var(--dnm-space-5);
  border-block-end: var(--dnm-rule-3);
}
.entry:last-child { border-block-end: 0; }
.entry-body { flex: 1; min-width: 0; }
.entry-head {
  display: flex; align-items: baseline; gap: var(--dnm-space-4);
  flex-wrap: wrap; margin-block-end: var(--dnm-space-2);
}
.entry-head .who { font-family: var(--dnm-font-display); font-weight: 700; font-size: var(--dnm-t-mono); color: var(--dnm-ink-900); }
.entry-head .when {
  margin-inline-start: auto;
  font-family: var(--dnm-font-mono); font-size: 10.5px; font-weight: 500; color: var(--dnm-slate);
}
.entry-text { font-size: var(--dnm-t-cell); line-height: 1.55; color: var(--dnm-body-ink); }
.entry-text p { margin: 0; margin-block-end: var(--dnm-space-4); }
.entry-text p:last-child { margin: 0; }
.entry-text ul { margin-block: var(--dnm-space-3); padding-inline-start: var(--dnm-space-6); }
.entry-text li { margin-block-end: var(--dnm-space-1); }

.msg-public { }
/* Internal notes get the ochre thread and the band ground. The same device as
   a selected row, a different colour, and it is the only thing that
   distinguishes them — so it must be a thread and not a tint alone. */
.msg-private {
  background: var(--dnm-band);
  border: var(--dnm-rule-2);
  padding-block: var(--dnm-space-4);
  padding-inline: var(--dnm-space-5);
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ochre);
}
.msg-private .entry-text { color: var(--dnm-prose-ink); }
.quote {
  border-inline-start: var(--dnm-edge-2) solid var(--dnm-warp);
  padding-inline-start: var(--dnm-space-5);
  margin-block-start: var(--dnm-space-4);
  color: var(--dnm-slate); font-size: var(--dnm-t-mono);
}

.event {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  font-size: var(--dnm-t-mono); color: var(--dnm-slate);
  border-block-end: var(--dnm-rule-3);
}
.event .ico-dot {
  width: 20px; height: 20px; flex: 0 0 20px;
  border: var(--dnm-rule-2); background: var(--dnm-ecru);
  display: grid; place-items: center; color: var(--dnm-slate);
}
.event b { color: var(--dnm-ink-900); font-weight: 600; }
.event .when {
  margin-inline-start: auto;
  font-family: var(--dnm-font-mono); font-size: 10.5px; color: var(--dnm-slate); white-space: nowrap;
}

/* Attachment chips. The gap trails each chip rather than leading it, so a row
   of them reads correctly in either direction. */
.attach {
  display: inline-flex; align-items: center; gap: var(--dnm-space-3);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-4);
  border: var(--dnm-rule-2); background: var(--dnm-surface);
  font-size: 11.5px; color: var(--dnm-ink-700);
  margin-block: var(--dnm-space-1) 0; margin-inline: 0 var(--dnm-space-2);
  transition: background var(--dnm-t-fast) var(--dnm-ease), border-color var(--dnm-t-fast) var(--dnm-ease);
}
.attach:hover { background: var(--dnm-ink-100); border-color: var(--dnm-ink-400); text-decoration: none; }
.attach .fx {
  width: 22px; height: 20px; display: grid; place-items: center;
  font-family: var(--dnm-font-mono); font-size: 8px; font-weight: 600;
  color: var(--dnm-surface); background: var(--dnm-slate);
}
.attach .sz { font-family: var(--dnm-font-mono); color: var(--dnm-slate); font-size: var(--dnm-t-micro); }

/* ----- composer ----- */
.composer { border: var(--dnm-rule-1); background: var(--dnm-surface); }
.composer.private-mode { border-color: var(--dnm-ochre); }
.composer-tabs { display: flex; gap: 0; background: var(--dnm-ecru); border-block-end: var(--dnm-rule-2); }
.composer-tab {
  padding-block: var(--dnm-space-3); padding-inline: var(--dnm-space-5);
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-micro); font-weight: 700;
  letter-spacing: .06em; text-transform: var(--dnm-case-label);
  color: var(--dnm-slate); cursor: pointer;
  display: flex; align-items: center; gap: var(--dnm-space-3);
}
.composer-tab:hover { color: var(--dnm-ink-900); }
.composer-tab.active { background: var(--dnm-ink-900); color: var(--dnm-surface); }
.composer-tab.active[data-mode="private"] { background: var(--dnm-ochre); color: var(--dnm-surface); }
.composer textarea {
  width: 100%; border: 0; min-height: 96px; resize: vertical;
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  font-size: var(--dnm-t-13); line-height: 1.55; background: transparent;
}
.composer textarea:focus { outline: 0; box-shadow: inset 0 -2px 0 var(--dnm-ink-500); caret-color: var(--dnm-selv); }
.composer-foot {
  display: flex; align-items: center; gap: var(--dnm-space-4); flex-wrap: wrap;
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  border-block-start: var(--dnm-rule-1); background: var(--dnm-ecru);
}

/* ═══════════════════════════════════════════════════ THE PROPERTIES RAIL ══
   Same object as the form: a label warp and a value column. The rail's warp is
   narrower because the rail is, but it is drawn by the same mechanism. */
.rail { display: flex; flex-direction: column; }
.rail-sec { border-block-end: var(--dnm-rule-1); background: var(--dnm-surface); }
.rail-sec:last-child { border-block-end: 0; }
.rail-sec h4 {
  font-family: var(--dnm-font-mono); font-size: 9px; font-weight: 600;
  letter-spacing: .16em; text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
  background: var(--dnm-ecru); border-block-end: var(--dnm-rule-2);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-5);
  display: flex; align-items: center; justify-content: space-between;
}
.rail-sec > *:not(h4) { padding-inline: var(--dnm-space-5); }
.rail-sec > *:not(h4):first-of-type { padding-block-start: var(--dnm-space-3); }
.rail-sec > *:not(h4):last-child { padding-block-end: var(--dnm-space-4); }

.prop {
  display: grid; grid-template-columns: 84px minmax(0, 1fr);
  gap: var(--dnm-space-4); align-items: center;
  padding-block: var(--dnm-space-2);
  border-block-end: var(--dnm-rule-3);
  font-size: var(--dnm-t-mono);
}
.prop:last-child { border-block-end: 0; }
.prop .pk {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-micro); font-weight: 600;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
}
.prop .pv { min-width: 0; }
.prop .select, .prop .input { height: 26px; font-size: 11.5px; padding-inline: var(--dnm-space-3); }
.prop .select { padding-inline-end: 22px; }

/* ═════════════════════════════════════════════════════ THE UNIT TREE ═════
   Depth is drawn, not indented by whitespace. Each level adds a fixed block of
   band tint closed by a warp rule, so the level can be read off the ruler even
   when the label is a long Arabic unit name that wraps the row. */
.tree { font-size: var(--dnm-t-13); user-select: none; }
.tree-node { }
.tree-row {
  display: flex; align-items: center; gap: var(--dnm-space-3);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-4);
  border-block-end: var(--dnm-rule-3);
  cursor: pointer;
  transition: background var(--dnm-t-fast) var(--dnm-ease);
}
.tree-row:hover { background: var(--dnm-row-hover); }
.tree-row.sel { background: var(--dnm-ink-100); box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.tree-row .tw { width: 14px; flex: 0 0 14px; color: var(--dnm-ink-500); display: grid; place-items: center; }
.tree-row .tw.leaf { visibility: hidden; }
.tree-row .nm { font-weight: 500; }
.tree-row .cd { font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro); color: var(--dnm-slate); }
/* The guide block *is* the border. 18px of band tint per level, and the warp
   rule that closes it is placed on the child rows so it lands exactly on the
   boundary — both fully logical, so the whole tree mirrors with no override. */
.tree-kids { border-inline-start: 18px solid var(--dnm-band); }
.tree-kids > .tree-node > .tree-row { border-inline-start: var(--dnm-rule-2); }
.tree-node.collapsed > .tree-kids { display: none; }
.tree-node.collapsed > .tree-row .tw svg { transform: rotate(-90deg); }
.tree-row .tw svg { transition: transform var(--dnm-t-base) var(--dnm-ease); }

/* ═══════════════════════════════════════════════════════ OVERLAY LAYER ════
   The two elements in the system that are allowed to lift. Everything else,
   including every data surface, sits flat on the cloth. */
.modal-bg {
  position: fixed; inset: 0; background: rgba(15, 23, 47, .48);
  display: grid; place-items: center; z-index: 100; padding: var(--dnm-space-7);
  animation: dnm-fade var(--dnm-t-base) var(--dnm-ease);
}
.modal {
  background: var(--dnm-surface); border-radius: var(--dnm-radius-0);
  box-shadow: var(--dnm-shadow-modal);
  width: 100%; max-width: 520px; max-height: 86vh;
  display: flex; flex-direction: column;
  animation: dnm-pop var(--dnm-t-base) var(--dnm-ease);
}
@keyframes dnm-pop { from { opacity: 0; transform: translateY(6px) } }
.modal-head {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-6);
  background: var(--dnm-ink-900); color: var(--dnm-surface);
}
.modal-head h3, .modal-head h2 {
  color: var(--dnm-surface);
  font-size: var(--dnm-t-label); letter-spacing: var(--dnm-track-label);
  text-transform: var(--dnm-case-label);
}
.modal-body { padding-block: var(--dnm-space-6); padding-inline: var(--dnm-space-6); overflow-y: auto; }
.modal-foot {
  display: flex; align-items: center; gap: var(--dnm-space-4); justify-content: flex-end;
  padding-block: var(--dnm-space-5); padding-inline: var(--dnm-space-6);
  border-block-start: var(--dnm-rule-1); background: var(--dnm-ecru);
}
.modal-lg { max-width: 760px; }

/* Toasts anchor to the trailing bottom corner, which mirrors in Arabic, and
   carry the same 3px status thread on their leading edge as a selected row. */
.toasts {
  position: fixed; inset-block-end: var(--dnm-space-6); inset-inline-end: var(--dnm-space-6);
  z-index: 200; display: flex; flex-direction: column; gap: var(--dnm-space-4);
}
.toast {
  display: flex; align-items: flex-start; gap: var(--dnm-space-4);
  min-width: 290px; max-width: 400px;
  background: var(--dnm-ink-900); color: var(--dnm-surface);
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-5);
  border: 1px solid var(--dnm-ink-900);
  box-shadow: var(--dnm-shadow-menu); font-size: var(--dnm-t-mono);
  animation: dnm-toast-in var(--dnm-t-base) var(--dnm-ease);
  border-inline-start: var(--dnm-edge) solid var(--dnm-moss);
}
.toast.err  { border-inline-start-color: var(--dnm-selv); }
.toast.info { border-inline-start-color: var(--dnm-ink-400); }
.toast .tt {
  font-family: var(--dnm-font-display); font-size: 11.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: var(--dnm-case-label);
  margin-block-end: var(--dnm-space-1);
}
.toast .td { color: var(--dnm-ink-200); font-size: 11.5px; line-height: 1.45; }
@keyframes dnm-toast-in { from { opacity: 0; transform: translateX(18px) } }

/* ═══════════════════════════════════════════════════ STRUCTURAL LAYOUT ═══ */
/* Screen regions are divided by the 2px structural rule, not by a gap. The
   gap is kept at zero so two panels read as one ruled sheet. */
.split { display: grid; gap: 0; }
.split > * { min-width: 0; }
.split > * + * { border-inline-start: var(--dnm-rule-1); }
.split-3-1 { grid-template-columns: minmax(0, 3fr) minmax(280px, 1fr); }
.split-2-1 { grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); }
.split-1-2 { grid-template-columns: minmax(240px, 1fr) minmax(0, 2fr); }
.split-1-1 { grid-template-columns: 1fr 1fr; }
@media (max-width: 1100px) {
  .split-3-1, .split-2-1, .split-1-2, .split-1-1 { grid-template-columns: minmax(0, 1fr); }
  .split > * + * { border-inline-start: 0; border-block-start: var(--dnm-rule-1); }
}

.kv {
  display: grid; grid-template-columns: 140px minmax(0, 1fr);
  gap: 0; font-size: var(--dnm-t-mono);
}
.kv dt {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-micro); font-weight: 600;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
  padding-block: var(--dnm-space-3); padding-inline-end: var(--dnm-space-5);
  border-block-end: var(--dnm-rule-3);
}
.kv dd {
  margin: 0; min-width: 0; overflow-wrap: anywhere;
  padding-block: var(--dnm-space-3); padding-inline-start: var(--dnm-space-5);
  border-inline-start: var(--dnm-rule-2);
  border-block-end: var(--dnm-rule-3);
}

/* The stagger on load. Kept from the previous system but cut to a fade and
   4px, because 140ms is the system's longest duration and anything that
   travels further than that reads as animation rather than arrival. */
.rise { animation: dnm-rise var(--dnm-t-base) var(--dnm-ease) backwards; }
.rise:nth-child(1) { animation-delay: 0s }    .rise:nth-child(2) { animation-delay: 30ms }
.rise:nth-child(3) { animation-delay: 60ms }  .rise:nth-child(4) { animation-delay: 90ms }
.rise:nth-child(5) { animation-delay: 120ms } .rise:nth-child(6) { animation-delay: 150ms }
@keyframes dnm-rise { from { opacity: 0; transform: translateY(4px) } }

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

/* ═════════════════════════════════════════════════════════════ LOGIN ════
   Its own world, and the only screen in the system with a full-bleed indigo
   field. The warp and weft here are real 1px rules at 9px pitch — the texture
   image is retired, but at this scale the rule system itself is the texture. */
.auth-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 460px; min-height: 100vh; }
@media (max-width: 900px) { .auth-wrap { grid-template-columns: minmax(0, 1fr); } .auth-art { display: none; } }
.auth-art {
  background: var(--dnm-ink-900);
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: var(--dnm-space-9); color: var(--dnm-surface);
}
.auth-art::before { /* warp */
  content: ''; position: absolute; inset: 0;
  background-image: repeating-linear-gradient(90deg, rgba(255, 255, 255, .05) 0 1px, transparent 1px 9px);
  mask-image: linear-gradient(180deg, transparent, #000 28%, #000 72%, transparent);
}
.auth-art::after { /* weft */
  content: ''; position: absolute; inset: 0;
  background-image: repeating-linear-gradient(0deg, rgba(255, 255, 255, .035) 0 1px, transparent 1px 9px);
  mask-image: radial-gradient(ellipse at 50% 45%, #000, transparent 72%);
}
.auth-art > * { position: relative; z-index: 1; }
.auth-panel {
  background: var(--dnm-surface); display: flex; align-items: center; justify-content: center;
  padding-block: var(--dnm-space-8); padding-inline: var(--dnm-space-9);
  border-inline-start: var(--dnm-edge) solid var(--dnm-selv);
}
.auth-inner { width: 100%; max-width: 340px; }
.auth-tabs { display: flex; border: var(--dnm-rule-2); margin-block-end: var(--dnm-space-6); }
.auth-tab {
  flex: 1; padding-block: var(--dnm-space-4); text-align: center;
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-label); font-weight: 700;
  letter-spacing: .06em; text-transform: var(--dnm-case-label);
  cursor: pointer; color: var(--dnm-slate); background: var(--dnm-ecru);
  transition: background var(--dnm-t-fast) var(--dnm-ease), color var(--dnm-t-fast) var(--dnm-ease);
}
.auth-tab:hover { color: var(--dnm-ink-900); }
.auth-tab.active { background: var(--dnm-ink-900); color: var(--dnm-surface); }

/* ═════════════════════════════════════════════════════════════ PRINT ════
   Reports print. Everything the design says survives greyscale must actually
   survive it here: the glyphs are text, so they do. */
@media print {
  .sidebar, .topbar, .toolbar, .composer, .toasts { display: none !important; }
  .page { padding: 0; }
  body { background: #fff; }
  .tbl-wrap { overflow: visible; }
  table.tbl thead th { background: #fff; color: #000; border-block-end: var(--dnm-rule-1); }
}

/* ═════════════════════════════════════════════════════════ RIGHT TO LEFT ══
   Everything above this line mirrors on its own, because it is written in
   logical properties. What is left is the short list of things CSS genuinely
   cannot express logically. Each one is here with the reason it had to be.

   The list is deliberately short. It was going to be one rule per leading-edge
   thread — eleven of them — until the shadow offset became a token; it is
   three rules now, and the first mirrors every thread in the system at once.
   ─────────────────────────────────────────────────────────────────────── */

/* 1. box-shadow offsets are physical and there is no logical form. Every
      leading-edge thread in this stylesheet — selected rows, P1 rows, private
      notes, callouts, invalid field rows, stat edges, active nav, selected
      tree row, hovered KB card — is drawn with `inset var(--dnm-edge-x) 0 0`.
      Negating one token here flips all of them, which is the entire reason
      the offset is a token and not a literal 3px at each site.

      --dnm-edge and --dnm-edge-2 are deliberately *not* flipped: they are used
      as widths (border-inline-start, outline-width, the selvedge nav strip),
      and a negative width is meaningless. */
[dir="rtl"] { --dnm-edge-x: -3px; }

/* 2. background-position has no logical form, so the select chevron does not
      follow its own padding. Without this it sits on top of the value. */
[dir="rtl"] .select { background-position: left 9px center; }

/* 3. transform: translateX is physical. The switch knob travels toward the
      trailing edge, which is leftward in Arabic, and a toast slides in from
      the edge it appears on. */
[dir="rtl"] .switch input:checked + .track::before { transform: translateX(-14px); }
[dir="rtl"] .toast { animation-name: dnm-toast-in-rtl; }
@keyframes dnm-toast-in-rtl { from { opacity: 0; transform: translateX(-18px) } }

/* Not in the list, and deliberately: the repeating-linear-gradient hatches on
   the breached SLA bar and the selvedge nav edge. Both patterns are
   symmetrical about the axis they would mirror across, so a flipped copy is
   pixel-identical and an override would be a no-op with a maintenance cost. */


/* ==========================================================================
   STAGE 2 — WORKSPACE CHROME, QUEUE AND DASHBOARD
   ========================================================================== */

/* ==========================================================================
   Stage 2 — the agent workspace chrome and the ticket queue.

   Everything in this file is a candidate for merging into dnm.css. It is a
   separate sheet only so that four ports can run at the same time without four
   people editing one file; nothing here introduces a new idea, it either
   reaches a class dnm.css already defines but nothing emitted, or it sizes an
   existing object to the measure the delivered design specifies.

   The two rules dnm.css imposes on itself are kept here without exception:

     1. No physical direction properties. Not one — no inline pair, and no
        block pair either. Arabic is a shipped language, and one physical edge
        in this file is a screen that has to be re-tested in two directions
        forever after. Nothing below names a physical edge, so grepping this
        file for one is a real check and not an approximate one.
     2. No shadow below modal level, no radius above 3px. Hierarchy is rule
        weight.

   ── THE CHROME BUDGET ───────────────────────────────────────────────────
   The delivered queue screen is 768px tall and holds 25 rows of 26px. That
   leaves 117px for everything that is not a ticket, and the design spends it
   as 32 (view bar) + 28 (filter line) + 27 (grid header) + 30 (the bar along
   the bottom). Those four numbers are the whole reason this file exists: the
   port had stacked a page head, a tab strip and a two-line filter toolbar and
   was spending about 180px on the same job, which is five rows of an agent's
   screen — a fifth of the queue — given away to whitespace.

   This sheet spends the budget the same way, with one addition the client
   asked for: a 28px summary strip between the view bar and the filters. So
   the page chrome here is 32 + 28 + 28 + 27 = 115px, and the application's
   persistent 40px top bar sits above all of it.
   ========================================================================== */

/* ═══════════════════════════════════════════════════════════ UTILITIES ════ */

/* Visually hidden but still announced and still focusable. dnm.css has only
   `.hidden`, which is display:none — that removes the element from the
   accessibility tree as well as from the page, so it cannot be used to label a
   checkbox. Every checkbox in the queue is labelled with one of these. */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A form that exists only to carry a POST — the sign-out button in the user
   chip. It has to not take part in the chip's flex layout as a block. */
.inline-form { display: inline; }

/* The layout's flash banners. Their padding used to be an inline style on a
   bare div; it is the page's own inline padding plus a little air above, and
   nothing else in the system needs the measure, so it stays a one-off class
   rather than becoming a token. */
.page-banner { padding-block: 14px 0; padding-inline: var(--dnm-space-7); }

/* An icon button drawn on the ink sidebar rather than on ecru. .icon-btn is
   sized correctly already; only the resting colour has to change, because
   ink-700 on ink-900 is invisible. */
.icon-btn-on-ink { color: var(--dnm-ink-200); }
.icon-btn-on-ink:hover { background: var(--dnm-ink-700); border-color: transparent; color: var(--dnm-surface); }

/* ═══════════════════════════════════════════════════════ THE VIEW BAR ════
   The queue's title, its saved views and its page action are one band. They
   were three: a page head with an h1 and a description, then the tab strip,
   then the toolbar. Stacked they came to about 110px before the grid header
   had been drawn at all.

   This is `.tabs` from dnm.css with a fixed height and stretched items, so the
   active tab is still the ink block dnm.css draws and nothing about the tab
   object is forked. The title sits before the tabs, the action after them. */
.viewbar {
  height: 32px;                 /* 30px of content under the 2px structural rule */
  flex: none;
  align-items: stretch;
  padding-inline: var(--dnm-space-5);
}
/* The title block is the only thing on the bar allowed to shrink. The tabs are
   the navigation and the action is the reason the screen exists; a description
   that has run out of room can be read from the tooltip. */
.viewbar .vb-title {
  display: flex; align-items: center; gap: var(--dnm-space-3);
  padding-inline-end: var(--dnm-space-5);
  flex: 0 1 auto; min-width: 0; overflow: hidden;
}
.viewbar .vb-name {
  font-family: var(--dnm-font-display); font-size: var(--dnm-t-mono); font-weight: 700;
  letter-spacing: .04em; text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-900); white-space: nowrap;
}
/* The count belongs to the title, not to the filter line: it is what the view
   is, not what the filters did. Tabular figures so it does not jump width as
   the queue drains. */
.viewbar .vb-n {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label);
  color: var(--dnm-slate); font-variant-numeric: tabular-nums;
}
/* The one-line description of the view. It is the first thing to go when the
   bar runs out of room — below 1240px the tabs and the action matter more, and
   a truncated sentence is worse than no sentence. */
.viewbar .vb-sub {
  font-size: var(--dnm-t-label); color: var(--dnm-slate);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  flex: 0 1 auto; min-width: 0;
}
@media (max-width: 1240px) { .viewbar .vb-sub { display: none; } }
/* The tab's own padding-block is what makes it 27px tall; inside a fixed-height
   bar the tab has to fill the bar instead, or the ink block of the active tab
   floats with a sliver of ecru above and below it. flex:none because a squeezed
   tab strip is a navigation nobody can read. */
.viewbar .tab { display: flex; align-items: center; padding-block: 0; flex: none; }
.viewbar .vb-actions {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  margin-inline-start: auto;
  padding-inline-start: var(--dnm-space-5);
}

/* ═════════════════════════════════════════════════ THE SUMMARY STRIP ════
   The four numbers an agent working a queue acts on. `.stats` and `.stat` are
   dnm.css's instrument row, which is 84px tall because on the dashboard the
   number is the content of the screen. Here it is chrome, and 84px is three
   rows of the queue, so the strip is laid on one 28px line: the label and the
   value share a baseline and the descriptive line is not emitted at all.

   Every cell that can lead somewhere is an anchor to the filtered queue that
   produced it, which is dnm.css's rule about summary numbers. "Breaching soon"
   is the exception and is deliberately a plain cell: there is no filter for it
   in TicketsController.Index, and a link that quietly showed the wrong queue
   would be worse than a number that admits it is only a number. */
.stats-strip {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  flex: none;
  /* The strip is butted between the view bar's 2px rule and the filter line,
     so it draws only its own closing rule; its own frame would double them. */
  border: 0;
  border-block-end: var(--dnm-rule-2);
}
.stats-strip .stat {
  display: flex; align-items: baseline; gap: var(--dnm-space-4);
  height: 27px;
  padding-block: 0;
  padding-inline: var(--dnm-space-5);
  overflow: hidden;
}
.stats-strip .stat .v {
  font-size: var(--dnm-t-15);
  margin-block-start: 0;
}
.stats-strip .stat:last-child { border-inline-end: 0; }

/* ══════════════════════════════════════════════════ THE FILTER LINE ═════
   `.toolbar` wraps, which is correct for a form and wrong for a screen: a
   wrapped filter strip is a second 30px line that appears and disappears with
   the window width, and the queue below it gains and loses a row as it does.
   Here it is one line that never wraps, and the controls are sized down to fit
   inside it rather than the line growing to fit the controls. */
.toolbar-line {
  height: 28px;
  flex: none;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--dnm-space-4);
  padding-block: 0;
  padding-inline: var(--dnm-space-5);
  background: var(--dnm-surface);
  border-block-end: var(--dnm-rule-2);
  overflow: hidden;
}
.toolbar-line .input,
.toolbar-line .select,
.toolbar-line .search input { height: 22px; font-size: var(--dnm-t-label); padding-inline: var(--dnm-space-3); }
/* A select still has to keep its chevron off the value; 20px is the smallest
   gap the 9px glyph plus its 9px offset will sit in. */
.toolbar-line .select { width: auto; min-width: 0; padding-inline-end: 20px; }
.toolbar-line .select-wide { min-width: 148px; }
.toolbar-line .btn { height: 22px; padding-inline: var(--dnm-space-4); font-size: var(--dnm-t-micro); }
/* The search field's leading gap is reserved for the glyph. dnm.css reserves
   30px for a 14px icon in a 26px field; at 22px the icon is 12px and 22px of
   reserve is the same optical inset. */
.toolbar-line .search { flex: 0 1 236px; max-width: 236px; }
.toolbar-line .search input { padding-inline: 22px var(--dnm-space-3); }
.toolbar-line .search .s-ico { inset-inline-start: var(--dnm-space-3); }
/* Two readouts trail the filters. They are the first things the line gives up,
   in the order they can be done without: the scope note is a standing fact, the
   count is repeated in the view bar. */
.toolbar-line .tl-count { white-space: nowrap; }
.toolbar-line .tl-note { white-space: nowrap; display: flex; align-items: center; gap: var(--dnm-space-3); }
@media (max-width: 1320px) { .toolbar-line .tl-note { display: none; } }
@media (max-width: 1100px) { .toolbar-line .tl-count { display: none; } }

/* ══════════════════════════════════════════════════ THE QUEUE SCREEN ════
   The queue is a screen and not a document: the header band, the filters and
   the bar along the bottom stay put and only the fabric between them moves.
   Without this the sticky grid header sticks to the window, the pager is
   wherever the last row happened to put it, and an agent who has scrolled has no idea how
   many tickets they are looking at.

   Scoped through :has so it applies to the queue and to nothing else in the
   application — .main is shared with every screen, and giving all of them a
   fixed height and a clipped overflow would break every long form. */
body:has(.queue-screen) .main { height: 100vh; overflow: hidden; }
.queue-screen { flex: 1 1 auto; min-height: 0; }
/* The fabric is the only part that scrolls. min-height:0 is load-bearing: a
   flex item's default min-height is auto, so without it the table refuses to
   shrink below its natural height and pushes the pager off the screen. */
.queue-screen > .tbl-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  border: 0;
}
.queue-screen > .empty { flex: 1 1 auto; }
/* 30px, the design's measure, and the same for both bars so the screen does
   not change height when a selection appears. */
.queue-screen > .pager,
.queue-screen > .bulkbar { height: 30px; flex: none; padding-block: 0; }

/* ════════════════════════════════════════════════════════ SORTABLE TH ════
   dnm.css styles th.sortable and th.sorted and nothing emitted either. The
   click target is an anchor rather than the th itself, because sorting must
   survive with scripting off and must be reachable from the keyboard, and a
   th is neither a link nor focusable. The anchor fills the cell so the whole
   header is still the target it looks like. */
table.tbl thead th.sortable > a {
  display: flex; align-items: center;
  height: 100%;
  color: inherit;
}
table.tbl thead th.sortable > a:hover { color: #fff; text-decoration: none; }
table.tbl thead th.sortable > a:focus-visible { outline-offset: -2px; }
/* The indicator is a mono glyph rather than an icon: it inherits the header's
   colour, it survives greyscale printing, and it costs no request. */
table.tbl thead th .sort-i { font-family: var(--dnm-font-mono); font-size: 9px; line-height: 1; }
/* A trailing-edge alignment for the columns that hold a measure rather than a
   name. The header link is a flex row, so it needs telling too. */
table.tbl th.num, table.tbl td.num { text-align: end; }
table.tbl thead th.num.sortable > a { justify-content: flex-end; }

/* ═════════════════════════════════════════════════════════ SELECTION ════
   The checkbox column. dnm.css styles tbody tr.sel and .bulkbar and nothing
   reached either, because no markup in the application produced a selection.

   The column is 28px, which is the design's measure, and the checkbox is 12px
   so that it sits inside a 26px compact row without setting the row's height.
   The label wraps the input and holds the ticket's reference, hidden: a
   plain-language target on a shared plant-floor terminal is the difference
   between a screen reader saying "checkbox" twenty-five times and saying
   "select DNM-1042". */
table.tbl th.pick-col, table.tbl td.pick-col {
  width: 28px;
  padding-inline: var(--dnm-space-4);
}
/* Positioned so the hidden label anchors to the cell rather than to the page —
   an absolutely positioned 1px element with no positioned ancestor lands at the
   document origin, which is harmless but leaves a stray in the a11y tree far
   from the control it names. */
.pick { display: flex; align-items: center; cursor: pointer; margin: 0; position: relative; }
.pick input {
  width: 12px; height: 12px; margin: 0;
  accent-color: var(--dnm-ink-700);
  cursor: pointer;
}
/* On the ink header band the browser's own unchecked border is nearly
   invisible, so the control is lightened to the band's own text colour. */
table.tbl thead .pick input { accent-color: var(--dnm-ink-300); }

.bulkbar .vr { width: 1px; height: 16px; background: var(--dnm-group-on-ink); flex: none; }
.bulkbar .bulk-n {
  font-family: var(--dnm-font-display); font-size: 11px; font-weight: 700;
  letter-spacing: .08em; text-transform: var(--dnm-case-label);
  white-space: nowrap;
}
.bulkbar .btn { height: 22px; padding-inline: var(--dnm-space-4); font-size: var(--dnm-t-micro); }
/* The bar states what a selection can and cannot do. It is muted rather than
   hidden because an agent who has selected five rows and found no button needs
   to be told why rather than allowed to conclude the page is broken. */
.bulkbar .bulk-note {
  margin-inline-start: auto;
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro);
  color: var(--dnm-ink-300); white-space: nowrap;
}
@media (max-width: 1100px) { .bulkbar .bulk-note { display: none; } }

/* ═══════════════════════════════════════════════════════ THE BREACH EDGE ══
   dnm.css hangs the row's leading-edge thread off `.p1` and says in a comment
   that it belongs on a breach class as soon as the port emits one. This is
   that class. Priority is a property of the ticket; a breach is a deadline
   that has already passed, and the selvedge marks the deadline.

   `.p1` stays in dnm.css untouched and is simply no longer emitted by the
   queue, so nothing regresses if this sheet is merged out of order. */
table.tbl tbody tr.breached { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }


/* ==========================================================================
   STAGE 2 — THE TICKET SCREEN AND THE FORMS THAT FEED IT
   ========================================================================== */

/* ==========================================================================
   STAGE 2 — the ticket screen and the forms that feed it.

   Everything here is destined for dnm.css and is kept separate only so that
   four ports can run at once without four people editing one file. It obeys
   the same two rules dnm.css states at the top:

     1. Nothing is renamed. Classes that already exist are used as they are;
        what is below is either a new object the design asks for and the
        stylesheet does not yet carry, or the replacement for an inline style
        that had to come out of a view.
     2. No physical direction properties. Arabic ships. Every offset here is
        margin-inline-*, padding-inline-*, border-inline-*, inset-inline-* or
        text-align:start, and the one inset box-shadow uses --dnm-edge-x so it
        mirrors with the token rather than with an override of its own.

   Grep target before merging: this file must contain no `left`, no `right`,
   no `margin-left`, no `padding-right` and no `text-align: left`.
   ========================================================================== */

/* ═══════════════════════════════════════════ THE TICKET SCREEN'S SPINE ════
   The delivered design builds the ticket screen as four full-bleed bands
   stacked under the shell chrome — identity, header, spec strip, body — and
   only the fourth is divided into columns. The bands are full-bleed because
   the strip's ten cells have to sit on the same warp as the body's column
   rule; put the whole screen inside the standard 24px page padding and the
   strip's last rule lands 24px short of the rail's, which is exactly the
   sort of near-miss the rule system exists to prevent.

   The views carry `class="page flush"`, which dnm.css already defines as a
   zero-padding flex column, so nothing here has to restate that.           */

/* The identity band: which queue this ticket came from and what it is
   called. Ink-800 directly under the topbar's 2px ink-900 rule, so the two
   read as one thickened beam rather than as two separate bars — the design's
   "ink beam the fabric runs under", carried down one level. */
.tk-idbar {
  flex: none;
  min-height: 28px;
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--dnm-space-2) var(--dnm-space-4);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-5);
  background: var(--dnm-ink-800);
}
.tk-idbar .ib-ou {
  font-family: var(--dnm-font-display); font-size: 10.5px; font-weight: 700;
  letter-spacing: var(--dnm-track-label); text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-200);
}
.tk-idbar .ib-ref {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-mono); font-weight: 600;
  color: var(--dnm-surface); font-variant-numeric: tabular-nums;
}
.tk-idbar .sep { color: var(--dnm-ink-300); }
.tk-idbar a { color: var(--dnm-ink-200); }
.tk-idbar a:hover { color: var(--dnm-surface); }
/* Anything pushed to the trailing end of the band — the resolve action. */
.tk-idbar .ib-end { margin-inline-start: auto; display: flex; align-items: center; gap: var(--dnm-space-4); }
.tk-idbar .btn { border-color: var(--dnm-group-on-ink); color: var(--dnm-header-ink); }
.tk-idbar .btn:hover { background: rgba(231, 236, 247, .1); color: #fff; }

/* The header band: state, subject and the SLA clock. Two columns, and the
   rail column is a fixed 300px rather than a fraction so that it lines up
   with the body's rail below it — the whole point of a warp is that the two
   bands share one vertical rule. */
.tk-head {
  flex: none;
  background: var(--dnm-surface);
  border-block-end: var(--dnm-rule-2);
  padding-block: 11px 12px; padding-inline: var(--dnm-space-6);
  display: grid; grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--dnm-space-7); align-items: start;
}
.tk-head-meta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 5px var(--dnm-space-5);
  margin-block-end: var(--dnm-space-3);
}
/* The subject. 20px rather than the h1 default 22px: it sits inside a band
   that is already dense with badges, and at 22px it collided with the SLA
   clock's 15px readout for optical weight. */
.tk-subject { font-size: 20px; line-height: 1.25; }

/* ----- the spec strip -----
   Ten cells of the ticket's defining facts, two rows of five, each one a
   mono key over a clipped value. This is the object that lets the rail stop
   being a filing cabinet: reference, status, priority, department, assignee
   and the resolution clock are all readable without scrolling.

   Deliberately NOT built on .stat. A .stat is the dashboard instrument — a
   28px display figure with a gauge under it — and ten of them would be a
   400px-tall band. Same idea, different scale, so it is its own object.

   Cells clip rather than wrap for the reason the compact grid clips: a
   wrapped value is not a slightly taller cell, it is a row that has left the
   grid and taken the second row's baseline with it.                        */
.spec-strip {
  flex: none;
  background: var(--dnm-surface);
  border-block-end: var(--dnm-rule-1);
}
.spec-row {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  border-block-end: var(--dnm-rule-2);
}
.spec-row:last-child { border-block-end: 0; }
.spec {
  padding-block: 5px 7px; padding-inline: 10px;
  border-inline-end: var(--dnm-rule-2);
  min-width: 0;
}
.spec:last-child { border-inline-end: 0; }
.spec .sk {
  font-family: var(--dnm-font-mono); font-size: 9px; font-weight: 600;
  line-height: 1.5; letter-spacing: .14em;
  text-transform: var(--dnm-case-label); color: var(--dnm-slate);
}
.spec .sv {
  font-size: var(--dnm-t-cell); line-height: 1.3; color: var(--dnm-body-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A cell whose value is itself a state carries the state's edge, which is the
   same three-colour vocabulary the stat row uses. The edge is the signal; the
   glyph inside the badge is the second, redundant one. */
.spec.alert { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-selv); }
.spec.warn  { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ochre); }
/* Badges sit on the value line, so the line has to stop clipping them: an
   ellipsis on a 20px badge crops the glyph, and the glyph is the accessible
   half of the encoding. */
.spec .sv.has-badge { overflow: visible; white-space: normal; }

/* The body. Same 300px rail as the header band, and the 2px rule between the
   two panels comes from .split, which the markup also carries. */
.tk-split { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }

/* Below 1100px the rail cannot hold a 300px column and a readable thread at
   once, so every band drops to one column. .split's own media query keys off
   its four size classes by name and cannot see .tk-split, so the collapse is
   restated here for all three bands at once. */
@media (max-width: 1100px) {
  .tk-head, .tk-split { grid-template-columns: minmax(0, 1fr); }
  .spec-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .spec-row { grid-template-columns: minmax(0, 1fr); }
}

/* ═══════════════════════════════════════════════════════ THE MAIN COLUMN ══
   Sections in the main column are the same ruled sheet as the rail's, so the
   two columns read as one fabric across the 2px divide. .rail-sec already is
   that object; .tk-main only has to supply the surface behind it and stop the
   last section leaving a hanging rule. */
.tk-main { background: var(--dnm-surface); display: flex; flex-direction: column; min-width: 0; }
.tk-main > .rail-sec:last-child { border-block-end: 0; }

/* A section of the main column. The same three parts as .rail-sec — an ecru
   header band closed by a 1px warp rule, a body, and the 2px structural rule
   that separates it from the next section — but with the rail's 12px inline
   padding widened to 16px and made removable, because the two things that go
   in this column (.thread and .composer) bring their own padding and would
   otherwise sit 28px in from a rule everything else is 16px from. */
.tk-sec { flex: none; background: var(--dnm-surface); border-block-end: var(--dnm-rule-1); }
.tk-sec:last-child { border-block-end: 0; }
.tk-sec > h4 {
  font-family: var(--dnm-font-mono); font-size: 9px; font-weight: 600;
  letter-spacing: .16em; text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
  background: var(--dnm-ecru); border-block-end: var(--dnm-rule-2);
  padding-block: var(--dnm-space-2); padding-inline: var(--dnm-space-6);
  display: flex; align-items: center; gap: var(--dnm-space-4);
}
.tk-sec-body { padding-block: var(--dnm-space-5); padding-inline: var(--dnm-space-6); }
.tk-sec-body.flush { padding: 0; }
/* The foot of a form that fills a section: the ecru strip the submit sits on,
   closed by the structural rule above it. Same object as .card-foot. */
.tk-sec-foot {
  display: flex; align-items: center; gap: var(--dnm-space-4); flex-wrap: wrap;
  padding-block: var(--dnm-space-4); padding-inline: var(--dnm-space-6);
  border-block-start: var(--dnm-rule-1); background: var(--dnm-ecru);
}

/* Page-level banners — the error and the confirmation — on a flush layout,
   which by definition has no page padding for them to sit in. */
.tk-banner { flex: none; padding-block: var(--dnm-space-5) 0; padding-inline: var(--dnm-space-6); }

/* The jump strip. The design puts a tab row at the top of the main column;
   ours are anchors rather than tabs because the sections are all on the page
   at once and nothing is hidden — a tab that does not hide anything is a lie
   about the state of the screen. Same .tabs/.tab object, no active state,
   because none of them is "current". */
.tk-jump { flex: none; }
.tk-jump .tab .n {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-micro);
  margin-inline-start: var(--dnm-space-2); opacity: .7;
}

/* The pinned original description. Band ground, the 3px ink thread on the
   leading edge, closed by the structural rule — the design's device for "this
   is the thing everything below is about". It replaces a card that carried
   `border-left:3px`, which was the screen's one hard RTL break: in Arabic the
   thread stayed on the wrong side of the description. */
.tk-pinned {
  flex: none;
  background: var(--dnm-band);
  border-block-end: var(--dnm-rule-1);
  padding-block: 9px 11px; padding-inline: var(--dnm-space-6);
  box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ink-900);
}
.tk-pinned .pin-k {
  font-family: var(--dnm-font-mono); font-size: 9px; font-weight: 600;
  letter-spacing: .14em; text-transform: var(--dnm-case-label);
  color: var(--dnm-slate);
}

/* ═══════════════════════════════════════════════════ STATE, NOT COLOUR ════
   dnm.css encodes status through .st-* + .pip and priority through .pr-*, and
   both are glyph-first. Approval state had no such encoding — it was a badge
   colour and an icon that is an <svg>, which does not survive a greyscale
   print and says nothing to a screen reader. These four give the approval
   states the same treatment: a mono glyph in front of the word.

   Glyphs match the status set they stand nearest to: ⊙ waiting (the same
   glyph as "pending approval"), ✓ approved, ▲ rejected (the breach glyph —
   a rejection is the edge), ⊘ withdrawn (the "not actionable" glyph).       */
.ap-wait, .ap-ok, .ap-no, .ap-off { gap: var(--dnm-space-2); }
.ap-wait::before, .ap-ok::before, .ap-no::before, .ap-off::before {
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-label);
  display: inline-block; line-height: 1;
}
.ap-wait { background: var(--ochre-soft); border-color: #e2c48a; color: var(--dnm-ochre-700); }
.ap-wait::before { content: "\2299"; }
.ap-ok { background: var(--moss-soft); border-color: #a9cbb9; color: var(--dnm-moss-700); }
.ap-ok::before { content: "\2713"; }
.ap-no { background: var(--dnm-selv-100); border-color: var(--dnm-selv); color: var(--dnm-selv-700); }
.ap-no::before { content: "\25B2"; }
.ap-off { background: var(--dnm-ecru); border-color: var(--dnm-warp); color: var(--dnm-off-ink); }
.ap-off::before { content: "\2298"; }

/* The requester's progress tracker. Four segments on a rule, the reached ones
   in ink. Colour alone would not say which step you are on, so the current
   step is also the only bold label and the only one that carries the caret. */
.prog { display: flex; gap: var(--dnm-space-2); align-items: flex-start; }
.prog-step { flex: 1 1 0; min-width: 0; text-align: center; }
.prog-bar { height: 4px; background: var(--canvas-deep); }
.prog-step.done .prog-bar { background: var(--dnm-ink-600); }
.prog-lb {
  font-size: var(--dnm-t-label); color: var(--dnm-off-ink);
  margin-block-start: var(--dnm-space-4);
}
.prog-step.done .prog-lb { color: var(--dnm-body-ink); }
.prog-step.now .prog-lb { font-weight: 600; }
/* A square rather than a caret: a caret points, and a pointing glyph is one
   of the few marks that has to be flipped by hand in Arabic. */
.prog-step.now .prog-lb::before { content: "\25AA\00a0"; font-family: var(--dnm-font-mono); font-size: 9px; }

/* The approval chain block on the approvals inbox. A ruled sheet on the band
   tint, which is what "a group of steps asked together" looks like in this
   system — not a card with a radius, which is what it was. */
.chain-step {
  border: var(--dnm-rule-2); background: var(--dnm-band);
  padding-block: 9px; padding-inline: 11px;
}
/* A quoted approver comment, indented to clear the 20px avatar plus its gap.
   It was `padding-left`, which put the indent on the wrong side in Arabic and
   left the quote hanging under nothing. */
.chain-note { padding-inline-start: 28px; }

/* ══════════════════════════════════════════════════════════ UTILITIES ════
   Every rule below replaced a style="" attribute in one of the four views in
   this port. They are named for what they mean rather than for what they set,
   so that a later change of measure does not leave a class called .w-90 that
   is 110px wide. */

/* A user-authored body of text. Newlines are meaningful — an operator's
   description of a fault is a list far more often than it is a paragraph —
   so they are preserved rather than collapsed. */
.pre { white-space: pre-wrap; }

/* A form that exists only to carry one button inside a row of other content:
   the remove-tag cross, the delete-attachment bin. Without this the form is a
   block and breaks the row it sits in. */
.form-inline { display: inline; }

/* A submit button that must look like the text or glyph inside it, because
   the surrounding element (a .tag, a .chip) is already the control. */
.btn-bare {
  background: none; border: 0; padding: 0;
  display: inline-flex; align-items: center;
  color: inherit; font: inherit; cursor: pointer;
}

/* The 20px icon button: a remove cross sitting on a 26px row. .icon-btn's own
   26px would set the row height instead of fitting inside it. */
.icon-btn-xs { width: 20px; height: 20px; }

/* A badge on a line of 11px metadata rather than on its own. */
.badge-xs { font-size: var(--dnm-t-micro); height: 18px; }

/* A select that states one choice rather than filling a field column — the
   composer's "then set status". Shrink-wrapped so two of them fit on the
   composer foot beside the send button. */
.select-auto { width: auto; max-width: 100%; font-size: var(--dnm-t-mono); }

/* The file input. Browsers render the button chrome inside the control and
   ignore the 30px field height, clipping it; auto height with even padding is
   the only way it lines up with the button beside it. */
.input-file { height: auto; padding: var(--dnm-space-3); }

/* Three widths for inputs whose content has a known size: a minute count, a
   money amount, a sentence. Set as max-width so they still shrink on a narrow
   rail rather than overflowing it. */
.input-narrow { max-width: 110px; }
.input-med { max-width: 260px; }
.input-wide { min-width: 220px; }

/* <details><summary> carrying a destructive or rarely-wanted action. The
   pointer is the only affordance a bare summary has. */
.summary-link { cursor: pointer; }

/* Icons that carry a signal in a heading. Three, matching the three signal
   tokens; the word beside them is always the primary encoding. */
.ico-ok { color: var(--dnm-moss); display: inline-flex; }
.ico-alert { color: var(--dnm-selv); display: inline-flex; }
.ico-mute { color: var(--dnm-slate); display: inline-flex; }

/* The composer's private-mode warning. It sits between the tab row and the
   textarea, so it is a band inside a bordered box rather than a callout: its
   own border and radius would draw a box inside a box. The leading thread and
   the ground come from .note-warn; only the frame is removed. */
.composer-hint {
  border: 0;
  border-block-end: 1px solid #e2c48a;
  border-radius: var(--dnm-radius-0);
}

/* .truncate needs a block box, and a <span> is not one. */
.block { display: block; }

/* The standard inset for content that is placed directly inside a ruled sheet
   rather than inside a .card-body — a list of records, a stack of forms. Same
   two measures .card-body uses, so the two line up when they sit side by side. */
.pad { padding-block: var(--dnm-space-5); padding-inline: var(--dnm-space-6); }

/* A sentence of prose set to a readable measure. 60ch is the same measure
   .empty p already uses, restated so it can be applied to a hint that is not
   inside an empty state. */
.measure { max-width: 60ch; }

/* The lead line of a record — what an approver is being asked to decide. One
   step above body, one below h3, and not a heading because it is content. */
.lede { font-size: 14px; font-weight: 600; line-height: 1.35; }

/* A key/value list whose keys are short words rather than field labels. */
.kv-narrow { grid-template-columns: 120px minmax(0, 1fr); }

/* The department picker on the request form: as many 250px cards as fit. Not
   .opt-cards, which is a single ruled column, because eleven departments in
   one column is a scroll rather than a choice. */
.opt-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 10px; margin-block-start: var(--dnm-space-4);
}
.opt-grid .opt-card { border: var(--dnm-rule-2); }

/* The 1-to-5 satisfaction radios. The gap between the control and its number
   was margin-right, which put it on the far side of the digit in Arabic. */
.rate-in { margin-inline-end: var(--dnm-space-2); }


/* ==========================================================================
   STAGE 2 — THE ADMINISTRATION CONSOLE
   ========================================================================== */

/* ==========================================================================
   STAGE 2 — ADMINISTRATION CONSOLE

   Everything the twenty views under Areas/Admin/Views needed that dnm.css does
   not already carry. It exists as a separate file only so that four ports could
   run at once without four people editing dnm.css; it is meant to be merged in
   and this file deleted.

   Two rules it inherits from dnm.css and does not relax:

   1. No physical direction properties. Not one `left`, `right`, `padding-left`
      or `text-align: right` below this line — Arabic ships, and a physical
      property is a bug that only shows up in Cairo. Everything is
      inline-start / inline-end / start / end.
   2. Nothing here introduces a colour, a radius, a shadow or a duration that is
      not already a token. Where a measurement is a bare pixel value it is a
      *measure* — a column width, a field width — which is the one thing the
      token scale deliberately does not cover.

   The classes below replaced inline `style=` attributes one for one. The port
   left exactly four inline styles in the admin views, all of them a percentage
   computed per row (a meter fill, a bar width). Those are data, not styling,
   and belong on the element.
   ========================================================================== */

/* ── FORM AND TOOLBAR CONTROL WIDTHS ───────────────────────────────────────
   .input and .select are width:100% by default, which is right inside a .field
   and wrong inside a filter strip — a full-width select in a toolbar row eats
   the row. These size a control to what it holds instead.

   The caps matter more than they look. Without one, a single department named
   "Corporate Services · Information Technology · Plant B" sets the width of the
   whole filter strip and pushes the Filter button off the end of the bar.      */
.ctl-auto { width: auto; }
.ctl-cap-200 { max-width: 200px; }
.ctl-cap-240 { max-width: 240px; }
.ctl-cap-280 { max-width: 280px; }

/* Fixed measures for controls whose content has a known width. A date input
   sized to its own value rather than to the row stops the "add a holiday" strip
   reflowing every time the year changes. Named for the job, not the number, so
   changing the measure does not mean renaming the class in nine views. */
.ctl-date { width: 170px; }        /* yyyy-mm-dd plus the browser's picker glyph */
.ctl-date-sm { width: 150px; }     /* the audit from/to pair, which sit two-up   */
.ctl-time { width: 130px; }        /* hh:mm plus the picker                      */
.ctl-year { width: 110px; }        /* four digits and a stepper                  */
.ctl-day { width: 170px; }         /* the longest weekday name in three languages */
.ctl-address { width: 220px; }     /* an email address, roughly                  */
.ctl-search { width: 200px; }      /* a short free-text filter                   */
.search-sm { max-width: 280px; }   /* the search field inside a toolbar, not a page head */

/* ── PANEL AND SECTION EDGES ───────────────────────────────────────────────
   A .card-body that is followed by a table, or that sits between two other
   bodies, needs its own rule rather than the card's outer border. These are the
   weft rule and the structural rule applied as a section boundary.

   flush-b exists because a note sitting directly above a grid should touch it:
   the padding under the note plus the grid's own header band reads as a gap the
   fabric has fallen through. */
.card-body.rule-b { border-block-end: var(--dnm-rule-3); }
.card-body.rule-t { border-block-start: var(--dnm-rule-3); }
.card-head.rule-t { border-block-start: var(--dnm-rule-3); }
.card-body.flush-b { padding-block-end: 0; }

/* The mailbox callout on an OU, and the routing callout on a category. Both are
   the single most operationally important value on their screen, so the note
   takes the indigo thread on its leading edge rather than the neutral slate one
   — the same device dnm.css uses for .note-info, stated explicitly because
   these two are .note-info already and would otherwise be indistinguishable
   from the four other info notes on the same page. */
.note-keyed { box-shadow: inset var(--dnm-edge-x) 0 0 var(--dnm-ink-600); }

/* The value inside that callout: the address itself, or the department a
   category routes to. One step up from body size and one weight up, which is as
   loud as anything gets outside a page heading. */
.keyed-value {
  font-size: var(--dnm-t-15);
  font-weight: 600;
  margin-block-start: var(--dnm-space-1);
}

/* ── HEADINGS ──────────────────────────────────────────────────────────────
   h1 is 22px. Two screens want it smaller because the heading is a person's
   name sitting beside a 40px avatar, or a preview of an article title inside a
   card, and 22px in either place reads as the page heading twice. */
.h1-sm { font-size: 20px; }
.h1-preview { font-size: 21px; line-height: 1.25; }

/* One instrument cell reads "On" or "Off" rather than a figure. 28px Archivo on a
   two-letter word is a slogan, not a measurement, so it drops to heading size.
   Qualified with `.stat .v` because that is the specificity dnm.css sets the 28px
   at, and a single class would lose to it. */
.stat .v.stat-word { font-size: 20px; }

/* ── GRIDS AND TABLES ──────────────────────────────────────────────────────
   .stats is auto-fit at 168px, which is right for a four- or six-up instrument
   row across the page. Inside a panel there is room for three at most, and
   168px leaves the third one orphaned on its own line. */
.stats-tight { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }

/* .kv defaults to a 140px term column. Two rails need it narrower — the terms
   are single words ("Views", "Helpful") and 140px puts the value halfway across
   the panel — and one needs it wider, because its terms are code fragments in
   mono that must not wrap. */
.kv-narrow { grid-template-columns: 96px minmax(0, 1fr); }
.kv-code { grid-template-columns: 120px minmax(0, 1fr); }

/* A table nested inside a card body — the expanded SLA targets, a calendar's
   week — is not a data surface in its own right. It keeps the grid's rules and
   drops the surface fill, so it reads as part of the panel rather than as a
   second sheet laid on top of it. */
.tbl-plain { background: transparent; }
.tbl-narrow { max-width: 640px; }

/* Column measures. Percentages rather than pixels because these three columns
   are inputs that should share the remaining width evenly however wide the
   window is. */
.col-26 { width: 26%; }
.col-date { width: 150px; }
.col-date-sm { width: 92px; }
.col-day { width: 130px; }

/* Cell caps. A cell with no cap takes the width of its longest value, and one
   500-character SMTP error is enough to push every other column off the screen.
   These are maximums, not widths: a short value still shrinks. */
.cell-cap-sm { max-width: 220px; }
.cell-cap-md { max-width: 320px; }
.cell-cap-lg { max-width: 340px; }
.cell-cap-xl { max-width: 360px; }
.cell-cap-2xl { max-width: 420px; }
/* An expansion row sits under the row it belongs to and must read as attached
   to it, so the top padding that would separate them is removed. */
.cell-flush-t { padding-block-start: 0; }

/* The delivery-failure text in the outbox. Selvedge as *text* is one of the
   four sanctioned uses; it is a boundary condition, not a fill. display:block
   is what makes .truncate work here — an inline span has no width to clip to. */
.cell-error { color: var(--dnm-selv-700); display: block; }

/* .matrix is sized by its content by default. The impact × urgency grid is a
   fixed 3 × 3 and should fill its panel, or the cells bunch at the start edge
   and the panel looks half-empty. */
.matrix-full { width: 100%; }
/* Its row headers are the impact labels, which read as a column of trailing-edge
   captions against the cells. text-align:end, so they mirror. Qualified with
   `.matrix th` because dnm.css sets `text-align: start` at that specificity and
   a bare class would lose to it. */
.matrix th.matrix-rowhead { text-align: end; }

/* ── SMALL PARTS ───────────────────────────────────────────────────────────
   The "move down" control is the "move up" glyph turned over. A rotation of
   exactly 180° is its own mirror image, so this needs no RTL override — unlike
   a 90° rotation, which would point the wrong way in Arabic. */
.ico-flip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transform: rotate(180deg);
}

/* .icon-btn is 26px, which is the toolbar size. Inside a grid row it has to fit
   a 26px row without setting its height, so it comes down to 22px. */
.icon-btn-xs { width: 22px; height: 22px; }

/* A whole tree row that is a link. The row already carries the hover, the
   selection thread and the rules; the anchor inside it must add nothing —
   an underline on hover would draw a line across the middle of the row. */
.tree-link { color: inherit; text-decoration: none; }
.tree-link:hover { color: inherit; text-decoration: none; }

/* The capability matrix tick. Moss is the "met" signal and the only place a
   signal colour is used as a glyph colour rather than as an edge; the cross
   beside it is .dim, so the pair still reads without colour. */
.tick-yes { color: var(--dnm-moss); }

/* Text that must keep the line breaks it was typed with — an email body preview
   rendered as the recipient will see it. 1.6 rather than the 1.55 used for
   thread text because this is a block of plain text with no paragraph spacing
   to separate its parts. */
.preformatted { white-space: pre-wrap; line-height: 1.6; }

/* The one-time password, and the only place in the system that overrides the
   mono face's own tracking. This string is transcribed by hand or read down a
   telephone; the extra half-pixel between glyphs is what separates l from 1
   from I at the moment it matters most. */
.otp-value { font-size: 16px; letter-spacing: .5px; }

/* A numbered explanation, as prose rather than as data. The indent is
   inline-start so the numbers sit on the reading edge in both directions; the
   browser default is padding-inline-start already, but it is stated because the
   measure is deliberate — 18px is the width of a two-digit number plus its
   full stop, so no item hangs. */
.steps { padding-inline-start: 18px; line-height: 1.7; }

/* The article preview's summary line. 13px because that is the size the reader
   gets it at, and the whole point of the preview is to be exactly what they see —
   .sm at 12px would make the preview quietly wrong. */
.summary-lede { font-size: var(--dnm-t-13); }


/* ==========================================================================
   STAGE 2 — THE SELF-SERVICE PORTAL, KNOWLEDGE BASE AND REPORTS
   ========================================================================== */

/* ==========================================================================
   STAGE 2 — the self-service side.

   Portal home, the knowledge base, the four screens that render outside the
   app shell (sign in, refusal, password change, the error page) and the
   reports screen. Everything here is a rule the delivered dnm.css does not
   carry yet; it lives in its own file only because dnm.css is being edited
   by three other ports at the same time. Each block names the dnm.css
   section it belongs in so the merge is mechanical.

   The two laws of dnm.css hold here without exception:

   1. Class names are load-bearing. Nothing below renames anything; it only
      adds names that did not exist.
   2. No physical direction properties. Every measure is logical —
      padding-inline, border-inline-end, inset-inline-start, text-align:start
      — because Arabic ships and RTL is one attribute on <html>, not a
      second stylesheet.

   Where a value had to stay a literal rather than a token, the reason is at
   the point of use.
   ========================================================================== */

/* ═══════════════════════════════════════════════ A. THE SOLO PAGE ═════════
   Merges into dnm.css beside the LOGIN block.

   Four screens render with Layout = null and no app shell: the error page,
   the permission refusal, the password change and the "your password is in
   Active Directory" message. Each of them had hand-rolled the same
   min-height/grid/place-items centring inline, which is exactly how the four
   drifted into three different widths and four different paddings.

   One object instead. Ecru ground, not the browser default: two of the four
   were rendering on #fff and read as a different product from the one the
   user had just been signed out of.
   ═════════════════════════════════════════════════════════════════════════ */
.solo {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--dnm-space-7);
  background: var(--dnm-ecru);
}
.solo-sheet { width: 100%; max-width: 520px; }
/* The password form is a form, not a message, and 520px of field is wider
   than any of the three inputs on it needs to be. */
.solo-sheet.narrow { max-width: 440px; }
/* Roomier than .card-body deliberately: this sheet is the only thing on the
   screen, and at the card's 12px it reads as a dialog that lost its window. */
.solo-body { padding-block: var(--dnm-space-8); padding-inline: var(--dnm-space-8); }

/* The glyph over the headline. Which kind of message this is becomes one
   class rather than an inline colour repeated on four screens. */
.solo-mark { color: var(--dnm-ink-600); margin-block-end: var(--dnm-space-5); }
.solo-mark.warn  { color: var(--dnm-ochre); }
.solo-mark.alert { color: var(--dnm-selv); }

.solo h1 { font-size: var(--dnm-t-22); margin-block-end: var(--dnm-space-4); }
.solo-actions {
  display: flex; align-items: center; gap: var(--dnm-space-4);
  margin-block-start: var(--dnm-space-7);
}
.solo-actions.center { justify-content: center; }
/* The sign-out control is a POST, so it is a form wrapping a button. Without
   this the form becomes a block-level flex item and the button beside it
   drops onto its own line. */
.solo-actions form { display: inline-flex; }

/* The brand lockup over the password screen. Same object as the sidebar
   brand, at the two sizes the standalone screens use. */
.brand-lockup { display: flex; align-items: center; justify-content: center; gap: 10px; }
.brand-lockup .nm {
  font-family: var(--dnm-font-display); font-weight: 700; font-size: 14px;
  letter-spacing: -.02em;
}
.brand-mark-md { width: 30px; height: 30px; font-size: var(--dnm-t-mono); background: var(--dnm-ink-800); color: var(--dnm-surface); }
.brand-mark-lg { width: 34px; height: 34px; font-size: var(--dnm-t-13); }

/* ═══════════════════════════════════════════════════════ B. SIGN IN ══════
   Merges into the LOGIN block. .auth-wrap, .auth-art and .auth-panel are
   already there; what was missing is everything *inside* the art panel,
   which the view was drawing with eleven inline styles.
   ═════════════════════════════════════════════════════════════════════════ */
.auth-brand { display: flex; align-items: center; gap: var(--dnm-space-5); }
.auth-brand .nm {
  font-family: var(--dnm-font-display); font-weight: 700; font-size: 16px;
  color: var(--dnm-surface); letter-spacing: -.02em; line-height: 1.15;
}
.auth-brand .sub {
  font-size: var(--dnm-t-micro); letter-spacing: .14em;
  text-transform: var(--dnm-case-label); color: var(--dnm-ink-400);
}

.auth-say { max-width: 440px; }
/* The 56px selvedge dash over the headline: the same "this is an edge"
   device as the sidebar's woven strip, cut down to a mark. Drawn by ::before
   rather than an empty <div>, so the view carries no presentational element. */
.auth-say::before {
  content: ''; display: block; width: 56px; height: var(--dnm-edge);
  background: var(--dnm-selv); margin-block-end: 22px;
}
.auth-say h1 { color: var(--dnm-surface); font-size: 36px; line-height: 1.15; letter-spacing: -.03em; }
.auth-say p { color: var(--dnm-ink-200); font-size: 14px; line-height: 1.65; margin-block-start: 14px; }
/* The quietest text in the system, on the darkest ground. ink-400 rather
   than the previous one-off #4d628e: it is one step brighter, which the
   plant's uncalibrated monitors need, and it is a token. */
.auth-foot { font-size: var(--dnm-t-label); color: var(--dnm-ink-400); }

.auth-inner h2 { font-size: 23px; margin-block-end: var(--dnm-space-3); }

/* The DOMAIN\ prefix in front of the username field. Two boxes sharing one
   rule: the addon drops its trailing border and both drop their radius, so
   the pair reads as one control. Radius is dropped rather than mirrored
   because the logical corner radii (border-start-start-radius and friends)
   are newer than the browsers on the shop floor, and at 2px a corner that is
   simply absent is better than one that lands on the wrong side in Arabic. */
.input-group { display: flex; }
.input-group .addon {
  display: grid; place-items: center;
  padding-inline: 10px;
  background: var(--canvas-deep);
  border: 1px solid var(--dnm-field-line);
  border-inline-end: 0;
  border-radius: var(--dnm-radius-0);
  font-family: var(--dnm-font-mono); font-size: var(--dnm-t-mono);
  color: var(--dnm-slate); white-space: nowrap;
}
.input-group .input { border-radius: var(--dnm-radius-0); }

/* ══════════════════════════════════════════════════ C. PORTAL HOME ═══════
   Merges into PANELS.

   The requester portal is a different product from the agent workspace and
   the delivered screen treats it as one: one question, one oversized field,
   and targets sized for a thumb on a terminal bolted to a loom.
   ═════════════════════════════════════════════════════════════════════════ */

/* The ask band. It sits above the requester's own queue because the portal's
   first job is to end the request before it becomes one. Surface ground
   closed by the 2px structural rule, so it reads as the head of the sheet
   rather than as a card floating on it. */
.ask {
  background: var(--dnm-surface);
  border: var(--dnm-rule-2);
  border-block-end: var(--dnm-rule-1);
  padding-block: var(--dnm-space-6);
  padding-inline: var(--dnm-space-6);
}
.ask h2 { font-size: var(--dnm-t-22); margin-block-end: var(--dnm-space-5); }
.ask-form { display: flex; align-items: stretch; gap: var(--dnm-space-4); max-width: 720px; flex-wrap: wrap; }
.ask-form .search { flex: 1 1 260px; }
.ask-form .btn { height: 38px; }

/* The oversized search field. .search already places the glyph on the
   leading edge in both directions, so this changes the two measures the band
   needs and nothing else — 26px and 12px are desk measurements and this
   field is used standing up. */
.search.search-lg { max-width: none; }
.search.search-lg input { height: 38px; font-size: 14px; }

/* "Who handles what", as the delivered category matrix: cells on the warp,
   two up, no icon and no illustration. A department becomes a block the size
   of a thumb rather than a line in a list. */
.dept-cells { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
@media (max-width: 700px) { .dept-cells { grid-template-columns: minmax(0, 1fr); } }
.dept-cell {
  min-width: 0;
  background: var(--dnm-surface);
  padding-block: var(--dnm-space-4) var(--dnm-space-5);
  padding-inline: var(--dnm-space-5);
  border-inline-end: var(--dnm-rule-2);
  border-block-end: var(--dnm-rule-3);
}
.dept-cell .t {
  font-family: var(--dnm-font-display); font-size: 11.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: var(--dnm-case-label);
  color: var(--dnm-ink-900); line-height: 1.25;
}
.dept-cell .d { font-size: var(--dnm-t-label); color: var(--dnm-slate); line-height: 1.4; margin-block-start: var(--dnm-space-2); }
.dept-cell .e {
  display: flex; align-items: center; gap: var(--dnm-space-3);
  margin-block-start: var(--dnm-space-3);
  font-size: var(--dnm-t-micro); color: var(--dnm-slate); min-width: 0;
}
.dept-cell .e .mono { overflow: hidden; text-overflow: ellipsis; }

/* ═════════════════════════════════════ D. THE REQUESTER'S OWN GRID ═══════
   Merges into LOOM GRID, beside .tbl-compact.

   The same Loom Grid as the agent queue with three columns instead of nine —
   one grid, two audiences. It runs one step *looser* than standard rather
   than tighter: the agent's 26px row is a desk measurement, and this table
   is read at arm's length beside a running machine. There is no 25-row proof
   to hold here, because a requester with 25 open requests has a different
   problem than a layout one.                                                */
.tbl-requester { --dnm-row-h: 40px; --dnm-cell-pad: 12px; }
.tbl-requester tbody td { font-size: var(--dnm-t-13); }
/* The whole subject cell is the target, not the four words of the link. */
.tbl-requester tbody td > a { display: block; min-width: 0; color: inherit; }
.tbl-requester tbody td > a:hover { text-decoration: none; }
/* The plain-language line under the subject wraps rather than clipping. It
   carries the only deadline the requester is given — "we aim to fix this by
   3 Mar · 14:00" — and an ellipsis in the middle of that is worse than a
   two-line row. */
.tbl-requester .subject-cell .m { flex-wrap: wrap; white-space: normal; }

/* ═════════════════════════════════════════════════ E. KNOWLEDGE BASE ═════
   Merges into PANELS, beside .kb-card.                                      */

/* Articles are a stack of ruled rows, not a grid of cards. .kb-card already
   draws a row and closes it with a weft rule; laid out in a three-column CSS
   grid it drew that rule under every *cell* and the result was a
   chequerboard rather than a fabric. */
.kb-list { display: flex; flex-direction: column; }
.kb-list > .kb-card:last-child { border-block-end: 0; }

/* The deflection strip carries prose beside two buttons. .grow sets min-width:0
   so a flex item can shrink below its content, which here let the sentence
   squeeze to one word per line the moment both buttons were present. */
.deflect > .row > .grow { min-width: 220px; }

/* An article title is prose. It is the one heading in the system that is not
   set in the tracked uppercase, for the same reason .kb-card h3 is not:
   uppercase is a label treatment and a sentence is not a label. */
.kb-title { font-size: 21px; line-height: 1.25; letter-spacing: -.015em; text-transform: none; }
.kb-lede { font-size: var(--dnm-t-13); color: var(--dnm-slate); margin-block-start: var(--dnm-space-4); }
/* The article's fact list is narrower than the default .kv warp: its keys
   are one word each, and a 140px column beside "Views · 412" is a gap. */
.kv-narrow { grid-template-columns: 96px minmax(0, 1fr); }

/* ══════════════════════════════════════════════════════ F. REPORTS ═══════
   Merges into INSTRUMENTS AND MEASURES.

   The charts on this page are drawn by dnm.css. What was inline here was
   never a chart — it was three measurements that had no class to live in.   */

/* The SLA compliance list stacks meters at reading distance rather than in a
   table cell, so the bar is thicker than the 6px inline gauge. */
.meter-lg { height: 9px; }
/* A meter inside a grid cell has no intrinsic width and collapses to the
   column's content width, which for a 0% bar is zero. */
.col-meter { min-width: 120px; }
