/* =============================================================
 * OxyLo Hub — theme v2 (contrast + typography pass)
 * =============================================================
 * Load AFTER css/oxylo.css so the token overrides here win:
 *
 *   <link href=".../css/oxylo.css?v=..." rel="stylesheet">
 *   <link href=".../css/oxylo-theme-v2.css?v=..." rel="stylesheet">
 *
 * Nothing in v1 is deleted. This file (a) retunes the --ox-*
 * tokens, (b) re-skins the app chrome (dark nav rail), and
 * (c) adds the small set of new components the redesign uses.
 * Existing templates keep working untouched; the new classes are
 * opt-in.
 *
 * Authoring rules from v1 still apply: no raw hex or px inside
 * component blocks — reference a var.
 * ============================================================= */


/* -------------------------------------------------------------
 * 1. Token overrides
 * ------------------------------------------------------------- */
/* -------------------------------------------------------------
 * Archivo - self-hosted (Tom 2026-08-26)
 * -------------------------------------------------------------
 * The typeface from the design handoff's prototype. Served out of
 * static/fonts/ rather than fonts.googleapis.com: the hub runs on a lab
 * network, and a page whose type depends on reaching Google is a page
 * that renders wrong the day that fails. It also keeps who-uses-the-hub
 * out of a third party's logs.
 *
 * One variable file per subset covers 400-800, so asking for weight 650
 * costs nothing extra - which is why the scale leans on weight rather
 * than pulling in a second family.
 * ------------------------------------------------------------- */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/archivo-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                 U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/archivo-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


