/* =========================================================================
 * Newton 65 — platform design system (Spec 2)
 * Identity: "print room, daylight". Warm paper surfaces, ink-black type,
 * process-cyan accent, channel colors reserved for data. No dark mode.
 *
 * Copied verbatim from /Volumes/NewtonWeb/html/onboard/onboard.css and edited
 * HERE. The original stays in place and the legacy onboard tools keep using
 * it; nothing in this repository ever writes to a source tree.
 *
 * This is the ONLY stylesheet the platform loads. The Vue app links it, and so
 * do the four vanilla instrument appliances served at /onboard/ — which is why
 * every URL in this file is absolute and why the build drops a copy into
 * web/public/assets/ rather than leaving it in web/shared/.
 * ========================================================================= */

:root {
  /* ── Palette ───────────────────────────────────────────────────────────
   * Every colour in the platform is declared here. Outside this block there
   * are no hex literals and no rgba() literals — that is a property worth
   * keeping, because it is the one that a grep can actually check. */
  --paper:        #F5F4EF;
  --surface:      #FFFFFF;
  --surface-2:    #FBFAF7;
  --surface-glass: rgba(255,255,255,.92);  /* topbar, over content scrolling under it */
  --ink:          #1D1B16;
  --ink-2:        #6B675C;
  --ink-3:        #99948A;
  --ink-deep:     #000000;   /* past --ink; the primary button's hover, nothing else */
  --line:         #E7E4DB;
  --line-strong:  #D7D3C6;
  --accent:       #0090C9;   /* process cyan */
  --accent-deep:  #00719E;
  --accent-wash:  #EAF6FB;
  --ok:           #0E8A5F;
  --ok-wash:      #E7F5EF;
  --warn:         #B26A04;
  --warn-wash:    #FBF1DF;
  --warn-line:    #EAD9B8;
  --danger:       #C22F21;
  --danger-wash:  #FBECEA;
  --danger-line:  #F0C9C4;

  /* White ON an ink field is a foreground, not a surface — hence its own name
   * rather than --surface used backwards. --ink-3 is unreadable on ink, so the
   * muted variant is a separate value and not an opacity of the first. */
  --on-ink:       #FFFFFF;
  --on-ink-2:     #B9B5A9;

  /* Pairs that were bare literals across onboard.css, measure.css and
   * material.css with no token at all (Spec 2 §2.4). */
  --legacy:       #E8A33D;   /* segbar: measured, but not to Newton spec */
  --legacy-wash:  #F3E7D6;
  --legacy-ink:   #9A6B22;   /* the "extra"/"capped" tag pair */
  --neutral-wash: #F1EFE9;
  --neutral-ink:  var(--ink-3);   /* the "not started"/"queued"/"optional" pair */
  --hover-wash:   rgba(29,27,22,.05);
  --scrim:        rgba(29,27,22,.42);   /* behind a modal */

  /* Ink technology, on the printer badge. Data in the same sense as the
   * channel colours, but it belongs to a chip rather than to a chart, so
   * unlike CHANNEL_COLORS (src/styles/channels.js) it stays in CSS. */
  --tech-uv:      #3B4CA0;   --tech-uv-wash:      #EEF1FB;
  --tech-uvgel:   #146B8A;   --tech-uvgel-wash:   #E9F4F8;
  --tech-latex:   #22753C;   --tech-latex-wash:   #EAF5EC;
  --tech-ecosolv: #9A5B04;   --tech-ecosolv-wash: var(--warn-wash);
  --tech-resin:   #7A5B23;   --tech-resin-wash:   #F4EFE7;

  /* Measurement mode, on the white-point plot (Spec 2 §6.1). M1 illuminates
   * with UV included, so a fluorescent substrate reads at a more negative b*
   * and the blue dot genuinely sits bluer — the convention has to survive
   * without a legend, which is why the colours are fixed rather than themed.
   *
   * --m1 happens to equal --tech-uv and --m2 to equal --danger. They are
   * written out rather than aliased: a measurement mode is not an ink
   * technology, and M2 is not an error. Aliasing would invite someone to
   * retune --danger and silently repaint half the catalogue's white points. */
  --m1:           #3B4CA0;
  --m2:           #C22F21;

  /* Chart chrome. Both values are already in production in
   * onboard/printer/charts.js as bare literals with no token anywhere. They
   * are named here so the ported components can obey "no hex outside :root"
   * without changing an appearance that is already proven. --chart-grid is
   * lighter than --line because a full grid at --line reads as a table. */
  --chart-grid:   #EEECE4;   /* dot-gain grid, ring-gauge track */
  --chart-ref:    #B9B5A9;   /* the 45° identity line — a reference, not data */

  --radius:       12px;
  --radius-sm:    8px;
  --shadow-card:  0 1px 2px rgba(29,27,22,.05), 0 10px 30px -18px rgba(29,27,22,.18);
  --shadow-pop:   0 2px 6px rgba(29,27,22,.08), 0 24px 60px -24px rgba(29,27,22,.28);
  --shadow-lift:  0 6px 16px -8px rgba(29,27,22,.5);   /* primary button, hover */
  --shadow-knob:  0 1px 2px rgba(0,0,0,.25);           /* the switch handle */

  /* The stepper carries the only two gradients in the system — a sheet of
   * paper catching daylight. Whole-value tokens for the same reason
   * --shadow-card is one: the value is a unit, not a colour. */
  --step-done-bg:   linear-gradient(180deg, #FDFDFB, #F7FBF6);
  --step-active-bg: linear-gradient(180deg, #FFFDF8, #FDF7EA);

  /* ── Spacing (Spec 2 §2.2) ─────────────────────────────────────────────
   * The source used 14/16/18/20/24/26/28 as literals. Rules applied here:
   * a value already on the scale becomes its step; a value of 10px or more
   * snaps to the nearest step and ties round DOWN (18→16, 26→24, 14→12);
   * anything smaller that is not already a step stays literal, because chip
   * and pill padding is optical — tuned against an 11px font — and snapping
   * 3px to 4px resizes the component instead of drifting it. */
  --space-1: 4px;   --space-2: 8px;   --space-3: 12px;  --space-4: 16px;
  --space-5: 20px;  --space-6: 24px;  --space-7: 32px;  --space-8: 48px;

  /* ── Stacking order (Spec 2 §2.3) ──────────────────────────────────────
   * Replaces seven numbers picked independently across three stylesheets
   * (40/50/60/90/100/120/200). The gaps are kept deliberately wide: sooner or
   * later something has to sit between a tooltip and a modal. */
  --z-sticky:  40;   --z-rail:  50;    --z-overlay-bar: 60;
  --z-tooltip: 90;   --z-modal: 100;   --z-menu: 120;    --z-toast: 200;

  /* ── Workspace geometry (Spec 2 §5) ────────────────────────────────────
   * The topbar height is subtracted by every full-height layout in the file
   * and was a literal 64 in two of them; they have to agree or the last row of
   * a list sits under the topbar or above the fold.
   *
   * The rail widths are tokens rather than literals in .workspace because
   * collapsing a rail is done by REDECLARING them per side — see the width
   * arithmetic above the .workspace rules, which is load-bearing. */
  --topbar-h:       64px;
  --rail-left:      236px;
  --rail-right:     420px;
  --rail-collapsed: 48px;

  --font-display: "Space Grotesk", "Inter", system-ui, sans-serif;
  --font-body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* ── Self-hosted type ──────────────────────────────────────────────────────
 * Both families were pulled from fonts.googleapis.com by every shell. Serving
 * them ourselves is what allows the CSP to stay `font-src 'self'` with no
 * external origin, and it is the difference between the platform working and
 * not working in a print room with no route to Google.
 *
 * The .woff2 files are NOT in the repository — see
 * web/public/assets/fonts/README.md for the exact filenames and licence.
 * Until they are dropped in, each rule simply 404s and the browser falls
 * through to the system stack already declared in --font-display /
 * --font-body. Nothing breaks; the type is only less distinctive.
 *
 * Paths are absolute so the same rules resolve from /assets/ (the Vue app) and
 * from /onboard/ (the instrument appliances).
 */
/* Self-hosted, so the CSP can stay script-src 'self' and the platform can be
 * lifted to any host without a third party in the render path.
 *
 * These are VARIABLE fonts: ONE file per family per subset covers every weight,
 * which is why there are four files and not the five static faces originally
 * planned. Google now ships both families this way — it emits an @font-face per
 * weight for old-browser compatibility, but every one points at the same file.
 * Declaring the axis range instead means 500 or 600 is available the day the
 * design wants it, with nothing to download.
 *
 * unicode-range is what makes latin-ext free: the browser fetches those 104 kB
 * only if a glyph actually needs them. A material or manufacturer name with a
 * diacritic renders correctly instead of falling back mid-word, and everyone
 * else never pays for it.
 *
 * Space Grotesk 300-700, Inter 100-900 — the real axis ranges. Declaring wider
 * than the font supports makes the browser clamp; declaring narrower makes it
 * synthesise, which is where faux-bold comes from.
 *
 * Both families are SIL Open Font License 1.1. See fonts/README.md.
 */
@font-face {
  font-family: "Space Grotesk";
  src: url("/assets/fonts/SpaceGrotesk-Variable-latin.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
  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: "Space Grotesk";
  src: url("/assets/fonts/SpaceGrotesk-Variable-latin-ext.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
  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;
}
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/Inter-Variable-latin.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
  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: "Inter";
  src: url("/assets/fonts/Inter-Variable-latin-ext.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
  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;
}

* { box-sizing: border-box; }

/* ── Form controls: ONE box for every text/number input and select ──────────
 * Standing rule (Marco): text and integer inputs must look IDENTICAL to the
 * dropdowns — same border weight, height, corners, colour. A BASE rule, NOT
 * scoped to .field, so an input anywhere — including an inline cell input in a
 * DataList — gets it without remembering a wrapper. Placed early on purpose:
 * the few deliberately-different inputs (.search, .combo, .credential, file,
 * range) come later in this file and override on source order. No `width` here,
 * so inline inputs keep their natural size; .field adds width:100% for stacked
 * form fields. */
select,
textarea,
input[type="text"], input[type="email"], input[type="number"],
input[type="password"], input[type="search"], input[type="tel"], input[type="url"] {
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); padding: var(--space-2) var(--space-3);
  font: inherit; color: var(--ink);
}
select:focus, textarea:focus,
input[type="text"]:focus, input[type="email"]:focus, input[type="number"]:focus,
input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="url"]:focus {
  outline: 2px solid var(--accent-wash); border-color: var(--accent);
}
/* Number inputs drop the spinner so their box height matches text/selects. */
input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

html, body { margin: 0; padding: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent-deep); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font-family: inherit; }
svg text { font-family: var(--font-body); }

/* ── Topbar ────────────────────────────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: flex; align-items: center; gap: var(--space-6);
  height: var(--topbar-h); padding: 0 var(--space-6);
  background: var(--surface-glass);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; cursor: pointer; user-select: none; }
.brandlogo { height: 21px; display: block; }
/* The brand block is an <a> in the appliances and a click handler in the SPA;
   neither wants link decoration. */
.brand, .brand:hover { text-decoration: none; color: inherit; }

/*
 * ⚠️ THE WORDMARK, THE NAV SEPARATOR AND THE USER CHIP'S COLOUR LIVED IN
 * App.vue's SCOPED STYLE, and that is why the appliances did not match. A
 * scoped rule reaches one component's template and nothing else — so the same
 * markup, rendered by a vanilla tool, got the platform's topbar with the
 * product name unstyled, no separator, and an ink chip where the application
 * shows a brand-blue one. Three rules, one screenshot's worth of "why is this
 * different".
 *
 * They are shared chrome and they belong here, where both runtimes read them.
 * Anything that styles .topbar must be in this file for the same reason.
 */
.brand .wordmark {
  font-family: var(--font-display);
  /* Matched to .brandlogo's 21px cap height rather than picked by eye, so the
     wordmark and the logo read as one lockup. */
  font-size: 21px;
  line-height: 21px;
  color: var(--accent);
  font-weight: 700;
  letter-spacing: -.02em;
  margin-left: var(--space-2);
}

/* Replaces the "Work" / "Manage" captions: the split is still visible, and
   neither half needs naming. */
.navsep {
  width: 1px; height: 18px; margin: 0 var(--space-2);
  background: var(--line-strong); flex: none;
}
.sections { display: flex; gap: var(--space-1); }
.sections a {
  font-size: 13.5px; font-weight: 600; color: var(--ink-2);
  padding: 7px var(--space-3); border-radius: 999px; white-space: nowrap;
}
.sections a:hover { text-decoration: none; background: var(--hover-wash); color: var(--ink); }
.sections a.on { background: var(--ink); color: var(--on-ink); }
.sections a.soon { opacity: .5; cursor: default; }
.sections a.soon:hover { background: transparent; color: var(--ink-2); }
.crumbs { display: flex; align-items: center; gap: var(--space-2); color: var(--ink-2); font-size: 13px; min-width: 0; }
.crumbs a { color: var(--ink-2); white-space: nowrap; }
.crumbs .sep { color: var(--ink-3); }
.crumbs .here { color: var(--ink); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar .spacer { flex: 1; }
/*
 * ⚠️ BRAND BLUE, NOT INK, AND IT IS AN IDENTITY RATHER THAN A CONTROL. This
 * override also came out of App.vue's scoped style — see the note above the
 * wordmark. It is why the appliances showed a black chip beside the
 * application's blue one.
 */
.topbar .userchip { background: var(--accent); color: var(--on-ink); }

.userchip {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--ink); color: var(--on-ink);
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; letter-spacing: .02em; flex: none;
}
.sections .grp {
  font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-3); padding: 0 var(--space-1) 0 var(--space-3); white-space: nowrap;
}
.sections .grp:first-child { padding-left: 0; }

/* ── Topbar menus ───────────────────────────────────────────────────────────
 * The user and organisation popovers. --z-menu was defined in §2.3 with nothing
 * in this file to apply it to: the menu it was named for lives in the source
 * tree's material.css, which is read-only and stays where it is. */
.menuwrap { position: relative; flex: none; }
/* The chip is a real <button> so it is reachable from the keyboard; the reset
 * is what stops the user agent drawing a border round a 34px circle. */
.menuwrap > .userchip { border: 0; padding: 0; cursor: pointer; }
.menu {
  position: absolute; top: calc(100% + var(--space-2)); right: 0; z-index: var(--z-menu);
  min-width: 232px; padding: var(--space-1);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-pop);
}
.menu .m-head {
  padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-1); font-size: 12px; color: var(--ink-2);
}
.menu .m-head b { display: block; color: var(--ink); font-size: 13px; word-break: break-all; }
.menu .m-note { padding: var(--space-2) var(--space-3); font-size: 11.5px; color: var(--ink-3); }
.menu button, .menu a {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  padding: var(--space-2) var(--space-3); border: 0; border-radius: 6px;
  background: transparent; color: var(--ink); font: inherit; font-size: 13px;
  text-align: left; cursor: pointer;
}
.menu button:hover, .menu a:hover { background: var(--hover-wash); text-decoration: none; }
.menu button.on { font-weight: 700; }
.menu button.on::after { content: '\2713'; margin-left: auto; color: var(--accent-deep); }
.menu .m-sep { height: 1px; margin: var(--space-1) 0; background: var(--line); }

