/* kuemue ceo — ONE design system with kuemue wholesale.
 *
 * Everything down to the "This app only" line is copied from
 * kuemue-wholesale/public/assets/base.css. Do not fork it. When a colour, a
 * radius or a button size changes there, copy it across rather than inventing a
 * second look: the owner moves between these two screens all day and they have
 * to read as one product.
 *
 * wholesale keeps this in an assets/ folder loaded by every page. ceo draws one
 * page, so it is one file, in the same order as base.css so a diff between the
 * two stays readable.
 */

/* ── Tokens ─────────────────────────────────────────────────────────────────
   Copied from wholesale. The colours were already identical; the corner scale,
   the shadow and the type steps were not, which is what made the two apps read
   as cousins rather than siblings. */
:root {
  color-scheme: light;
  --ink: #1a1a1a;
  --ink-2: #303030;
  --paper: #ffffff;
  --card: #ffffff;
  --line: #e3e3e3;
  --line-2: #c9c9c9;
  --accent: #005bd3;
  --accent-ink: #004299;
  --accent-soft: #eaf4ff;
  --muted: #616161;
  --hover: #f6f6f6;
  --chip: #f1f1f1;
  --glass: rgba(255,255,255,.92);
  --skel-a: #ebebeb;
  --skel-b: #f7f7f7;
  --ok: #008060;    --ok-soft: #e5f3ee;
  --warn: #8a6116;  --warn-soft: #fdf3d8;
  --bad: #c5280c;   --bad-soft: #fedad9;
  --info: #00527c;  --info-soft: #e0f0fa;
  --violet: #6b4fa3; --violet-soft: #ece6f6;
  --teal: #1f7a72;  --teal-soft: #e0f0ee;
  /* Chart series colours — fixed order, CVD-validated. s1 mirrors --accent.
     ceo draws no chart yet. They stay defined and unused so the day it does,
     its lines are the same four colours as every other kuemue app. An earlier
     version of this file declared them and then redefined --s1..--s4 as a
     spacing scale forty lines further down, which silently deleted the palette.
     Spacing lives on --sp1..--sp6 now, so the two can never collide again. */
  --s1: #005bd3; --s2: #00897b; --s3: #6b4fa3; --s4: #b07d3f;
  --r: 12px;
  --r-sm: 8px;
  --r-xs: 6px;
  --r-lg: 16px;
  --shadow: 0 1px 2px rgba(20,20,20,.05), 0 4px 16px rgba(20,20,20,.06);
  --shadow-lg: 0 4px 12px rgba(20,20,20,.08), 0 16px 48px rgba(20,20,20,.14);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* Motion curves, same three as wholesale: --ease-out for anything entering or
     leaving, --ease-in-out for something moving on screen, --ease-drawer for the
     phone nav drawer. */
  --ease-out: cubic-bezier(.23, 1, .32, 1);
  --ease-in-out: cubic-bezier(.77, 0, .175, 1);
  --ease-drawer: cubic-bezier(.32, .72, 0, 1);

  /* Type steps and a 4px spacing grid. wholesale writes these as literal pixels
     at each site; ceo names them because one file draws every screen here. The
     numbers ARE wholesale's: 26/20/16/15/13/12 and 4/8/12/16/24/32. */
  --t-hero: 26px; --t-h1: 26px; --t-h2: 20px; --t-h3: 16px;
  --t-body: 15px; --t-small: 13px; --t-tiny: 12px;
  --sp1: 4px; --sp2: 8px; --sp3: 12px; --sp4: 16px; --sp5: 24px; --sp6: 32px;
  /* Density. These were finance's Profitability numbers for one pass and the
     owner called the result too small: that page is a 129-row money table, and
     borrowing its compression put ceo BELOW wholesale, which is the app he
     actually moves between. So they are wholesale's again — .table td 12px,
     th 10px 12px, body 14px — and wholesale is the size of record. Reach for
     finance's tighter set only on a screen that is genuinely a dense table. */
  --gap: 16px;
  --pad-cell: 12px;
  --pad-cell-head: 10px 12px;
}

/* Dark theme — soft dark grey, deliberately not black. Same values as wholesale. */
html[data-theme="dark"] {
  color-scheme: dark;
  --ink: #e7e7e7;
  --ink-2: #cfcfcf;
  --paper: #202020;
  --card: #2a2a2a;
  --line: #3d3d3d;
  --line-2: #555555;
  --accent: #4c9aff;
  --accent-ink: #8ab8ff;
  --accent-soft: #1d2c40;
  --muted: #a3a3a3;
  --hover: #333333;
  --chip: #383838;
  --glass: rgba(32,32,32,.92);
  --skel-a: #2e2e2e;
  --skel-b: #383838;
  --ok: #41bd90;    --ok-soft: #16352b;
  --warn: #d9ab52;  --warn-soft: #372c13;
  --bad: #ff7f68;   --bad-soft: #422019;
  --info: #5cb3e6;  --info-soft: #15303f;
  --violet: #ab90d9; --violet-soft: #2c2439;
  --teal: #58b6ac;  --teal-soft: #17312e;
  --s1: #3a7fe6; --s2: #219e90; --s3: #8f73c6; --s4: #b28a39;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.35);
  --shadow-lg: 0 4px 12px rgba(0,0,0,.5), 0 16px 48px rgba(0,0,0,.55);
}