:root {
  /* Brand — graphite. The navy went (Tom 2026-08-26): the hub read as
   * uniformly blue, and while the accent was the visible part, the
   * blue-tinted greys below were where most of that feeling lived.
   *
   * Graphite rather than another colour because the palette is already
   * full. Teal is --ox-success, bronze is --ox-warning and --ox-accent,
   * red is --ox-danger. An accent adjacent to any of them costs that
   * status its signal — the same reason the design handoff's prototype
   * red was not ported. So colour on these pages means something now,
   * and the brand carries weight instead of hue.
   *
   * 12.6:1 on --ox-surface and 14.1:1 under white as a button ground.
   * Deeper than this stops reading as a colour at all.
   *
   * on-dark is NOT derived from this. It sits on the var(--ox-surface-dark) rail, so
   * it travels the other way as a light warm tint; darkening it in
   * sympathy with the brand would make the rail nav illegible. */
  --ox-brand:           #2D2B2B;
  --ox-brand-hover:     #201E1D;
  --ox-brand-pressed:   #141312;
  --ox-brand-tint:      #F0EEED;
  --ox-brand-tint-2:    #D2CDCA;
  --ox-brand-on-dark:   #E8E4E2;   /* brand legible on the nav rail */
  --ox-brand-icon-dark: #C6C1BE;

  /* Ink — warm. Muted held at 5.8:1 on white, matching what the
   * cool grey it replaces measured, so captions and table
   * secondaries do not start reading as disabled text. */
  --ox-ink:             #201E1D;
  --ox-ink-muted:       #6A6563;
  --ox-ink-secondary:   #4A4644;   /* NEW — table values, meta */
  --ox-ink-faint:       #918B89;

  /* Surfaces — warm. Same lightness steps as before, hue taken off
   * blue. A whole app of blue-tinted grey is most of what "too much
   * blue" actually meant; the accent was only the loud part. */
  --ox-surface:         #F3F2F2;
  --ox-surface-raised:  #FFFFFF;
  --ox-surface-sunken:  #EDEBEA;
  --ox-surface-footer:  #FAF9F8;   /* NEW — table footers, toolbars */

  /* Nav rail (NEW) — warm near-black rather than blue-black. */
  --ox-nav-bg:          #1E1B1A;
  --ox-nav-bg-active:   #33302E;
  --ox-nav-hairline:    #2E2B29;
  --ox-nav-text:        #BDB7B4;
  --ox-nav-text-active: #FFFFFF;
  --ox-nav-icon:        #8B8481;
  --ox-nav-eyebrow:     #8B8481;

  /* Hairlines — one step stronger; the v1 pair was too close to
   * the surface to read as structure. */
  --ox-hairline:        #DCD8D6;
  --ox-hairline-soft:   #EBE8E6;   /* NEW — table row rules */
  --ox-hairline-strong: #C6C1BE;
  --ox-hairline-table:  #ABA5A2;   /* NEW — under table headers */
  --ox-surface-header:  #E7E4E3;   /* NEW — table header band. Deliberately
                                    * two steps off the page ground so the
                                    * header reads as a rule, not a wash. */

  /* Status — tints keep their hue, text steps darkened to clear
   * 4.5:1 on their own tint. */
  --ox-success:         #0F766E;
  --ox-success-ink:     #0F5F58;
  --ox-success-tint:    #E4F1EC;
  --ox-success-line:    #BFE0D6;
  --ox-warning:         #D9880F;
  --ox-warning-ink:     #8C4307;
  --ox-warning-tint:    #FBF0DC;
  --ox-warning-line:    #EDD7AC;
  --ox-warning-wash:    #FCF9F2;   /* NEW — row-level wash */
  --ox-danger:          #B91C1C;
  --ox-danger-ink:      #93211F;
  --ox-danger-tint:     #FAE9E7;
  --ox-danger-line:     #EDC7C3;
  --ox-danger-wash:     #FCF6F5;   /* NEW — row-level wash */
  /* Info stays blue, and is now the only blue in the hub. That is the
   * point of moving the brand off it: blue reads as "this is a note"
   * rather than as the house colour. */
  --ox-info:            #0B4A9E;
  --ox-info-tint:       #EAF1FA;
  --ox-info-line:       #C9DBF1;
  --ox-accent:          #B45309;   /* focus ring — unchanged */

  /* Decoration — the tier with no loud member (2026-09-24).
   *
   * The problem these solve: the palette above is full, so any new
   * hue used decoratively costs an existing one its meaning. The way
   * out is not to ration hue, it is to ration *salience*. A status is
   * a SATURATED MARK — a chip, a clock bar, a dot, a left rule. A
   * pale ground is never a status on its own, which is why
   * --ox-warning-wash can sit under a whole row without claiming the
   * row is overdue; the clock bar beside it is what claims that.
   *
   * So every token here is pale by construction: tints at L 94.5%,
   * lines at L 84.5%, against saturated statuses at L 26-45%. There
   * is deliberately no --ox-decor-*-strong, and adding one would undo
   * the whole arrangement. tests/test_colour_tiers.py reads this file
   * and fails if any decor token drops below L 78%.
   *
   * Use them for anything: card grounds, section headers, empty
   * states, illustration, a table band. They mean nothing, which is
   * the point — a reader never has to learn them.
   *
   * Hue order is deliberate. The first four are hues no status owns;
   * sage and sky sit nearest --ox-success and --ox-info, so reach for
   * them last if a status is already on the same screen. */
  --ox-decor-violet:    #F1EAF8;
  --ox-decor-violet-line: #D8CAE5;
  --ox-decor-plum:      #F8EAF6;
  --ox-decor-plum-line: #E5CAE0;
  --ox-decor-rose:      #F8EAED;
  --ox-decor-rose-line: #E5CAD1;
  --ox-decor-citron:    #F5F8EA;
  --ox-decor-citron-line: #E0E5CA;
  --ox-decor-clay:      #F8EEEA;
  --ox-decor-clay-line: #E5D2CA;
  --ox-decor-moss:      #EDF8EA;
  --ox-decor-moss-line: #D1E5CA;
  --ox-decor-sage:      #EAF8F1;
  --ox-decor-sage-line: #CAE5D7;
  --ox-decor-sky:       #EAF3F8;
  --ox-decor-sky-line:  #CADCE5;
  --ox-decor-stone:     #F2F1F0;   /* the hueless one */
  --ox-decor-stone-line: #DBD7D4;

  /* Category — the seven notebook-template section types (2026-09-24).
   *
   * These replace seven Bootstrap STATUS classes. A calculation table
   * was `text-danger` and a checklist `text-warning`, so the editor
   * said a calculation was an error and a checklist a caution — red
   * was not chosen, it was left over, and the violet below existed
   * only because Bootstrap ran out at six.
   *
   * One hue, seven lightnesses. A ramp rather than seven hues because
   * seven hues is a rainbow, and a rainbow next to a four-colour
   * status vocabulary is how a status stops being noticed. Violet
   * because no status owns it.
   *
   * Be honest about what this buys: seven steps of one hue are
   * distinguishable side by side, in a list, and are NOT individually
   * recallable — nobody will learn that L 44.5% means "parameters".
   * The seven distinct icons remain the identifier; the ramp groups
   * and decorates. If a pilot lab says they scan for a colour rather
   * than an icon, that is the signal to cut to four or five
   * categories, not to add hues.
   *
   * Order is by how structured the content is — prose, checklist,
   * table, parameters, calculation, plate — with the file last
   * because it is the one that leaves the system. Arbitrary would
   * have been fine visually; this is merely cheaper to remember.
   *
   * Every step clears 3:1 against --ox-surface-raised AND
   * --ox-surface (the tightest is cat-7 at 4.05:1 and 3.63:1), so the
   * icon reads on a card and on the page ground. Pinned by
   * tests/test_colour_tiers.py, which recomputes the contrast rather
   * than trusting these comments. */
  --ox-cat-1:           #401E71;   /* text */
  --ox-cat-2:           #4D2487;   /* checklist */
  --ox-cat-3:           #5A2A9D;   /* table */
  --ox-cat-4:           #6730B3;   /* parameter table */
  --ox-cat-5:           #7336C9;   /* calculation table */
  --ox-cat-6:           #8455C6;   /* well plate */
  --ox-cat-7:           #936ACD;   /* file upload */

  /* Type — Archivo, self-hosted at the top of this file. Hierarchy
   * still comes from weight, size and tracking rather than a second
   * family; the variable file makes that cheap. The platform stack
   * stays behind it, so a browser that cannot load the woff2 degrades
   * to exactly what shipped before rather than to Times. Mono is the
   * platform mono, used for every identifier, quantity, date and
   * timestamp. --ox-font-cond is kept as an alias so existing rules
   * that reference it keep resolving. */
  --ox-font-sans:  'Archivo', system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --ox-font-cond:  var(--ox-font-sans);
  --ox-font-mono:  ui-monospace, "SF Mono", Menlo, Consolas, "Roboto Mono", monospace;

  /* The gutter every page's content sits inside: 2.5% of the page
     width either side (2026-09-25). One token, because every page is
     wrapped by the same <main>.

     A percentage rather than the half-inch it started as, and the
     difference is not only the number. A fixed gutter has to be
     argued about at both ends -- 0.5in took a quarter of a 375px
     phone and looked mean on a 27" monitor -- so it needed a
     breakpoint to correct it. 5% is ~19px on a phone and ~128px on a
     2560px display without one. Percentage padding resolves against
     the containing block's WIDTH, which for <main> is the page. */
  --ox-page-gutter: 5%;

  --ox-fs-xxs:     0.6875rem;  /* 11px — eyebrows, mono chips */
  --ox-fs-xs:      0.75rem;    /* 12px */
  --ox-fs-sm:      0.84375rem; /* 13.5px — BASE BODY (denser) */
  --ox-fs-md:      0.9375rem;  /* 15px */
  /* CAREFUL: lg is SMALLER than md. Not a typo — v1 had lg at 20px for
   * h4, and v2 retuned it to 13.5px for uppercase section titles,
   * where the capitals carry the emphasis instead of the size. The
   * name is now wrong for what it does and h3 rides on it.
   * Left alone deliberately: changing the value restyles every h3 in
   * the app, which is a design decision rather than a tidy-up. When
   * somebody makes it, the fix is a `--ox-fs-section` token and a
   * proper step for lg. */
  --ox-fs-lg:      0.84375rem; /* 13.5px — section titles (uppercase) */
  --ox-fs-xl:      1.125rem;   /* 18px */
  --ox-fs-xxl:     2.1875rem;  /* 35px — page title */

  /* Radius — squarer. Data tools read as instruments, not cards. */
  --ox-radius-sm:  3px;
  --ox-radius-md:  5px;
  --ox-radius-lg:  6px;

  /* Weight (2026-09-23). The scale leaned on weight already — 24 rules
   * in this file alone say 600 — but the numbers were written out each
   * time, so "make the UI a little lighter" meant finding all of them.
   * Archivo is a variable font from 400 to 800, so these cost nothing
   * extra and any value between them is available if a design asks. */
  --ox-weight-normal:   400;
  --ox-weight-medium:   500;
  --ox-weight-semibold: 600;   /* the workhorse: labels, table headers */
  --ox-weight-bold:     700;

  /* Border width (2026-09-23). The missing half of "what a button
   * outline looks like" — the colour was a token, the width was 1px
   * written out 24 times. Focus is deliberately thicker than any
   * component border so a focus ring cannot be mistaken for a state. */
  --ox-bw-hairline: 1px;
  --ox-bw-thick:    2px;
  --ox-bw-focus:    3px;
  --ox-bw-edge:     3px;   /* the category rule down a card's start edge */

  /* Layout */
  --ox-topbar-h:   52px;
  --ox-sidebar-w:  224px;

  /* Referenced by oxylo.css and hub-module.css but defined nowhere
   * until 2026-09-23, so those rules resolved to nothing and silently
   * did without. Defined here rather than there because this file is
   * where tokens live and it loads last. */
  --ox-header-h:   var(--ox-topbar-h);
  --ox-mono:       var(--ox-font-mono);
}