/* The workspace switcher menu — wider than the user menu so an enterprise's
   "<name> — all sites" fits on one line, and each row leads with an icon that
   tells an enterprise (a group of sites) from a single organization. */
.ws-menu { min-width: 300px; max-width: 380px; }
.ws-menu .ws-ic {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 6px;
}
.ws-menu .ws-ic.group { background: var(--accent-wash); color: var(--accent-deep); }
.ws-menu .ws-ic.org { background: var(--neutral-wash); color: var(--ink-2); }
.ws-menu .ws-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The role chip, right-aligned so every row's chip lines up in one column. */
.ws-menu .ws-role {
  flex: 0 0 auto; margin-left: var(--space-2);
  font-size: 10.5px; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
}
.ws-menu .ws-role.admin { background: var(--accent-wash); color: var(--accent-deep); }
.ws-menu .ws-role.plain { background: var(--neutral-wash); color: var(--ink-2); }
/* Selection reads from the accent row and the bold name — the tick would fight
   the right-aligned chip for the same edge, so it is dropped in this menu. */
.ws-menu button.on { background: var(--accent-wash); }
.ws-menu button.on::after { content: none; }

/* ── Buttons ───────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 9px var(--space-4); font-size: 13.5px; font-weight: 600;
  cursor: pointer; transition: all .15s ease; white-space: nowrap;
}
.btn svg { flex: none; }
.btn-primary { background: var(--ink); color: var(--on-ink); }
.btn-primary:hover { background: var(--ink-deep); transform: translateY(-1px); box-shadow: var(--shadow-lift); }
.btn-secondary { background: var(--surface); color: var(--ink); border-color: var(--line-strong); }
.btn-secondary:hover { border-color: var(--ink-3); background: var(--surface-2); }
.btn-accent { background: var(--accent); color: var(--on-ink); }
.btn-accent:hover { background: var(--accent-deep); }
.btn-ghost { background: transparent; color: var(--ink-2); padding: 7px var(--space-2); }
.btn-ghost:hover { background: var(--hover-wash); color: var(--ink); }
/* Destructive. Washed at rest and solid only under the pointer: purge and the
 * cascade delete are the two controls that reach it, and a button that already
 * looks committed is one a person can press on the way past. */
.btn-danger { background: var(--danger-wash); color: var(--danger); border-color: var(--danger-line); }
.btn-danger:hover { background: var(--danger); color: var(--on-ink); border-color: var(--danger); }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-sm { padding: 6px var(--space-3); font-size: 12.5px; }

/* ── Layout ────────────────────────────────────────────────────────────── */
/* 120px of bottom padding is scroll runway, not rhythm — it keeps the last row
 * clear of the compare tray, which is fixed to the bottom of the viewport. */