* { box-sizing: border-box; }
/* display rules on classes (.btn etc.) beat the UA's [hidden] — restore its authority */
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font-family: var(--font); font-size: var(--t-body); line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
h1,h2,h3,h4 { margin: 0 0 .5em; line-height: 1.25; letter-spacing: -.01em; }
h1 { font-size: var(--t-h1); } h2 { font-size: var(--t-h2); } h3 { font-size: var(--t-h3); }
p { margin: 0 0 1em; }
a { color: var(--accent-ink); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; }
::placeholder { color: #b5b5b5; }

/* ── Brand ── */
.wordmark { font-weight: 700; font-size: 22px; letter-spacing: -.5px; color: var(--ink); }
.wordmark img.logo-img { height: 24px; width: auto; display: inline-block; vertical-align: middle; border: 0; }
html[data-theme="dark"] .wordmark img.logo-img { background: #fff; padding: 3px 7px; border-radius: 7px; height: 26px; }

/* ── Buttons ── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 18px; border-radius: 10px; border: 1px solid var(--line-2);
  background: var(--card); color: var(--ink); font: 600 14px/1.2 var(--font);
  cursor: pointer; text-decoration: none;
  transition: background .12s, border-color .12s, opacity .12s, transform .16s var(--ease-out);
  min-height: 42px;
}
.btn:active { transform: scale(.97); }
.btn:disabled:active, .btn[disabled]:active { transform: none; }
.btn:hover { background: var(--hover); text-decoration: none; }
.btn:disabled { opacity: .5; cursor: default; pointer-events: none; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-primary, .btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover, .btn-accent:hover { background: var(--accent-ink); }
.btn-danger { background: var(--card); border-color: var(--bad); color: var(--bad); }
.btn-danger:hover { background: var(--bad-soft); }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover { background: var(--hover); }
.btn-sm { padding: 6px 12px; min-height: 32px; font-size: 13px; border-radius: 8px; }
.btn-block { width: 100%; }
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }

/* ── Forms ── */
.label { display: block; font: 600 13px/1.3 var(--font); color: var(--ink-2); margin: 14px 0 6px; }
.input, .select, .textarea {
  width: 100%; padding: 10px 12px; border: 1px solid var(--line-2); border-radius: 10px;
  background: var(--card); color: var(--ink); font: 400 15px/1.4 var(--font);
  outline: none; transition: border-color .12s, box-shadow .12s;
}
.input:focus, .select:focus, .textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.textarea { min-height: 90px; resize: vertical; field-sizing: content; }
.field-err { color: var(--bad); font-size: var(--t-small); margin-top: 4px; }
.hint { color: var(--muted); font-size: var(--t-small); margin-top: 4px; }

/* ── Cards / layout ── */
.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow); }
.card-pad { padding: 20px; }
.card h3 { margin-bottom: 12px; }
.stack > * + * { margin-top: 14px; }
.split { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.muted { color: var(--muted); }
.small { font-size: var(--t-small); }
.mono { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.right { text-align: end; }
.nowrap { white-space: nowrap; }

/* ── Tables ────────────────────────────────────────────────────────────────
   wholesale's, value for value (its base.css .table block). These were briefly
   finance's tighter numbers and the owner called the result too small — finance
   compresses because Profitability is a 129-row money table, and copying that
   onto a three-page app just made it harder to read. wholesale is the size of
   record, because that is the screen he moves between all day. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: start; font: 600 12px/1.3 var(--font); text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); padding: var(--pad-cell-head); border-bottom: 1px solid var(--line);
}
.table th.right { text-align: end; }
/* 1.55 is right for a paragraph and wrong for a table row: it adds ~8px of air
   to every line and there is no paragraph here to need it. */
.table td { padding: var(--pad-cell); border-bottom: 1px solid var(--line);
  vertical-align: middle; line-height: 1.4; }
.table tr:last-child td { border-bottom: 0; }
.table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.table tbody tr:hover { background: var(--hover); }

/* ── Badges & status pills ── */
.badge {
  display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px;
  font: 600 12px/1.4 var(--font); background: var(--chip); color: var(--ink-2); white-space: nowrap;
}
.badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-bad { background: var(--bad-soft); color: var(--bad); }
.badge-muted { background: var(--chip); color: var(--muted); }
.count-pill {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 20px; padding: 0 7px; border-radius: 999px;
  background: var(--chip); color: var(--muted);
  font: 600 12px/1 var(--font); font-variant-numeric: tabular-nums;
  vertical-align: middle; margin-inline-start: 8px; translate: 0 -1px;
}

/* ── Global top bar ── an almost-white rail, a whisker off the page. */
.topbar {
  --tb-bg: #fbfbfb;
  --tb-line: var(--line);
  --tb-btn-hover: rgba(0,0,0,.05);
  position: fixed; top: 0; left: 0; right: 0; z-index: 45; height: 44px;
  display: flex; align-items: center; gap: 10px; padding: 0 16px;
  background: var(--tb-bg); border-bottom: 1px solid var(--line); color: var(--ink);
  border-top: 3px solid var(--rolec, var(--accent));
}
html[data-theme="dark"] .topbar { --tb-bg: #232323; --tb-btn-hover: rgba(255,255,255,.07); }
.topbar .brand { flex: none; display: flex; align-items: center; gap: 8px; padding: 0; margin: 0; }
.topbar .brand .wordmark { display: inline-flex; align-items: center; }
.topbar .brand .wordmark:hover { text-decoration: none; }
/* The logo JPG has a white box baked in — blend it into the band rather than chipping it. */
.topbar .brand .wordmark img.logo-img { height: 20px; mix-blend-mode: multiply; background: none; padding: 0; }
html[data-theme="dark"] .topbar .brand .wordmark img.logo-img { filter: invert(1); mix-blend-mode: screen; background: none; padding: 0; }
/* "ceo" — the surface badge, same recipe as wholesale's. */
.topbar .tb-badge {
  flex: none; font: 800 9px/1 var(--font); letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  padding: 3px 8px; border-radius: 999px; white-space: nowrap;
}
.topbar .tb-right { flex: none; display: flex; align-items: center; gap: 8px; margin-inline-start: auto; }
.topbar .tb-role { flex: none; display: inline-flex; align-items: center; gap: 6px; }
.topbar .tb-role .tag {
  display: inline-block; font: 800 10px/1 var(--font); letter-spacing: .08em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px;
  color: var(--rolec, var(--accent));
  background: color-mix(in srgb, var(--rolec, var(--accent)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--rolec, var(--accent)) 25%, transparent);
}
.ubtn {
  position: relative; width: 34px; height: 34px; padding: 0; border-radius: 999px;
  border: 1px solid var(--tb-line, var(--line)); background: transparent; color: var(--ink-2);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  transition: background .12s;
}
.ubtn:hover { background: var(--tb-btn-hover, var(--hover)); }
.ubtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ubtn .ic { width: 16px; height: 16px; }
.navbtn { display: none; margin-inline-end: 2px; }   /* phone-only; the drawer rule below shows it */
.navctl { position: relative; display: inline-flex; }
.menu {
  position: absolute; inset-inline-end: 0; top: calc(100% + 8px); background: var(--card);
  border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow-lg);
  min-width: 200px; padding: 6px; z-index: 60; color: var(--ink);
}
.menu .me { padding: 7px 12px 5px; font-size: 12px; color: var(--muted); word-break: break-all; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.menu .me b { color: var(--ink); display: block; font-size: 13px; margin-bottom: 2px; }
.menu button {
  display: block; width: 100%; text-align: start; border: 0; background: none;
  padding: 8px 12px; border-radius: 7px; font: 500 14px/1.2 var(--font); color: var(--ink); cursor: pointer;
}
.menu button:hover { background: var(--hover); }

/* ── Shell: rail + content, both offset below the fixed 44px bar ── */
.shell { display: flex; min-height: 100vh; padding-top: 44px; }
.side {
  width: 220px; flex: none; border-inline-end: 1px solid var(--line); background: #fbfbfb;
  padding: 18px 12px; position: sticky; top: 44px; height: calc(100vh - 44px);
  overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column;
}
html[data-theme="dark"] .side { background: #232323; }
.sidenav { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; }
.side .navlink {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px;
  color: var(--ink-2); font: 600 14px/1.2 var(--font); text-decoration: none; margin-bottom: 2px;
  border: 0; background: none; cursor: pointer; text-align: start; width: 100%;
}
.side .navlink:hover { background: rgba(20,20,20,.05); text-decoration: none; }
html[data-theme="dark"] .side .navlink:hover { background: rgba(255,255,255,.06); }
.side .navlink.on { background: var(--card); color: var(--ink); box-shadow: var(--shadow), inset 0 0 0 1px var(--line); }
.side .navlink .ic { width: 18px; height: 18px; flex: none; }
.main { flex: 1; min-width: 0; padding: 20px 24px 60px; background: var(--card); }

/* Collapsible rail, ported from wholesale base.css (its own comment sits at the
   "Collapsible sidebar (desktop)" block). Same geometry, same behaviour: the
   labels go, the icons centre, and clicking empty rail space toggles it — there
   is no dedicated button, because wholesale does not have one either.
   Remembered per device under kceo_side, the way kceo_theme already is.
   Desktop only: under 1000px the rail is already a horizontal strip. */
@media (min-width: 1000px) {
  .side { transition: width .18s var(--ease-out); }
  html.side-min .side { width: 64px; padding: 18px 8px; }
  html.side-min .nl-t { display: none; }
  html.side-min .side .navlink { justify-content: center; padding: 10px 0; }
  /* The rail is the click target, so it has to look like one. */
  .side { cursor: pointer; }
  .side .navlink, .side .sidenav { cursor: default; }
  .side .navlink { cursor: pointer; }
}
@media (prefers-reduced-motion: reduce) { .side { transition: none; } }

/* The content column centres, the way every kuemue screen does. Connections is
   the one page that releases it: the map and its detail panel have to sit side
   by side, and a fixed column just squeezes the drawing (the same trap that left
   the wholesale portal with a 412px settings pane). */
#view { max-width: 1160px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--gap); }
#view.wide { max-width: 1480px; }

/* Breadcrumbs, at finance's values. On a three-section app they are not for
   finding your way — they are the line that tells you the title below is a
   page and not a card, which is half of why finance reads as composed. */
.crumbs { display: flex; align-items: center; gap: 8px; font-size: var(--t-small); margin: 0; }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--accent); text-decoration: underline; }
.crumb-sep { color: var(--line-2); }
.crumb-now { color: var(--ink); font-weight: 600; }

.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin: 4px 0 0; }
/* Title and its count on one baseline, the way finance sets "Profitability 129". */
.page-head .ttl { display: flex; align-items: center; gap: var(--sp2); }
.page-head h1 { margin: 0 0 2px; }
.page-head .sub { color: var(--muted); font-size: var(--t-small); }

/* Section label above a block of numbers: small, tracked, muted. The figures
   below it lead, not the heading. Same as the wholesale dashboard. */
section { display: flex; flex-direction: column; gap: var(--sp3); }
.hd { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.hd h2 { font: 700 10.5px/1 var(--font); text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted); margin: 0; }
.hd .sub { color: var(--muted); font-size: 12.5px; opacity: .85; }

/* ── Empty / skeleton ── */
.empty { text-align: center; color: var(--muted); padding: 48px 16px; }
.skel { background: linear-gradient(90deg, var(--skel-a) 25%, var(--skel-b) 50%, var(--skel-a) 75%);
  background-size: 200% 100%; animation: skel 1.1s infinite linear; border-radius: 8px; min-height: 16px; }
@keyframes skel { to { background-position: -200% 0; } }

/* ── Modal ── the kit's answer to a destructive action. wholesale never shows a
   browser confirm() box and neither does this app now. */
.modal-back {
  position: fixed; inset: 0; z-index: 90; background: rgba(20,20,20,.45);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.modal {
  background: var(--card); border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  width: 100%; max-width: 460px; max-height: 88vh; overflow-y: auto; padding: 22px;
}
.modal h3 { margin: 0 0 8px; }
.modal p { color: var(--muted); font-size: var(--t-small); }
.modal .acts { display: flex; gap: var(--sp2); justify-content: flex-end; }
@media (max-width: 560px) {
  .modal-back { align-items: flex-end; padding: 0; }
  .modal { border-radius: var(--r-lg) var(--r-lg) 0 0; max-height: 92vh; }
}

/* ── Phone: the rail becomes a drawer behind the bar's ☰ ── */
@media (max-width: 999px) {
  .navbtn { display: inline-flex; }
  .shell { display: block; }
  .side {
    position: fixed; inset-block: 0; inset-inline-start: 0; z-index: 80;
    width: min(78vw, 286px); height: auto; padding: 54px 10px 16px;
    transform: translateX(-102%); visibility: hidden;
    transition: transform .22s var(--ease-drawer), visibility 0s .22s;
  }
  html[dir="rtl"] .side { transform: translateX(102%); }
  html.nav-open .side { transform: none; visibility: visible; transition: transform .22s var(--ease-drawer), visibility 0s; }
  .nav-back { position: fixed; inset: 0; z-index: 79; background: rgba(20,20,20,.45); }
  .side .navlink { min-height: 46px; }
  .main { padding: 16px 14px 60px; }
}
@media (max-width: 560px) {
  .main { padding-left: 12px; padding-right: 12px; }
  h1 { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) { .side { transition: none; } }
/* Top-bar utilities are 34px, under the 44px touch floor. Grow the tappable box
   on touch pointers only; the glyph and the desktop look are unchanged. */
@media (pointer: coarse) { .ubtn { width: 44px; height: 44px; } }

/* ── Sign in ── the wholesale auth page: centred column, brand, one card. */
.auth-wrap { max-width: 440px; margin: 0 auto; padding: 48px 16px 64px; }
.auth-head { text-align: center; margin-bottom: 20px; }
.auth-head .brand { display: inline-block; margin-bottom: 18px; text-decoration: none; }
.auth-head h1 { margin: 0 0 6px; }
.auth-head p { margin: 0; color: var(--muted); font-size: var(--t-small); }
.auth-wrap .label { font: 600 12px/1.3 var(--font); color: var(--ink-2); margin: 13px 0 4px; }
.auth-wrap .input {
  padding: 6px 10px; height: 34px; border-radius: 7px; border-color: var(--line);
  font-size: 14px; line-height: 1.35;
}
.auth-wrap .input:hover { border-color: var(--line-2); }
.auth-wrap .btn-block { min-height: 40px; border-radius: 9px; margin-top: 16px; }
.alert { border-radius: 9px; padding: 11px 13px; font-size: 13.5px; margin: 0 0 14px; }
.alert.err { background: var(--bad-soft); color: var(--bad); }
.alert.info { background: var(--info-soft); color: var(--info); }
@media (max-width: 640px) {
  .auth-wrap .input { font-size: 16px; padding: 10px 12px; height: 44px; }
  .auth-wrap .btn-block { min-height: 46px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   This app only. Everything below draws something wholesale does not have: the
   alarm strip, the money tiles, the connection map. Each one is built out of the
   tokens above, so it inherits the same corners, the same shadow and the same
   type as every wholesale card.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The alarm strip ────────────────────────────────────────────────────────
   A stripe down the side means the row reads as urgent before you read a word. */
.alarms { display: flex; flex-direction: column; gap: var(--sp2); }
.alarm { display: flex; gap: var(--sp3); align-items: flex-start; background: var(--card);
  border: 1px solid var(--line); border-inline-start: 4px solid var(--warn);
  border-radius: var(--r); box-shadow: var(--shadow); padding: var(--sp3) var(--sp4); }
.alarm.bad { border-inline-start-color: var(--bad); }
.alarm .what { font-weight: 600; }
.alarm .why { color: var(--muted); font-size: var(--t-small); }
.alarm .age { margin-left: auto; color: var(--muted); font-size: var(--t-small);
  white-space: nowrap; font-variant-numeric: tabular-nums; }
.allclear { background: var(--ok-soft); color: var(--ok); border-radius: var(--r);
  padding: var(--sp4); font-weight: 600; text-align: center; }

/* ── Tiles ── the wholesale dashboard stat tile, with a foot line for the one
   sentence a number on this screen usually needs. Centred, like every other
   card grid the owner reads. */
.tiles { display: grid; gap: var(--sp3); grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.tile { background: var(--card); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--shadow); padding: 18px 14px; min-height: 88px; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 5px; }
.tile .label { color: var(--muted); font-size: 12.5px; font-weight: 600; line-height: 1.3; }
.tile .fig { font-size: var(--t-hero); font-weight: 700; letter-spacing: -.02em;
  line-height: 1.05; font-variant-numeric: tabular-nums; }
.tile .unit { font-size: var(--t-h3); font-weight: 500; color: var(--muted); }
.tile .foot { font-size: 12.5px; color: var(--muted); margin-top: auto; padding-top: var(--sp2);
  max-width: 32ch; }
.up { color: var(--ok); font-weight: 600; }
.down { color: var(--bad); font-weight: 600; }

/* The tile that refuses. When the number would be a lie, the screen says so
   instead of showing it. */
.tile.blocked { border-color: var(--warn); background: var(--warn-soft); }
.tile.blocked .fig { font-size: var(--t-h2); color: var(--warn); }
.tile.blocked .foot { color: var(--warn); }

/* ── Notes ── */
.note { border-inline-start: 3px solid var(--accent); background: var(--accent-soft);
  border-radius: 0 var(--r-sm) var(--r-sm) 0; padding: var(--sp3) var(--sp4); font-size: var(--t-small); }
.note b { display: block; margin-bottom: var(--sp1); }
.note.warn { border-inline-start-color: var(--warn); background: var(--warn-soft); }

.key { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-small); background: var(--chip); border-radius: var(--r-sm);
  padding: var(--sp3); word-break: break-all; user-select: all; margin-top: var(--sp2); }

/* ── Connections ────────────────────────────────────────────────────────────
   Colour says WHAT a link carries and nothing else. Line style says how alive
   it is, and the arrowhead says direction. Three encoders, three jobs. */
.mapcard { background: var(--card); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--shadow); overflow: hidden; }
.maptools { display: flex; gap: var(--sp2); flex-wrap: wrap; align-items: center;
  padding: var(--sp3) var(--sp4); border-bottom: 1px solid var(--line); background: var(--hover); }
.maptools .flabel { font-size: 14px; color: var(--muted); }
/* Chips at wholesale's own .btn-sm — 32px, 13px, its compact control. They were
   briefly 28px, which is a size wholesale does not have and the owner read as
   too small. Full 42px .btn is the other extreme: two rows of those stacked
   150px of furniture above the drawing. .btn-sm is wholesale's answer to
   exactly this, so it is the one to use. */
.maptools .btn { padding: 6px 12px; min-height: 32px; font-size: var(--t-small);
  border-radius: 999px; }
.maptools .btn.on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.maptools .clear { margin-inline-start: auto; }
.mapempty { padding: var(--sp3) var(--sp4); background: var(--ok-soft); color: var(--ok);
  font-size: var(--t-small); font-weight: 600; border-bottom: 1px solid var(--line); }
/* ── The drawing, and its zoom ───────────────────────────────────────────────
   A map is the one thing on a screen that has no single right size: the whole
   estate at once and one box read close up are different jobs. Every other
   attempt to pick a number for it failed in a different direction — width:100%
   inflated it 35% on a wide monitor, then a hard 1100px cap made it small on
   one. So the size is the reader's now, and the buttons are next to the map.

   Zoom sets an explicit pixel width, which is why max-width has to give way:
   the SVG scales on width alone and height follows the viewBox. "Fit" hands it
   back to the column and is what a first-time reader gets. */
.mapscroll { overflow: auto; }
svg.map { display: block; width: 100%; min-width: 900px; max-width: 1100px;
  height: auto; margin-inline: auto; }
/* Only a ZOOMED map gets penned into a scrolling box. At Fit it keeps its full
   height and the page scrolls, so the whole estate is still one downward read —
   capping it always would have put the map in a 738px window and made "see
   everything at once" impossible, which is the thing this drawing is for. */
.mapscroll.zoomed { max-height: 82vh; }
.mapscroll.zoomed svg.map { max-width: none; min-width: 0; margin-inline: 0; }

.zoomer { display: flex; align-items: center; gap: var(--sp1);
  margin-inline-start: auto; }
.zoomer .btn { min-width: 32px; padding: 6px 10px; }
.zoomer .lvl { font-size: var(--t-small); color: var(--muted); min-width: 44px;
  text-align: center; font-variant-numeric: tabular-nums; }

/* ── Detail beside the map, not a screen below it ────────────────────────────
   Clicking a box used to filter a table roughly 950px further down the page, so
   reading the detail meant scrolling the map off the screen. The panel docks
   alongside instead, and sticks rather than stretching: the map is tall and the
   detail for one system is five rows. */
.mapbody { display: grid; grid-template-columns: minmax(0, 1fr); }
.mapbody.open { grid-template-columns: minmax(0, 1fr) 330px; }
.mapside { border-inline-start: 1px solid var(--line); background: var(--card);
  align-self: start; position: sticky; top: calc(44px + var(--sp3));
  max-height: calc(100vh - 44px - 2 * var(--sp3)); overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp3); padding: var(--sp4); }
.mapside .shd { display: flex; align-items: baseline; gap: var(--sp2); }
.mapside .shd h3 { margin: 0; font-size: var(--t-h3); letter-spacing: -.01em; }
.mapside .shd .x { margin-inline-start: auto; }
.mapside .scount { font-size: var(--t-small); color: var(--muted); }

/* One link, as a card. The table downstairs is the same data in rows; this is
   the same data at the point where you are looking. */
.slink { border: 1px solid var(--line); border-radius: var(--r-sm); padding: var(--sp3);
  display: flex; flex-direction: column; gap: var(--sp2); }
.slink.dormant { background: var(--hover); }
.slink .sway { font-size: var(--t-small); font-weight: 600; display: flex;
  align-items: center; gap: var(--sp2); flex-wrap: wrap; }
.slink .sway .arrow { font-size: var(--t-small); }
.slink .srow { display: flex; gap: var(--sp2); flex-wrap: wrap; align-items: center; }
.slink .sacts { display: flex; gap: var(--sp2); margin-top: var(--sp1); }

/* ── The edit form ───────────────────────────────────────────────────────────
   Plain stacked fields on the wholesale form controls. This edits a drawing, so
   nothing here needs to feel dangerous — but the banner above it does have to be
   unmissable. */
/* The warning has to stay — the owner asked outright whether editing this screen
   switches real systems on and off, and the Add and Delete buttons sit right
   above it. But it was a 65px slab of prose. One tight line at chip size says
   the same thing and stops competing with the drawing for the eye. */
.mapwarn { display: flex; gap: var(--sp2); align-items: baseline; flex-wrap: wrap;
  padding: var(--sp3) var(--sp4); background: var(--warn-soft); color: var(--warn);
  border-bottom: 1px solid var(--line); font-size: var(--t-small); line-height: 1.45; }
.mapwarn b { flex: none; }
.mapwarn span { color: var(--warn); opacity: .85; }
.lform { display: flex; flex-direction: column; gap: var(--sp2); }
.lform .label { margin: 0 0 4px; }
.lform .input, .lform .select, .lform .textarea { padding: 6px 10px; font-size: 14px; border-radius: 8px; }
.lform .textarea { min-height: 54px; }
.lform .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp3); }
.lform .acts { display: flex; gap: var(--sp2); margin-top: var(--sp1); }
.lform .acts .del { margin-inline-start: auto; }