/* Bootstrap bridge for the changed values */
:root {
  --bs-primary: var(--ox-brand);
  --bs-primary-rgb: 45, 43, 43;
  --bs-link-color: var(--ox-brand);
  --bs-link-color-rgb: 45, 43, 43;
  --bs-link-hover-color: var(--ox-brand-hover);
  --bs-body-color: var(--ox-ink);
  --bs-body-color-rgb: 32, 30, 29;
  --bs-body-bg: var(--ox-surface);
  --bs-border-color: var(--ox-hairline);
  --bs-body-font-size: var(--ox-fs-sm);
  --bs-font-sans-serif: var(--ox-font-sans);
  --bs-font-monospace: var(--ox-font-mono);
}


/* -------------------------------------------------------------
 * 2. Typography
 * ------------------------------------------------------------- */
body { font-family: var(--ox-font-sans); font-size: var(--ox-fs-sm); }

h1, .h1, h2, .h2, h3, .h3,
.ox-page-title, .ox-section-title, .card-header,
.ox-stat-value, .nb-lib-pane h1 {
  font-family: var(--ox-font-sans);
}
h1, .h1 { font-size: var(--ox-fs-xxl); font-weight: 700; line-height: 1.08; letter-spacing: -0.025em; }
h2, .h2 { font-size: var(--ox-fs-xl); font-weight: 600; }
h3, .h3 { font-size: var(--ox-fs-lg); font-weight: 600; }

/* Page title now announces itself: condensed, 34px, over a
 * 2px ink rule instead of a hairline. */
.ox-page-title {
  font-size: var(--ox-fs-xxl);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin: 0;
}
.ox-page-subtitle {
  font-size: var(--ox-fs-sm);
  color: var(--ox-ink-muted);
  margin: var(--ox-space-1) 0 0;
}
.ox-page-header {
  border-bottom: 2px solid var(--ox-ink);
  padding-bottom: var(--ox-space-3);
  margin-bottom: var(--ox-space-5);
}

/* Eyebrow above the page title — states which module you are in. */
.ox-eyebrow,
.ox-page-eyebrow {
  font-family: var(--ox-font-cond);
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--ox-brand);
  margin-bottom: 3px;
}
.ox-eyebrow { color: var(--ox-ink-muted); letter-spacing: 0.1em; }

/* Identifiers, quantities, dates and timestamps are all mono —
 * they align in a column and stop competing with prose. */