.page { max-width: 1500px; margin: 0 auto; padding: var(--space-7) var(--space-6) 120px; }
.layout { display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: var(--space-6); align-items: start; }
@media (max-width: 980px) { .layout { grid-template-columns: 1fr; } }
.page-head { display: flex; align-items: flex-end; gap: var(--space-6); margin: var(--space-1) 0 var(--space-6); flex-wrap: wrap; }
.page-head .titleblock { flex: 1; min-width: 260px; }
.overline {
  font-size: 11px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 6px;
}
h1.display {
  font-family: var(--font-display); font-size: 32px; font-weight: 700;
  letter-spacing: -.025em; margin: 0; line-height: 1.1;
}
.page-head .sub { color: var(--ink-2); margin-top: var(--space-2); max-width: 680px; }

/* KPI strip */
.kpis { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.kpi {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--space-3) var(--space-4); min-width: 118px; box-shadow: var(--shadow-card);
}
.kpi .num { font-family: var(--font-display); font-size: 25px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.15; }
.kpi .num small { font-size: 15px; color: var(--ink-3); font-weight: 500; }
.kpi .lbl { font-size: 12px; color: var(--ink-2); margin-top: 2px; }
.kpi.kpi-accent .num { color: var(--accent-deep); }

/* ── Filter sidebar ────────────────────────────────────────────────────── */
.filters {
  position: sticky; top: 84px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: var(--space-4);
}
.filters .search { margin-bottom: var(--space-4); min-width: 0; width: 100%; }
.fgroup { padding: var(--space-3) 2px var(--space-1); border-top: 1px solid var(--line); }
.fgroup:first-of-type { border-top: 0; padding-top: 0; }
.fgroup h4 {
  margin: 0 0 var(--space-2); font-size: 11px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3);
}
.frow {
  display: flex; align-items: center; gap: 9px; padding: 5px 4px;
  border-radius: 6px; cursor: pointer; user-select: none; font-size: 13px;
}
.frow:hover { background: var(--surface-2); }
/* Option labels are data — manufacturer and material names — and the rail is
 * 236px. Without this a long one pushes the count out of the card entirely. */
.frow .fname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.frow .fcount { margin-left: auto; font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.fbox {
  width: 16px; height: 16px; border-radius: 4px; flex: none;
  border: 1.5px solid var(--line-strong); background: var(--surface);
  display: inline-flex; align-items: center; justify-content: center; color: transparent;
  transition: all .1s ease;
}
.frow:hover .fbox { border-color: var(--accent); }
.fbox.on { background: var(--accent); border-color: var(--accent); color: var(--on-ink); }
/* .fclear is GONE, not renamed: the clear control is a real <button> at the top
 * of the rail now, styled in FilterGroup.vue where its disabled state lives. It
 * was a span at the FOOT that appeared only once a filter was on — so it moved
 * every time a group's length changed, and its absence had to be read as "there
 * is nothing to clear", which is not what an absent control says. */

/* ── Search ────────────────────────────────────────────────────────────── */
.search {
  display: flex; align-items: center; gap: var(--space-2);
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3); color: var(--ink-3);
}
.search input { border: 0; outline: 0; background: transparent; font: inherit; color: var(--ink); width: 100%; }

/* ── Scrolling list frame ──────────────────────────────────────────────── */
/* header + filters static, list scrolls in its own frame. Spec 2 §5 rule 1
 * builds the workspace on this pattern: only the list scrolls. */
.plist { display: flex; flex-direction: column; gap: var(--space-3); }
.page.fixedvh { height: calc(100vh - var(--topbar-h)); overflow: hidden; display: flex; flex-direction: column; padding-bottom: var(--space-4); }
.page.fixedvh .layout { flex: 1; min-height: 0; }
.page.fixedvh .filters { position: static; max-height: 100%; overflow-y: auto; }
.page.fixedvh .plist { overflow-y: auto; min-height: 0; max-height: 100%; padding: 2px 6px var(--space-6) 2px; scrollbar-width: thin; }

/* ── Workspace (Spec 2 §5) ─────────────────────────────────────────────────
 * Filters rail · list · detail stack. Every one of the seven screens is this
 * layout with different content, which is why the grid is declared HERE and no
 * screen is allowed its own (§11).
 *
 * WIDTH ARITHMETIC — the correction §11 makes to itself, and the reason the
 * left rail's default is not "open". At 1280px:
 *
 *     1280 − 236 (left) − 420 (right) − 2×24 (gap) − 2×24 (padding) = 528px
 *
 * of list, against a materials row that needs ~682px. Solving for the width at
 * which both rails fit open:
 *
 *     W − 236 − 420 − 48 − 48 ≥ 682   →   W ≥ 1434
 *
 * so between 1250 (below which the right rail becomes a drawer and the space
 * comes back) and 1433 the left rail DEFAULTS COLLAPSED — Workspace.vue applies
 * it. The rails do not fit at 1280. Do not "fix" the default; check the sum.
 */
.workspace {
  display: grid;
  grid-template-columns: var(--rail-left) minmax(0, 1fr) var(--rail-right);
  gap: var(--space-6);
  height: calc(100vh - var(--topbar-h));
  padding: var(--space-6);
  overflow: hidden;              /* rule 1: only the list scrolls */
}
.workspace.left-collapsed  { --rail-left:  var(--rail-collapsed); }
.workspace.right-collapsed { --rail-right: var(--rail-collapsed); }
/* A screen that opts out of the left rail (Workspace `hide-left`): the list +
 * detail reclaim its track. Higher specificity than the responsive rules below
 * (three classes vs. two), so it holds at every width. */
.workspace.no-left { grid-template-columns: minmax(0, 1fr) var(--rail-right); }

/* Sides are .ws-left/.ws-right, never .left/.right: `.right` is the
 * margin-left:auto utility at the foot of this file, and on a grid item it
 * would shrink the rail to its content and shunt it across its own track. */
.ws-rail { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
/* THE COLLAPSE CONTROL IS A TAB ON THE RAIL'S INNER EDGE, not a row above its
 * content. As a row it occupied vertical space, so both rails began lower than
 * the list beside them and the three columns never shared a top line. Riding
 * in the grid gutter, it costs no height, needs no caption, and sits where the
 * rail meets the list — which is the edge it moves. */
.ws-rail { position: relative; --tab-offset: -21px; }
.ws-rail .rail-tab {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 1;
  width: 16px; height: 46px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--ink-3);
  border: 1px solid var(--line); cursor: pointer;
}
.ws-rail .rail-tab:hover { color: var(--ink); border-color: var(--line-strong); background: var(--surface-2); }
.ws-rail.ws-left  .rail-tab { right: var(--tab-offset); border-radius: 0 6px 6px 0; border-left: 0; }
.ws-rail.ws-right .rail-tab { left:  var(--tab-offset); border-radius: 6px 0 0 6px; border-right: 0; }
/* THE SAME TAB EXPANDS AND COLLAPSES, and the arrow is the only difference —
 * it points the way the rail's inner edge will travel. It used to be hidden
 * when collapsed, with a round ghost button at the top of the strip taking
 * over: a different shape, a different size and a different place for the same
 * toggle, so the control seemed to jump when you pressed it.
 *
 * The SVG turns, not the button: .rail-tab's own transform is the
 * translateY(-50%) that centres it, and rotating the element would discard it. */
.ws-rail .rail-tab.inverted svg { transform: rotate(180deg); }
/* The rail does not scroll WITH the page — that is what rule 1 means — but a
 * detail stack of four panels is taller than the viewport, so it scrolls
 * inside itself. Exactly the .page.fixedvh .filters pattern above. */
.ws-rail .rail-body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; }
.ws-rail .rail-strip { display: none; }
.ws-rail.collapsed .rail-body { display: none; }
/* The label and nothing else: the expand button that used to sit above it is
 * gone, and the toggle is the tab in the gutter in both states. No gap, because
 * there is no longer a second child to separate. */
.ws-rail.collapsed .rail-strip {
  display: flex; flex-direction: column; align-items: center;
  flex: 1; min-height: 0; overflow: hidden;
}
.rail-strip .rail-label {
  writing-mode: vertical-rl; white-space: nowrap; overflow: hidden; user-select: none;
  font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
}
/* .filters carries a sticky offset for the .layout grid, where the page scrolls
 * beneath it. Nothing scrolls beneath it here, so the offset would only push
 * the card 84px down its own rail. */
.ws-rail .filters { position: static; }

.ws-list { min-width: 0; min-height: 0; overflow-y: auto; scrollbar-width: thin; padding: var(--space-1) var(--space-1) var(--space-6); }

/* Drawer scrim. Positioned at --z-rail like the rails themselves and placed
 * immediately BEFORE its own rail in the DOM, so paint order alone puts the
 * drawer above it and there is no second z-index to keep in step. */
.ws-scrim { display: none; position: fixed; inset: var(--topbar-h) 0 0; background: var(--scrim); z-index: var(--z-rail); }

/* An OPEN rail leaves the grid and overlays; a COLLAPSED one keeps its 48px
 * track, so the toggle never moves out from under the pointer as the window
 * crosses a breakpoint. The breakpoints are literals because a media query
 * cannot read a custom property — a CSS limitation, not an oversight. */