@media (max-width: 900px) {
  .mapbody.open { grid-template-columns: minmax(0, 1fr); }
  .mapside { position: static; max-height: none; border-inline-start: 0;
    border-top: 1px solid var(--line); }
  .lform .two { grid-template-columns: minmax(0, 1fr); }
}

/* ── One channel, one meaning ────────────────────────────────────────────────
   A line says four things at once, so each one gets a channel of its own and
   nothing else is allowed to touch that channel:

     what it carries  -> the colour        (set inline from RISK)
     which way        -> the arrowhead
     HOW it travels   -> the dash pattern  (below)
     WHETHER it runs  -> weight + fade     (below)

   These last two used to SHARE the dash pattern, so any link that was off or
   not-known lost its "how" completely — 19 of the 36 — and a dotted line meant
   two different things. */

/* HOW it travels. Solid, dashed, dotted — matching the legend swatches, which
   are real CSS border styles, so the key cannot drift from the drawing. */
.lk { stroke-width: 2.2; }                 /* by itself, computer to computer */
.lk.s-machine { stroke-dasharray: 10 3; }  /* a script runs it */
.lk.s-hand { stroke-dasharray: 2 4; }      /* a person carries it */

/* WHETHER it runs. Weight and fade only — never the dash pattern. */
.lk.unknown { stroke-width: 1.6; opacity: .5; }
.lk.off { stroke-width: 1.4; opacity: .26; }

