:root{
  --bg:#F5F0E8; --card:#FFFFFF; --side:#FFFBEB; --row-alt:#FDFAF5; --row-hover:#FEF9EC;
  --line:#E8D5B0; --line-soft:#F0E4C8; --ink:#1A0A00; --ink-2:#44260A; --muted:#8C6A3A;
  --label:#A07840; --accent:#D97706; --accent-deep:#B45309; --accent-pale:#FFFBEB;
  --ok:#15803D; --ok-bg:#ECFDF3; --warn:#92400E; --warn-bg:#FEF6E7; --bad:#B91C1C; --bad-bg:#FEF2F2;
  --info:#1D4ED8; --info-bg:#EFF4FF;
  --select:#7C2D12; --select-ink:#FFFFFF;
}
*{box-sizing:border-box}
/* `hidden` means hidden, whatever the element's class also says.
   The browser's own rule is [hidden]{display:none}, and a user-agent rule loses to any author one —
   so .addbar{display:flex} won and the add-an-ingredient panel stayed on screen with the attribute
   faithfully set. Every panel in the app opens and closes with this attribute, so it is worth the
   !important: the alternative is remembering the trap at each of them. */
[hidden]{display:none !important}
body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.5 "Segoe UI",Inter,system-ui,sans-serif;
  font-variant-numeric:tabular-nums}
a{color:var(--accent-deep);text-decoration:none}
a:hover{text-decoration:underline}
h1{font-size:19px;margin:0}
h2{font-size:14px;margin:0 0 10px;letter-spacing:.2px}
h3{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--label);margin:0 0 8px}

/* ---------- shell ---------- */
/* The app is a frame, and only what is inside it moves.
   The whole page used to scroll, so dragging a long list took the sidebar and the header away with
   it — on a screen whose entire job is a table, the two things that tell you where you are were the
   first to leave. Now the shell is exactly the viewport and the scrolling happens inside. */
.shell{display:flex;height:100vh;overflow:hidden}
.side{width:212px;flex:0 0 212px;background:var(--side);border-right:1px solid var(--line);
  display:flex;flex-direction:column;height:100%}
/* One gutter down the whole sidebar.
   The logo, the group headings, every menu item and the footer all start at the same x — it was
   14px, 16px and 17px before, which is close enough to look like a mistake rather than a choice.
   The 6px on .nav is the inset the hover and active backgrounds sit in; 6 + 6 lands the text on
   the same 12px as the logo and the footer. */
/* The brand block and the top bar are the same height, so their bottom borders form one
   unbroken line across the seam between sidebar and content. They were 53px and 52px — one pixel
   out, which is exactly enough to look like a mistake. */
.side{--side-gutter:12px;--side-inset:6px;--bar-height:56px}
.brand{display:flex;align-items:center;gap:9px;border-bottom:1px solid var(--line);
  height:var(--bar-height);padding:0 var(--side-gutter);box-sizing:border-box}
.brand{text-decoration:none;gap:8px}
/* Height drives both marks and the width follows, because they are not square: the leaf is
   609x832 (taller than wide) and the wordmark 1180x154 (seven times wider than tall). Forcing
   either into a box squashes the artwork — the first attempt made the leaf a 28px square and
   flattened it. */
.brand .lg{height:34px;width:auto;flex:0 0 auto;display:block}
.brand .wordmark{height:19px;width:auto;min-width:0;display:block}
.nav{padding:10px var(--side-inset);flex:1;overflow-y:auto}
/* The group headings carry the shape of the menu, so they are allowed to be seen: a size up, a
   darker ink than the items beneath them, and a hairline above each one so the four sections read
   as four sections rather than one long list with occasional small words in it. */
.nav .grp{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--accent-deep);
  font-weight:800;padding:14px calc(var(--side-gutter) - var(--side-inset)) 6px;
  margin-top:8px;border-top:1px solid var(--line-soft)}
.nav .grp:first-of-type{margin-top:4px;border-top:0}
/* the icon keeps its own column so the labels line up whatever shape the glyph is, and it never
   squashes when a name is long enough to wrap */
.nav .nav-ico{flex:0 0 15px;opacity:.72}
.nav a:hover .nav-ico,.nav a.on .nav-ico{opacity:1}
.nav a{display:flex;align-items:center;gap:8px;border-radius:6px;color:var(--ink-2);font-size:12.5px;
  padding:7px calc(var(--side-gutter) - var(--side-inset))}