.ox-id, .ox-id-link {
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xs);
  font-weight: 500;
  letter-spacing: 0;
}
.ox-num, .ox-date, .ox-qty {          /* NEW */
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xs);
  font-variant-numeric: tabular-nums;
}
.ox-num-strong { font-weight: 600; color: var(--ox-ink); }

/* Muted text is no longer near-disabled grey. */
.text-muted, .form-text, small, .small {
  color: var(--ox-ink-muted) !important;
}
.ox-text-secondary { color: var(--ox-ink-secondary); }


/* -------------------------------------------------------------
 * 3. App chrome — dark nav rail, lighter top bar
 * ------------------------------------------------------------- */
.ox-topbar {
  height: var(--ox-topbar-h);
  background: var(--ox-surface-raised);
  border-bottom: 1px solid var(--ox-hairline);
  box-shadow: none;
  gap: var(--ox-space-3);
}
.ox-topbar-brand {
  font-size: 0.96875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.ox-topbar-divider {                  /* NEW */
  width: 1px; height: 22px;
  background: var(--ox-hairline);
  margin: 0 var(--ox-space-1);
}
.ox-topbar-search .form-control {
  background: var(--ox-surface-sunken);
  border: 1px solid var(--ox-hairline);
}
.ox-topbar-user-avatar {
  border-radius: var(--ox-radius-sm);
  font-family: var(--ox-font-mono);
}

/* Environment chip — "this is the validated production system",
 * the single cheapest credibility signal in the whole UI. */
.ox-env-chip {                        /* NEW */
  display: inline-flex;
  align-items: center;
  gap: var(--ox-space-1);
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xxs);
  letter-spacing: 0.06em;
  color: var(--ox-success);
  background: var(--ox-success-tint);
  border: 1px solid var(--ox-success-line);
  border-radius: var(--ox-radius-sm);
  padding: 3px 7px;
}
.ox-env-chip::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* ── Nav rail ─────────────────────────────────────────────── */
.ox-sidebar {
  top: var(--ox-topbar-h);
  width: var(--ox-sidebar-w);
  background: var(--ox-nav-bg);
  border-right: 0;
}
/* Top padding reserves the row occupied by the absolutely-positioned
   .ox-sidebar-toggle. v1 did this with calc(space-2 + 36px); the v2
   handoff shorthand dropped it, so the first nav row slid up under
   the toggle — most visible when collapsed, where the toggle centres
   in the icon rail and landed directly on the Dashboard icon
   (Tom 2026-07-27). Keep the 36px reservation. */
.ox-sidebar-nav {
  padding: calc(var(--ox-space-3) + 36px) var(--ox-space-2) var(--ox-space-3);
  gap: 1px;
}

/* ── Hover-to-peek (Tom 2026-07-27) ──────────────────────────────
   A collapsed sidebar temporarily expands after a hover dwell, then
   re-collapses shortly after the pointer leaves. `.ox-sidebar-peek`
   is applied by JS to <body>; it intentionally does NOT touch the
   saved collapse preference, so an explicit toggle still wins and
   survives the peek. Same widths + reveal rules as the real
   expanded state. */
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar { width: var(--ox-sidebar-w); }
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-nav-label,
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-nav-section,
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-sidebar-footer {
  opacity: 1;
}
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-nav-section {
  padding-left: var(--ox-space-3);
  padding-right: var(--ox-space-3);
}
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-nav-section-row {
  padding: var(--ox-space-3) var(--ox-space-3) var(--ox-space-1);
  justify-content: space-between;
}
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar .ox-nav-section-row .ox-nav-section {
  display: block;
}
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar-toggle {
  right: var(--ox-space-2);
  left: auto;
  transform: none;
}
/* The peek floats over content rather than reflowing the page —
   re-flowing the whole grid on a hover would be visually violent. */
body.ox-sidebar-collapsed.ox-sidebar-peek .ox-sidebar {
  z-index: 1045;
  box-shadow: 4px 0 18px rgba(0, 0, 0, 0.28);
}
.ox-nav-link {
  color: var(--ox-nav-text);
  border-radius: var(--ox-radius-md);
  padding: 7px var(--ox-space-3);
  font-size: var(--ox-fs-sm);
}
.ox-nav-link i { color: var(--ox-nav-icon); flex: 0 0 20px; font-size: var(--ox-fs-md); }
.ox-nav-link:hover { background: var(--ox-nav-bg-active); color: var(--ox-ink-inverse); }
.ox-nav-link:hover i { color: var(--ox-brand-icon-dark); }
.ox-nav-link.active {
  background: var(--ox-nav-bg-active);
  color: var(--ox-nav-text-active);
  font-weight: 600;
}
.ox-nav-link.active i { color: var(--ox-brand-icon-dark); }

/* ── Module sub-navigation ────────────────────────────────────────
 * Sub-pages of the module you are currently in, indented under its
 * row. Only rendered for the active module, so the rail never shows
 * more than one module's internals at a time.
 *
 * This replaced an in-page tab row. Tabs only existed on the pages
 * that had been rebuilt, so navigation appeared and disappeared as you
 * moved between them; the rail is on every page by construction.
 *
 * The indent guide is a left border on the container rather than a
 * marker per row, so the group reads as one branch rather than as
 * several unrelated items that happen to be inset.
 */