/* Filtered out. Must come last: it has to beat the state fades above, which
   carry the same specificity. */
.lk.dim { opacity: .06; }

/* Bands. A background and one small label do more for "organised" than any
   amount of tidying the boxes. */
.band { fill: var(--hover); stroke: var(--line); stroke-width: 1; }
.band-t { font: 600 10px var(--font); fill: var(--muted);
  letter-spacing: .09em; text-transform: uppercase; }

.nd rect { fill: var(--card); stroke: var(--line-2); stroke-width: 1.5; }
.nd.ours rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 2; }
.nd .nm { font: 600 13px var(--font); fill: var(--ink); }
.nd .nsub { font: 400 10.5px var(--font); fill: var(--muted); }
.nd { cursor: pointer; }
.nd:hover rect { stroke: var(--ink); }
.nd:focus-visible rect { stroke: var(--ink); stroke-width: 2.5; outline: none; }
.nd.picked rect { stroke: var(--ink); stroke-width: 2.5; }
/* Boxes dim far LESS than lines. At .22 the labels became unreadable, so a
   filtered map turned into a grey field you could not orient yourself in. */
.nd.dim { opacity: .45; }
.nd.dim .nm { fill: var(--muted); }
.nd.dim rect { fill: var(--card); }

/* How well a claim is actually known. The caveat colours the sub-label rather
   than adding a second line, which used to overlap it on every 48px box. */