@media (max-width: 1250px) {
  .workspace:not(.right-collapsed) { grid-template-columns: var(--rail-left) minmax(0, 1fr); }
  /* .ws-rail.ws-right, not .ws-right: the scrim carries the side class too, and
   * it must not be handed the drawer's width, padding and paper background. */
  .workspace:not(.right-collapsed) > .ws-rail.ws-right {
    position: fixed; top: var(--topbar-h); right: 0; bottom: 0;
    width: var(--rail-right); max-width: 92vw; z-index: var(--z-rail);
    background: var(--paper); border-left: 1px solid var(--line);
    box-shadow: var(--shadow-pop); padding: var(--space-4);
  }
  .workspace:not(.right-collapsed) > .ws-scrim.ws-right { display: block; }
  /* No left rail: the right rail is the only rail, so an open one leaves the
   * list a single full-width column (the base `.no-left` two-column rule assumes
   * an in-grid right rail, which it no longer is once it overlays). */
  .workspace.no-left:not(.right-collapsed) { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 980px) {
  .workspace:not(.left-collapsed) { grid-template-columns: minmax(0, 1fr) var(--rail-right); }
  .workspace:not(.left-collapsed):not(.right-collapsed) { grid-template-columns: minmax(0, 1fr); }
  .workspace:not(.left-collapsed) > .ws-rail.ws-left {
    position: fixed; top: var(--topbar-h); left: 0; bottom: 0;
    width: var(--rail-left); max-width: 92vw; z-index: var(--z-rail);
    background: var(--paper); border-right: 1px solid var(--line);
    box-shadow: var(--shadow-pop); padding: var(--space-4);
  }
  .workspace:not(.left-collapsed) > .ws-scrim.ws-left { display: block; }
  .workspace { padding: var(--space-3); gap: var(--space-3); }
  /* The gutter halves to 12px here and the tab is 16px, so -21px would hang it
   * over the list. Centred on the rail's edge instead — the only offset that
   * fits a gutter narrower than the tab. */
  .ws-rail { --tab-offset: -8px; }
}

/* ── Data list (Spec 2 §5 rules 2 and 6) ───────────────────────────────────
 * ONE grid-template-columns string. DataList computes it from `columns` and
 * publishes it as --dl-cols on the container; the single declaration below is
 * what both the header and every row read. The legacy screens wrote the track
 * list out twice — material.css:20 for the head, :27 for the row — and the two
 * drifted. Here there is no second copy to drift from.
 *
 * NO overflow:hidden on .dlist, deliberately. It would make the card a scroll
 * container with nothing to scroll, and position:sticky on .dl-head would
 * silently stop working — the header would scroll away with the rows and
 * nothing would look broken enough to investigate. The last row rounds its own
 * corners instead.
 */
.dlist {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}
/* List-wide actions, inside the card and above the sticky header. flex: none so
 * it keeps its height in .matlist's column flexbox instead of being squeezed by
 * the rows below it. */
/*
 * ⚠️ THE KEY/VALUE LIST EVERY DETAIL PANEL DRAWS. It was defined identically in
 * four components' scoped blocks — measurements' details, error check and
 * analytics, and printers' details — which is the same duplication that made
 * the appliances look like a different product. A pattern wanted by more than
 * one component belongs here.
 */
.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-1) var(--space-3); margin: 0; }
.kv dt { font-size: 12px; color: var(--ink-2); }
.kv dd { margin: 0; font-size: 13px; min-width: 0; overflow-wrap: anywhere; }

/*
 * ⚠️ THE CAPABILITY FOLD-OUT, ported from the legacy tool's onboard.css:227-260
 * onto this platform's tokens. Marco, 2026-08-03: use the legacy design.
 *
 * IT IS A GRID, AND THAT IS THE WHOLE DIFFERENCE. `20px 40px minmax(220px,1fr)
 * 92px 150px 150px` puts the thumb, the name, the bucket chip, the material bar
 * and the status chip on the same columns DOWN the fold. The first attempt used
 * a flex row, every row found its own rhythm, and the eye had no column to run
 * along — which is what "the design is terrible" was.
 *
 * Here rather than in a scoped block because PrinterList has no <style> at all,
 * by design: everything it draws is this file's. A scoped copy is the drift a
 * single stylesheet exists to prevent — the same lesson as `.kv` and the topbar.
 */
.dl-expansion { grid-column: 1 / -1; }

.capfold { background: var(--surface-2); border-top: 1px solid var(--line); }
.capfold .capempty { padding: var(--space-3) var(--space-6); margin: 0; }

.caprow {
  display: grid; align-items: center; gap: 14px; width: 100%;
  grid-template-columns: 40px minmax(220px, 1fr) 92px 150px 130px;
  padding: 11px 20px 11px 54px;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  cursor: pointer; text-align: left; font: inherit; color: inherit;
}
.caprow:last-child { border-bottom: 0; }
.caprow:hover { background: var(--hover-wash); }
.caprow.on { background: var(--accent-wash); }
.caprow .grow { min-width: 0; }
.caprow .capname { font-weight: 600; font-size: 13.5px; }
.caprow .capsub {
  font-size: 12px; color: var(--ink-2); margin-top: 1px;
  display: flex; align-items: center; gap: 7px;
}

/*
 * ⚠️ THE THUMB IS AN <svg> WITH width/height ATTRIBUTES, so its shape is
 * geometry rather than a CSS argument. The styled-box version rendered as an
 * ellipse three times over — the rule was correct and served, and something in
 * the row's layout stretched the box anyway. Nothing here may set its height.
 */
.caprow .thumb { flex: none; align-self: center; justify-self: start; display: block; }

.bkcount {
  display: inline-flex; align-items: center; gap: 5px; justify-self: start;
  font-size: 11.5px; font-weight: 700; color: var(--ink-2);
  border: 1px solid var(--line-strong); background: var(--surface);
  border-radius: 999px; padding: 3px 9px; white-space: nowrap;
}
.bkcount i {
  width: 8px; height: 8px; border-radius: 2px; background: var(--accent);
  box-shadow: 3px 3px 0 -1px var(--line-strong);
}
.bkcount.none { color: var(--ink-3); border-style: dashed; }

/* ⚠️ GREEN IS A NEWTON PRINT, ORANGE IS LEGACY, GREY IS NOTHING — the legacy
   tool's own three-way. It matters because print_stage='legacy' counts toward
   NOTHING, so a bar drawing legacy as done shows an onboarded press that is not. */
.segwrap { display: flex; flex-direction: column; gap: 5px; min-width: 140px; }
.segbar { display: flex; gap: 3px; }
.segbar i { height: 6px; flex: 1; min-width: 9px; border-radius: 2px; background: var(--line); }
.segbar i.newton { background: var(--ok); }
.segbar i.legacy { background: var(--legacy); }
.seglbl { font-size: 11px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.seglbl .extra { color: var(--accent-deep); font-weight: 700; }

/* The status chip is a link into the measure tool; it must not pick up the
   underline every other anchor gets. */
.backlink { padding-left: 0; margin-bottom: var(--space-2); }
.statuslink { text-decoration: none; cursor: pointer; }
.statuslink:hover { text-decoration: none; filter: brightness(.97); }
.stagechip.queued { background: var(--accent-wash); color: var(--accent-deep); }

.dl-toolbar {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2);
  flex: none; padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
}
/* The search takes the left of the toolbar and pushes everything else right:
 * margin-right:auto rather than space-between, so a toolbar with ONE control
 * still puts it where the eye expects and does not centre it.
 *
 * Capped, because the list is most of the screen and a search field the width
 * of a catalogue reads as a text editor. 380px is about sixty characters, which
 * is longer than any name this platform stores. */
.dl-toolbar .search { flex: 1 1 auto; max-width: 380px; margin-right: auto; }
.dl-head, .dl-row { display: grid; grid-template-columns: var(--dl-cols); gap: var(--space-3); align-items: center; }
.dl-head {
  /* local order inside the list's own scroll frame, not part of the --z-*
   * scale: it only has to beat the rows sliding underneath it */
  position: sticky; top: 0; z-index: 2;
  background: var(--surface); border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  padding: var(--space-2) var(--space-4);
  font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
}
.dl-rows:focus { outline: none; }
.dl-rows:focus-visible { outline: 2px solid var(--accent-wash); outline-offset: -2px; border-radius: var(--radius); }
.dl-row {
  padding: var(--space-2) var(--space-4); font-size: 13px;
  cursor: pointer; border-top: 1px solid var(--line);
}
.dl-row:first-child { border-top: 0; }
.dl-row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
.dl-row:hover { background: var(--surface-2); }
.dl-row.sel { background: var(--accent-wash); }
.dl-c { min-width: 0; }
.dl-c.num { text-align: right; font-variant-numeric: tabular-nums; }
.dl-c.center { text-align: center; }
/* Opt-in per column. The identity column must NOT get this — §5 rule 2 keeps
 * it legible by giving it minmax(180px,1fr) and capping the trailing tracks,
 * not by shortening the name. */