.ox-subnav {
  margin: 2px 0 6px var(--ox-space-3);
  padding-left: var(--ox-space-3);
  border-left: 1px solid var(--ox-nav-hairline, rgba(255,255,255,.09));
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.ox-subnav-link {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: 5px var(--ox-space-3);
  border-radius: var(--ox-radius-md);
  color: var(--ox-nav-text);
  text-decoration: none;
  /* One step down from the parent row, so the hierarchy is legible
   * without the labels becoming hard to read on the dark ground. */
  font-size: .8125rem;
}
.ox-subnav-link:hover { background: var(--ox-nav-bg-active); color: var(--ox-ink-inverse); }
.ox-subnav-link.active {
  background: var(--ox-nav-bg-active);
  color: var(--ox-nav-text-active);
  font-weight: 600;
}
.ox-subnav-badge {
  margin-left: auto;
  background: var(--ox-danger);
  color: var(--ox-ink-inverse);
  border-radius: 999px;
  font-size: .6875rem;
  font-weight: 700;
  line-height: 1;
  padding: 3px 6px;
  font-variant-numeric: tabular-nums;
}

/* Collapsed rail: the labels are hidden, so a list of unlabelled
 * sub-rows is a column of blank strips. Hide the group instead. */
body.ox-sidebar-collapsed .ox-subnav,
html.ox-sidebar-collapsed-init .ox-sidebar .ox-subnav { display: none; }

/* If overflow moves the parent module row into the More dropdown, its
 * sub-pages must go with it — otherwise the group is left indented under
 * whatever row happens to precede it, which reads as belonging to that
 * module instead. */
.ox-nav-collapsible.d-none + .ox-subnav { display: none; }
.ox-nav-link.active::before {
  left: 0;
  top: 5px; bottom: 5px;
  width: 3px;
  background: var(--ox-brand-on-dark);
}
.ox-nav-section, .ox-nav-section-row .ox-nav-section {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--ox-nav-eyebrow);
  padding: var(--ox-space-3) var(--ox-space-3) var(--ox-space-1);
}
.ox-sidebar-toggle,
.ox-nav-section-row .ox-section-menu-trigger { color: var(--ox-nav-eyebrow); }
.ox-sidebar-toggle:hover,
.ox-nav-section-row .ox-section-menu-trigger:hover {
  background: var(--ox-nav-bg-active);
  color: var(--ox-ink-inverse);
}
.ox-sidebar-footer {
  border-top: 1px solid var(--ox-nav-hairline);
  color: var(--ox-nav-eyebrow);
  font-size: var(--ox-fs-xxs);
  display: flex; align-items: center; justify-content: space-between;
}
.ox-sidebar-footer kbd { background: var(--ox-nav-bg-active); color: var(--ox-nav-text); border: 0; }
.ox-main { padding: 22px 28px 28px; }


/* -------------------------------------------------------------
 * 4. Panels — flat, ruled, square
 * ------------------------------------------------------------- */
/* Cards lose the shadow-and-radius float and become ruled panels.
 * Shadows on a light grey ground read as mush at this density;
 * a 1px border reads as structure. */
.card {
  background: var(--ox-surface-raised);
  border: 1px solid var(--ox-hairline);
  border-radius: 0;
  box-shadow: none;
}
.card-header {
  font-size: var(--ox-fs-lg);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: transparent;
  border-bottom: 1px solid var(--ox-hairline-soft);
  padding: var(--ox-space-3) var(--ox-space-4);
  display: flex;
  align-items: center;
  gap: var(--ox-space-2);
}
.card-body { padding: var(--ox-space-3) var(--ox-space-4); }
.card-footer {
  background: var(--ox-surface-footer);
  border-top: 1px solid var(--ox-hairline);
  font-size: var(--ox-fs-xs);
  color: var(--ox-ink-muted);
}

/* Metric strip — replaces five identical floating tiles with one
 * divided instrument panel. Tiles carry a 3px top rule in the
 * colour of their worst open item, so weight differs by state
 * instead of every tile looking the same. */
.ox-metrics {                          /* NEW */
  display: flex;
  background: var(--ox-surface-raised);
  border: 1px solid var(--ox-hairline);
  margin-bottom: var(--ox-space-5);
}
.ox-metric {
  flex: 1;
  padding: var(--ox-space-3) var(--ox-space-4);
  border-right: 1px solid var(--ox-hairline-soft);
  border-top: 3px solid var(--ox-hairline);
}
.ox-metric:last-child { border-right: 0; }
.ox-metric.is-brand   { border-top-color: var(--ox-brand); }
.ox-metric.is-warning { border-top-color: var(--ox-warning); }
.ox-metric.is-danger  { border-top-color: var(--ox-danger); }
.ox-metric.is-success { border-top-color: var(--ox-success); }
.ox-metric-label {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ox-ink-muted);
}
.ox-metric-row { display: flex; align-items: baseline; gap: var(--ox-space-2); margin-top: 2px; }
.ox-metric-value {
  font-size: 1.625rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}
.ox-metric-meta { font-size: var(--ox-fs-xs); font-weight: 600; color: var(--ox-ink-muted); }
.ox-metric-meta.is-warning { color: var(--ox-warning-ink); }
.ox-metric-meta.is-danger  { color: var(--ox-danger-ink); }
.ox-metric-meta.is-brand   { color: var(--ox-brand); }

/* Spec plate — label/value grid for record headers. Replaces the
 * four-column stack of small-muted-over-value in batch/view. */