.nd .nsub.weak-told { fill: var(--warn); }
.nd .nsub.weak-unknown { fill: var(--bad); }
/* A system of yours that really has reported in the last week. */
.live-dot { fill: var(--ok); }

/* The key. Every pattern the map can draw appears here exactly once, grouped by
   the question it answers, so a reader is never left guessing which of two
   meanings a dotted line carries. */
.legend { display: flex; gap: var(--sp3) var(--sp4); flex-wrap: wrap; align-items: center;
  padding: var(--sp3) var(--sp4); border-top: 1px solid var(--line); background: var(--hover);
  font-size: var(--t-small); color: var(--muted); }
.legend span { display: inline-flex; align-items: center; gap: var(--sp2); }
.legend i { width: 22px; height: 0; border-top: 2px solid var(--muted); flex: none; }
.legend .lgroup { font-size: var(--t-tiny); font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink); }
.legend .lsep { width: 1px; height: 16px; background: var(--line-2); }

/* How it travels — the dash pattern, matching .lk above. */
.legend i.k-self { border-top-style: solid; }
.legend i.k-script { border-top-style: dashed; }
.legend i.k-person { border-top-style: dotted; }

/* Whether it runs — weight and fade, matching .lk above. */
.legend i.k-run { border-top-width: 2.2px; }
.legend i.k-unk { border-top-width: 1.6px; opacity: .5; }
.legend i.k-off { border-top-width: 1.4px; opacity: .26; }