.dl-c.ellip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Merged identity cell (Spec 2 §11) ─────────────────────────────────────
 * Manufacturer and name in ONE track, the .mt-title pattern §11 names when it
 * corrects itself. Two separate columns cost ~150px the workspace does not
 * have between 1250 and 1433px; the arithmetic is written out in
 * MaterialsView.vue, next to the tracks it constrains.
 */
.mt-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mt-title .mfrn { color: var(--ink-3); }
.mt-title .matn { font-weight: 600; color: var(--ink); }

/* ── Masked: WITHHELD, NOT MISSING ─────────────────────────────────────────
 * The Serializer NULLS the licensable fields of a row the principal may see
 * but may not read, and sets masked:true. Handed to a chart, those nulls draw
 * the chart's empty state — which says "not measured yet", the opposite of the
 * truth, and hides the content-licensing feature from the people who built it.
 *
 * So the hatch. It is deliberately a TEXTURE, because every empty state in
 * this design system is an absence — a dash, a hollow ring, a baseline tick —
 * and something present-but-barred must not look like one more of them.
 *
 * Colour is the --legacy pair, not --danger: a mask is not an error and must
 * not be dressed as one. The row is behaving exactly as licensed.
 */
.maskcell {
  display: block; width: 100%; height: var(--space-5); cursor: help;
  border-radius: var(--radius-sm); border: 1px dashed var(--line-strong);
  background: repeating-linear-gradient(
    135deg,
    var(--line-strong) 0 var(--space-1),
    var(--neutral-wash) var(--space-1) var(--space-3)
  );
}
.maskcell.tall { height: var(--space-8); }
.stagechip.masked { background: var(--legacy-wash); color: var(--legacy-ink); cursor: help; }

/* the collapse chevron — the only element the collapse primitive animates.
 * .15s is the figure in Spec 2 §4, and the reason it is the ONLY animation:
 * the legacy fold transitioned the body's height, which forces layout on every
 * frame of a panel holding an SVG chart. That is what made panels feel slow. */
.chev {
  flex: none; width: 26px; height: 26px; border-radius: 50%; color: var(--ink-3);
  display: flex; align-items: center; justify-content: center; transition: transform .15s ease, background .12s ease;
}
.chev.open { transform: rotate(90deg); }
.pimg {
  flex: none; width: 92px; height: 60px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; color: var(--ink-3);
  overflow: hidden;
}
.pimg img { max-width: 100%; max-height: 100%; object-fit: contain; }
.badge {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  border-radius: 6px; padding: 3px var(--space-2); white-space: nowrap;
}
.badge.ink-UV         { background: var(--tech-uv-wash);      color: var(--tech-uv); }
.badge.ink-UVgel      { background: var(--tech-uvgel-wash);   color: var(--tech-uvgel); }
.badge.ink-Latex      { background: var(--tech-latex-wash);   color: var(--tech-latex); }
.badge.ink-Eco-solvent{ background: var(--tech-ecosolv-wash); color: var(--tech-ecosolv); }
.badge.ink-Resin      { background: var(--tech-resin-wash);   color: var(--tech-resin); }

/* ink configuration stack (dots + name per configuration) */
.cfgstack { display: flex; flex-direction: column; gap: 5px; min-width: 190px; flex: 1; }
.cfg { display: flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--ink-2); }
.cfg .cfgname { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chanrow { display: flex; align-items: center; gap: 3.5px; flex: none; }
.chan {
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--line-strong);
  font-size: 0; flex: none;
}
.chan.lg { width: 24px; height: 24px; font-size: 9px; font-weight: 800; color: var(--on-ink); display: inline-flex; align-items: center; justify-content: center; }

.ringnum-done { fill: var(--ok); }

/* bucket count chip (capability rows) */
.bkcount {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; font-weight: 700; color: var(--ink-2);
  border: 1px solid var(--line-strong); background: var(--surface);
  border-radius: 999px; padding: 3px var(--space-2); white-space: nowrap; justify-self: start;
}
.bkcount i {
  width: 8px; height: 8px; border-radius: 2px; background: var(--accent);
  box-shadow: 3px 3px 0 -1px var(--line-strong);
}
.bkcount.none { color: var(--ink-3); border-style: dashed; }

/* Segmented material progress (green = Newton target, orange = legacy) */
.segbar { display: flex; gap: 3px; }
.segbar i { height: 6px; flex: 1; min-width: 9px; border-radius: 2px; background: var(--line); }
.segbar i.newton { background: var(--ok); }
.segbar i.legacy { background: var(--legacy); }
.segwrap { display: flex; flex-direction: column; gap: 5px; min-width: 140px; }
.segwrap .seglbl { font-size: 11px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.segwrap .seglbl .extra { color: var(--accent-deep); font-weight: 700; }

/* The status chip is a link into the measure tool; it must not pick up the
   underline every other anchor gets. */
.backlink { padding-left: 0; margin-bottom: var(--space-2); }
.statuslink { text-decoration: none; cursor: pointer; }
.statuslink:hover { text-decoration: none; filter: brightness(.97); }
.stagechip.queued { background: var(--accent-wash); color: var(--accent-deep); }

/* Status chips */
.status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700; border-radius: 999px; padding: var(--space-1) var(--space-3); white-space: nowrap; }
.status i { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.status.onboarded   { background: var(--ok-wash);    color: var(--ok); }
.status.onboarded i { background: var(--ok); }
.status.onboarded_legacy   { background: var(--warn-wash); color: var(--warn); cursor: help; }
.status.in_progress   { background: var(--warn-wash); color: var(--warn); }
.status.in_progress i { background: var(--warn); }
.status.not_started   { background: var(--neutral-wash); color: var(--neutral-ink); }
.status.not_started i { background: var(--ink-3); }

/* multiselect checkbox */
.selbox {
  width: 20px; height: 20px; flex: none; border-radius: 6px;
  border: 1.5px solid var(--line-strong); background: var(--surface);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: all .12s ease; color: transparent;
}
.selbox:hover { border-color: var(--accent); }
.selbox.on { background: var(--accent); border-color: var(--accent); color: var(--on-ink); }

/* ── Tooltip (pure CSS) ────────────────────────────────────────────────── */
[data-tip] { position: relative; }
[data-tip]:hover::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  background: var(--ink); color: var(--on-ink); font-size: 11.5px; font-weight: 500; line-height: 1.45;
  padding: var(--space-2) var(--space-3); border-radius: 8px; width: max-content; max-width: 260px;
  box-shadow: var(--shadow-pop); z-index: var(--z-tooltip); pointer-events: none; white-space: normal;
  text-align: left;
}
[data-tip]:hover::before {
  content: ''; position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--ink); z-index: var(--z-tooltip); pointer-events: none;
}

/* bucket chip (material cluster) */
.bucket {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .03em;
  border-radius: 999px; padding: 2px 8px 2px 5px;
  border: 1px solid var(--line-strong); background: var(--surface);
  color: var(--ink-2); cursor: help; white-space: nowrap;
}
.bucket i { width: 8px; height: 8px; border-radius: 50%; flex: none; }

/* source tag (newton vs legacy measurement) */
.srct { font-size: 10px; font-weight: 800; letter-spacing: .05em; border-radius: 4px; padding: 2px 6px; }
.srct.newton { background: var(--ok-wash); color: var(--ok); }
.srct.legacy { background: var(--warn-wash); color: var(--warn); cursor: help; }

/*
 * ⚠️ A SECTION INSIDE A PANEL. A LINE SEPARATES SECTIONS, NEVER THE ROWS INSIDE
 * ONE — the standing instruction (Marco, 2026-08-04), and this is where it is
 * expressed once instead of in five components.
 *
 * `.block` was defined near-identically in MaterialDetailsPanel,
 * MaterialFormModal, InkSetupPanel, PrinterDetailsPanel and ErrorCheckBody. Five
 * scoped copies of one rule is five chances for one of them to drift, and the
 * copy nobody is looking at is the one that drifts first.
 *
 * ⚠️ THE FIRST SECTION OF A CONTAINER MUST NOT BE A `.block`. Every rule here
 * opens a group by closing the one above it, so a `.block` as the first child
 * draws a line directly under a panel header or at the top of a modal body.
 * Every current user follows that rule — identity, gloss and the first overline
 * are plain markup — and a new one has to as well.
 *
 * A component that genuinely needs different spacing overrides `margin-top` and
 * `padding-top` in its own scoped block; the RULE is not a component's to
 * decide.
 */