.ox-spec {                             /* NEW */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
.ox-spec > div {
  padding: 11px var(--ox-space-4);
  border-right: 1px solid var(--ox-hairline-soft);
  border-bottom: 1px solid var(--ox-hairline-soft);
}
.ox-spec > div:nth-child(4n) { border-right: 0; }
.ox-spec-label {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ox-ink-muted);
}
.ox-spec-value { font-size: var(--ox-fs-md); margin-top: 2px; }
.ox-spec-value.ox-mono { font-family: var(--ox-font-mono); font-size: var(--ox-fs-sm); }
@media (max-width: 991.98px) { .ox-spec { grid-template-columns: repeat(2, 1fr); }
  .ox-spec > div:nth-child(2n) { border-right: 0; } }


/* -------------------------------------------------------------
 * 5. Tables — denser rows, structural header, status as a rule
 * ------------------------------------------------------------- */
.table { font-size: var(--ox-fs-sm); }
.table > :not(caption) > * > * {
  padding: 8px 12px;                          /* was 12px 16px */
  border-bottom: 1px solid var(--ox-hairline-soft);
}
.table thead th {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ox-ink-secondary);
  background: var(--ox-surface-header);
  border-bottom: 1px solid var(--ox-hairline-table);
}
.table-hover > tbody > tr:hover > * { background-color: var(--ox-brand-tint); }

/* Primary cell in a list row — the thing you scan for. */
.ox-cell-primary { font-weight: 600; color: var(--ox-ink); }
.ox-cell-sub {
  font-size: var(--ox-fs-xxs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ox-ink-muted);
}

/* Row status: a 3px left rule + a very light wash, instead of
 * flooding the whole row with a saturated tint. Keeps the text
 * legible and lets several states coexist in one table. Add
 * <td class="ox-row-rule"></td> as the first cell of every row. */
.ox-row-rule {                          /* NEW */
  width: 3px !important;
  padding: 0 !important;
  background: var(--ox-hairline-soft);
  border-bottom: 0 !important;
}
tr.is-ok      > .ox-row-rule { background: var(--ox-success); }
tr.is-warning > .ox-row-rule { background: var(--ox-warning); }
tr.is-danger  > .ox-row-rule { background: var(--ox-danger); }
tr.is-review  > .ox-row-rule { background: var(--ox-brand); }
tr.is-warning > td:not(.ox-row-rule) { background: var(--ox-warning-wash); }
tr.is-danger  > td:not(.ox-row-rule) { background: var(--ox-danger-wash); }
tr.is-muted   > td:not(.ox-row-rule) { color: var(--ox-ink-faint); }

/* Retire the Bootstrap full-row floods in favour of the above. */
.table > tbody > tr.table-danger  > *,
.table > tbody > tr.table-warning > *,
.table > tbody > tr.table-success > *,
.table > tbody > tr.table-secondary > * { --bs-table-bg-state: transparent; }
.table > tbody > tr.table-danger  > * { box-shadow: inset 3px 0 0 var(--ox-danger);  background: var(--ox-danger-wash); }
.table > tbody > tr.table-warning > * { box-shadow: inset 3px 0 0 var(--ox-warning); background: var(--ox-warning-wash); }
.table > tbody > tr.table-success > * { box-shadow: inset 3px 0 0 var(--ox-success); background: transparent; }
.table > tbody > tr.table-secondary > * { color: var(--ox-ink-faint); background: transparent; }
.table > tbody > tr.table-danger  > *:not(:first-child),
.table > tbody > tr.table-warning > *:not(:first-child),
.table > tbody > tr.table-success > *:not(:first-child),
.table > tbody > tr.table-secondary > *:not(:first-child) { box-shadow: none; }

/* Inline stock / fill meter — 3px bar under a quantity. */
.ox-meter {                             /* NEW */
  height: 3px;
  background: var(--ox-hairline-soft);
  margin-top: 4px;
}
.ox-meter > span { display: block; height: 3px; background: var(--ox-success); }
.ox-meter.is-warning > span { background: var(--ox-warning); }
.ox-meter.is-danger  > span { background: var(--ox-danger); }

/* Table footer row: count + pager. */
.ox-table-foot {                        /* NEW */
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px var(--ox-space-3);
  border-top: 1px solid var(--ox-hairline);
  background: var(--ox-surface-footer);
  font-size: var(--ox-fs-xs);
  color: var(--ox-ink-muted);
}


/* -------------------------------------------------------------
 * 6. Status vocabulary
 * ------------------------------------------------------------- */
/* Pills become square, bordered and condensed — they read as
 * record states, not marketing chips, and they hold contrast at
 * 11px where a pale rounded badge does not. */
.ox-pill, .badge {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  border-radius: var(--ox-radius-sm);
  padding: 2px 8px;
  border: 1px solid transparent;
}
.ox-pill.is-draft    { background: var(--ox-surface-footer); border-color: var(--ox-hairline); color: var(--ox-ink-muted); }
.ox-pill.is-review   { background: var(--ox-info-tint);    border-color: var(--ox-info-line);    color: var(--ox-brand); }
.ox-pill.is-progress { background: var(--ox-warning-tint); border-color: var(--ox-warning-line); color: var(--ox-warning-ink); }
.ox-pill.is-approved { background: var(--ox-success-tint); border-color: var(--ox-success-line); color: var(--ox-success-ink); }
.ox-pill.is-danger   { background: var(--ox-danger-tint);  border-color: var(--ox-danger-line);  color: var(--ox-danger-ink); }
.ox-pill.is-released { background: var(--ox-ink);          border-color: var(--ox-ink);          color: var(--ox-ink-inverse); }