.legend .dotlive { width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok); display: inline-block; }

/* Field chips.
   These rows carry four lines where finance's Profitability carries one — a
   badge, the chips, a note, and the reason. That is why a row here will never
   be its 37px, and it should not: dropping a line would cost the reader the
   evidence. What it can do is stop padding each line. Every margin and leading
   below is trimmed to the smallest that still reads: 96px to 89px, and the
   cell padding above it did the rest. If this row ever has to reach finance's
   height, the honest lever is moving the note and the reason into the side
   panel, not squeezing the leading further. */
.fields { display: flex; flex-wrap: wrap; gap: var(--sp1); margin-top: var(--sp1); }
.rownote { font-size: var(--t-small); line-height: 1.45; color: var(--muted);
  margin-top: var(--sp1); max-width: 52ch; }
#linkTable td .riskbadge { margin: 0; }
#linkTable td.st .pill + .pill { margin-inline-start: 3px; }

/* The filter buttons ARE the legend: each carries the swatch AND the words, so
   nothing has to be memorised. Click one and the map answers. */
.btn.key { display: inline-flex; align-items: center; gap: var(--sp2); }
.btn.key i { width: 14px; height: 3px; border-radius: 2px; flex: none; }
.btn.k-personal i { background: var(--bad); }
.btn.k-commercial i { background: var(--warn); }
.btn.k-secret i { background: var(--violet); }
.btn.k-public i { background: var(--ok); }
/* The swatch has to survive the button turning dark when selected. */
.btn.key.on i { box-shadow: 0 0 0 1px var(--paper); }
.fsep { width: 1px; height: 20px; background: var(--line-2); margin: 0 var(--sp1); }