.block {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.block .overline { margin-bottom: var(--space-2); }

/*
 * ⚠️ A FILE PICKER THAT LOOKS LIKE THE REST OF THE PLATFORM.
 *
 * `<input type="file">` renders the browser's own "Choose File" button, which
 * cannot be styled and is the one control on a screen that looks like it came
 * from somewhere else. The input stays REAL — hidden, not replaced — and a
 * <label> pointed at it becomes the button: the label is what browsers already
 * treat as a proxy for the input, so clicking and keyboard both work with no
 * script at all.
 *
 * ⚠️ HIDDEN WITH width/height/opacity, NOT `display: none`. A display:none input
 * leaves the tab order and stops being focusable, so the control becomes
 * mouse-only — and the focus ring below would have nothing to attach to.
 *
 * PrinterDetailsPanel solved this first with a scoped `.hiddenfile` driven by a
 * click handler; this is the same idea without the script, in one place, for the
 * four screens that need it.
 */
.filepick { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.filepick > input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0;
}
/* The label carries the ring the hidden input would have shown. */
.filepick > input[type="file"]:focus-visible + label {
  outline: 2px solid var(--accent-wash); outline-offset: 2px;
}
.filepick-name {
  font-size: 12.5px; color: var(--ink-2);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/*
 * ⚠️ THE SCREEN-LEVEL TAB STRIP. One pattern, one definition — Accounts
 * (Companies | Users), the Cortex API (Pipelines | Models) and a pipeline's own
 * four tabs are the same control and must look and behave identically.
 *
 * It was written three times in three scoped blocks before this. Marco,
 * 2026-08-04, on a screen that had grown a button where the others have tabs:
 * "stick to the design, keep it coherent."
 *
 * `.tabsrow` carries the rule and holds actions on the right; `align-items:
 * flex-end` puts the `+` on the baseline the tabs establish. The active tab's
 * 2px underline overlaps the row's own border by -1px, which is what makes the
 * two read as one line.
 */
.tabsrow {
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-3);
  margin-top: var(--space-3); border-bottom: 1px solid var(--line);
  /* An action button is 32px and a tab about 34: the row keeps the taller, so
     the header does not change height when a strip has no tabs to draw. */
  min-height: 40px;
}
.tabs { display: flex; gap: var(--space-1); }
.tab {
  border: 0; background: none; font: inherit; cursor: pointer;
  padding: var(--space-2) var(--space-3); color: var(--ink-2);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--ink); }
.tab.on { color: var(--ink); font-weight: 700; border-bottom-color: var(--accent); }

/* ── Panels ────────────────────────────────────────────────────────────── */
.panel {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}
.panel-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line);
}
.panel.collapsed .panel-head { border-bottom: 0; }
.panel-head h3 { margin: 0; font-family: var(--font-display); font-size: 15.5px; font-weight: 700; letter-spacing: -.01em; }
.panel-head .hint { font-size: 12px; color: var(--ink-3); }
.panel-head .right { margin-left: auto; display: flex; gap: var(--space-2); align-items: center; }
.panel-head .fold { cursor: pointer; }
.panel-body { padding: var(--space-4); }

.cols { display: grid; gap: var(--space-4); }
.cols.c2 { grid-template-columns: 1fr 1fr; }
.cols.c-meas { grid-template-columns: minmax(430px, 1fr) 600px; align-items: start; }
@media (max-width: 1250px) { .cols.c2, .cols.c-meas { grid-template-columns: 1fr; } }

/* capability page: left column pinned to the viewport — while the page
 * scrolls (charts on the right), only the measurement LIST scrolls inside */
.cols.c-meas > .capside {
  position: sticky; top: 80px; align-self: start;
  display: flex; flex-direction: column; gap: var(--space-4);
  max-height: calc(100vh - 100px); min-height: 0;
}
.capside .panel.meas { display: flex; flex-direction: column; min-height: 0; flex: 0 1 auto; overflow: hidden; }
.capside .panel:not(.meas) { flex: none; }
.panel.meas .tscroll { overflow-y: auto; min-height: 0; flex: 0 1 auto; scrollbar-width: thin; }
.panel.meas table.data thead th {
  /* local order inside the scroll frame, not part of the --z-* scale: it only
   * has to beat the rows sliding underneath it */
  position: sticky; top: 0; background: var(--surface); z-index: 2;
  box-shadow: 0 1px 0 var(--line); border-bottom: 0;
}
@media (max-width: 1250px) {
  .cols.c-meas > .capside { position: static; max-height: none; }
  .panel.meas .tscroll { max-height: 480px; }
}

/* toggle switch */
.switch { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font-size: 12px; color: var(--ink-2); font-weight: 600; user-select: none; white-space: nowrap; }
.switch i { width: 30px; height: 17px; border-radius: 999px; background: var(--line-strong); position: relative; transition: background .15s; flex: none; }
.switch i::after { content: ''; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: var(--surface); transition: left .15s; box-shadow: var(--shadow-knob); }
.switch.on i { background: var(--ok); }
.switch.on i::after { left: 15px; }

/* ── Tables ────────────────────────────────────────────────────────────── */
table.data { width: 100%; border-collapse: collapse; font-size: 13px; }
table.data th {
  text-align: left; font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line);
}
table.data td { padding: var(--space-3); border-bottom: 1px solid var(--line); vertical-align: middle; }
table.data tr:last-child td { border-bottom: 0; }
table.data tr.rowlink { cursor: pointer; }
table.data tr.rowlink:hover td { background: var(--surface-2); }
table.data tr.rowsel td { background: var(--accent-wash); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data th.num { text-align: right; }
table.data tr.subhead td {
  background: var(--surface-2); font-size: 11px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-2); padding: var(--space-2) var(--space-3);
}
td .cfg.cell-end { justify-content: flex-end; }

/* stage chips inside measurement lists */
.stagechip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 6px; white-space: nowrap;
}
.stagechip.done    { background: var(--ok-wash); color: var(--ok); }
.stagechip.missing { background: var(--neutral-wash); color: var(--neutral-ink); }
.stagechip.uncapped-done { background: var(--accent-wash); color: var(--accent-deep); }
.stagechip.legacy  { background: var(--warn-wash); color: var(--warn); cursor: help; }

/* demo/offline banner */
.banner {
  background: var(--warn-wash); color: var(--warn);
  border-bottom: 1px solid var(--warn-line);
  padding: var(--space-2) var(--space-6); font-size: 12.5px; font-weight: 600; text-align: center;
}
.banner a { color: var(--warn); text-decoration: underline; }

/* ── Stepper ───────────────────────────────────────────────────────────── */
.stepper { display: flex; gap: 0; margin: var(--space-5) 0 var(--space-6); }
.step { flex: 1; position: relative; padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5); background: var(--surface); border: 1px solid var(--line); border-left: 0; }
.step:first-child { border-left: 1px solid var(--line); border-radius: var(--radius) 0 0 var(--radius); padding-left: var(--space-8); }
.step:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
.step .s-num {
  position: absolute; top: 13px; left: -14px; z-index: 2;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--surface); border: 1.5px solid var(--line-strong);
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 800; color: var(--ink-3);
}
.step:first-child .s-num { left: 14px; }
.step.done .s-num { background: var(--ok); border-color: var(--ok); color: var(--on-ink); }
.step.active .s-num { background: var(--warn); border-color: var(--warn); color: var(--on-ink); }
.step .s-title { font-weight: 700; font-size: 13px; padding-left: var(--space-5); }
.step:first-child .s-title { padding-left: 0; }
.step .s-sub { font-size: 11.5px; color: var(--ink-2); padding-left: var(--space-5); margin-top: 1px; }
.step:first-child .s-sub { padding-left: 0; }
.step.done { background: var(--step-done-bg); }
.step.active { background: var(--step-active-bg); }

/* ── Charts ────────────────────────────────────────────────────────────── */
.chartwrap { display: flex; justify-content: center; }
.legend { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.legend .li { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-2); font-weight: 600; }
.legend .li i { width: 16px; height: 0; border-top: 3px solid currentColor; border-radius: 2px; }
.legend .li i.solid { border-top-style: solid; }
.legend .li i.dashed { border-top-style: dashed; }
.legend .li.off { opacity: .35; }
.chip-toggle { cursor: pointer; user-select: none; }

/* Lab patches (white point + black point) */
.labpatch {
  border-radius: 10px; border: 1px solid var(--line-strong);
  padding: var(--space-3); min-height: 92px; position: relative;
}
.labpatch .lp-name { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; opacity: .75; }
.labpatch .lp-lab { font-family: var(--font-display); font-size: 15px; font-weight: 700; margin-top: var(--space-3); font-variant-numeric: tabular-nums; }
.labpatch .lp-sub { font-size: 11px; opacity: .7; }
.labpatch.sm { min-height: 54px; padding: 9px var(--space-3); border-radius: 8px; }
.labpatch.sm .lp-name { font-size: 9px; letter-spacing: .06em; }
.labpatch.sm .lp-lab { font-size: 11px; margin-top: 5px; }

/* Material readings: compact restriction bars + white/black patches side by side */
.mreadings { display: flex; gap: var(--space-4); align-items: flex-start; }
.mreadings .mr-inks { flex: 1; min-width: 0; }
.mreadings .mr-patches { width: 190px; flex: none; display: flex; flex-direction: column; gap: var(--space-2); }
.restr-row.sm { padding: 5px 0; gap: var(--space-2); }
.restr-row.sm .rr-name { width: 62px; font-size: 11.5px; }
.restr-row.sm .restr-track { height: 6px; }
.restr-row.sm .rr-val { width: 38px; font-size: 11.5px; }