.badge.bg-success   { background: var(--ox-success-tint) !important; border-color: var(--ox-success-line); color: var(--ox-success-ink) !important; }
.badge.bg-warning   { background: var(--ox-warning-tint) !important; border-color: var(--ox-warning-line); color: var(--ox-warning-ink) !important; }
.badge.bg-danger    { background: var(--ox-danger-tint) !important;  border-color: var(--ox-danger-line);  color: var(--ox-danger-ink) !important; }
.badge.bg-info      { background: var(--ox-info-tint) !important;    border-color: var(--ox-info-line);    color: var(--ox-brand) !important; }
.badge.bg-secondary { background: var(--ox-surface-sunken) !important; border-color: var(--ox-hairline); color: var(--ox-ink-secondary) !important; }
.badge.bg-light     { background: var(--ox-surface-sunken) !important; border-color: var(--ox-hairline); color: var(--ox-ink-secondary) !important; }

/* Module tag — mono, boxed. Used where a row names its source
 * module (dashboard action items, search results). */
.ox-tag-module {                        /* NEW */
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xxs);
  letter-spacing: 0.05em;
  color: var(--ox-ink-secondary);
  background: var(--ox-surface-sunken);
  border: 1px solid var(--ox-hairline);
  padding: 2px 6px;
}
/* Count chips beside a section title: "4 CRITICAL / 6 DUE SOON". */
.ox-count {                             /* NEW */
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  padding: 1px 6px;
  border: 1px solid transparent;
}
.ox-count.is-danger  { color: var(--ox-danger-ink);  background: var(--ox-danger-tint);  border-color: var(--ox-danger-line); }
.ox-count.is-warning { color: var(--ox-warning-ink); background: var(--ox-warning-tint); border-color: var(--ox-warning-line); }
.ox-count.is-review  { color: var(--ox-brand);       background: var(--ox-info-tint);    border-color: var(--ox-info-line); }
.ox-count.is-ok      { color: var(--ox-success);     background: var(--ox-success-tint); border-color: var(--ox-success-line); }


/* -------------------------------------------------------------
 * 7. Progress + pipeline visuals
 * ------------------------------------------------------------- */
/* Proportional severity bar — sits directly under a panel header
 * and shows the shape of the queue before you read a single row. */
.ox-sevbar {                            /* NEW */
  display: flex;
  height: 6px;
}
.ox-sevbar > span { display: block; height: 6px; }
.ox-sevbar .seg-danger  { background: var(--ox-danger); }
.ox-sevbar .seg-warning { background: var(--ox-warning); }
.ox-sevbar .seg-review  { background: var(--ox-brand); }
.ox-sevbar .seg-ok      { background: var(--ox-success); }

/* Step ticks — one cell per batch step, coloured by state. Reads
 * as "12 of 18" at a glance without a number. */
.ox-steps {                             /* NEW */
  display: flex;
  gap: 2px;
}
.ox-steps > span { flex: 1; height: 10px; background: var(--ox-hairline); }
.ox-steps > span.is-done     { background: var(--ox-success); }
.ox-steps > span.is-running  { background: var(--ox-warning); }
.ox-steps > span.is-blocked  { background: var(--ox-danger); }