/* The row says the risk in WORDS first, coloured to match. Colour reinforces
   the label; it never has to carry the meaning on its own. */
.riskbadge { display: inline-block; font-size: var(--t-tiny); font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; padding: 2px var(--sp2);
  border-radius: var(--r-sm); margin-bottom: var(--sp2); }
.r-public { background: var(--ok-soft); color: var(--ok); }
.r-commercial { background: var(--warn-soft); color: var(--warn); }
.r-personal { background: var(--bad-soft); color: var(--bad); }
.r-secret { background: var(--violet-soft); color: var(--violet); }
/* The field names themselves are plain. Four hues repeated across every row was
   the noise that made the colour unreadable in the first place. */
.ff { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-tiny); padding: 1px var(--sp2); border-radius: var(--r-sm);
  background: var(--chip); color: var(--ink-2); white-space: nowrap; }

/* State. Colour marks only what is NOT running, so a healthy row stays quiet. */
.badge.st-live { background: var(--chip); color: var(--muted); }
.badge.st-off { background: var(--bad-soft); color: var(--bad); }
.badge.st-unknown { background: var(--warn-soft); color: var(--warn); }
.badge.told { background: var(--violet-soft); color: var(--violet); margin-inline-start: var(--sp1); }
td.st { white-space: nowrap; }
td.st .rownote { white-space: normal; max-width: 30ch; }