.nav a:hover{background:var(--row-hover);text-decoration:none}
.nav a.on{background:var(--accent);color:#fff;font-weight:600}
.nav .n{margin-left:auto;background:rgba(0,0,0,.12);border-radius:9px;padding:0 6px;font-size:10.5px}
.side .foot{border-top:1px solid var(--line);padding:10px var(--side-gutter);font-size:11px;color:var(--muted)}
.main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
/* no longer sticky: it cannot move, because nothing around it scrolls */
.top{height:var(--bar-height, 56px);background:var(--card);border-bottom:1px solid var(--line);display:flex;align-items:center;
  gap:12px;padding:0 18px;flex:0 0 auto;z-index:5}
.top .who{margin-left:auto;text-align:right;font-size:11.5px;color:var(--muted);line-height:1.3}
.top .who b{display:block;color:var(--ink);font-size:12.5px}
.content{padding:18px;flex:1;min-height:0;overflow-y:auto}

/* A list page is a column: the bar at the top, the grid taking whatever is left, the pager at the
   bottom. The grid is sized by the frame rather than by its own rows, so its height no longer
   depends on how many rows the server sent — which is what let rows.js stop guessing.

   The pager is pushed down rather than placed: margin-top:auto puts it against the bottom whatever
   is above it, so it sits in the same place on a full page and a page with four rows on it. */
.content:has(> [data-grid]){display:flex;flex-direction:column;overflow:hidden;gap:0}
.content:has(> [data-grid]) > *{flex:0 0 auto}
.content:has(> [data-grid]) > [data-grid]{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.content:has(> [data-grid]) > [data-grid] > .table-wrap{flex:1 1 auto;min-height:0;overflow:auto}
.content:has(> [data-grid]) > .pager{margin-top:auto;padding-top:12px}

/* An accordion list is framed the same way a table is: it takes what is left and scrolls inside,
   with the pager against the bottom. FG Codes and FG Data both build one, and without this the
   whole page scrolled again the moment a count strip or an explainer was added above it. */
/* A tree that IS the page: the list scrolls inside the frame and the pager stays on screen, the
   same bargain every grid here makes.

   `.flow` opts out, for a tree that is one section of a longer page. The dashboard is that: a chart
   above it and a card below, so pinning gave the tree a slot a few hundred pixels tall to show six
   branches in while the page itself could not scroll at all. A section does not get to hold the
   page still. */
.content:has(> .tree:not(.flow)){display:flex;flex-direction:column;overflow:hidden}
.content:has(> .tree:not(.flow)) > *{flex:0 0 auto}
.content:has(> .tree:not(.flow)) > .tree{flex:1 1 auto;min-height:0;overflow-y:auto;padding-right:2px}
.content:has(> .tree:not(.flow)) > .pager{margin-top:auto;padding-top:12px}

/* The TSU tab on Item Mapping wraps its grid in the form that saves the codes, so the grid is a
   grandchild rather than a child. Same frame, one level down — without this that one tab would be
   the only screen in the app where the whole page still moves. */
.content:has(> form > [data-grid]){display:flex;flex-direction:column;overflow:hidden}
.content:has(> form > [data-grid]) > *{flex:0 0 auto}
.content:has(> form > [data-grid]) > form{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.content:has(> form > [data-grid]) > form > [data-grid]{flex:1 1 auto;min-height:0;
  display:flex;flex-direction:column}
.content:has(> form > [data-grid]) > form > [data-grid] > .table-wrap{flex:1 1 auto;min-height:0;
  overflow:auto}

/* Now that the rows scroll inside the card, the column names have to stay. Reading row forty of a
   list whose header has gone is guesswork. */
.content:has(> [data-grid]) thead th,
.content:has(> form > [data-grid]) thead th{position:sticky;top:0;z-index:2}
.crumb{display:flex;align-items:baseline;gap:0;min-width:0;overflow:hidden}
.crumb .up{color:var(--muted);font-size:12px;white-space:nowrap}
.crumb .sep{color:var(--muted);font-size:12px;margin:0 7px}
.crumb .title{color:var(--ink);font-size:15px;font-weight:700;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}

/* one line under the top bar: what this screen is, and what you can do on it */
.pagebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px;min-height:30px}
.pagebar .lead{margin:0;color:var(--muted);font-size:12.5px;min-width:0}
.pagebar .lead b{color:var(--ink-2);font-weight:600}

/* ---------- bits ---------- */
.btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:var(--card);
  color:var(--ink-2);border-radius:7px;padding:6px 11px;font-size:12.5px;cursor:pointer;font-family:inherit}
.btn:hover{background:var(--row-hover);text-decoration:none}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.btn.primary:hover{background:var(--accent-deep)}
.btn.danger{color:var(--bad);border-color:#F3C6C6}
.btn.sm{padding:3px 8px;font-size:11.5px;border-radius:6px}
.btn[disabled]{opacity:.5;cursor:not-allowed}

/* a segmented switch: two readings of one list, sharing one border so they read as one control
   rather than as two buttons that happen to sit together */
.seg{display:inline-flex;border:1px solid var(--line);border-radius:7px;overflow:hidden;background:var(--card)}
.seg a{padding:6px 11px;font-size:12.5px;color:var(--ink-2);border-right:1px solid var(--line)}
.seg a:last-child{border-right:0}
.seg a:hover{background:var(--row-hover);text-decoration:none}
.seg a.on{background:var(--accent);color:#fff;font-weight:600}
/* A secondary count inside the SELECTED segment. --muted is grey for a light ground; on the accent
   it is nearly the same value as the fill, so "To check (174)" lost its number and read as a
   disabled control. Inherit the segment's own colour and step back with opacity instead, which
   stays right whatever the palette does to --accent. */
.seg a.on .muted{color:inherit;opacity:.78}
.seg .dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:6px;vertical-align:middle}
.seg .dot.s-active{background:var(--ok)}
.seg .dot.s-pending{background:var(--info)}
.seg .dot.s-draft{background:#94A3B8}
.seg .dot.s-archived{background:#CBD5E1}
.seg a.on .dot{box-shadow:0 0 0 1.5px rgba(255,255,255,.85)}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px}
.card + .card{margin-top:14px}
.row{display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap}
.grow{flex:1;min-width:0}
.split{display:flex;gap:14px;align-items:flex-start}
.split .col-main{flex:1;min-width:0}
.split .col-side{width:320px;flex:0 0 320px}
.muted{color:var(--muted)}

/* ---------- top strip: the yield check as one line, not a side panel ---------- */
.strip{display:flex;flex-wrap:wrap;align-items:stretch;gap:0;background:var(--card);
  border:1px solid var(--line);border-radius:10px;margin-bottom:12px;overflow:hidden}
.strip .s{display:flex;flex-direction:column;gap:2px;padding:9px 16px;min-width:118px;
  border-right:1px solid var(--line-soft)}
.strip .s:last-child{border-right:0}
.strip .s.bar-cell{flex:1;min-width:150px;justify-content:center}
.strip .l{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.strip .v{font-size:14px;font-weight:700}
.strip .v.good{color:var(--ok)}
.strip .v.warn{color:var(--warn)}
.strip .bar{margin:5px 0 0}

/* ---------- multi-select dropdown ---------- */
.multi{position:relative;min-width:190px}
.multi summary{list-style:none;cursor:pointer;border:1px solid var(--line);border-radius:6px;
  padding:5px 9px;background:#fff;display:flex;flex-direction:column;gap:1px;min-height:34px;
  justify-content:center}
.multi summary::-webkit-details-marker{display:none}
.multi summary::after{content:"▾";position:absolute;right:9px;top:9px;color:var(--muted);font-size:11px}
.multi summary:hover{border-color:var(--accent)}
.multi .lbl{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.multi .val{font-size:12.5px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  padding-right:14px}
.multi[open] summary{border-color:var(--accent)}
.multi .panel{position:absolute;z-index:200;top:calc(100% + 3px);left:0;min-width:250px;max-width:340px;
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px;
  box-shadow:0 8px 22px rgba(var(--shadow-rgb),.13)}
.multi .filter{margin-bottom:6px}
.multi .opts{max-height:230px;overflow-y:auto;display:flex;flex-direction:column;gap:1px}
.multi .opts label{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:5px;
  font-size:12.5px;cursor:pointer;white-space:nowrap}
.multi .opts label:hover{background:var(--row-hover)}
.multi .opts input{width:auto;margin:0}

/* ---------- inline add rows ---------- */
.addbar{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}
.addline{display:flex;flex-wrap:wrap;gap:8px;align-items:end}
.addline .field{flex:1;min-width:120px}
.addline .field:first-child{flex:2.2}
.addline.prep{border-top:1px dashed var(--line);padding-top:12px}
.inline-check{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2)}

/* ---------- reach into the company master ---------- */
.reach{border-top:1px dashed var(--line);padding-top:10px}
.reach summary{cursor:pointer;font-size:12px;color:var(--accent-deep);font-weight:600;
  list-style:none;padding:2px 0}
.reach summary::-webkit-details-marker{display:none}
.reach summary::before{content:"+ ";font-weight:700}
.reach[open] summary::before{content:"− "}
.reach[open] summary{margin-bottom:10px}

/* ---------- preparation header row ---------- */
tbody tr.secrow td{background:var(--accent-pale);border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);font-size:12px;padding:6px 10px}
tbody tr.secrow:hover td{background:var(--accent-pale)}
tbody tr.secrow b{font-size:12.5px}
tbody tr.secrow .chip{margin:0 0 0 6px}

/* ---------- tabs ---------- */
.tabs{display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid var(--line)}
.tabs a{display:flex;align-items:center;gap:7px;padding:8px 14px;border:1px solid transparent;
  border-bottom:0;border-radius:8px 8px 0 0;color:var(--ink-2);font-size:12.5px;margin-bottom:-1px}
.tabs a:hover{background:var(--row-hover);text-decoration:none}
.tabs a.on{background:var(--card);border-color:var(--line);color:var(--ink);font-weight:600}
.tabs .n{background:var(--accent-pale);border:1px solid var(--line);border-radius:9px;padding:0 6px;
  font-size:10.5px;color:var(--muted)}
.tabs a.on .n{background:var(--accent);border-color:var(--accent);color:#fff}
.tabs .n.warn{background:var(--warn-bg);border-color:#EBD6A8;color:var(--warn)}
/* actions that belong to a tab, pushed to the right of the row the tabs already occupy.
   The holder carries the position so every button inside it is the same shape and sits on the same
   line — alignment on the buttons themselves is how two of them end up 6px apart. */
.tabs .tab-actions{display:flex;align-items:center;gap:6px;margin:0 0 6px auto}

/* ---------- card action bar ---------- */
.cardbar{display:flex;align-items:center;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}
.cardbar.bottom{border-bottom:0;border-top:1px solid var(--line)}

/* ---------- icon-only row action ---------- */
.iconbtn{display:inline-grid;place-items:center;width:26px;height:26px;border:1px solid var(--line);
  background:var(--card);color:var(--muted);border-radius:6px;cursor:pointer;padding:0}
.iconbtn:hover{color:var(--bad);border-color:#F3C6C6;background:var(--bad-bg)}
.iconbtn[disabled]{opacity:.35;cursor:not-allowed}
.iconbtn[disabled]:hover{color:var(--muted);border-color:var(--line);background:var(--card)}
.iconbtn svg{display:block}
.mono{font-family:"Cascadia Mono",Consolas,monospace;font-size:11.5px}
.right{text-align:right}
.center{text-align:center}
/* ---------- toasts: float, never take part in the layout ---------- */
.toasts{position:fixed;top:64px;right:18px;z-index:60;display:flex;flex-direction:column;gap:8px;
  max-width:440px;pointer-events:none}
.toast{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:9px;
  font-size:12.5px;line-height:1.45;box-shadow:0 10px 26px rgba(var(--shadow-rgb),.18);
  pointer-events:auto;animation:toast-in .16s ease-out}
.toast.ok{background:var(--ok-bg);border:1px solid #BBE5C6;color:var(--ok)}
.toast.bad{background:var(--bad-bg);border:1px solid #F3C6C6;color:var(--bad)}
.toast .x{border:0;background:transparent;color:inherit;opacity:.5;cursor:pointer;font-size:15px;
  line-height:1;padding:0 2px}
.toast .x:hover{opacity:1}
.toast.leaving{opacity:0;transform:translateX(8px);transition:opacity .2s,transform .2s}
@keyframes toast-in{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.toast{animation:none}.toast.leaving{transition:none}}

.flash{background:var(--ok-bg);border:1px solid #BBE5C6;color:var(--ok);padding:9px 12px;
  border-radius:8px;margin-bottom:14px;font-size:12.5px}
.errors{background:var(--bad-bg);border:1px solid #F3C6C6;color:var(--bad);padding:9px 12px;
  border-radius:8px;margin-bottom:14px;font-size:12.5px}
.errors ul{margin:4px 0 0 16px;padding:0}

/* ---------- form ---------- */
.field{display:flex;flex-direction:column;gap:4px;min-width:0}
.field label{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
input[type=text],input[type=search],input[type=email],input[type=password],input[type=number],select,textarea{
  border:1px solid var(--line);border-radius:6px;padding:6px 9px;font:inherit;font-size:12.5px;
  background:#fff;color:var(--ink);width:100%}
input:focus,select:focus,textarea:focus{outline:2px solid rgba(217,119,6,.25);border-color:var(--accent)}
/* Safari and Chrome give type=search a look of their own — an inset shadow, a different radius and
   a native clear button that is not this app's grey. Reset it to the same field as every other. */
input[type=search]{-webkit-appearance:none;appearance:none}
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
.readonly{border:1px dashed var(--line);border-radius:6px;padding:6px 9px;font-size:12.5px;
  background:var(--accent-pale);color:var(--ink-2)}
/* A field the server rejected. The toast says what went wrong; this says where, so a
   refused save cannot look like a completed one. */
.field.bad>input,.field.bad>select,.field.bad>textarea{border-color:#F3C6C6;background:var(--bad-bg)}
.field.bad>input:focus,.field.bad>select:focus,.field.bad>textarea:focus{
  outline:2px solid rgba(185,28,28,.2);border-color:var(--bad)}
.field .err{font-size:11px;color:var(--bad);line-height:1.35}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.checks{display:flex;flex-wrap:wrap;gap:8px 16px}
.checks label{display:flex;align-items:center;gap:6px;font-size:12.5px;text-transform:none;
  letter-spacing:0;color:var(--ink-2);font-weight:400}
.inline{display:inline}

/* ---------- table ---------- */
table{width:100%;border-collapse:collapse}
th{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);text-align:left;
  padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;background:var(--card)}
td{padding:7px 10px;border-bottom:1px solid var(--line-soft);font-size:12.5px;vertical-align:middle;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
td.key{max-width:320px}
td:has(.multi){overflow:visible}
/* a row is exactly one line high — long names truncate with the full text in the tooltip, so the
   number of rows that fit is a fixed calculation rather than a guess about wrapping */
td .sub{color:var(--muted);font-size:11px;margin-left:6px}
td.wrap{white-space:normal}
tbody tr:nth-child(even){background:var(--row-alt)}
tbody tr:hover{background:var(--row-hover)}
td.key{font-weight:600}
td input,td select{padding:4px 7px;font-size:12px}
.table-wrap{overflow-x:auto}
.totrow{display:flex;gap:12px;align-items:center;padding:8px 10px;background:var(--accent-pale);
  border-top:1px solid var(--line);font-size:12.5px;font-weight:600}
.totrow .sp{flex:1}
.empty{padding:26px;text-align:center;color:var(--muted)}

/* ---------- pills, chips, kv ---------- */
.pill{display:inline-flex;align-items:center;gap:5px;border-radius:20px;padding:2px 9px;font-size:11px;
  font-weight:600;white-space:nowrap}
.pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
.p-draft{background:#F1F5F9;color:#475569}
.p-pending{background:var(--info-bg);color:var(--info)}
.p-active{background:var(--ok-bg);color:var(--ok)}
.p-archived{background:#F1F5F9;color:#94A3B8}
.p-review{background:var(--warn-bg);color:var(--warn)}
.p-ok{background:var(--ok-bg);color:var(--ok)}
.p-bad{background:var(--bad-bg);color:var(--bad)}
a.pill{text-decoration:none}
a.pill:hover{filter:brightness(.96)}

/* A mis-tagged recipe, said in red rather than in a muted pill. A wrong mapping reaches every menu
   carrying the name, so this is a thing to act on, not a note. See \App\Support\NameMismatch. */
.warn-box{background:var(--bad-bg);border:1px solid #F3C6C6;border-left:3px solid var(--bad);
  border-radius:8px;padding:10px 12px;margin:0 0 12px}
.warn-box b{color:var(--bad);display:block;margin-bottom:4px}
.warn-box p{margin:0;color:var(--ink-2);font-size:12px;line-height:1.5}
[data-grid] tbody tr.flagged,table tbody tr.flagged{background:var(--bad-bg)}
tr.flagged td:first-child{box-shadow:inset 3px 0 0 var(--bad)}
/* somebody else has this recipe open: amber, and only ever shown to the people it stops. Red is
   kept for a wrong dish attached to a menu — this one is temporary and nobody's mistake. */
[data-grid] tbody tr.busy,table tbody tr.busy{background:var(--warn-bg)}
tr.busy td:first-child{box-shadow:inset 3px 0 0 var(--warn)}
.p-busy{background:var(--warn-bg);color:var(--warn);border:1px solid #EBD6A8}
.chip{display:inline-flex;align-items:center;gap:5px;background:var(--accent-pale);border:1px solid var(--line);
  color:var(--ink-2);border-radius:20px;padding:2px 4px 2px 10px;font-size:11.5px;margin:0 5px 5px 0}
.chip form{display:inline}
.chip button{border:0;background:transparent;color:var(--muted);cursor:pointer;font-size:13px;
  line-height:1;padding:0 4px;border-radius:50%}
.chip button:hover{color:var(--bad)}
.chip.plain{padding-right:10px}
.kv{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px solid var(--line-soft);
  font-size:12.5px}
.kv:last-child{border-bottom:0}
.kv .k{color:var(--muted)}
.kv .v{font-weight:600}
.kv .v.warn{color:var(--warn)}
.kv .v.good{color:var(--ok)}
.note{font-size:11.5px;color:var(--muted);line-height:1.5;margin:8px 0 0}
.bar{height:6px;border-radius:4px;background:var(--line-soft);overflow:hidden;margin:6px 0}
.bar i{display:block;height:100%;background:var(--accent)}
.bar.good i{background:var(--ok)}
.log{list-style:none;margin:0;padding:0;font-size:12px}
.log li{padding:6px 0;border-bottom:1px solid var(--line-soft)}
.log li:last-child{border-bottom:0}
.log .when{color:var(--muted);font-size:11px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
/* A figure that opens the list behind it. It must not read as a link — a row of six underlined
   accent-coloured numbers is unreadable as figures — so the affordance is the border on hover,
   the same way the tree nodes behave. */
a.kpi{display:block;color:inherit;text-decoration:none;transition:border-color .12s ease}
a.kpi:hover{border-color:var(--accent);text-decoration:none}
a.kpi:hover .l{color:var(--accent)}
.kpi .l{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.kpi .v{font-size:23px;font-weight:700;margin-top:4px}

/* ---------- coverage bar ----------
   How much of one client's menu is answered. Three states, each with its own hue AND its own number
   beside the bar: the blue/green pair sits at the tritan floor, where colour alone is not enough, so
   the labels are not decoration. Segments are separated by a 2px gap of the surface rather than a
   border, so the join reads at any width and nothing is added to the length it encodes. */
.cov{display:flex;gap:2px;height:10px;border-radius:5px;overflow:hidden;background:var(--line-soft)}
.cov i{display:block;min-width:2px}
.cov i.linked{background:#15803D}
.cov i.direct{background:#1D4ED8}
.cov i.needed{background:#D97706}
.cov i:first-child{border-radius:5px 0 0 5px}
.cov i:last-child{border-radius:0 5px 5px 0}
.cov i:only-child{border-radius:5px}
.cov.empty{background:var(--line-soft)}

/* the legend, once per page rather than once per row */
.covkey{display:flex;flex-wrap:wrap;gap:14px;font-size:11.5px;color:var(--muted)}
.covkey span{display:inline-flex;align-items:center;gap:6px}
.covkey i{width:10px;height:10px;border-radius:3px;display:inline-block}

/* in a summary row the bar is a fixed-width gauge, not something that stretches: every branch's bar
   has to be the same length or the eye compares lengths that mean different things */
.cov.gauge{width:150px;flex:0 0 150px}
.cov.mini{width:110px;flex:0 0 110px;height:8px;border-radius:4px}
.cov.mini i:first-child{border-radius:4px 0 0 4px}
.cov.mini i:last-child{border-radius:0 4px 4px 0}
.cov.mini i:only-child{border-radius:4px}
/* the percentage sits with its bar and is the thing you actually scan down the column */
.pcent{font-variant-numeric:tabular-nums;font-weight:700;font-size:12px;min-width:38px;text-align:right}
.pcent.low{color:var(--warn)}

/* ---------- column chart ----------
   One stacked column per branch-client menu: the height is how many names that menu wants, the
   segments are how far it has got. Vertical because the labels are short by construction (ATQ-IX),
   which is the only reason vertical bars are safe here — long category names on a column chart end
   up rotated, and rotated labels are the thing people stop reading.

   Heights are two nested percentages: the column against the tallest menu, the segments against
   their own column. No SVG and no viewBox arithmetic, so it reflows with the page. */
.colchart{display:flex;align-items:flex-end;gap:10px;height:170px;padding:22px 2px 0;
  background:repeating-linear-gradient(to top,transparent 0,transparent 24.9%,var(--line-soft) 25%,var(--line-soft) calc(25% + 1px));
  background-size:100% 148px;background-position:left bottom;background-repeat:no-repeat;min-width:min-content}
.colchart .col{flex:1 1 0;min-width:34px;max-width:78px;display:flex;flex-direction:column;
  justify-content:flex-end;height:100%;position:relative}
/* the stack itself: segments bottom-up, a 2px surface gap between them, rounded only at the top */
.colchart .stack{display:flex;flex-direction:column-reverse;gap:2px;border-radius:4px 4px 0 0;
  overflow:hidden;width:100%}
.colchart .stack i{display:block;min-height:2px}
.colchart .stack i.linked{background:#15803D}
.colchart .stack i.direct{background:#1D4ED8}
.colchart .stack i.needed{background:#D97706}
/* the one number worth printing on every column is the work that is left */
.colchart .top{position:absolute;left:0;right:0;bottom:100%;margin-bottom:3px;text-align:center;
  font-size:10.5px;font-weight:700;color:var(--warn);font-variant-numeric:tabular-nums}
.colchart .top.done{color:var(--ok)}
.colframe{overflow-x:auto}
.colaxis{display:flex;gap:10px;padding:6px 2px 0;min-width:min-content}
.colaxis .lab{flex:1 1 0;min-width:34px;max-width:78px;text-align:center;font-size:10px;
  color:var(--muted);line-height:1.3;overflow:hidden}
.colaxis .lab b{display:block;color:var(--ink-2);font-size:10.5px}
.colscale{display:flex;justify-content:space-between;font-size:10.5px;color:var(--muted);
  margin-bottom:2px}
.pager{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:12px;font-size:12px}
.pager a,.pager .on,.pager .off{padding:3px 9px;border-radius:6px;border:1px solid var(--line);
  background:var(--card);color:var(--ink-2);line-height:1.6}
.pager a:hover{background:var(--row-hover);text-decoration:none}
.pager .on{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.pager .off{color:var(--muted);opacity:.6}
.pager .count{margin-left:auto;border:0;background:transparent;color:var(--muted)}
.pager svg{width:14px;height:14px}

/* ---------- login ---------- */
/* ---- sign in ------------------------------------------------------------------------------- */
/* Two panels: what this is, and the way in. The left one collapses on a narrow screen so a phone
   gets the form and nothing else — a pitch is no use to somebody who already works here and is
   holding a trolley. */
.auth{min-height:100vh;display:grid;grid-template-columns:1.1fr 1fr}

.auth-pitch{background:var(--side);border-right:1px solid var(--line);
  padding:44px 48px;display:flex;flex-direction:column;justify-content:center}
/* the mark stacked over the wordmark, and the panel centred on it: here the logo has room to be a
   logo, rather than the thumbnail it has to be beside the wordmark in the sidebar */
.auth-pitch{align-items:center;text-align:center}
.auth-brand{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:30px}
.auth-brand .lg{height:160px;width:auto;display:block}
.auth-brand .wordmark{height:34px;width:auto;display:block}
.auth-pitch h1{font-size:25px;line-height:1.3;max-width:14em;margin-bottom:26px;color:var(--ink)}
/* the points stay left-aligned inside the centred column — centred body text is hard to read */
.auth-points{list-style:none;margin:0;padding:0;max-width:30em;text-align:left}
.auth-points li{display:flex;gap:11px;align-items:flex-start;margin-bottom:15px;
  font-size:13px;line-height:1.55;color:var(--ink-2)}
.auth-points svg{flex:0 0 15px;margin-top:2px;color:var(--accent)}
.auth-points b{color:var(--ink)}
.auth-foot{margin-top:36px;font-size:11.5px;color:var(--muted);letter-spacing:.3px}
.auth-foot b{color:var(--ink-2);font-weight:700}

.auth-form{display:grid;place-items:center;padding:32px 24px}
.auth-form .box{width:100%;max-width:352px}
.auth-form h2{font-size:20px;margin-bottom:2px}
.auth-form .sub{color:var(--muted);font-size:12.5px;margin-bottom:20px}
.auth-form .field{margin-bottom:13px}
.auth-form .btn{width:100%;justify-content:center;margin-top:6px}
.auth-form .checks{margin:2px 0 4px}
.auth-help{margin-top:18px;padding-top:14px;border-top:1px solid var(--line-soft);
  font-size:11.5px;line-height:1.5;color:var(--muted)}

@media (max-width:820px){
  .auth{grid-template-columns:1fr}
  .auth-pitch{display:none}
  .auth-form{padding:28px 20px}
}

/* ---- what a page is for, behind an icon --------------------------------------------------- */
/* The text used to be a paragraph at the top of every screen. On a grid page that is not free:
   the page bar counts towards the height the row count is measured against, so a three-line
   explanation costs the reader two rows of their data, every visit, forever. */
.info{position:relative;display:inline-block}
.info summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;
  width:24px;height:24px;border-radius:50%;color:var(--muted);border:1px solid transparent}
.info summary::-webkit-details-marker{display:none}
.info summary:hover{color:var(--accent);border-color:var(--line);background:var(--card)}
.info[open] summary{color:var(--accent);border-color:var(--accent)}
.info .info-pop{position:absolute;z-index:300;top:calc(100% + 6px);left:0;width:min(430px,80vw);
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:11px 13px;
  box-shadow:0 10px 30px rgba(0,0,0,.13);font-size:12.5px;line-height:1.55;color:var(--muted)}
.info .info-pop b{color:var(--ink)}

/* ---- type-to-search over a long list ------------------------------------------------------- */
/* The material master runs to ten thousand rows. A native select opens as an alphabetical wall,
   so this filters as you type. Generic costing items sit in their own group at the top: a chef
   reaches for "Oil" far more often than for one of the forty branded oils behind it. */
.combo{position:relative;min-width:230px}
.combo .combo-input{width:100%;padding-right:26px}
.combo .combo-clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;
  background:none;color:var(--muted);font-size:16px;line-height:1;cursor:pointer;padding:2px 4px}
.combo .combo-clear:hover{color:var(--ink)}
.combo .combo-list{position:absolute;z-index:300;top:calc(100% + 4px);left:0;min-width:280px;
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:4px;
  box-shadow:0 12px 34px rgba(0,0,0,.16);overflow:auto;max-height:320px}
.combo .combo-group{font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:700;
  color:var(--label);padding:7px 8px 4px;position:sticky;top:0;background:var(--card)}
.combo .combo-opt{display:flex;gap:7px;align-items:baseline;padding:5px 8px;border-radius:5px;
  cursor:pointer;font-size:12.5px;white-space:nowrap}
.combo .combo-opt b{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px;color:var(--muted)}
.combo .combo-opt .nm{color:var(--ink);overflow:hidden;text-overflow:ellipsis}
.combo .combo-opt:hover,.combo .combo-opt.on{background:var(--accent);color:#fff}
.combo .combo-opt:hover b,.combo .combo-opt.on b,
.combo .combo-opt:hover .nm,.combo .combo-opt.on .nm,
.combo .combo-opt:hover .muted,.combo .combo-opt.on .muted{color:#fff}
.combo .combo-none{margin:6px 8px;color:var(--muted);font-size:12px}

/* ---- the add-ingredient row ---------------------------------------------------------------- */
/* The combo box wrapper must not carry a min-width of its own: it sits in a flex column that is
   already sized, and a wrapper wider than its column spills over whatever comes next — which is
   how the ingredient box came to sit on top of the preparation select. The dropdown keeps its own
   width, and is moved to position:fixed when open anyway. */
.addline .combo{width:100%;min-width:0}
/* a number and a two-letter unit do not need a share of the row */
.addline .field.narrow{flex:0 0 92px;min-width:92px}
.addline .field.tiny{flex:0 0 68px;min-width:68px}
.addline .field.tiny select{padding-left:6px;padding-right:4px}
/* the hero mark: a checkbox, sitting on the baseline with the inputs */
.addline .field.mark{flex:0 0 auto;min-width:0}
.addline .field.mark .inline-check{white-space:nowrap;padding-bottom:7px}
.hero-dot{color:var(--accent);font-weight:700}
.combo .combo-hint{margin:6px 8px;color:var(--muted);font-size:12px}
.combo.busy .combo-input{background-image:none;opacity:.75}

/* ---- a button that is waiting on something slow ------------------------------------------- */
/* The AI review takes twenty-odd seconds. A page that looks identical for that long reads as
   broken, and people press again — which sends a second request and costs money twice. */
.btn.is-busy{opacity:.85;cursor:progress}
.spin{display:inline-block;width:11px;height:11px;margin-right:6px;vertical-align:-1px;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---- recipe history, one entry per line ----------------------------------------------------- */
/* It used to sit in the side column, where a detail like "Started from BomBridge R04366 · 3 Pepper
   And Onion Omelette — 7 of 7 ingredient(s) placed" wrapped three times. Given the full width it
   is one line: what happened, what it was, and when. */
.log.wide li{display:flex;align-items:baseline;gap:12px;padding:7px 0}
.log.wide .act{flex:0 0 108px;font-weight:700;color:var(--ink)}
.log.wide .det{flex:1;min-width:0;color:var(--ink-2)}
.log.wide .when{flex:0 0 auto;margin-left:auto;white-space:nowrap;color:var(--muted);font-size:11px}
@media (max-width:900px){
  .log.wide li{flex-wrap:wrap;gap:4px 10px}
  .log.wide .det{flex:1 0 100%;order:3}
}

/* ---- Recipes Needed: one row, one line ------------------------------------------------------ */
/* The row grew a recipe picker, three buttons, a material search and a bin, and wrapped into three
   lines — which made a list of 700 unreadable. The controls now sit on one line and the table
   scrolls sideways if the window is too narrow, which is the behaviour every other grid here has. */
.needs-actions{display:flex;align-items:center;gap:6px;white-space:nowrap}
.needs-actions .combo{width:200px;min-width:200px}
.needs-actions .btn.sm{flex:0 0 auto}
td.needs-recipe select{min-width:180px;max-width:230px}
.side .foot b{color:var(--ink-2);font-weight:700}

/* ---- the loaded data, nested the way it is owned ------------------------------------------- */
/* Branch › client › category, as two levels of <details> rather than a script. The whole app
   works this way — the multi-select, the info popup, the reach-into-the-master panel — and it
   means the tree opens and closes with nothing loaded, keeps its state through a form post, and
   is reachable from a keyboard without anybody having written that part.

   The two levels are told apart by weight rather than by indentation alone: a branch summary is
   the page's accent band, a client summary is plain card. Indentation on its own stops reading as
   a hierarchy the moment a branch has one client and the row is nearly full width. */
.tree{display:flex;flex-direction:column;gap:10px}
/* An accordion never shrinks to fit the frame.
   .tree is a column flex container, so every node in it is a flex item — and a flex item shrinks by
   default. Once the tree was given a height of its own (so the list scrolls inside the page rather
   than the page scrolling), opening a node made it want to be tall, flex squashed it back down, and
   `overflow:hidden` on the node — which is there for the rounded corners — cut it off after the
   first line or two. The node keeps its natural height and the tree scrolls instead. */
.tree > *{flex:0 0 auto}
.tnode{background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}
.tnode>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:10px;
  padding:9px 12px;background:var(--accent-pale);border-bottom:1px solid transparent}
.tnode[open]>summary{border-bottom-color:var(--line)}
.tnode>summary::-webkit-details-marker{display:none}
.tnode>summary:hover{background:var(--row-hover)}
/* the twisty, drawn rather than typed: the platform triangle is a different shape and a different
   size on every OS, and it cannot take the row's colour */
.tnode>summary .twist{flex:0 0 auto;width:9px;height:9px;border-right:1.6px solid var(--muted);
  border-bottom:1.6px solid var(--muted);transform:rotate(-45deg);margin:0 2px 2px 0;
  transition:transform .12s ease}
.tnode[open]>summary .twist{transform:rotate(45deg);margin:0 2px 4px 0}
.tnode .tname{font-weight:700;font-size:13px}
.tnode .tsub{color:var(--muted);font-size:11.5px;font-weight:400}
.tnode .tacts{margin-left:auto;display:flex;align-items:center;gap:6px;white-space:nowrap}
.tbody{padding:10px 12px 12px}

/* the client level: no accent band, so the branch band above it stays the loudest thing */
.tnode .tnode>summary{background:var(--card);border-bottom:1px solid var(--line-soft)}
.tnode .tnode>summary:hover{background:var(--row-hover)}
.tnode .tnode{border-radius:8px}
.tnode .tnode+.tnode{margin-top:8px}
.tnode .tbody .tbody{padding:8px 10px 10px;background:var(--row-alt)}

/* the measures on a summary row: one line, aligned, and quiet enough that the name still leads */
.tfacts{display:flex;flex-wrap:wrap;gap:4px 14px;align-items:baseline;font-size:12px;color:var(--muted)}
.tfacts b{color:var(--ink);font-weight:700}
.tfacts .warn{color:var(--warn)}
.tfacts .bad{color:var(--bad)}
.tfacts .bad b{color:var(--bad)}
.tempty{padding:14px;text-align:center;color:var(--muted);font-size:12.5px}
/* the tidy-up prompt: a strip, not a toast — it is a standing fact about the client, not an event */
.tstale{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 8px;
  background:var(--warn-bg);border:1px solid #F0DCB4;border-radius:8px;padding:7px 10px;
  font-size:12px;color:var(--warn)}
.tstale b{color:var(--ink)}

/* ---- create-a-recipe dialog, from Recipes Needed -------------------------------------------- */
/* A native <dialog>: the browser gives the backdrop, the focus trap, Escape to close and the
   inertness of everything behind it. A hand-rolled div does none of that, and this screen is one
   a chef works down for an hour — losing the row you were on because a "modal" did not hold focus
   is the kind of small wrong that makes people stop using a screen. */
dialog.mk{border:1px solid var(--line);border-radius:12px;padding:0;max-width:min(560px,92vw);
  background:var(--card);color:var(--ink);box-shadow:0 24px 60px rgba(0,0,0,.22)}
dialog.mk::backdrop{background:rgba(26,10,0,.38)}
dialog.mk form{padding:16px 18px 14px}
dialog.mk h3{margin:0 0 2px}
dialog.mk .mk-name{margin:0 0 12px;font-size:17px;font-weight:700;line-height:1.25}
dialog.mk .grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
dialog.mk .mk-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;
  border-top:1px solid var(--line-soft);padding-top:12px}

/* where the dish already exists. One row per place, the choice explicit — including the choice to
   start empty, so "I looked and I do want a blank one" is a thing the form can say. */
.mk-sources{display:flex;flex-direction:column;gap:2px;margin:0 0 12px;
  border:1px solid var(--line);border-radius:8px;padding:4px;background:var(--accent-pale)}
.mk-sources label{display:flex;align-items:baseline;gap:8px;padding:7px 8px;border-radius:6px;
  cursor:pointer;font-size:12.5px}
.mk-sources label:hover{background:var(--card)}
.mk-sources input{margin:0}
.mk-sources .muted{font-size:11.5px}
/* a field the copy decided: still readable, plainly not yours to change */
dialog.mk select.locked{background:var(--accent-pale);color:var(--muted);cursor:not-allowed;
  border-style:dashed}
/* a picker row that can be chosen but cannot be costed yet */
.combo-opt.no-basis .nm{color:var(--muted)}
.combo-note{display:block;font-size:11px;color:var(--warn);line-height:1.35;margin-top:1px}

/* ---- filters on the page bar, one line ------------------------------------------------------ */
/* The filters used to be a card of their own: four fields with labels above them, then a row
   holding Filter and Reset. That is about 110px of a screen whose whole job is to show rows — and
   the fields do not need it. A branch is a three-letter code, a status is one word, a tag is one
   word. Laid out inline beside the page's own note they cost nothing, and the table gets the rows
   back.

   No Filter button either: a select applies when it changes, and the search box when you stop
   typing. Pressing a button to make a dropdown mean something is a step the dropdown already
   made. The hidden submit is what makes Enter work, and what makes the form still usable if the
   script has not loaded. */
.filters{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0}

/* Longhands throughout, never the `background` shorthand.
   The search box carries its magnifier as a background image, and a shorthand anywhere else on the
   same element resets the parts it does not mention — so `background: var(--card)` here quietly set
   background-repeat back to `repeat`, and a 13px icon tiled seventeen times across the box. Setting
   only background-color leaves the image, its position and its no-repeat exactly where they were. */
.filters input,.filters select{width:auto;height:30px;padding:4px 8px;font-size:12px;
  border-radius:6px;background-color:var(--card)}
.filters input{min-width:150px}
.filters select{max-width:190px;cursor:pointer}

/* An active filter is tinted, and again only its colour is touched. */
.filters select:not(:has(option:checked[value=""])){border-color:var(--accent);
  background-color:var(--accent-pale);font-weight:600}
.filters input:not([value=""]){border-color:var(--accent);background-color:var(--accent-pale)}

/* The search always comes first and always looks the same: leftmost, beside the page's note, with
   the magnifier inside it. It was third on some screens and first on others, so the eye had to find
   it again on every page. Declared last and with the type in the selector, so no rule above can win
   on specificity and leave the icon half-styled. */
.filters input.search{order:-1;min-width:230px;padding-left:28px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238C6A3A' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7.2' cy='7.2' r='4.2'/%3E%3Cpath d='M10.4 10.4l3 3'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:9px 50%;background-size:13px 13px}
.filters input.search:focus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23B45309' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7.2' cy='7.2' r='4.2'/%3E%3Cpath d='M10.4 10.4l3 3'/%3E%3C/svg%3E")}

.filters .hidden-submit{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* only offered once something is set — a Reset beside empty fields resets nothing */
.filters .clear{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
  border-radius:50%;color:var(--muted);font-size:15px;line-height:1;text-decoration:none}
.filters .clear:hover{background-color:var(--bad-bg);color:var(--bad);text-decoration:none}

/* ---- how tall a row is, declared once ------------------------------------------------------- */
/* public/js/rows.js reads this instead of measuring the tallest rendered row. Measuring made the
   answer depend on the answer: fewer rows meant no scrollbar, which meant a wider page, which meant
   no name wrapped, which meant more rows fitted — and more rows brought the scrollbar back. The
   Recipe Pool sat between 13 and 16 forever. A number the design states cannot oscillate.

   Screens whose rows carry form controls are taller and say so. If a row does render taller than it
   claims the page scrolls by a row, which is the failure rows.js already preferred. */
[data-grid]{--row-h:36px}
[data-grid][data-rowsize="buttons"]{--row-h:42px}
[data-grid][data-rowsize="controls"]{--row-h:46px}
[data-grid][data-rowsize="pickers"]{--row-h:54px}

/* And the row is held to it. `td` is already nowrap with an ellipsis — a row is one line high by
   design — so pinning the height makes what renders equal to what was declared, rather than close
   to it. Without this the arithmetic above is a good guess; with it, it is the answer. */
[data-grid] tbody tr{height:var(--row-h)}

/* There is deliberately no scrollbar-gutter here.
   Reserving one was the first attempt at stopping the width changing when a list overflowed. It
   worked and it was not worth it: once the tail became a reservation and the row height a pinned
   number, nothing in the calculation depended on the width any more — a vertical scrollbar does not
   change innerHeight, and a row that cannot wrap cannot change height. So the gutter stopped
   earning its keep and became a permanent empty strip down the right of every page, visible in the
   header where the bar stopped short of the edge. The scrollbar is styled below instead, so on the
   rare page that does scroll it looks like part of the app rather than part of Windows. */

/* ---- the last ten per cent ------------------------------------------------------------------ */
/* Nothing here changes a size or a position — the grid maths above depends on both. It is depth,
   colour and motion only: the parts that make an app feel considered rather than assembled. */

/* Scrollbars that belong to the app. The platform default is a wide grey trench with square ends;
   on a cream page it is the loudest thing on screen. */
*{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px;
  border:2px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--label);background-clip:content-box}

/* A card sits on the page rather than being drawn on it. One soft shadow, not a heavier border —
   weight reads as cheap, and a shadow this quiet only registers as the edge being real. */
.card{box-shadow:0 1px 2px rgba(26,10,0,.04),0 1px 8px rgba(26,10,0,.03)}
.kpi{box-shadow:0 1px 2px rgba(26,10,0,.04)}

/* Interactive things answer. 120ms is under the threshold where a change reads as a delay, and
   the 1px lift on a primary button is the whole of the feedback — anything more is a toy. */
.btn,.iconbtn,.filters input,.filters select,.pager a{transition:background-color .12s ease,
  border-color .12s ease,color .12s ease,box-shadow .12s ease,transform .06s ease}
.btn:active,.iconbtn:active{transform:translateY(1px)}
.btn.primary{box-shadow:0 1px 2px rgba(180,83,9,.25)}
.btn.primary:hover{box-shadow:0 2px 6px rgba(180,83,9,.28)}

/* The table. A row lights up as a whole rather than the cell under the cursor.
   No sticky head: the row count is chosen so a grid does not scroll, and sticking it would be a
   position change — which is the one thing this block promised not to make. */
tbody tr{transition:background-color .1s ease}
td.key a{text-decoration:none}
td.key a:hover{text-decoration:underline;text-underline-offset:2px}

/* Selection in the app's own colour rather than the browser's blue — but a dark one. The pale
   amber it used to take is the sidebar's own background (#FFFBEB), so dragging over text on a
   cream or white surface highlighted it in a colour the surface already was: the user could not
   see what they had selected. Deep burnt amber with white text reads on every surface here,
   the accent-orange nav and buttons included. */
::selection{background:var(--select);color:var(--select-ink)}
::-moz-selection{background:var(--select);color:var(--select-ink)}

/* One focus ring everywhere, and only for the keyboard — a mouse user clicking a button has not
   asked to be told where the focus went. */
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.btn:focus:not(:focus-visible),.iconbtn:focus:not(:focus-visible){outline:none}

/* a field the screen is asking for and has not been given yet */
input.wants{border-color:var(--warn);background-color:var(--warn-bg)}

/* ---- names shown whole inside an accordion --------------------------------------------------- */
/* A grid row is held to one line on purpose: rows.js works out how many fit from a height the
   design states, so a wrapped cell would break the arithmetic. Inside a <details> nothing depends
   on the height — the panel is as tall as its contents — so the ellipsis buys nothing and costs the
   end of the name. "Black Forest Upside Down Danish Filled With Dark Cherry And Mascorpone Cheese"
   is 77 characters and a real item; half of it is not identifiable. */
.tree .tbody td{white-space:normal;vertical-align:top}
.tree .tbody td.key{max-width:none;line-height:1.45;padding-top:9px}
.tree .tbody td .mono{word-break:break-all}

/* the branch's own line under its clients: the same row, said once for the whole unit */
tr.subtotal td{background:var(--accent-pale);border-top:1px solid var(--line);font-weight:600}
tr.subtotal + tr td{border-top:1px solid var(--line)}
td .sub{display:block;color:var(--muted);font-size:10.5px;margin:2px 0 0;font-weight:400}

/* ---- the sidebar as a rail, once the screen is a tablet's ---------------------------------- */
/* the burger belongs to the rail; at full width the menu is already open, permanently */
.railtoggle{display:none;flex:0 0 auto;margin-right:2px}
.railtoggle svg{display:block}
/* .iconbtn goes red on hover because everywhere else it is a delete bin; opening a menu is
   not that kind of button */
.railtoggle:hover{color:var(--accent-deep);border-color:var(--line);background:var(--accent-pale)}

/* 212px of menu out of a 1024px tablet is a fifth of the screen spent on where you are not, and
   these are table pages: the width belongs to the columns. Below 1024px the sidebar keeps only
   its icons — 52px, wide enough that the row highlight still reads as a row — and the labels come
   back when the pointer arrives (sidenav.js sets .open; a tap on the ☰ in the top bar does the
   same where there is no pointer).
   The panel floats over the page rather than pushing it. Widening a flex item reflows the content
   beside it, and on a grid page a reflow re-measures how many rows fit — so merely hovering the
   menu would re-page the table underneath it. Fixed and out of flow, nothing behind it moves. */
@media (max-width:1024px){
  .shell{--rail:52px;padding-left:var(--rail)}
  .side{position:fixed;top:0;left:0;bottom:0;width:var(--rail);flex:0 0 auto;
    overflow:hidden;z-index:40;transition:width .16s ease,box-shadow .16s ease}
  .side.open{width:212px;box-shadow:6px 0 20px rgba(26,10,0,.16)}

  /* One gutter for both states, chosen so the icon sits centred in the rail: 18 + 15 + 18 = 51.
     The point is that nothing moves sideways when the panel opens — the icons stay exactly where
     they were and the labels appear beside them. A gutter that changed with the width would slide
     every icon 6px to the right on hover, which reads as the menu twitching. */
  .side{--side-gutter:18px}
  .brand{padding:0 13px}
  .side:not(.open) .brand .wordmark{visibility:hidden}

  /* The label leaves the layout, rather than being clipped by the edge of the rail.
     Clipping was the first attempt and it read as a fault: "BomBridge Search" became "Bo",
     "Item Mapping" became "Ite", and a menu of half-words looks like a page that failed to
     load rather than one showing icons on purpose. It is taken out of flow instead of hidden,
     so the row is still announced as "Branch Issues" to a screen reader and still findable by
     a browser's find-in-page. */
  .nav{overflow-x:hidden}
  .nav a{position:relative;white-space:nowrap}
  .side:not(.open) .nav .lbl{position:absolute;width:1px;height:1px;margin:0;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .side:not(.open) .nav{scrollbar-width:none}
  .side:not(.open) .nav::-webkit-scrollbar{display:none}

  /* A group heading clipped to 52px says "REC" and "MAT", which is worse than saying nothing.
     Closed, each one becomes the hairline it already carries — the sections still read as
     sections, without a word of it being guessed at. */
  .side:not(.open) .nav .grp{height:0;padding:9px 0 0;overflow:hidden;color:transparent}

  /* the count of what is waiting, kept as a dot: the number has nowhere to go in a rail, but
     "there is something here" is the half of it worth keeping */
  .side:not(.open) .nav .n{position:absolute;top:5px;right:6px;width:7px;height:7px;padding:0;
    margin:0;border-radius:50%;background:var(--accent-deep);color:transparent;font-size:0;
    overflow:hidden}
  .side:not(.open) .nav a.on .n{background:#fff}

  /* visibility, not display: the footer keeps its space so the rail still ends on the same
     hairline the wide sidebar does, and the nav above it does not resize as the panel opens */
  .side:not(.open) .foot{visibility:hidden}
  .side .foot{white-space:nowrap}

  .railtoggle{display:inline-grid}
}

/* Touch has no walking away from a menu. The panel opens on a tap, and the tap that dismisses it
   would otherwise also press whatever it landed on — so a scrim takes that one, and says while it
   is there which half of the screen is live. A pointer gets no scrim: it would flash on and off
   every time the mouse crossed the menu on its way somewhere else. */
@media (max-width:1024px) and (hover:none){
  .side.open ~ .main::before{content:'';position:fixed;inset:0;left:var(--rail);
    background:rgba(26,10,0,.26);z-index:30}
}

@media (prefers-reduced-motion:reduce){.side{transition:none}}