/* Horizontal stage bars — the CRM/batch pipeline panel. */
.ox-stage {                             /* NEW */
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.ox-stage-row-head {
  display: flex; justify-content: space-between;
  font-size: var(--ox-fs-xs); color: var(--ox-ink-secondary);
  margin-bottom: 4px;
}
.ox-stage-row-head b { font-family: var(--ox-font-mono); color: var(--ox-ink); }
.ox-stage-bar { height: 7px; background: var(--ox-hairline-soft); }
.ox-stage-bar > span { display: block; height: 7px; background: var(--ox-ink-faint); }
.ox-stage-bar.is-progress > span { background: var(--ox-warning); }
.ox-stage-bar.is-review   > span { background: var(--ox-brand); }
.ox-stage-bar.is-approved > span { background: var(--ox-success); }
.ox-stage-bar.is-released > span { background: var(--ox-ink); }

/* Legend under a progress visual. */
.ox-legend {                            /* NEW */
  display: flex; align-items: center; gap: 18px;
  font-size: var(--ox-fs-xs); color: var(--ox-ink-muted);
}
.ox-legend span.key { width: 9px; height: 9px; display: inline-block; }


/* -------------------------------------------------------------
 * 8. Buttons + toolbar
 * ------------------------------------------------------------- */
.btn {
  border-radius: var(--ox-radius-md);
  font-weight: 500;
  font-size: var(--ox-fs-sm);
  padding: 7px var(--ox-space-3);
}
.btn-primary { font-weight: 600; }
.btn-secondary, .btn-light {
  background: var(--ox-surface-raised);
  border-color: var(--ox-hairline-strong);
  color: var(--ox-ink);
}
.btn-outline-secondary {
  border-color: var(--ox-hairline-strong);
  color: var(--ox-ink-secondary);
  background: var(--ox-surface-raised);
}
.btn:active, .btn-primary:active { background: var(--ox-brand-pressed); }

/* Segmented filter with counts baked in. The active segment is
 * ink-filled — one solid object per toolbar, easy to find. */
.ox-seg {                               /* NEW */
  display: flex;
  border: 1px solid var(--ox-hairline-strong);
  border-radius: var(--ox-radius-md);
  overflow: hidden;
  background: var(--ox-surface-raised);
}
.ox-seg > a, .ox-seg > button {
  padding: 7px var(--ox-space-3);
  font-size: var(--ox-fs-xs);
  color: var(--ox-ink-secondary);
  border: 0;
  border-left: 1px solid var(--ox-hairline);
  background: transparent;
  text-decoration: none;
}
.ox-seg > :first-child { border-left: 0; }
.ox-seg > .active { background: var(--ox-ink); color: var(--ox-ink-inverse); font-weight: 600; }
.ox-seg > .is-warning { color: var(--ox-warning-ink); }
.ox-seg > .is-danger  { color: var(--ox-danger-ink); }
.ox-seg > :hover:not(.active) { background: var(--ox-surface-sunken); color: var(--ox-ink); }

/* Breadcrumb */
.ox-breadcrumb { font-size: var(--ox-fs-xs); color: var(--ox-ink-muted); }
.ox-breadcrumb [aria-current="page"] {
  font-family: var(--ox-font-mono);
  font-weight: 600;
  color: var(--ox-ink);
}


/* -------------------------------------------------------------
 * 9. Alerts + forms
 * ------------------------------------------------------------- */
.alert {
  border: 1px solid;
  border-left-width: 3px;
  border-radius: var(--ox-radius-sm);
  box-shadow: none;
  padding: var(--ox-space-2) var(--ox-space-3);
  font-size: var(--ox-fs-sm);
}
.alert-success { background: var(--ox-success-tint); border-color: var(--ox-success-line); border-left-color: var(--ox-success); color: var(--ox-success-ink); }
.alert-warning { background: var(--ox-warning-tint); border-color: var(--ox-warning-line); border-left-color: var(--ox-warning); color: var(--ox-warning-ink); }
.alert-danger  { background: var(--ox-danger-tint);  border-color: var(--ox-danger-line);  border-left-color: var(--ox-danger);  color: var(--ox-danger-ink); }
.alert-info    { background: var(--ox-info-tint);    border-color: var(--ox-info-line);    border-left-color: var(--ox-brand);   color: var(--ox-brand); }

.form-control, .form-select {
  border-radius: var(--ox-radius-md);
  border-color: var(--ox-hairline-strong);
  padding: 7px var(--ox-space-3);
}
.form-label {
  font-size: var(--ox-fs-xxs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ox-ink-secondary);
}
.dropdown-menu { border-radius: var(--ox-radius-md); border: 1px solid var(--ox-hairline); }
.nav-tabs .nav-link {
  font-size: var(--ox-fs-md);
  font-weight: 600;
}


/* -------------------------------------------------------------
 * 10. Page-header stat row
 * -------------------------------------------------------------
 * Sits under .ox-page-title in place of a prose subtitle on list
 * pages: the four numbers that decide what you do next, divided by
 * hairlines. Numbers are mono so they align between pages. */
.ox-headline-stats {
  display: flex;
  align-items: center;
  gap: var(--ox-space-3);
  font-size: var(--ox-fs-xs);
  color: var(--ox-ink-muted);
  margin-top: 5px;
}
.ox-headline-stats > span { display: inline-flex; align-items: center; gap: 5px; }
.ox-headline-stats b {
  font-family: var(--ox-font-mono);
  font-weight: 600;
  color: var(--ox-ink);
}
.ox-headline-stats b.is-warning { color: var(--ox-warning-ink); }
.ox-headline-stats b.is-danger  { color: var(--ox-danger-ink); }
.ox-headline-stats b.is-brand   { color: var(--ox-brand); }
.ox-headline-stats .sep { width: 1px; height: 12px; background: var(--ox-hairline-strong); }


/* -------------------------------------------------------------
 * 11. Cryo box grid
 * -------------------------------------------------------------
 * Replaces the bordered <table> grid. Square cells, cell-line tint
 * as the fill, status as a corner ribbon (unchanged semantics from
 * cryo/box_view.html — only the drawing changed). */
.ox-cryo-grid {
  display: grid;
  gap: 3px;
}
.ox-cryo-axis {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ox-font-mono);
  font-size: var(--ox-fs-xxs);
  color: var(--ox-ink-muted);
}
.ox-cryo-cell {
  position: relative;
  height: 62px;
  padding: 5px 6px;
  overflow: hidden;
  display: block;
  text-decoration: none;
  color: var(--ox-ink);
  border: 1px solid var(--ox-hairline);
}
.ox-cryo-cell:hover { text-decoration: none; outline: 2px solid var(--ox-brand); outline-offset: -2px; }
.ox-cryo-line { font-size: 11.5px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ox-cryo-meta { font-family: var(--ox-font-mono); font-size: 10.5px; color: var(--ox-ink-secondary); }
.ox-cryo-ribbon {
  position: absolute; top: 0; right: 0;
  width: 0; height: 0;
  border-top: 9px solid var(--ox-ink-faint);
  border-left: 9px solid transparent;
}
.ox-cryo-empty {
  height: 62px;
  background: var(--ox-surface-footer);
  border: 1px dashed var(--ox-hairline);
  display: flex; align-items: center; justify-content: center;
  color: var(--ox-hairline-strong);
  text-decoration: none;
}
.ox-cryo-empty:hover { border-color: var(--ox-brand); color: var(--ox-brand); }