.arrow { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--muted); text-align: center; white-space: nowrap; }
.sys { font-weight: 600; }
.sys.ours { color: var(--accent); }
tr.dormant td { opacity: .55; }
.emptyrow td { padding: var(--sp5); text-align: center; color: var(--muted); }

/* ── Toasts ─────────────────────────────────────────────────────────────────
   finance's, value for value (its style.css "Toast" block). Bottom-centre,
   pill-shaped, red when something failed. The point is that nothing in the
   kuemue apps uses a browser alert, so a save that worked says so quietly and a
   save that failed says so in a way you cannot miss. */
#toasts { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 120; display: flex; flex-direction: column; align-items: center; gap: 8px;
  pointer-events: none; }
.toast { background: var(--ink); color: var(--paper); border-radius: 999px;
  padding: 10px 18px; font-size: var(--t-small); text-align: center;
  box-shadow: var(--shadow-lg); animation: toastin .18s var(--ease-out); }
.toast.bad { background: var(--bad); color: #fff; }
@keyframes toastin { from { opacity: 0; transform: translateY(8px); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

/* ── Search ─────────────────────────────────────────────────────────────────
   finance opens a global palette on Ctrl K. This is the same door on a much
   smaller estate: three pages, 41 boxes and 54 links, so it searches those
   rather than carrying finance's scope chips for data this app does not have. */
.gs-back { position: fixed; inset: 0; z-index: 130; background: rgba(20,20,20,.34);
  display: flex; justify-content: center; align-items: flex-start; padding: 12vh 16px 16px; }
.gs { width: 100%; max-width: 560px; background: var(--card); border: 1px solid var(--line-2);
  border-radius: var(--r); box-shadow: var(--shadow-lg); overflow: hidden;
  display: flex; flex-direction: column; max-height: 70vh; }
.gs input { width: 100%; border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  padding: 14px 16px; font-size: var(--t-h3); background: none; color: var(--ink); outline: none; }
.gs-body { overflow-y: auto; padding: 6px; }
.gs-grp { font-size: var(--t-tiny); font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); padding: 8px 10px 4px; }
.gs-row { display: flex; align-items: center; gap: 8px; width: 100%; text-align: start;
  padding: 7px 10px; border: 0; background: none; border-radius: var(--r-sm);
  color: var(--ink); font: inherit; font-size: var(--t-small); cursor: pointer; }
.gs-row:hover, .gs-row.on { background: var(--hover); }
.gs-row .gs-sub { color: var(--muted); margin-inline-start: auto; font-size: var(--t-tiny);
  white-space: nowrap; }
.gs-empty { padding: 18px; text-align: center; color: var(--muted); font-size: var(--t-small); }
.gs-foot { border-top: 1px solid var(--line); padding: 7px 12px; font-size: var(--t-tiny);
  color: var(--muted); display: flex; gap: 12px; }
kbd { font: 600 11px/1 var(--font); background: var(--chip); border: 1px solid var(--line-2);
  border-radius: 4px; padding: 3px 5px; color: var(--ink-2); }

/* The rail carries its own count, like wholesale's. It is the only number on
   this app that is visible without opening anything. */
.side .navlink .cnt { margin-inline-start: auto; font-size: var(--t-tiny); color: var(--muted);
  background: var(--chip); border-radius: 999px; padding: 1px 7px; }
.side .navlink.on .cnt { background: var(--accent-soft); color: var(--accent-ink); }
html.side-min .side .navlink .cnt { display: none; }