/* restriction list */
.restr-row { display: flex; align-items: center; gap: var(--space-2); padding: 7px 0; border-bottom: 1px solid var(--line); }
.restr-row:last-child { border-bottom: 0; }
.restr-row .rr-name { width: 78px; font-weight: 600; font-size: 12.5px; }
.restr-track { flex: 1; height: 8px; border-radius: 4px; background: var(--line); position: relative; overflow: hidden; }
.restr-track i { position: absolute; inset: 0 auto 0 0; border-radius: 4px; }
.restr-row .rr-val { width: 46px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; font-size: 12.5px; }
.tacline { display: flex; align-items: baseline; gap: var(--space-2); padding: 2px 0 var(--space-3); border-bottom: 1px solid var(--line); margin-bottom: var(--space-2); }
.tacline .tac-num { font-family: var(--font-display); font-size: 26px; font-weight: 700; letter-spacing: -.02em; }

/* bucket analysis (inference restriction buckets) */
.tolctl { display: flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--ink-2); }
.tolctl input[type="range"] { width: 130px; accent-color: var(--accent); }
.tolctl .tolval { font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; min-width: 38px; }
.bkgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--space-3); }
.bucketcard {
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface-2); padding: var(--space-3);
}
.bucketcard .bk-title { display: flex; align-items: baseline; gap: var(--space-2); font-weight: 800; font-size: 13px; margin-bottom: var(--space-2); }
.bucketcard .bk-title .bk-n { font-size: 11.5px; color: var(--ink-2); font-weight: 600; }
.bk-vals { display: flex; flex-wrap: wrap; gap: 6px var(--space-3); margin-bottom: var(--space-2); }
.bk-val { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
.bk-val i { width: 10px; height: 10px; border-radius: 50%; }
.bk-tac { font-size: 12px; font-weight: 800; padding: 1px var(--space-2); border-radius: 6px; background: var(--ink); color: var(--on-ink); }
.bk-mats { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.bk-mat { font-size: 11px; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding: 2px var(--space-2); }

/* ── Compare tray ──────────────────────────────────────────────────────── */
.tray {
  position: fixed; left: 50%; bottom: var(--space-6); transform: translateX(-50%);
  background: var(--ink); color: var(--on-ink); border-radius: 999px;
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-5); box-shadow: var(--shadow-pop);
  z-index: var(--z-rail); animation: tray-in .2s ease;
}
@keyframes tray-in { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } }
.tray .t-label { font-size: 13px; font-weight: 600; }
.tray .t-label b { font-variant-numeric: tabular-nums; }
.tray .btn { border-radius: 999px; }
.tray .clear { color: var(--on-ink-2); font-size: 12.5px; cursor: pointer; }
.tray .clear:hover { color: var(--on-ink); }

/* ── Modals ────────────────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0; background: var(--scrim);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 8vh var(--space-5) var(--space-5); z-index: var(--z-modal); animation: fade-in .15s ease;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.modal {
  background: var(--surface); border-radius: 16px; box-shadow: var(--shadow-pop);
  width: 560px; max-width: 100%; max-height: 84vh; overflow: auto;
  animation: modal-in .18s ease;
}
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(.99); } to { opacity: 1; transform: none; } }
/* Two wider variants. `xwide` came from material.css:69 — the width the scan
 * modal was tuned to, carried here because material.css is reference-only. */
.modal.wide  { width: 780px; }
.modal.xwide { width: 880px; }
/* A form long enough to want more of the viewport's height than the base 84vh
 * — the enterprise/company/site forms of the licensing overhaul (spec §3.A),
 * composable with `wide`/`xwide` via a space-separated modalClass. */
.modal.tall  { max-height: 92vh; }

/* ⚠️ A MODAL WHOSE FIELDS ARE COMBOBOXES MUST NOT CLIP ITS OWN LISTS.
 *
 * `.modal` is `overflow: auto` so long content scrolls. A `.combo-list` is
 * absolutely positioned and opens BELOW its field, which means the plain modal
 * treats an open list as overflowing content: the dialog grows a scrollbar of
 * its own, the list is cut off at the dialog's edge, and choosing an option
 * becomes a scroll-then-click. `overflow: visible` lets the list draw over the
 * dialog's edge the way a dropdown is supposed to.
 *
 * The height is the other half and it belongs to each dialog, not here: the
 * room a list needs depends on how far down its field sits, so each one sets
 * its own `min-height` and says what it reserved it for. Column flex with a
 * growing body is what puts that reserved space BETWEEN the last field and the
 * footer — which is exactly where the lists open — rather than under the
 * footer. */
.modal.pickers { display: flex; flex-direction: column; overflow: visible; }
.modal.pickers .modal-body { flex: 1 1 auto; }
.modal-head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-5) var(--space-6) 0; }
.modal-head h2 { margin: 0; font-family: var(--font-display); font-size: 20px; font-weight: 700; letter-spacing: -.02em; }
.modal-head .x { margin-left: auto; cursor: pointer; color: var(--ink-3); font-size: 22px; line-height: 1; padding: var(--space-1); }
.modal-head .x:hover { color: var(--ink); }
.modal-body { padding: var(--space-4) var(--space-6) var(--space-6); }
.modal-foot { display: flex; justify-content: flex-end; gap: var(--space-2); padding: 0 var(--space-6) var(--space-6); }

/* forms */
.field { margin-bottom: var(--space-4); }
.field label { display: block; font-size: 12px; font-weight: 700; color: var(--ink-2); margin-bottom: 6px; letter-spacing: .02em; }
/* email and password were absent because the source file had no sign-in screen:
 * the focus ring below already applied to every input, so an unstyled box with
 * a correct focus ring looked like a deliberate variant rather than a gap. */
.field select,
.field input[type="text"],
.field input[type="email"],
.field input[type="number"],
.field input[type="password"] {
  width: 100%; border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); padding: var(--space-2) var(--space-3); font: inherit; color: var(--ink);
}
.field select:focus, .field input:focus { outline: 2px solid var(--accent-wash); border-color: var(--accent); }
/* Number fields drop the spinner: its controls carry a min-height in WebKit that
   makes the box a hair taller than the text inputs and selects beside it. */
.field input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.field input[type="number"]::-webkit-outer-spin-button,
.field input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.radio-cards { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.radio-card {
  border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: var(--space-3); cursor: pointer; transition: all .12s ease;
}
.radio-card:hover { border-color: var(--ink-3); }
.radio-card.on { border-color: var(--accent); background: var(--accent-wash); }
.radio-card .rc-title { font-weight: 700; font-size: 13px; }
.radio-card .rc-sub { font-size: 11.5px; color: var(--ink-2); margin-top: 2px; }

/* searchable combobox */
.combo { position: relative; }
.combo > input {
  width: 100%; border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); padding: var(--space-2) var(--space-7) var(--space-2) var(--space-3); font: inherit; color: var(--ink);
}
.combo > input:focus { outline: 2px solid var(--accent-wash); border-color: var(--accent); }
.combo .combo-chev { position: absolute; right: 11px; top: 12px; color: var(--ink-3); pointer-events: none; }
.combo.has-sel > input::placeholder { color: var(--ink); opacity: 1; }
.combo:focus-within > input { border-color: var(--accent); border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.combo-list {
  display: none;
  /* local order: the list only has to clear its own field and the rows below
   * it inside the same positioned ancestor, so it is not on the --z-* scale */
  position: absolute; z-index: 20; left: 0; right: 0; top: 100%;
  max-height: 240px; overflow: auto; background: var(--surface);
  border: 1px solid var(--accent); border-top: 0;
  border-bottom-left-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm);
  box-shadow: var(--shadow-pop);
}
.combo:focus-within .combo-list { display: block; }
.combo-opt { display: flex; align-items: baseline; gap: var(--space-2); padding: var(--space-2) var(--space-3); cursor: pointer; }
.combo-opt:hover { background: var(--accent-wash); }
.combo-opt .co-name { font-weight: 600; font-size: 13px; }
.combo-opt .co-sub { font-size: 11.5px; color: var(--ink-3); margin-left: auto; }
.combo-empty { padding: var(--space-3); color: var(--ink-3); font-size: 12.5px; text-align: center; }

/* dropzone */
.dropzone {
  border: 2px dashed var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); padding: var(--space-7) var(--space-5); text-align: center;
  cursor: pointer; transition: all .15s ease;
}
.dropzone:hover { border-color: var(--accent); background: var(--accent-wash); }
.dropzone.drag { border-color: var(--accent); background: var(--accent-wash); }
.dropzone.busy { pointer-events: none; opacity: .8; }
.dropzone .dz-icon { font-size: 30px; margin-bottom: var(--space-2); }
.dropzone .dz-main { font-weight: 700; }
.dropzone .dz-sub { font-size: 12px; color: var(--ink-2); margin-top: 3px; }

/* parsed-file summary in upload flow */
.filesummary {
  display: flex; gap: var(--space-3); align-items: center;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4);
}
.filesummary .fs-icon {
  width: 40px; height: 40px; border-radius: 10px; background: var(--accent-wash);
  color: var(--accent-deep); display: flex; align-items: center; justify-content: center; font-weight: 800;
}
.filesummary .fs-name { font-weight: 700; font-size: 13px; }
.filesummary .fs-meta { font-size: 12px; color: var(--ink-2); }
.filesummary.ok { border-color: var(--ok); background: var(--ok-wash); margin-bottom: 0; }
.filesummary.ok .fs-icon { background: var(--ok); color: var(--on-ink); }
.resolve-ok { display: flex; align-items: center; gap: var(--space-2); color: var(--ok); font-size: 12.5px; font-weight: 600; margin: -6px 0 var(--space-4) 2px; }

/* duplicate dialog choices */
.dup-choices { display: grid; gap: var(--space-2); margin-top: var(--space-3); }
.dup-choice {
  display: flex; gap: var(--space-3); align-items: flex-start; text-align: left;
  border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); padding: var(--space-3) var(--space-4); cursor: pointer; transition: all .12s ease;
  font: inherit;
}
.dup-choice:hover { border-color: var(--accent); background: var(--accent-wash); }
.dup-choice .dc-title { font-weight: 700; }
.dup-choice .dc-sub { font-size: 12px; color: var(--ink-2); margin-top: 2px; }

/* delete measurement row */
.delx { display: inline-flex; align-items: center; justify-content: center; padding: var(--space-1); border-radius: 6px; color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity .12s, color .12s, background .12s; }
.rowlink:hover .delx { opacity: 1; }
.delx:hover { color: var(--danger); background: var(--danger-wash); }

/* toast */
.toast {
  position: fixed; bottom: var(--space-6); right: var(--space-6); z-index: var(--z-toast);
  background: var(--ink); color: var(--on-ink); border-radius: 10px;
  padding: var(--space-3) var(--space-4); font-size: 13px; font-weight: 600;
  box-shadow: var(--shadow-pop); animation: tray-in .2s ease;
}

/* ── Inline notice ─────────────────────────────────────────────────────────
 * A consequence stated next to the control that causes it, as opposed to
 * `.banner`, which is a page-width strip at the top of a screen and is read
 * once, if at all. Trash needs "this cannot be undone" beside the button, and
 * Spec 3's delete needs its dependent counts in the same place. */
.notebox {
  border: 1px solid var(--line); background: var(--surface-2);
  border-radius: var(--radius-sm); padding: var(--space-3);
  font-size: 12.5px; color: var(--ink-2);
}
.notebox b { color: var(--ink); }
.notebox p { margin: 6px 0 0; }
.notebox p:last-child { margin-bottom: 0; }
.notebox.danger { border-color: var(--danger-line); background: var(--danger-wash); color: var(--danger); }
.notebox.warn   { border-color: var(--warn-line);   background: var(--warn-wash);   color: var(--warn); }
/* No --ok-line exists; the wash doubles as the border rather than inventing a
 * colour, which would be the one hex outside :root. */
.notebox.ok     { border-color: var(--ok-wash);     background: var(--ok-wash);     color: var(--ok); }
.notebox.danger b, .notebox.warn b, .notebox.ok b { color: inherit; }

/* empty states */
.empty { text-align: center; padding: var(--space-8) var(--space-5); color: var(--ink-2); }
.empty .e-icon { font-size: 30px; margin-bottom: var(--space-2); }
.empty b { color: var(--ink); }

/* utility */
.mt0{margin-top:0}.mb0{margin-bottom:0}
.muted { color: var(--ink-2); }
.small { font-size: 12px; }
.mono { font-variant-numeric: tabular-nums; }
.flexrow { display: flex; align-items: center; gap: var(--space-2); }
.right { margin-left: auto; }
hr.hair { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }

/* ── Measured Materials list (printer capability overview) ──────────────── */
.matlist .secrow td { background: var(--surface-2); border-top: 1px solid var(--line); padding: 7px var(--space-3); }
.matlist .sec-l { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
.matlist .sec-h { font-size: 11.5px; color: var(--ink-3); margin-left: var(--space-2); }
.matlist td.csel { white-space: nowrap; }
.matlist .matrow.pending { color: var(--ink-2); }
.matlist .matrow.pending td b { font-weight: 600; color: var(--ink-2); }
.matlist .matrow.pending:hover { background: var(--surface-2); cursor: pointer; }
.matlist .rowfold { display: inline-flex; vertical-align: middle; color: var(--ink-3); cursor: pointer; padding: 2px; margin-left: 2px; }
.matlist .rowfold:hover { color: var(--ink); }
.matlist .mdot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--line-strong); margin-left: var(--space-2); }
.matlist .mini-onboard { font-size: 11px; font-weight: 700; color: var(--accent-deep); white-space: nowrap; }
.matlist .subrow td { background: var(--surface-2); border-top: 1px dashed var(--line); padding: 5px var(--space-3); font-size: 12px; }
/* 30/34px are alignment against the sub-row checkbox, not rhythm — snapping
 * them to the scale would step them out of line with the column above */
.matlist .subrow .sublabel { color: var(--ink-2); font-weight: 600; padding-left: 34px; }
.matlist .subrow td.csel { padding-left: 30px; }
.mtag { display: inline-block; font-size: 9.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; padding: 1px 6px; border-radius: 999px; vertical-align: middle; margin-left: 3px; }
.mtag.mand { background: var(--accent-wash); color: var(--accent-deep); }
.mtag.opt { background: var(--neutral-wash); color: var(--neutral-ink); }
.mtag.extra { background: var(--legacy-wash); color: var(--legacy-ink); }
.matlist .secrow { cursor: pointer; user-select: none; }
.matlist .secrow:hover td { background: var(--line); }
.matlist .secfold { display: inline-flex; vertical-align: middle; color: var(--ink-3); margin-right: 5px; }
.matlist .subrow.rowlink { cursor: pointer; }
.matlist .subrow.rowlink:hover td { background: var(--line); }
.matlist .subrow.subsel td { background: var(--accent-wash); }
.selbox.sub { width: 15px; height: 15px; }

/* ── Add-material modal ─────────────────────────────────────────────────── */
.am-tabs { display: inline-flex; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); overflow: hidden; margin-bottom: var(--space-3); }
.am-tabs button { border: none; background: var(--surface); color: var(--ink-2); padding: var(--space-2) var(--space-4); font-size: 13px; font-weight: 600; cursor: pointer; }
.am-tabs button + button { border-left: 1px solid var(--line); }
.am-tabs button.on { background: var(--ink); color: var(--on-ink); }
.am-search { width: 100%; padding: var(--space-2); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 13px; margin-bottom: var(--space-2); background: var(--surface-2); }
.am-search:focus { outline: 2px solid var(--accent-wash); border-color: var(--accent); }
.am-list { max-height: 320px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.am-item { display: flex; justify-content: space-between; gap: var(--space-2); align-items: center; padding: 9px var(--space-3); cursor: pointer; border-bottom: 1px solid var(--line); font-size: 13px; }
.am-item:last-child { border-bottom: 0; }
.am-item:hover { background: var(--surface-2); }
.am-item.on { background: var(--accent-wash); }
.am-item .sub { color: var(--ink-3); font-size: 12px; }
.am-item .new { font-size: 10px; font-weight: 800; color: var(--warn); text-transform: uppercase; letter-spacing: .04em; }
.am-empty { padding: var(--space-5); text-align: center; color: var(--ink-3); font-size: 13px; }
.am-err { color: var(--danger); background: var(--danger-wash); border: 1px solid var(--danger-line); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); font-size: 12.5px; margin-bottom: var(--space-2); }

/* ── Roles, and one-time credentials (Spec 1 §3.5) ──────────────────────────
 * A role chip is never shown on its own: the grammar is (user, organization,
 * role), so it always follows the organization it is held in. A free-floating
 * role chip would read as a global role — the `users.role` column this platform
 * is migrating away from.
 *
 * org_admin is marked out because it is the role that can administer people;
 * platform_admin gets the warn pair because its reach crosses organizations,
 * which is a different KIND of grant rather than a higher one. */
.rolechip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; white-space: nowrap;
  border-radius: 999px; padding: 2px var(--space-2);
  background: var(--neutral-wash); color: var(--ink-2);
}
.rolechip.admin    { background: var(--accent-wash); color: var(--accent-deep); }
.rolechip.platform { background: var(--warn-wash);   color: var(--warn); }
.rolechip.none     { background: transparent; color: var(--ink-3); border: 1px dashed var(--line-strong); }
/* The unified org/user tree's "this is you" marker (licensing overhaul spec
 * §3.A) — the same chip shape as a role, in the `ok` tone so it never reads as
 * a role itself. It answers "where do I sit, and why can I see this" rather
 * than describing a grant. */
.rolechip.you      { background: var(--ok-wash); color: var(--ok); }

/* A secret the server returns ONCE and never again — an admin-issued password
 * reset link today. It is not stored anywhere, so it has to be visibly
 * copyable rather than merely displayed, and the warn border says "act on this
 * now" without dressing a credential up as an error. */
.credential {
  display: flex; align-items: center; gap: var(--space-2);
  background: var(--surface-2); border: 1px solid var(--warn-line);
  border-radius: var(--radius-sm); padding: var(--space-1) var(--space-2);
}
.credential input {
  flex: 1; min-width: 0; border: 0; background: transparent;
  font: inherit; font-size: 12px; color: var(--ink); font-variant-numeric: tabular-nums;
}
.credential input:focus { outline: none; }
