/* ChefsLab Pro — app design system (ported from the prototype). Apple-style, CSS-variable themed,
   OLED-true-black dark mode. Drives the real app shell + all in-app screens. */
:root{
  color-scheme:light;   /* native controls (the <select> option popup, scrollbars) render light */
  --font:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",system-ui,sans-serif;
  --r-card:20px; --r-ctrl:12px;
  --appbar-h:56px;   /* height of the sticky .top bar — cart supplier headers stick just under it */
  --accent:#34C759; --accent-press:#2aa84a;
  /* Focus ring colour — deliberately NOT --accent: green #34C759 is only 1.99:1 on --bg and 2.22:1 on
     a card, so an accent ring fails WCAG 1.4.11 (3:1 for a focus indicator). #0b6b2c gives 5.97:1 on
     --bg / 6.66:1 on --bg-elev. Dark theme keeps the bright accent (10.39:1 on black). (a11y audit) */
  --focus:#0b6b2c;
  --red:#FF3B30; --orange:#FF9500; --green:#34C759; --blue:#007AFF;
  --bg:#f2f2f7; --bg-elev:#fff; --card:#fff;
  --nav-bg:rgba(245,245,247,.8); --bar-bg:rgba(255,255,255,.72);
  --text:#1c1c1e; --text2:rgba(60,60,67,.72); --text3:rgba(60,60,67,.3);  /* --text2 .6->.72: WCAG-AA contrast on light bg (a11y audit P1) */
  --sep:rgba(60,60,67,.16); --hair:rgba(60,60,67,.12);
  --fill:rgba(120,120,128,.12); --fill-2:rgba(120,120,128,.08);
  --shadow:0 1px 2px rgba(0,0,0,.04),0 6px 20px rgba(0,0,0,.06);
  --shadow-lift:0 8px 30px rgba(0,0,0,.12);
  --ok-bg:rgba(52,199,89,.12); --ok-fg:#1d8a3f;
  --warn-bg:rgba(255,149,0,.14); --warn-fg:#9a5a00;
  --al-bg:rgba(255,59,48,.10); --al-fg:#c52a20;
  --accent-tint:rgba(52,199,89,.14);
  --cost-tint:rgba(180,130,20,.09);   /* tint for the recipe costing columns — a muted ledger amber */

  /* ---- design-system scales (Superplan Phase 0, 2026-07-10). Theme-independent — the dark block
     below overrides only colours. Migrating the app's magic numbers onto these is ongoing Phase-0 work
     (see docs/SUPERPLAN-APPLE-UI-AND-SCALE.md). ---- */
  /* Type ramp (Apple text styles) — replaces 20+ ad-hoc sizes incl. the half-point 10.5/11.5/…/15.5px. */
  --fs-caption2:11px; --fs-caption:12px; --fs-footnote:13px; --fs-subhead:15px; --fs-callout:16px;
  --fs-body:17px; --fs-title3:20px; --fs-title2:22px; --fs-title1:28px; --fs-large:34px;
  /* Spacing — strict 4pt grid; replaces off-grid 9/11/13/17/22/26 values. */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;
  /* Radius scale (extends --r-card:20 / --r-ctrl:12). */
  --r-xs:6px; --r-sm:8px; --r-md:12px; --r-lg:16px; --r-pill:999px;
  /* Motion — one spring for direct manipulation, one standard curve, three durations. */
  --ease-spring:cubic-bezier(.2,.7,.3,1); --ease:cubic-bezier(.4,0,.2,1);
  --dur-fast:120ms; --dur:200ms; --dur-slow:320ms;
}
:root[data-theme="dark"]{
  color-scheme:dark;    /* so the native <select> dropdown list themes dark (was light bg + light text) */
  --bg:#000; --bg-elev:#1c1c1e; --card:#1c1c1e;
  --nav-bg:rgba(10,10,12,.7); --bar-bg:rgba(0,0,0,.6);
  --text:#fff; --text2:rgba(235,235,245,.6); --text3:rgba(235,235,245,.3);
  --sep:rgba(84,84,88,.55); --hair:rgba(84,84,88,.4);
  --fill:rgba(120,120,128,.24); --fill-2:rgba(120,120,128,.16);
  --shadow:none; --shadow-lift:0 10px 40px rgba(0,0,0,.6);
  --accent:#30D158; --accent-press:#28b94c; --green:#30D158; --red:#FF453A; --orange:#FF9F0A; --blue:#0A84FF;
  --focus:#30D158;   /* on OLED black this is 10.39:1 — the bright accent is a fine ring in dark mode */
  --ok-bg:rgba(48,209,88,.18); --ok-fg:#30D158;
  --warn-bg:rgba(255,159,10,.20); --warn-fg:#FFB340;
  --al-bg:rgba(255,69,58,.18); --al-fg:#FF6961;
  --accent-tint:rgba(48,209,88,.18);
  --cost-tint:rgba(255,196,90,.10);
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%}
/* normalize: the `hidden` attribute must always win. The UA `[hidden]{display:none}` is only (0,1,0) and
   loses to element display rules like `.respcards tr{display:block}` — so a `<tr … hidden>` (e.g. the
   data-empty / data-filter rows) would still show. This is the standard normalize.css hardening and lets
   us hide via the `hidden` attribute instead of an inline style="display:none" (audit 9.1 CSP). */
[hidden]{display:none!important}
body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:var(--fs-subhead);line-height:1.45;-webkit-font-smoothing:antialiased}
a{color:var(--accent);text-decoration:none} button{font-family:inherit;cursor:pointer;color:inherit}
::selection{background:var(--accent-tint)}
/* ---- accessibility: keyboard focus ring + skip-to-content ---- */
:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:4px}
main.content:focus{outline:none}
.skip{position:absolute;left:8px;top:-48px;z-index:200;background:var(--bg-elev);border:.5px solid var(--sep);
  border-radius:8px;padding:8px 14px;font-weight:600;box-shadow:var(--shadow-lift);transition:top .15s}
.skip:focus{top:8px}

/* ---- app shell ---- */
.app{display:grid;grid-template-columns:248px 1fr;grid-template-rows:auto 1fr;min-height:100vh;grid-template-areas:"top top" "nav main"}
.top{grid-area:top;display:flex;align-items:center;gap:14px;padding:11px 22px;background:var(--bar-bg);
  backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);
  border-bottom:.5px solid var(--sep);position:sticky;top:0;z-index:20}
/* BRAND LOCKUP (kit 2026-07-27). The mark and the wordmark are two background images, so the phone
   breakpoint can drop the wordmark and leave the kit's "symbol alone" mark — exactly the fallback the
   brand guide prescribes. The file swaps on data-theme, NOT prefers-color-scheme: this app themes by an
   attribute on <html> and a user can pick a theme against their OS preference, so a media query would
   show the light lockup on a dark bar. The mark stays 28px — above the guide's 24px floor, so the
   bubbled mark is legible and mark-solid-* is not needed. */
.brand{display:flex;align-items:center;gap:9px;min-width:0}
.brand .mark{width:28px;height:28px;flex:0 0 auto;
  background:url("../brand/svg/mark-green.4b25220a83de.svg") no-repeat center/contain}
.brand .bword{width:108px;height:24px;flex:0 0 auto;
  background:url("../brand/svg/wordmark-on-light.eb61d5e8dd58.svg") no-repeat left center/contain}
:root[data-theme="dark"] .brand .bword{background-image:url("../brand/svg/wordmark-on-dark.ab7f55f44cfe.svg")}
.kitchen{font-size:var(--fs-footnote);color:var(--text2)} .kitchen b{color:var(--text);font-weight:600}
.top .spacer{flex:1}
.pill{display:inline-flex;align-items:center;gap:7px;font-size:var(--fs-footnote);font-weight:500;border-radius:999px;padding:6px 13px;background:var(--fill);color:var(--text2)}
.pill .dot{width:7px;height:7px;border-radius:50%;background:var(--green)}
.pill.role{background:var(--accent-tint);color:var(--accent);font-weight:600;text-transform:capitalize}
.trialpill{font-size:var(--fs-caption);font-weight:600;border-radius:999px;padding:6px 12px;white-space:nowrap;
  background:var(--warn-bg);color:var(--warn-fg);transition:filter .2s}
.trialpill:hover{filter:brightness(1.05)} .trialpill.ending{background:var(--al-bg);color:var(--al-fg)}
/* "prices fresh" status pill — green when every cost is current, amber + clickable when some are stale */
.pill.fresh{font-weight:600;white-space:nowrap}
.pill.fresh.ok{background:var(--ok-bg);color:var(--ok-fg)}
.pill.fresh.warn{background:var(--warn-bg);color:var(--warn-fg);transition:filter .2s}
.pill.fresh.warn:hover{filter:brightness(1.05)}
.iconbtn{width:36px;height:36px;border:0;border-radius:50%;background:var(--fill);display:grid;place-items:center;font-size:var(--fs-callout);transition:transform .12s,background .2s}
.iconbtn:hover{background:var(--fill-2)} .iconbtn:active{transform:scale(.92)}
/* Touch devices (phones at the pass): meet the 44x44 minimum tap target; desktop stays compact. */
@media (pointer:coarse){.iconbtn{width:44px;height:44px;font-size:18px}}

nav.side{grid-area:nav;background:var(--nav-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  border-right:.5px solid var(--sep);padding:14px 12px;display:flex;flex-direction:column;gap:2px;
  /* Pin the sidebar and let it scroll internally so the 21 items + footer stay reachable on short
     laptop screens (<~1030px tall) instead of overflowing below the fold. (2026-06 nav-overflow fix) */
  position:sticky;top:0;align-self:start;max-height:100vh;overflow-y:auto}
nav.side .navsec{font-size:var(--fs-caption2);font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text3);margin:16px 12px 6px}
nav.side a{display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:10px;font-size:var(--fs-subhead);font-weight:500;color:var(--text);transition:background .15s}
nav.side a .ic{font-size:var(--fs-body);width:22px;text-align:center}
nav.side a:hover{background:var(--fill-2)}
nav.side a.active{background:var(--accent-tint);color:var(--accent);font-weight:600}
nav.side .foot{margin-top:auto;font-size:var(--fs-caption);color:var(--text3);padding:12px;line-height:1.5}

/* ---- mobile bottom nav + "More" sheet (phones only; revealed in the <=760px media query below).
   Hidden on desktop, where nav.side is the navigation. Frosted like the sidebar/top bar. ---- */
.botnav{display:none}
.botnav{position:fixed;left:0;right:0;bottom:0;z-index:30;
  background:var(--nav-bg);backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);
  border-top:.5px solid var(--sep);padding:4px 4px calc(4px + env(safe-area-inset-bottom,0px))}
.botnav a,.botnav .botmore-btn{flex:1;min-width:0;min-height:44px;border:0;background:transparent;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  padding:6px 2px;border-radius:12px;color:var(--text2);font-weight:600;transition:background .15s,color .15s}
.botnav a .ic,.botnav .botmore-btn .ic{font-size:21px;line-height:1}
.botnav a .bl,.botnav .botmore-btn .bl{font-size:var(--fs-caption2);letter-spacing:.01em}
.botnav a:hover,.botnav .botmore-btn:hover{background:var(--fill-2)}
.botnav a.active{color:var(--accent)}
/* "More" bottom sheet — slides up over a dimmed backdrop; hidden via [hidden] by default */
.botmore{position:fixed;inset:0;z-index:40;background:rgba(0,0,0,.4);
  display:flex;align-items:flex-end;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
.botmore[hidden]{display:none}
.botmore-panel{width:100%;max-height:80vh;overflow:auto;
  background:var(--bg-elev);border-top-left-radius:var(--r-card);border-top-right-radius:var(--r-card);
  box-shadow:var(--shadow-lift);padding:8px 12px calc(16px + env(safe-area-inset-bottom,0px))}
.botmore-grip{width:38px;height:5px;border-radius:999px;background:var(--text3);margin:8px auto 6px}
.botmore-h{font-size:var(--fs-caption2);font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text3);margin:4px 8px 8px}
.botmore-panel a{display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;
  font-size:var(--fs-subhead);font-weight:500;color:var(--text);min-height:44px;transition:background .15s}
.botmore-panel a .ic{font-size:18px;width:24px;text-align:center}
.botmore-panel a:hover{background:var(--fill-2)}
.botmore-panel a.active{background:var(--accent-tint);color:var(--accent);font-weight:600}

main.content{grid-area:main;overflow:auto;padding:26px 30px 70px}

/* SCROLLBARS — always there, never an overlay that fades away (chef 2026-07-27: "the scroll when you
   point it with a mouse it disappears"). This app scrolls INNER containers — main.content above, the
   sidebar, the Cmd-K list, the assistant log, dialogs — rather than the window, and it never styled the
   scrollbar, so each one inherited whatever the OS/browser preferred. On Windows that is an overlay bar
   that fades out after scrolling and can vanish exactly as you reach for it, which is unusable as a drag
   target. Styling it explicitly opts out of the overlay behaviour entirely, so it looks and behaves the
   same on every machine.

   The thumb uses the existing text tokens, so it themes itself in light AND OLED-dark with no new
   variables; the transparent border + background-clip trick gives the thumb padding without a second
   element. scrollbar-gutter keeps the space reserved so content never reflows when a bar appears. */
main.content{scrollbar-gutter:stable}
*{scrollbar-width:thin;scrollbar-color:var(--text3) transparent}   /* Firefox */
::-webkit-scrollbar{width:12px;height:12px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-corner{background:transparent}
::-webkit-scrollbar-thumb{background:var(--text3);border-radius:8px;
  border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--text2)}
::-webkit-scrollbar-thumb:active{background:var(--text2)}
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:2px 0 4px}
h1{font-size:30px;font-weight:700;letter-spacing:-.03em;margin:0}
h2{letter-spacing:-.02em;margin:0} h3{letter-spacing:-.01em}
.sub{color:var(--text2);margin:0 0 22px;font-size:14px;max-width:760px}
.sech{font-size:var(--fs-footnote);font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text2);margin:24px 4px 10px}

/* ---- cards / kpis ---- */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:15px;margin-bottom:18px}
.kpi{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);padding:var(--sp-4) var(--sp-5);box-shadow:var(--shadow)}
.kpi .lab{font-size:var(--fs-caption);font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.4px}
.kpi .val{font-size:var(--fs-large);font-weight:700;letter-spacing:-.03em;line-height:1;margin-top:var(--sp-2)}
.kpi .note{font-size:var(--fs-footnote);color:var(--text2);margin-top:8px}
.kpi.good .val{color:var(--green)} .kpi.bad .val{color:var(--red)} .kpi.warn .val{color:var(--orange)}
.theo{display:inline-block;font-size:10px;font-weight:700;background:var(--ok-bg);color:var(--ok-fg);border-radius:6px;padding:2px 7px;vertical-align:middle;margin-left:7px;letter-spacing:.3px}

/* ---- hint banner ---- */
.hint{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);padding:13px 16px;
  font-size:var(--fs-footnote);color:var(--text2);margin-bottom:20px;display:flex;gap:10px;align-items:center;box-shadow:var(--shadow)}
.hint b{color:var(--text)} .hint a{font-weight:600}

/* ---- recipe cards ---- */
.rgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px}
.rcard{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow);
  overflow:hidden;display:block;color:inherit;transition:transform .18s cubic-bezier(.2,.7,.3,1),box-shadow .18s}
a.rcard:hover{transform:translateY(-4px);box-shadow:var(--shadow-lift)}   /* lift only clickable cards */
.rcard .top2{height:92px;background:linear-gradient(135deg,var(--accent-tint),var(--fill-2));display:grid;place-items:center;font-size:42px}
.rcard .body{padding:14px 16px}
.rcard .nm{font-weight:600;font-size:var(--fs-callout);letter-spacing:-.01em}
.rcard .meta{display:flex;justify-content:space-between;align-items:baseline;margin-top:10px}
.rcard .cost{font-size:21px;font-weight:700;letter-spacing:-.02em}
.rcard .tgt{font-size:var(--fs-caption);color:var(--text2)}
/* pro monogram placeholder (chef feedback: the emoji looked cartoonish) — the dish's initial on the tint */
.rcard .top2.rmono{font-size:34px;font-weight:800;color:var(--accent);letter-spacing:-.02em}
.rview-toggle{margin-left:auto}   /* push the Table|Cards view switch to the right of the control bar */
/* ---- horizontal bar chart (analytics) ---- */
.barrow{display:grid;grid-template-columns:150px 1fr 150px;gap:14px;align-items:center;margin:11px 0;font-size:var(--fs-footnote)}
.barrow .nm{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.barrow .bt{background:var(--fill);border-radius:var(--r-sm);height:13px;overflow:hidden}
.barrow .bf{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#7AD98F);border-radius:var(--r-sm);min-width:2px}
.barrow .bv{font-variant-numeric:tabular-nums;color:var(--text2);text-align:right}
.barrow .bv b{color:var(--text);font-weight:600}

/* ---- top/bottom movers list (menu-engineering quadrants + price-intel biggest moves).
   Was page-scoped in price_intel.html's <style>; promoted here so menu_engineering (which reuses
   .movers-grid/.moverow) isn't left unstyled. ---- */
.movers-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start}
@media (max-width:760px){.movers-grid{grid-template-columns:1fr}}
/* Flex + wrap so name / price / badge reflow instead of overflowing at ~360px. */
.moverow{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;
  padding:10px 2px;border-bottom:.5px solid var(--sep)}
.moverow:last-child{border-bottom:0}
.moverow .nm{font-weight:600;color:var(--text);min-width:0;flex:1 1 40%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.moverow .spk{display:inline-flex;align-items:center;flex:0 0 auto}
.moverow .spk.up{color:var(--red)} .moverow .spk.down{color:var(--green)}
.moverow .spk svg{display:block}
/* Let the price group wrap its own contents on the narrowest screens. */
.moverow .mv{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px 7px;
  font-variant-numeric:tabular-nums;flex:1 1 auto;justify-content:flex-end}
.moverow .act{flex:1 1 100%;font-size:var(--fs-caption);font-weight:600;color:var(--accent);
  text-decoration:none}
.moverow .act:hover{text-decoration:underline}

.panel{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow);padding:var(--sp-5);margin-bottom:var(--sp-5)}
.panel>h2,.panel>.sech:first-child{margin-top:0}

/* ---- tables ---- */
.tablewrap{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);overflow:hidden;box-shadow:var(--shadow)}
table{border-collapse:collapse;width:100%;font-size:14px}
th,td{text-align:left;padding:13px 16px;vertical-align:middle}
thead th{font-size:var(--fs-caption2);font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--text2);border-bottom:.5px solid var(--sep)}
tbody tr+tr td{border-top:.5px solid var(--hair)} tbody tr{transition:background .12s} tbody tr:hover{background:var(--fill-2)}
tfoot td{border-top:.5px solid var(--sep)}
.num{text-align:right;font-variant-numeric:tabular-nums}
/* sticky first column for wide horizontally-scrolling tables (e.g. the allergen matrix) */
.stickycol th:first-child,.stickycol td:first-child{position:sticky;left:0;background:var(--bg-elev)}
.stickycol thead th:first-child{z-index:2} .stickycol tbody td:first-child{z-index:1}

/* ---- tap-to-edit price (ingredients) ---- */
.price{display:inline-flex;gap:6px;align-items:center;justify-content:flex-end;border-radius:8px;padding:4px 8px;cursor:text;transition:background .15s}
.price:hover{background:var(--accent-tint)}
.price .pen{opacity:.4;font-size:var(--fs-caption)}
.priceedit{display:inline-flex;gap:6px;align-items:center;justify-content:flex-end}

/* ---- chips / badges ---- */
.chip{display:inline-block;font-size:var(--fs-caption);font-weight:500;border-radius:var(--r-sm);padding:4px 10px;margin:1px 2px;background:var(--fill);color:var(--text2)}
.badge{font-size:var(--fs-caption2);font-weight:600;border-radius:var(--r-sm);padding:3px 8px;display:inline-block}
.badge.ok,.badge.good{background:var(--ok-bg);color:var(--ok-fg)} .badge.warn{background:var(--warn-bg);color:var(--warn-fg)} .badge.bad{background:var(--al-bg);color:var(--al-fg)}
/* Neutral count badge — for a non-zero problem count that is NOT good news, so it must not borrow the
   green "ok" style (e.g. info-severity Today cards). */
.badge.neutral{background:var(--fill);color:var(--text2)}
/* Allergen badges (shared core/_allergen_badge.html): CONTAINS = filled red chip, MAY-CONTAIN = outlined
   amber chip prefixed "~" — presence carried by SHAPE+FILL, not colour alone, so it survives colour-blind
   readers and a black-&-white inspection photocopy. The pictogram is decorative (aria-hidden). */
.badge.may{background:transparent;color:var(--warn-fg);border:1px solid var(--warn-fg)}
.allergen-ic{margin-right:3px}
@media print{
  .badge.bad.allergen{background:#fff !important;color:#000 !important;border:1.5px solid #000}
  .badge.may.allergen{background:#fff !important;color:#000 !important;border:1px dashed #000}
}

/* ---- buttons / forms ---- */
.btn{border:0;border-radius:var(--r-ctrl);padding:10px 17px;font-weight:600;font-size:var(--fs-subhead);transition:transform .12s,filter .2s;display:inline-flex;align-items:center;gap:8px}
.btn:active{transform:scale(.97)} .btn.primary{background:var(--accent);color:#fff} .btn.ghost{background:var(--fill);color:var(--text)}
.btn:hover{filter:brightness(1.05)} .btn.ghost:hover{background:var(--fill-2);filter:none}   /* CTA hover feedback */

/* ---- toasts (Django messages) ---- */
.toasts{position:fixed;top:64px;right:18px;z-index:120;display:flex;flex-direction:column;gap:8px;max-width:340px}
.toast{background:var(--bg-elev);border:.5px solid var(--sep);border-left:3px solid var(--accent);
  border-radius:12px;box-shadow:var(--shadow-lift);padding:12px 15px;font-size:var(--fs-footnote);color:var(--text);
  animation:toastin .25s ease;display:flex;align-items:flex-start;gap:10px}
.toast-msg{flex:1 1 auto}
.toast-x{flex:0 0 auto;background:none;border:0;color:var(--text3);font-size:18px;line-height:1;
  cursor:pointer;padding:0 2px;margin:-2px -4px 0 0}
.toast-x:hover{color:var(--text)}
.toast.success{border-left-color:var(--green)} .toast.warning{border-left-color:var(--orange)}
.toast.error{border-left-color:var(--red)}
.toast.cl-hide{opacity:0;transition:opacity .4s}
@keyframes toastin{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}}
@media(max-width:760px){.toasts{left:12px;right:12px;max-width:none;top:56px}}
/* Responsive "stacked cards" for wide, interaction-heavy tables (e.g. the invoice-import preview),
   so each row is a tap-friendly label:value card on a phone instead of a 10-col horizontal scroll.
   Opt-in via class="respcards"; cells carry data-label for the field name. */
@media(max-width:760px){
  .respcards thead{display:none}
  .respcards, .respcards tbody, .respcards tr, .respcards td{display:block;width:auto}
  .respcards tr{border:.5px solid var(--sep);border-radius:12px;padding:10px 14px;margin-bottom:12px;
    background:var(--bg-elev);box-shadow:var(--shadow)}
  .respcards tr.row-warn{border-left:3px solid var(--orange)}
  .respcards tr.row-save{border-left:3px solid var(--accent)}
  .respcards td{padding:6px 0;border:0;text-align:left}
  .respcards td.num{text-align:left}
  .respcards td::before{content:attr(data-label);display:block;font-weight:600;font-size:var(--fs-caption);
    color:var(--text2);margin-bottom:3px}
  .respcards td[data-label=""]::before,.respcards td:not([data-label])::before{content:none}
}
/* Per-column filter row under a table header: compact, full-width search inputs. */
.colfilter th{padding-top:2px;padding-bottom:8px;vertical-align:top}
.colfilter input,.colfilter select{width:100%;min-width:80px;margin:0;font-size:var(--fs-caption);padding:5px 8px;font-weight:400}
/* Dashboard mini-panels: 2-col grid of compact, click-through rows (collapses to 1 col on phones). */
.dashgrid{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start}
@media(max-width:760px){.dashgrid{grid-template-columns:1fr}}
.dashrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 0;
  border-bottom:.5px solid var(--sep)}
.dashrow:last-child{border-bottom:0}
.dashrow a{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Feed card rows (dashboard recent price moves) — a coloured direction icon gives each move visual
   weight + per-item separation vs the flat .dashrow text. Price UP = bad (red), DOWN = good (green). */
.feed{display:flex;flex-direction:column}
.fr{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:.5px solid var(--sep)}
.fr:last-child{border-bottom:0}
.fr .ic{flex:0 0 28px;width:28px;height:28px;border-radius:50%;display:grid;place-items:center;font-size:var(--fs-caption)}
.fr .ic.up{color:var(--red);background:var(--al-bg)}
.fr .ic.down{color:var(--green);background:var(--ok-bg)}
.fr .ic.flat{color:var(--text2);background:var(--fill)}
.fr-body{flex:1;min-width:0}
.fr-body a{font-weight:600;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fr-meta{font-size:var(--fs-caption);color:var(--text2)}
/* Import-preview cost-delta tint: a price drop (saving) reads green, a moderate rise amber, a big
   rise red — so the dangerous jumps pop on a long sheet. .delta-flat (±5%) stays untinted. */
.delta-down{background:var(--ok-bg)}
.delta-up{background:var(--warn-bg)}
.delta-up-big{background:var(--al-bg)}
.btn.sm{padding:8px 13px;font-size:var(--fs-footnote)}
/* visible keyboard focus for everything interactive (mouse users see nothing, per :focus-visible) */
a:focus-visible,button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible,
.price:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:8px}
label{display:block;font-size:var(--fs-footnote);font-weight:600;margin-bottom:5px}
.sinput,input[type=text],input[type=email],input[type=password],input[type=number],select,textarea{
  background:var(--fill);border:.5px solid var(--sep);border-radius:9px;padding:9px 12px;font:inherit;font-size:14px;color:var(--text);width:100%}
/* NOTE: this rule has the same specificity as the :focus-visible block above and comes LATER, so it wins
   for every input/select/textarea. It used to paint the ring in --accent-tint, which composites to ~1.11:1
   on --bg — a de-facto invisible focus indicator on the app's most common control. (a11y audit, critical) */
.sinput:focus,input:focus,select:focus,textarea:focus{outline:2px solid var(--focus);outline-offset:2px;border-color:var(--accent)}
select{cursor:pointer;color-scheme:light}
/* Dark theme: the OPEN option list is drawn by the browser, not our CSS. Root-level color-scheme
   alone proved unreliable for the popup in Chromium (chef report, 2026-07), so belt-and-braces:
   (1) color-scheme on the <select> ELEMENT (the popup follows the element's used scheme), and
   (2) explicit option colors — Chromium applies option background/color inside the popup on
   Windows/Linux, which guarantees readable dark-on-dark even where (1) is ignored. */
:root[data-theme="dark"] select{color-scheme:dark}
option{background:var(--bg-elev);color:var(--text)}
/* datalist (type-to-search) inputs keep the styled box but drop the native dropdown arrow, which
   rendered as a dated beveled control; suggestions still appear on type/focus. */
input[list]::-webkit-calendar-picker-indicator{display:none}
.formrow{display:flex;gap:12px;flex-wrap:wrap;align-items:end}
/* Default sizing for formrow fields, but at 0-specificity for .formrow (:where) so a field can override
   flex/min-width with a plain utility class (.flex-0 .minw-170 …). Inline styles used to win here; after
   the CSP inline-style removal (audit 9.1) the overrides are utility classes, which must beat this. */
:where(.formrow)>div{flex:1;min-width:120px}
.muted{color:var(--text2)} .right{text-align:right} .up{color:var(--red);font-weight:600} .down{color:var(--green);font-weight:600}
/* Visually-hidden but screen-reader-readable. Used to carry severity labels that are otherwise only
   conveyed by colour/emoji (e.g. Today cards). Standard clip pattern. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* ---- command palette (Ctrl/Cmd+K) ---- */
.cmdk{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.4);display:grid;place-items:start center;
  padding-top:13vh;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
.cmdk[hidden]{display:none}
.cmdk-panel{width:min(560px,92vw);background:var(--bg-elev);border:.5px solid var(--sep);
  border-radius:16px;box-shadow:var(--shadow-lift);overflow:hidden}
.cmdk-input{width:100%;border:0;border-bottom:.5px solid var(--sep);background:transparent;
  padding:16px 18px;font-size:var(--fs-callout);color:var(--text);outline:none}
.cmdk-list{list-style:none;margin:0;padding:6px;max-height:52vh;overflow:auto}
.cmdk-list li{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;font-size:var(--fs-subhead)}
.cmdk-list li[aria-selected="true"]{background:var(--accent-tint);color:var(--accent)}
.cmdk-list li .ic{width:22px;text-align:center;font-size:var(--fs-callout)}
.cmdk-empty{padding:14px 18px;color:var(--text2);font-size:var(--fs-footnote)}

/* ---- kitchen service mode (big-type, hands-free) ---- */
.svc-wrap{max-width:920px;margin:0 auto;padding:22px 20px 80px}
.svc-ing{font-size:19px;line-height:1.7}
.svc-line{padding:3px 0}
.svc-step{display:flex;gap:16px;align-items:flex-start;background:var(--bg-elev);border:.5px solid var(--sep);
  border-radius:var(--r-card);box-shadow:var(--shadow);padding:18px 20px;margin-bottom:12px;cursor:pointer;
  transition:transform .12s,opacity .2s,border-color .2s}
.svc-step .svc-no{flex:0 0 40px;height:40px;border-radius:50%;background:var(--fill);color:var(--text2);
  display:grid;place-items:center;font-weight:700;font-size:18px}
.svc-step .svc-txt{font-size:var(--fs-title2);line-height:1.4;letter-spacing:-.01em}
.svc-step.current{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-tint),var(--shadow)}
.svc-step.current .svc-no{background:var(--accent);color:#fff}
.svc-step.done{opacity:.45}
.svc-step.done .svc-txt{text-decoration:line-through}
.svc-step.done .svc-no::after{content:"✓"}
.svc-step.done .svc-no{font-size:0} .svc-step.done .svc-no::after{font-size:18px}

/* ---- centered auth/standalone shell ---- */
.authwrap{min-height:100vh;display:grid;place-items:center;padding:24px}
.authcard{width:100%;max-width:460px;background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow-lift);padding:34px 32px}

/* ---- print: drop the app chrome, keep the content (recipe sheet / stock count / allergen matrix) ---- */
@media print{
  .top,nav.side,.botnav,.botmore,.iconbtn,.btn,.hint,[data-filter],.price .pen{display:none !important}
  .app{display:block}
  main.content{padding:0;overflow:visible}
  html,body{background:#fff;color:#000}
  a{color:#000}
  thead th{color:#000}
  .panel,.tablewrap,.kpi,.rcard{box-shadow:none;border:1px solid #ddd;break-inside:avoid}
  .print-only{display:block !important}             /* legends/notes hidden on screen, shown on paper */
  .stickycol [role=img]{color:#000 !important}       /* allergen ●/◐/· stay legible in B&W (colour is lost) */
  @page{margin:14mm}
}
.print-only{display:none}                            /* screen: hidden; @media print reveals it */

@media(max-width:760px){
  .app{grid-template-columns:1fr;grid-template-areas:"top" "main"}
  nav.side{display:none}
  main.content{padding:20px 16px 76px}   /* room for the fixed bottom nav so content isn't hidden */
  h1{font-size:24px}
  .top{padding:10px 14px;gap:8px}
  .top .kitchen{display:none}            /* keep the top bar from overflowing on phones */
  .top .pill{display:none}               /* hide ALL pills incl. role — decorative on phones, and the
                                            prices-fresh alert lives in the More sheet */
  .top .trialpill{display:none}          /* the trial / payment-failed nudge is in the More sheet too */
  .top .sitesw{display:none}             /* site switch lives in the More sheet on phones (.botmore) */
  .top .cs-switch{display:none}          /* Chef Space switch lives in the More sheet on phones too */
  .brand .bword{display:none}            /* mark-only brand on phones so the 5 action icons + logo fit
                                            a 375px bar (full wordmark pushed the row to ~436px) */
  .tablewrap{overflow-x:auto}            /* wide tables scroll inside their card, not off-page */
  .ph{flex-wrap:wrap}                    /* page-header actions wrap below the title instead of overflowing a ~375px bar */
  .ph>span{flex-wrap:wrap}               /* and a multi-button action group wraps too (e.g. payroll Print + Export CSV) */
  .botnav{display:flex}                  /* phone-only: reveal the bottom nav (the sidebar is hidden) */
}

/* ---- AI Kitchen Assistant drawer (reuses the .cmdk overlay shell) ---- */
.asst-panel{padding:0;display:flex;flex-direction:column;max-height:80vh;height:min(80vh,640px)}
.asst-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:14px 16px 12px;border-bottom:.5px solid var(--sep)}
.asst-title{font-weight:700;font-size:var(--fs-callout);letter-spacing:-.01em;display:flex;align-items:center;gap:8px}
.asst-head .iconbtn{width:30px;height:30px;font-size:14px}
/* Chat log: the growing, scrolling middle; chips + input sit below it (chef 2026-07-22 "like a chat"). */
.asst-log{flex:1 1 auto;min-height:80px;overflow-y:auto;padding:16px 18px;display:flex;
  flex-direction:column;gap:10px;scroll-behavior:smooth}
.asst-hint{margin:0;font-size:var(--fs-footnote)} .asst-hint b{color:var(--text)}
/* One chat message. AI bubbles tint left; the chef's own messages sit right in a neutral fill. */
.asst-msg{max-width:88%;border-radius:14px;padding:11px 14px}
.asst-msg.ai{align-self:flex-start;background:var(--accent-tint)}
.asst-msg.me{align-self:flex-end;background:var(--fill);color:var(--text)}
.asst-b{font-size:var(--fs-subhead);line-height:1.5;color:var(--text);white-space:pre-wrap}
.asst-msg.ai .asst-b{font-weight:500}
.asst-d{font-size:var(--fs-caption);margin-top:6px;line-height:1.4}
.asst-dots{animation:asstpulse 1.2s ease-in-out infinite}
@keyframes asstpulse{0%,100%{opacity:.45}50%{opacity:.9}}
@media (prefers-reduced-motion:reduce){.asst-dots{animation:none}.asst-log{scroll-behavior:auto}}
.asst-chips{padding:8px 18px;border-top:.5px solid var(--hair)}
.asst-chiprow{display:flex;flex-wrap:wrap;gap:7px}
.asst-chip{cursor:pointer;border:0;background:var(--fill);color:var(--text2);font-size:var(--fs-footnote);
  transition:background .15s,color .15s}
.asst-chip:hover{background:var(--accent-tint);color:var(--accent)}
/* Input row pinned at the bottom: text field + Send. */
.asst-form{display:flex;gap:8px;align-items:center;padding:10px 14px;border-top:.5px solid var(--sep)}
.asst-input{flex:1 1 auto;border:0}
.asst-send{flex:0 0 auto}
/* Voice dictation mic (chef 2026-07-22): a ghost button that turns red + pulses while listening. */
.asst-mic{flex:0 0 auto;font-size:16px;line-height:1;padding:8px 11px}
.asst-mic.rec{background:#e5484d;color:#fff;border-color:#e5484d;animation:asstpulse 1s ease-in-out infinite}
@media(max-width:760px){.asst-panel{max-height:88vh;height:88vh}}
/* ---- Recipe spec sheet (core/specsheet.py + recipe_spec.html) ---- */
.specsheet{max-width:820px;margin:18px auto;padding:34px 40px;color:var(--text);
  background:var(--card);border-radius:var(--r-card)}
.specsheet .ss-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;
  padding-bottom:16px;margin-bottom:18px;border-bottom:2px solid var(--text)}
.specsheet .ss-org{font-size:var(--fs-title2);font-weight:800;letter-spacing:-.01em}
.specsheet .ss-doc{font-size:var(--fs-caption);text-transform:uppercase;letter-spacing:.12em;color:var(--text3);margin-top:2px}
.specsheet .ss-meta{font-size:var(--fs-caption);color:var(--text2);text-align:right;line-height:1.7}
.specsheet .ss-meta span{color:var(--text3);display:inline-block;min-width:54px}
.specsheet .ss-title{font-size:var(--fs-title1);font-weight:800;margin:0 0 2px;letter-spacing:-.02em}
.specsheet .ss-sub{color:var(--text2);margin:0 0 22px;font-size:14px}
.specsheet .ss-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:26px}
.specsheet .ss-kpi{border:.5px solid var(--hair);border-radius:var(--r-card,12px);padding:12px 14px}
.specsheet .ss-lab{font-size:var(--fs-caption2);text-transform:uppercase;letter-spacing:.06em;color:var(--text3)}
.specsheet .ss-val{font-size:var(--fs-title3);font-weight:700;margin:4px 0 2px}
.specsheet .ss-note{font-size:var(--fs-caption2);color:var(--text3)}
.specsheet .ss-h{font-size:var(--fs-footnote);text-transform:uppercase;letter-spacing:.08em;color:var(--accent);
  margin:26px 0 10px;padding-bottom:6px;border-bottom:.5px solid var(--hair)}
.specsheet .ss-table{width:100%;border-collapse:collapse;font-size:14px}
.specsheet .ss-table th,.specsheet .ss-table td{padding:7px 8px;text-align:left;
  border-bottom:.5px solid var(--hair)}
.specsheet .ss-table thead th{font-size:var(--fs-caption2);text-transform:uppercase;letter-spacing:.05em;color:var(--text3)}
.specsheet .ss-table .num{text-align:right;white-space:nowrap}
.specsheet .ss-table tfoot th{font-weight:700;border-top:1.5px solid var(--text);border-bottom:none}
.specsheet .ss-method{list-style:none;margin:0;padding:0;counter-reset:none}
.specsheet .ss-method li{display:flex;gap:12px;padding:9px 0;font-size:14px;line-height:1.5;
  border-bottom:.5px solid var(--hair)}
.specsheet .ss-step-n{flex:0 0 24px;height:24px;border-radius:50%;background:var(--accent-tint);
  color:var(--accent);display:grid;place-items:center;font-weight:700;font-size:var(--fs-caption)}
.specsheet .ss-step-body{flex:1}
.specsheet .ss-step-meta{display:block;margin-top:3px;font-size:var(--fs-caption);color:var(--text3)}
.specsheet .ss-allergen{font-size:14px;line-height:1.7;margin:0}
.specsheet .ss-foot{display:flex;gap:40px;margin-top:38px}
.specsheet .ss-sign{flex:1}
.specsheet .ss-line{display:block;border-bottom:1px solid var(--text);height:30px}
.specsheet .ss-cap{font-size:var(--fs-caption2);color:var(--text3);text-transform:uppercase;letter-spacing:.06em}
.specsheet .ss-fineprint{margin-top:24px;font-size:var(--fs-caption2);color:var(--text3);text-align:center}
.specsheet .muted{color:var(--text3)}
@media (max-width:680px){.specsheet{padding:22px 18px}.specsheet .ss-kpis{grid-template-columns:repeat(2,1fr)}}

/* Print: a clean white A4 sheet regardless of light/OLED-dark screen theme. */
@media print{
  @page{size:A4;margin:14mm}
  /* Hide the app chrome + on-screen-only controls. Adjust the chrome selectors to match
     base.html if its top bar / sidebar use different class names. */
  .no-print,.topbar,.sidebar,nav,header.appbar,.app-nav,.trial-pill{display:none !important}
  body{background:#fff !important}
  .specsheet{max-width:none;margin:0;padding:0;border-radius:0;background:#fff !important;color:#000 !important;
    box-shadow:none}
  .specsheet .ss-org,.specsheet .ss-title{color:#000 !important}
  .specsheet .ss-head{border-bottom-color:#000 !important}
  .specsheet .ss-h{color:#000 !important;border-bottom-color:#999 !important}
  .specsheet .ss-table th,.specsheet .ss-table td,.specsheet .ss-method li{border-bottom-color:#ccc !important}
  .specsheet .ss-table tfoot th,.specsheet .ss-line{border-color:#000 !important}
  .specsheet .ss-step-n{background:#eee !important;color:#000 !important;
    -webkit-print-color-adjust:exact;print-color-adjust:exact}
  .specsheet .ss-lab,.specsheet .ss-note,.specsheet .ss-doc,.specsheet .ss-meta span,
  .specsheet .ss-cap,.specsheet .ss-fineprint,.specsheet .ss-step-meta{color:#555 !important}
  .specsheet .ss-method li,.specsheet .ss-kpi{break-inside:avoid}
  .specsheet .ss-h{break-after:avoid}
}
/* ---- Batch QR label sheet (core/labels.py + core/templates/core/qr_labels.html) ---- */
.qrlabels-sheet{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:14px;
  margin-top:6px;
}
.qrlabel-cell{
  background:var(--bg-elev);
  border:.5px solid var(--sep);
  border-radius:var(--r-card);
  box-shadow:var(--shadow);
  padding:14px;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:8px;
  break-inside:avoid;
  page-break-inside:avoid;
}
.qrlabel-qr{
  background:#fff;                 /* QR needs a white quiet-zone to scan reliably */
  border-radius:10px;
  padding:8px;
  width:128px;
  height:128px;
  display:grid;
  place-items:center;
}
.qrlabel-qr svg{width:100%;height:100%}
.qrlabel-name{font-weight:600;color:var(--text);line-height:1.25}
.qrlabel-allergens{display:flex;flex-wrap:wrap;gap:5px;justify-content:center;font-size:var(--fs-caption)}
.qrlabel-tag{
  background:var(--fill);
  color:var(--red);
  border:.5px solid var(--sep);
  border-radius:6px;
  padding:1px 6px;
  font-weight:600;
  letter-spacing:.02em;
}
.qrlabel-url{font-size:var(--fs-caption2);color:var(--text3);word-break:break-all;line-height:1.3}

/* ---- Price import (core/invoices.py): flag rows needing attention ---- */
tr.row-warn>td{background:var(--warn-bg)}

/* A4 print: drop app chrome, fit 3 columns, never split a label across pages */
@media print{
  /* hide global app chrome — match whatever the shell uses; these are defensive selectors */
  .sidebar, .topbar, .app-nav, nav, .topright, .skip, .qrlabels-noprint{display:none !important}
  body{background:#fff !important}
  @page{size:A4;margin:12mm}
  .qrlabels-sheet{grid-template-columns:repeat(3,1fr);gap:8mm}
  .qrlabel-cell{
    box-shadow:none;
    border:.4mm solid #bbb;
    -webkit-print-color-adjust:exact;
    print-color-adjust:exact;
  }
  .qrlabel-qr{padding:2mm}
  .qrlabel-tag{color:#b00020;border-color:#ccc}
  .qrlabel-url{color:#444}
}

/* ---- Activation funnel widget (core/activation.py, dashboard.html) ---- */
.actv-bar{height:8px;border-radius:6px;background:var(--fill);overflow:hidden;margin:2px 0 16px}
.actv-bar-fill{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#7AD98F);
  border-radius:6px;min-width:2px;transition:width .3s ease}
.actv-steps{display:flex;flex-direction:column;gap:2px}
.actv-step{display:flex;gap:11px;align-items:flex-start;padding:8px 0;border-top:.5px solid var(--sep)}
.actv-step:first-child{border-top:none}
.actv-tick{flex:0 0 22px;height:22px;border-radius:50%;display:grid;place-items:center;
  font-size:var(--fs-footnote);font-weight:700;background:var(--fill);color:var(--text3)}
.actv-step.done .actv-tick{background:var(--ok-bg);color:var(--ok-fg)}
.actv-body{flex:1;min-width:0}
.actv-label{font-weight:600;color:var(--text)}
a.actv-label{color:var(--accent)}
.actv-step.done .actv-label{color:var(--text3)}
.actv-hint{font-size:var(--fs-caption);color:var(--text3);margin-top:2px;line-height:1.4}
.actv-empty{border:.5px solid var(--accent);box-shadow:0 0 0 2px var(--accent-tint),var(--shadow)}

/* ---- Price intelligence (core/price_intel.py, price_intel.html + ingredient_detail.html) ---- */
/* Sparklines inherit text colour via stroke="currentColor" — theme-safe in light + OLED-dark. */
.spark{display:block;overflow:visible}
.pi-trend{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.pi-spark{display:inline-flex;align-items:center;color:var(--text2)}
.pi-spark svg{display:block}
/* Movers columns + signed % badges: up = rising cost (red, bad), down = easing cost (green, good). */
.pi-movers{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start}
@media (max-width:760px){.pi-movers{grid-template-columns:1fr}}
.pi-up{color:var(--red);font-weight:600}
.pi-down{color:var(--green);font-weight:600}

/* Searchable combobox (data-combobox): a text input + a floating filtered panel; "＋ New" is sticky at
   the TOP so it stays visible while scrolling a long list. */
.cbx{position:relative}
.cbx-panel{position:absolute;z-index:50;left:0;right:0;top:100%;margin-top:4px;background:var(--bg-elev);
  border:1px solid var(--fill);border-radius:10px;box-shadow:var(--shadow-lift);max-height:260px;overflow-y:auto;padding:4px}
.cbx-opt{padding:7px 10px;border-radius:var(--r-sm);cursor:pointer;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cbx-opt:hover,.cbx-opt.cb-hi{background:var(--fill)}
.cbx-add{color:var(--accent);font-weight:600;position:sticky;top:0;background:var(--bg-elev);border-bottom:1px solid var(--fill);border-radius:var(--r-sm) var(--r-sm) 0 0}
.cbx-add:hover,.cbx-add.cb-hi{background:var(--accent-tint)}
/* Touch: ≥44px option rows + the panel becomes a bottom sheet on coarse pointers so options sit above
   the on-screen keyboard (chef on a phone/tablet, wet hands). (UI spec: touch-first) */
@media (pointer: coarse) {
  .cbx-opt{min-height:44px;display:flex;align-items:center;font-size:16px}
  .cbx-panel:not([hidden]){position:fixed;left:0;right:0;bottom:0;top:auto;margin:0;max-height:55vh;
    border-radius:16px 16px 0 0;padding:8px 8px calc(8px + env(safe-area-inset-bottom))}
}

/* ---- States: empty + loading (Superplan Phase 3 primitives, built on the Phase-0 tokens). ADDITIVE:
   no existing markup uses these yet — templates adopt them to replace blank pages and bare spinners. ---- */
/* Empty state — centred + generous (replaces left-aligned text panels). Compose:
   <div class="empty"><div class="empty-ic">🍳</div><h3 class="empty-h">No recipes yet</h3>
     <p class="empty-sub">Add your first dish to start costing.</p>
     <a class="btn primary" href="…">＋ New recipe</a></div> */
.empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--sp-2);
  padding:var(--sp-10) var(--sp-5);max-width:440px;margin:0 auto}
.empty-ic{font-size:46px;line-height:1;margin-bottom:var(--sp-1)}
.empty-h{font-size:var(--fs-title3);font-weight:700;letter-spacing:-.02em;color:var(--text);margin:0}
.empty-sub{font-size:var(--fs-subhead);color:var(--text2);margin:0;line-height:1.5}
.empty .btn{margin-top:var(--sp-3)}
/* Skeleton loading — a shimmer of the eventual layout; prefer over a spinner. The global
   prefers-reduced-motion block below stills the shimmer for motion-sensitive users. */
.skeleton{position:relative;overflow:hidden;background:var(--fill);border-radius:var(--r-sm)}
.skeleton::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--fill-2),transparent);
  animation:skel 1.3s var(--ease) infinite}
@keyframes skel{100%{transform:translateX(100%)}}
.skeleton.line{height:14px;margin:var(--sp-2) 0} .skeleton.line.short{width:55%}
.skeleton.card{height:88px;border-radius:var(--r-card)}

/* ---- Segmented control (.seg) + food-cost pill (.fcpill) — promoted from recipes_list.html's per-page
   <style> into the shared system (frontend-code audit P2). .seg is the canonical Apple segmented control;
   .fcpill is the dish food-cost status chip. Rules verbatim (font sizes tokenised to the ramp). ---- */
.seg{display:inline-flex;gap:2px;background:var(--fill);border-radius:var(--r-md);padding:var(--sp-1);flex-wrap:wrap}
.seg button{border:0;background:transparent;color:var(--text2);font-size:var(--fs-footnote);font-weight:600;
  border-radius:8px;padding:6px 12px;transition:background .12s,color .12s}
.seg button[aria-pressed="true"]{background:var(--bg-elev);color:var(--accent);box-shadow:var(--shadow)}
.fcpill{font-size:var(--fs-caption2);font-weight:700;border-radius:var(--r-sm);padding:3px 8px;display:inline-block;white-space:nowrap}
.fcpill.good{background:var(--ok-bg);color:var(--ok-fg)}
.fcpill.near{background:var(--warn-bg);color:var(--warn-fg)}
.fcpill.over{background:var(--al-bg);color:var(--al-fg)}

/* ---- .rtabs — the LINK flavour of .seg: a segmented control whose segments are <a>s, because each one
   is a server-side scope (?type=, ?program=) and must be a real, shareable, back-button-able URL rather
   than a button that mutates the page. .seg's own rules only style `button`, so the two compose:
   `<div class="seg rtabs">` gets the trough from .seg and the raised active segment from here.

   Promoted out of recipes_list.html's per-page <style> (Staff Food Program, stage 2) the moment the
   kitchen list grew the same control — the same reason .seg, .bulkbar and .modtile were promoted before
   it. A COPIED widget always drifts, and this one now carries a privacy scope, so a drift between the
   two copies would be a drift in how visibly the staff/menu split is presented. Rules verbatim. ---- */
.rtabs-wrap{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:6px 0 18px}
.rtabs-lab{font-size:11.5px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--text3)}
.rtabs{display:inline-flex;gap:4px;background:var(--fill);border:1px solid var(--sep);border-radius:12px;padding:4px}
.rtabs a{display:inline-flex;align-items:center;gap:7px;color:var(--text2);font-size:15px;font-weight:600;
  border-radius:9px;padding:9px 18px;text-decoration:none;transition:background .14s,color .14s,box-shadow .14s}
.rtabs a:hover{color:var(--text)}
.rtabs a.active{background:var(--accent);color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.22)}

/* ---- Bulk selection: a tick-box column + an action bar that appears once a row is ticked. Promoted
   out of recipes_list.html's per-page <style> (2026-07-27) when the Market list asked for the same
   thing ("implement the tick boxes as we did in the recipes. Bulk delete"). A COPIED widget always
   drifts — see the warning at the top of _allergen_badge.html — so there is exactly one set of rules
   and one driver (app.js "BULK SELECTION"). Only the ACTION buttons stay page-local, because each list
   bulk-does something different. Rules verbatim from the recipes page, including the coarse-pointer
   enlargement (a 13px checkbox is not a thumb target on the pass). ---- */
.bulkcol{width:34px;text-align:center;padding-left:8px;padding-right:0}
.bulkcol input{width:auto;cursor:pointer;margin:0}
.bulkbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:0 0 14px;padding:10px 14px;
  background:var(--accent-tint);border:1px solid var(--accent);border-radius:12px}
.bulkbar-count{font-weight:600}
.bulkbar-hint{margin-left:auto}
@media (pointer:coarse){.bulkcol input{width:22px;height:22px}}
/* The bar is revealed by JS and the ticks are only meaningful to JS, so without it the column is a row
   of controls that do nothing. .cl-js is set in <head> (base.html), so this hides before first paint —
   the same gate .plan-add-js uses. The no-JS baseline keeps the per-row Delete, which needs no script. */
html:not(.cl-js) .bulkcol{display:none}

/* ---- Disclosure (progressive disclosure: collapse a secondary panel so the primary content leads —
   e.g. recipe settings tucked above the costing). Styled <summary>; the ▸ rotates open. No JS. ---- */
.disclose > summary{display:flex;align-items:center;gap:8px;width:max-content;cursor:pointer;
  list-style:none;user-select:none;font-size:var(--fs-footnote);font-weight:600;text-transform:uppercase;
  letter-spacing:.5px;color:var(--text2);margin:24px 4px 10px}
.disclose > summary::-webkit-details-marker{display:none}
.disclose > summary::before{content:"▸";font-size:.85em;color:var(--text3);
  transition:transform var(--dur-fast) var(--ease)}
.disclose[open] > summary::before{transform:rotate(90deg)}

/* ---- Ingredient detail: Supplier prices as the page hero (chef UI feedback 2026-07-11 — uniform
   boxes were hard to orient; this is the table to focus on). Titled header (name + "cost last updated")
   over an elevated table card; the Add form is folded into the card as an accent "+" disclosure so it
   reads as "add another supplier here" rather than a separate box. ---- */
.ing-suppliers-head{display:flex;align-items:baseline;justify-content:space-between;gap:6px 16px;flex-wrap:wrap;margin:28px 4px 8px}
.ing-suppliers-head h2{font-size:var(--fs-title3);font-weight:700;margin:0}
.ing-suppliers-head .upd{font-size:var(--fs-footnote);color:var(--text2);white-space:nowrap}
.perunit{font-size:var(--fs-caption);margin-top:2px;font-weight:400}   /* muted per-unit rate under the pack price */
/* Inline-edit title (.inedit): the whole title is ONE focusable button; a quiet ✎ + dotted underline
   signal editability on hover AND focus (never hover-only — keyboard/touch reveal via :focus-visible).
   Editing swaps to a heading-sized input; Enter or click-away saves, Esc reverts — no Save button
   (chef feedback 2026-07-12: the old pencil + Save form read as clunky). (UI spec: inline rename) */
.ie-title{background:none;border:0;margin:0 -4px;padding:2px 4px;font:inherit;color:inherit;cursor:text;
  display:inline-flex;align-items:baseline;gap:6px;border-radius:8px;text-align:left;max-width:100%}
/* Give the title the row's free width so a short name isn't squeezed; wrap long names at spaces rather
   than collapsing to "B…" (chef 2026-07-18: "shows B... instead of Beer / splits into 2 rows"). The old
   overflow:hidden made the flex item's min-content 0, letting the layout crush it to one char. */
.ph .inedit{flex:1 1 auto;min-width:0}
.ie-title{flex-wrap:wrap}
.ie-title .ie-val{white-space:normal;overflow-wrap:break-word;min-width:0}
.ie-title .pen{opacity:0;font-size:.6em;flex:0 0 auto;transition:opacity var(--dur-fast) var(--ease)}
.ie-title:hover .pen,.ie-title:focus-visible .pen{opacity:.55}
.ie-title:hover .ie-val,.ie-title:focus-visible .ie-val{text-decoration:underline dotted;text-underline-offset:4px;text-decoration-color:var(--text3)}
@media (pointer:coarse){.ie-title{min-height:44px;align-items:center}.ie-title .pen{opacity:.5}}
/* The rename input reads as the heading itself — same size/weight, only a calm accent underline marks it
   as editable — so there's no jarring "form" look or layout jump. */
.ie-form{display:inline-flex;gap:10px;align-items:baseline} .ie-form[hidden]{display:none}
.ie-input{font-size:var(--fs-title2);font-weight:700;line-height:1.1;border:0;border-bottom:2px solid var(--accent);
  background:transparent;color:var(--text);padding:1px 2px;min-width:160px;max-width:min(520px,80vw)}
.ie-input:focus{outline:none}
.ie-hint{font-size:var(--fs-caption2);color:var(--text3);align-self:center}
@media (max-width:560px){.ie-hint{display:none}}
/* Per-supplier price-history expander (chef feedback #4): a full-width sub-row under the supplier row. */
.subrow > td{background:var(--fill);padding:0}
.subrow-in{padding:10px 14px 14px}
/* recipe_detail: per-line cost detail folded under the › toggle in the merged Ingredients table (audit UX) */
.cost-detail{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;font-size:var(--fs-footnote)}
.detailtog{padding:4px 9px;transition:transform var(--dur-fast) var(--ease)}
.detailtog[aria-expanded="true"]{transform:rotate(90deg)}
.histtable{width:100%;font-size:var(--fs-footnote)}
.histbtn{vertical-align:baseline;margin-left:2px}
@media (pointer:coarse){.histbtn{min-height:44px}}
/* Reorder-supplier arrows (chef feedback #5): keyboard-native, touch-sized, disabled at the ends. */
.moveorder{display:inline-flex;gap:1px;margin-right:4px;vertical-align:middle}
.mvbtn{padding:5px 7px;line-height:1;font-size:11px}
.mvbtn[disabled]{opacity:.3;cursor:default}
@media (pointer:coarse){.mvbtn{min-width:44px;min-height:44px;font-size:14px}}
/* Add-to-Order dialog (native <dialog>): centered card on desktop, bottom sheet on phones. (UI spec) */
.ao-dialog{border:0;border-radius:16px;padding:0;max-width:420px;width:calc(100% - 32px);background:var(--bg-elev);color:var(--text);box-shadow:var(--shadow-lift)}
/* One backdrop for every modal in the app (the event page's guest allergen sheet joins it here rather
   than repeating the literal — the scrim is a shared surface, not a per-feature choice). */
.ao-dialog::backdrop,.gal-dialog::backdrop{background:rgba(0,0,0,.4)}
.ao-body{padding:20px}
.ao-name{font-size:var(--fs-title3);font-weight:700;margin:0 0 14px}
.ao-field{margin-bottom:14px}
.ao-lab{display:block;font-size:var(--fs-footnote);font-weight:600;color:var(--text2);margin-bottom:6px}
.ao-sup{display:flex;flex-direction:column;gap:6px}
.ao-radio{display:flex;align-items:center;gap:8px;padding:9px 11px;border:.5px solid var(--sep);border-radius:10px;cursor:pointer;font-size:var(--fs-subhead)}
/* stretch so the − / + buttons are exactly the input's height (were floating, mis-aligned); grid-place
   centres the glyph in the box regardless of the −/＋ glyph metrics. */
.ao-qty{display:flex;align-items:stretch;gap:8px}
.ao-qty input{flex:1;text-align:center;font-size:var(--fs-title3);font-weight:600;min-height:48px}
.ao-step{min-width:52px;min-height:48px;font-size:22px;line-height:1;padding:0;display:grid;place-items:center}
/* Per-unit price on a supplier option — the comparable figure across different pack sizes (accent). */
.ao-perunit{font-weight:600;color:var(--accent);white-space:nowrap}
.ao-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}
/* Live order total in the ＋ Order dialog — qty × pack price, so the spend is clear before adding. */
.ao-total{display:flex;justify-content:space-between;align-items:baseline;margin:14px 0 2px;padding-top:12px;
  border-top:1px solid var(--sep);font-size:var(--fs-callout);font-weight:600}
.ao-total b{font-variant-numeric:tabular-nums;font-size:var(--fs-title3);color:var(--accent)}
.ao-total[hidden]{display:none}
.toast-act{margin-left:10px;font-weight:600;white-space:nowrap;text-decoration:underline;
  background:none;border:0;padding:0;font-size:inherit;color:var(--accent);cursor:pointer}
@media (max-width:560px){
  .ao-dialog{position:fixed;inset:auto 0 0 0;margin:0;width:100%;max-width:100%;border-radius:16px 16px 0 0}
}

/* ---- Round 5 (chef feedback 2026-07-12): confirm-to-save inline edits, +Order accent + in-cart rows,
   order-preview popup, category pill ---- */
/* Tap-to-edit-and-confirm: the read-only value (.tapval mirrors .price) opens an editor with explicit
   ✓ / ✕ so a price/pack change is deliberate, not an accidental blur-commit. */
.tapval{display:inline-flex;gap:6px;align-items:center;border-radius:8px;padding:4px 8px;cursor:text;transition:background .15s}
.tapval:hover{background:var(--accent-tint)}
.tapval .pen{opacity:.4;font-size:var(--fs-caption)}
.tapval:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:8px}
.uomedit{display:inline-flex;gap:5px;align-items:center;flex-wrap:wrap}
.uomedit input{width:66px;text-align:right} .uomedit select{width:auto;padding:6px 8px}
.tapedit[hidden]{display:none}                                   /* beats .priceedit/.uomedit inline-flex */
.tapok,.tapx{border:0;border-radius:var(--r-sm);width:28px;height:28px;line-height:1;font-size:13px;
  display:inline-grid;place-items:center;padding:0;flex:0 0 auto}
.tapok{background:var(--accent);color:#fff} .tapx{background:var(--fill);color:var(--text2)}
.tapx:hover{background:var(--fill-2)}
@media (pointer:coarse){.tapok,.tapx{width:40px;height:40px;font-size:16px}}
/* ＋ Order — a distinct BLUE action so it reads apart from the green primary + grey ghost buttons. */
.btn.order{background:rgba(0,122,255,.16);color:var(--blue)}
.btn.order:hover{background:rgba(0,122,255,.26);filter:none}
/* Rows already on a draft order: a clear blue tint + accent edge (chef feedback: the old .06 alpha was
   barely visible — boosted in BOTH themes, dark overrides below). */
tbody tr.in-order>td{background:rgba(0,122,255,.13)}
tbody tr.in-order:hover>td{background:rgba(0,122,255,.19)}
tbody tr.in-order td:first-child{box-shadow:inset 4px 0 0 var(--blue)}
:root[data-theme="dark"] .btn.order{background:rgba(10,132,255,.22)}
:root[data-theme="dark"] .btn.order:hover{background:rgba(10,132,255,.34)}
:root[data-theme="dark"] tbody tr.in-order>td{background:rgba(10,132,255,.24)}
:root[data-theme="dark"] tbody tr.in-order:hover>td{background:rgba(10,132,255,.34)}
.inorder-chip{cursor:pointer;white-space:nowrap}
.is-hidden{display:none}
/* order-action swap: NOT on order -> ＋Order; ON order -> ✎ Edit order + ✕ Remove (driven by tr.in-order).
   Hide rules are scoped `tbody .order-*` (0,1,1) so they beat the later `.d-inline`/`.btn` utilities (0,1,0). */
tbody .order-edit,tbody .order-remove{display:none}
tbody tr.in-order .order-add{display:none}
tbody tr.in-order .order-edit{display:inline-flex}
tbody tr.in-order .order-remove{display:inline}
@media (max-width:600px){.respcards tr.in-order{border-color:var(--blue);border-left-width:4px}}
/* Order-preview popup (reuses .ao-dialog): supplier + date + line list, checked without leaving the list. */
.op-dialog{max-width:480px}
.op-meta{margin:-6px 0 12px;font-size:var(--fs-footnote)}
.op-lines{max-height:46vh;overflow-y:auto;margin:0 -2px}
.op-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
/* Editable order line: product | qty input + unit | line total | remove. Wraps the note under it. */
.op-list li{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 2px;border-bottom:.5px solid var(--hair)}
.op-prod{flex:1 1 40%;font-weight:500;min-width:120px} .op-note{font-weight:400}
.op-qtywrap{display:inline-flex;align-items:center;gap:5px;color:var(--text2);white-space:nowrap}
.op-qtyin{width:64px;text-align:right;padding:5px 7px} .op-qtyv{font-variant-numeric:tabular-nums}
.op-linetot{min-width:76px;text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
.op-rm{flex:0 0 auto;border:0;border-radius:var(--r-sm);width:28px;height:28px;line-height:1;font-size:13px;
  display:inline-grid;place-items:center;padding:0;background:var(--fill);color:var(--text2)}
.op-rm:hover{background:var(--al-bg,var(--warn-bg));color:var(--al-fg,var(--warn-fg))}
@media (pointer:coarse){.op-rm{width:40px;height:40px;font-size:15px}.op-qtyin{width:72px}}
.op-total{display:flex;justify-content:space-between;align-items:baseline;margin:12px 2px 2px;
  padding-top:10px;border-top:1px solid var(--sep);font-size:var(--fs-callout)}
.op-total b{font-variant-numeric:tabular-nums;font-size:var(--fs-subhead)}
.op-total[hidden]{display:none}
/* Category pill + popover (ingredient detail): one clean control that opens the searchable picker. */
/* Category — a MAJOR product attribute, so make it prominent (chef 2026-07-18: "hardly to notice"):
   an uppercase label + a sizeable pill, accent-tinted when set, amber+dashed when still uncategorised
   (reads as a to-do that draws the eye). */
.catrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:8px 0 18px;position:relative}
.catrow-lab{font-size:11.5px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--text3)}
.catpill{display:inline-flex;align-items:center;gap:8px;background:var(--accent-tint);color:var(--accent);
  border:1px solid transparent;border-radius:10px;padding:8px 15px;font-size:14.5px;font-weight:700;
  transition:filter .15s,background .15s;cursor:pointer}
.catpill:hover{filter:brightness(1.06)}
.catpill.uncat{background:var(--al-bg);color:var(--al-fg);border-style:dashed;border-color:currentColor}
.catpill .caret{font-size:.72em;opacity:.8}
.catpop{position:absolute;z-index:60;top:calc(100% + 6px);left:0;min-width:280px;background:var(--bg-elev);
  border:.5px solid var(--sep);border-radius:14px;box-shadow:var(--shadow-lift);padding:12px}
.catpop[hidden]{display:none}
.catform{display:flex;gap:8px;align-items:flex-start}
@media (max-width:560px){.catpop{left:0;right:0;min-width:0}}
.panel.hero{box-shadow:var(--shadow-lift)}
/* .disclose.addrow (the accent "＋ Add a supplier price" summary) was removed 2026-07-28: its only user,
   ingredient_detail.html, no longer folds that form behind a disclosure — the chef wants to add a
   supplier price the way he adds a product on the Market List, i.e. with the form always visible. */

/* ---- Dashboard module tiles (.modgrid + .modtile) — promoted from dashboard.html's per-page <style>
   into the shared system (frontend-code P2). Font sizes tokenised to the ramp (13.5px->footnote). ---- */
.modgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
.modtile{display:flex;align-items:center;gap:10px;background:var(--bg-elev);border:.5px solid var(--sep);
  border-radius:14px;padding:13px 14px;box-shadow:var(--shadow);font-weight:600;font-size:var(--fs-footnote);
  transition:transform .15s,box-shadow .15s}
.modtile:hover{transform:translateY(-2px);box-shadow:var(--shadow-lift)}
.modtile .mic{font-size:var(--fs-title3)}

/* ---- Reduced motion (Superplan Phase 0 — a11y + polish, flagged by ui-visual & a11y audits). Honour
   the OS "Reduce Motion" setting: near-zero every transition/animation and neutralise the card lift +
   toast slide, so motion-sensitive chefs aren't hit by movement. Placed last so it overrides all rules. ---- */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  a.rcard:hover{transform:none}
}

/* ============================================================================================
   UTILITY CLASSES — the CSP inline-style removal toolkit (audit 9.1). Goal: eliminate every inline
   style="" attribute (599 across 51 templates) so style-src can drop 'unsafe-inline'. These cover the
   dominant inline patterns; convert templates to these, then flip the CSP directive (see
   docs/CSP-INLINE-STYLE-REMOVAL.md). Kept small + composable; add sparingly as conversions need them.
   ============================================================================================ */
/* margin / spacing */
.m-0{margin:0} .mt-0{margin-top:0} .mt-1{margin-top:8px} .mt-2{margin-top:12px} .mt-3{margin-top:14px} .mt-4{margin-top:16px}
.mb-1{margin-bottom:8px} .mb-2{margin-bottom:12px} .mb-3{margin-bottom:14px}
/* flex item + row */
.flex-0{flex:0} .flex-1{flex:1} .self-end{align-self:flex-end} .items-end{align-items:flex-end}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.row-8{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
/* display */
.d-none{display:none} .d-inline{display:inline} .d-inline-block{display:inline-block}
/* sizing */
.w-auto{width:auto} .w-80{width:80px} .w-full{width:100%}
.mw-640{max-width:640px} .mw-760{max-width:760px}
.minw-90{min-width:90px} .minw-110{min-width:110px} .minw-120{min-width:120px} .minw-130{min-width:130px}
.minw-150{min-width:150px} .minw-160{min-width:160px} .minw-200{min-width:200px} .minw-230{min-width:230px} .minw-260{min-width:260px}
/* text */
.nowrap{white-space:nowrap} .fs-12{font-size:12px} .fs-13{font-size:13px} .fw-400{font-weight:400} .fw-600{font-weight:600}
/* CSP component classes (audit 9.1) — semantic styles that were inline; not one-off utilities. */
.mb-4{margin-bottom:18px}
.hint.warn{background:var(--warn-bg);border-color:transparent}
.hint.ok{background:var(--ok-bg);border-color:transparent;color:var(--ok-fg)}
.glance{display:flex;flex-wrap:wrap;gap:8px 20px;align-items:center}      /* dashboard "At a glance" panel row */
.sw-select{width:auto;padding:6px 26px 6px 10px}                          /* org/site switcher <select> */
.botmore-form{margin:0 8px 12px}                                          /* switcher form in the mobile More sheet */
.botmore-pills{display:flex;flex-wrap:wrap;gap:8px;margin:0 8px 12px}     /* trial/stale-price pills row in the More sheet */
nav.side .foot a{color:inherit}   /* footer links inherit the muted foot colour; must out-specify nav.side a (0,1,2) */
/* Compositional flex / gap / align + a few sizing utilities (added for recipe_detail.html; reused by the
   remaining templates). Compose e.g. class="flex gap-12 items-center". */
.flex{display:flex} .fwrap{flex-wrap:wrap}
.gap-4{gap:4px} .gap-6{gap:6px} .gap-8{gap:8px} .gap-10{gap:10px} .gap-12{gap:12px} .gap-14{gap:14px} .gap-18{gap:18px}
.items-center{align-items:center} .items-start{align-items:flex-start}
.justify-end{justify-content:flex-end} .justify-between{justify-content:space-between}
.grow-full{flex:1 1 100%} .grow-200{flex:1 1 200px} .flex-2{flex:2}
.minw-100{min-width:100px} .minw-140{min-width:140px} .minw-170{min-width:170px} .minw-180{min-width:180px} .minw-210{min-width:210px} .minw-220{min-width:220px} .minw-320{min-width:320px}
/* Fixed-width form fields: the `input.` companion reaches specificity (0,1,1) so it beats the base control
   rule `input[type=number]{width:100%}` (0,1,1) by source order — a plain .w-NN (0,1,0) would LOSE to it.
   The plain selector still serves non-input uses. */
.w-64,input.w-64{width:64px} .w-66,input.w-66{width:66px} .w-84,input.w-84{width:84px}
.w-88,input.w-88{width:88px} .w-90,input.w-90{width:90px} .w-120,input.w-120{width:120px}
.tar{text-align:right} .mw-300{max-width:300px}
.ml-1{margin-left:8px} .valign-mid{vertical-align:middle} .no-marker{list-style:none} .pointer{cursor:pointer}
.pb-9{padding-bottom:9px} .fs-14{font-size:14px} .fw-500{font-weight:500}
.pt-3{padding-top:var(--sp-3)}   /* was used in recipe_detail.html but never defined -> divider had no padding */
/* recipe_detail.html semantic components (audit 9.1) — named things, not one-off utilities. */
.hint.alert{background:var(--al-bg);border-color:transparent}
.hint.alert-fg{background:var(--al-bg);border-color:transparent;color:var(--al-fg)}
.chip.fill{background:var(--fill);font-weight:400}                        /* line-note / source chip */
.badge.prep{background:var(--fill);color:var(--text2)}                    /* sub-recipe "prep" status badge */
.card-foot{padding:14px 16px;border-top:.5px solid var(--sep)}           /* add-ingredient footer inside a card */
.hair-top{border-top:.5px solid var(--hair);padding-top:12px}            /* hairline separator above a block (compose with .mt-*) */
.qr-swatch{background:#fff;border-radius:12px;padding:10px;width:140px;height:140px}  /* QR stays on white so it scans, even in dark mode */
.grid-fill-250{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:8px}  /* allergen checkbox grid */
.hist-row{font-size:12.5px;padding:3px 0;display:flex;gap:8px;align-items:center}  /* allergen change-history row */
.stepnum{flex:0 0 26px;height:26px;border-radius:50%;background:var(--accent-tint);color:var(--accent);display:grid;place-items:center;font-weight:700;font-size:13px}
.step-block{padding:10px 0} .step-sep{border-top:.5px solid var(--hair)} /* method step wrapper (+ hairline between steps) */
.step-meta{margin-top:4px} .step-edit{margin:6px 0 0 38px}               /* chips under a step; edit disclosure indented past the number */
.step-instr{white-space:pre-wrap;line-height:1.5}                        /* preserve the chef's line breaks in the instruction */
/* recipe method step editor — a large, expandable instruction box (chef feedback) */
.step-textarea{width:100%;min-height:66px;resize:vertical;padding:8px 10px;border-radius:10px;
  border:.5px solid var(--sep);background:var(--bg-elev);color:var(--text);font-family:inherit;font-size:var(--fs-footnote);line-height:1.5}
/* recipe Ingredients: the costing detail as tinted columns in the table, toggled by "Hide costing" */
.ingtbl .cost-col{background:var(--cost-tint)}
#ingtable.costing-hidden .cost-col{display:none}
/* ingredients_list.html (market list) semantic components (audit 9.1). */
.cat-manage{margin-top:12px;background:var(--fill);max-width:460px}      /* nested "Manage categories" panel (toggled via [hidden]) */
.chip.warn{background:var(--warn-bg);color:var(--warn-fg)}               /* "Unverified yields ✕" active-filter chip */
/* org_settings.html semantic components (audit 9.1). */
.logo-preview{max-height:64px;max-width:200px;border-radius:10px;background:#fff;padding:6px}  /* logo on white so it reads in dark mode */
.check-note{margin:6px 0 0 26px}                                        /* helper note indented under a checkbox+label */
.sech.compact{margin:2px 4px 8px}                                       /* tight section header inside a nested subrow (ingredient price history) */
/* waste.html — moved out of the page's inline <style> block so style-src can drop 'unsafe-inline' (audit 9.1). */
.spread-row{display:flex;justify-content:space-between;gap:10px;margin:7px 0;font-size:14px}  /* label -> value row (Top items) */
.wchips{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0}                /* reason chips = radio pills, one-tap capture at the bin */
.wchip{position:relative;cursor:pointer}
.wchip input{position:absolute;opacity:0;width:0;height:0}
.wchip span{display:inline-block;padding:7px 12px;border:.5px solid var(--sep);border-radius:999px;
  background:var(--bg-elev);font-size:13px;white-space:nowrap;transition:background .12s,border-color .12s}
.wchip input:checked+span{background:var(--accent-tint);border-color:var(--accent);color:var(--accent)}
.wchip input:focus-visible+span{outline:2px solid var(--focus);outline-offset:2px}
.wbars{display:flex;align-items:flex-end;gap:4px;height:72px;margin:6px 0 2px}   /* 14-day trend + by-reason bars */
.wbar{flex:1;min-width:5px;min-height:2px;background:var(--accent);border-radius:3px 3px 0 0;opacity:.85}
.wbar.z{background:var(--sep);opacity:.5}
.wreason{display:grid;grid-template-columns:150px 1fr auto;align-items:center;gap:10px;margin:7px 0;font-size:14px}
.wreason .track{background:var(--fill);border-radius:6px;height:10px;overflow:hidden}
.wreason .fill{background:var(--accent);height:100%;border-radius:6px}
.wperiod a{padding:5px 12px;border-radius:999px;font-size:13px;text-decoration:none;color:var(--text2)}
.wperiod a.on{background:var(--accent-tint);color:var(--accent);font-weight:600}

/* order_planner.html — the unified "Today's shop" (cross-supplier order planner). CSP-clean: bar widths
   come from data-width-pct via app.js CSSOM, no inline style; switch is a native <select data-switch-supplier>. */
.plan-ph-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.plan-datebar{display:flex;gap:10px;align-items:center;margin:2px 0 14px}
.plan-date{font-weight:600}
.plan-note{font-size:var(--fs-caption);color:var(--text2);margin-top:2px}
/* top band: grand total(s) + savings pill + a per-supplier rail */
.plan-top{margin-bottom:18px}
.plan-totals{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
.plan-total-chip{background:var(--bg-elev);border:.5px solid var(--sep);border-radius:12px;padding:8px 14px;font-size:var(--fs-body)}
.plan-total-chip b{font-size:var(--fs-subhead)}
.plan-savings{display:inline-flex;align-items:center;gap:6px;background:var(--ok-bg);color:var(--ok-fg);
  font-weight:600;border-radius:12px;padding:8px 14px;text-decoration:none;border:.5px solid transparent}
.plan-savings:hover{border-color:var(--ok-fg)}
.plan-rail{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
.plan-supcard{display:block;background:var(--bg-elev);border:.5px solid var(--sep);
  border-left:3px solid hsl(var(--sup,220 8% 52%));border-radius:var(--r-card);
  box-shadow:var(--shadow);padding:12px 14px;text-decoration:none;color:inherit;transition:border-color .12s}
.plan-supcard:hover{border-color:hsl(var(--sup,220 8% 52%))}
.plan-supcard-h{display:flex;align-items:center;gap:8px}
.plan-supcard-h b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.plan-supcard-h .muted{margin-left:auto;white-space:nowrap}
.plan-supcard-tot{font-size:var(--fs-subhead);font-weight:600;margin:2px 0 6px}
.plan-mov{display:flex;align-items:center;gap:8px;margin-top:4px}
.plan-mov-bar{flex:1;height:8px;background:var(--fill);border-radius:6px;overflow:hidden}
.plan-mov-fill{display:block;height:100%;border-radius:6px;background:var(--accent);width:0}
.plan-mov.under .plan-mov-fill{background:var(--warn-fg)}
.plan-mov-lab{font-size:var(--fs-caption);color:var(--text2);white-space:nowrap}
.plan-mov.met .plan-mov-lab{color:var(--ok-fg)}
.plan-mov.under .plan-mov-lab{color:var(--warn-fg)}
/* body: one COLOR-SPINED, sticky-headed card per supplier — the supplier is the LANE its products live in,
   so mid-scroll there is never doubt which supplier a row belongs to. --sup is set by the .sup-cN palette. */
.plan-section{position:relative;background:var(--bg-elev);border:.5px solid var(--sep);
  border-left:4px solid hsl(var(--sup,220 8% 52%));border-radius:var(--r-card);box-shadow:var(--shadow);
  margin-bottom:22px;overflow:hidden}
.plan-section:target{box-shadow:0 0 0 2px hsl(var(--sup,220 8% 52%)),var(--shadow)}
.plan-section .tablewrap{border:0;box-shadow:none;margin:0}
.plan-sech{position:sticky;top:var(--appbar-h);z-index:5;padding:12px 16px;
  background:linear-gradient(hsl(var(--sup,220 8% 52%)/.12),var(--bg-elev) 92%);
  border-bottom:1px solid hsl(var(--sup,220 8% 52%)/.4)}
.plan-sech-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.plan-avatar{display:inline-grid;place-items:center;width:26px;height:26px;border-radius:50%;flex:0 0 auto;
  background:hsl(var(--sup,220 8% 52%));color:#fff;font-weight:700;font-size:13px;
  text-shadow:0 1px 1.5px rgba(0,0,0,.35)}   /* legible white initial across the lighter muted hues */
.plan-sech-name{margin:0;font-size:var(--fs-title3)}
.plan-sech-tot{font-weight:600;color:var(--text2)}
.plan-send{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.plan-sech-details{display:flex;flex-wrap:wrap;gap:5px 16px;margin:6px 0 0 36px;font-size:var(--fs-caption);color:var(--text2)}
.plan-sech-details a{color:var(--text2)}
.plan-sech-min{color:var(--warn-fg);font-weight:600}
/* 8 stable per-supplier accent hues (H S% L%). MUTED, harmonious set (chef feedback 2026-07-14: the old
   full-saturation rainbow "looked like kindergarten") — low saturation (34-52%) + even lightness so the
   spine/avatar/chip read as a refined label system, still distinct from semantic green/orange/red. */
.sup-c0{--sup:210 52% 56%} .sup-c1{--sup:172 40% 46%} .sup-c2{--sup:244 44% 64%} .sup-c3{--sup:284 34% 62%}
.sup-c4{--sup:340 46% 62%} .sup-c5{--sup:198 48% 54%} .sup-c6{--sup:32 44% 54%} .sup-c7{--sup:220 13% 56%}
.plan-switch{display:inline-block}
.plan-switch select{max-width:100%;padding:6px 8px;border-radius:8px;border:.5px solid var(--sep);
  background:var(--bg-elev);color:var(--text);font-family:inherit;font-size:var(--fs-footnote)}
.save-badge{display:inline-block;margin-top:5px;font-size:var(--fs-caption);font-weight:600;
  color:var(--ok-fg);background:var(--ok-bg);border-radius:var(--r-sm);padding:3px 8px}
/* add-a-product row */
.plan-add{padding:14px 16px}
.plan-add .formrow{align-items:flex-end}
.plan-add-prod{flex:1;min-width:220px}
.plan-add-qty{flex:0;width:96px;min-width:96px}
.plan-add-qty input{width:100%}
.plan-add-btn{flex:0}
.plan-add-hint{margin:6px 0 0}
/* NO-JS FLOOR for the add box (chef 2026-07-27: add to the cart the way the market list does). With JS,
   picking a product opens the shared ＋Add-to-order dialog, which owns the supplier, the pack and the
   quantity — so the bare Qty box would be a second, contradictory control and is hidden. Without .cl-js
   the dialog never opens, so the Qty box and the honest old hint come back and the form posts as before.
   Hidden, NOT removed: it is still the field app.js writes the chosen quantity into before submitting. */
.cl-js .plan-add-qty{display:none}
.cl-js .plan-add-nojs{display:none}
html:not(.cl-js) .plan-add-js{display:none}
/* inline-editable line qty — a − [digit] + stepper (dense variant of the Add-to-Order dialog's .ao-step) */
.plan-qty{display:inline-block}
.plan-qty-ctl{display:inline-flex;align-items:center;gap:4px}
.btn.plan-step{padding:0;width:30px;height:30px;min-width:30px;justify-content:center;font-size:17px;
  font-weight:600;background:var(--fill);border-radius:8px}
.btn.plan-step:hover{background:var(--fill-2);filter:none}
.plan-qty-input{width:52px;padding:6px 4px;border-radius:8px;border:.5px solid var(--sep);
  background:var(--bg-elev);color:var(--text);font-family:inherit;font-size:var(--fs-footnote);text-align:center}
.plan-qty-unit{margin-left:4px;font-size:var(--fs-caption);color:var(--text2)}
/* The "n × UOM = total" line: the LOUD, single-line quantity statement the chef reads first (feedback
   2026-07-14: "qty x UOM = total, all big one line"). Tabular figures so the numbers align down the card. */
[data-qty-readout]{font-size:var(--fs-callout);font-weight:600;color:var(--text);letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;margin-top:6px;line-height:1.2}
[data-qty-readout]:empty{display:none}
/* fixed layout + shared col widths so columns line up ACROSS supplier sections (screen only; on a phone
   .respcards turns the table into stacked cards, where table-layout/colgroup no longer apply) */
/* Stock count sheets (chef 2026-07-22): the ingredients + preps tables are separate <table>s, so a shared
   fixed layout + colgroup makes their columns line up, and a left-aligned Counted cell keeps the input at
   a constant x so a wider unit like "portion" no longer shoves it. Screen only; phone card-stacking
   ignores table-layout, same as .plan-tbl. */
.stock-tbl{table-layout:fixed;width:100%}
.stock-tbl col.col-cat{width:150px}
.stock-tbl col.col-count{width:200px}
.stock-tbl col.col-value{width:120px}
.stock-tbl col.col-status{width:120px}
.stock-tbl th.c-count,.stock-tbl td.c-count{text-align:left;white-space:nowrap}

/* Add-product Name typeahead: shows existing same/similar products so a chef doesn't create a duplicate. */
.ing-name-field{position:relative}
.ing-name-matches{position:absolute;z-index:30;left:0;right:0;top:100%;margin-top:4px;background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow);overflow-y:auto;max-height:280px}
.ing-name-matches .inm-head{padding:7px 12px;font-size:12px;color:var(--text3);border-bottom:.5px solid var(--hair)}
.ing-name-matches .inm-warn{padding:9px 12px;font-size:13px;font-weight:600;color:var(--orange);background:var(--warn-bg,rgba(255,149,0,.12))}
.ing-name-matches .inm-row{display:flex;justify-content:space-between;gap:10px;align-items:center;padding:8px 12px;font-size:14px;color:var(--text);text-decoration:none;border-bottom:.5px solid var(--hair)}
.ing-name-matches .inm-row:last-child{border-bottom:0}
.ing-name-matches .inm-row:hover{background:var(--fill-2)}
.ing-name-matches .inm-go{color:var(--accent);font-size:12px;white-space:nowrap}

/* Inline step-instruction suggestions (past steps), a CSSOM-positioned listbox on the step textarea. */
.step-sugg{z-index:60;background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow);max-height:240px;overflow-y:auto}
.step-sugg-row{padding:8px 12px;font-size:14px;color:var(--text);cursor:pointer;border-bottom:.5px solid var(--hair)}
.step-sugg-row:last-child{border-bottom:0}
.step-sugg-row.on,.step-sugg-row:hover{background:var(--fill-2)}
.plan-tbl{table-layout:fixed}
.plan-tbl col.c-prod{width:23%}
.plan-tbl col.c-qty{width:27%}   /* wider: holds the stepper + the "n × UOM = total" line (Pack col folded in) */
.plan-tbl col.c-unit{width:11%}
.plan-tbl col.c-line{width:11%}
.plan-tbl col.c-sup{width:24%}
.plan-tbl col.c-act{width:4%}
.plan-tbl th,.plan-tbl td{overflow-wrap:anywhere}
/* cheaper-elsewhere row highlight — mirrors .row-warn (desktop tinted cells, mobile left border) */
tr.row-save>td{background:var(--ok-bg)}
/* per-supplier "add a product" row at each card's foot (existing product, or create a new one inline) */
.plan-addrow{padding:10px 16px 14px;border-top:.5px solid var(--sep)}
.plan-addrow-main{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.plan-addrow-prod{flex:1;min-width:200px}
/* New-product sub-row (chef feedback 2026-07-14: "fields in one column, 'Pack price' doesn't fit"). Each
   field is a stacked label+control of a fixed, comfortable width so they sit as a tidy inline row and the
   caption lives ABOVE the input (no more clipped placeholder). Wraps gracefully on a narrow card. */
.plan-addrow-new{display:flex;gap:14px;align-items:flex-end;flex-wrap:wrap;margin-top:12px;
  padding-top:12px;border-top:.5px dashed var(--sep)}
.plan-newlab{flex-basis:100%;font-size:var(--fs-caption);font-weight:600;color:var(--text2);margin-bottom:-4px}
.plan-field{display:flex;flex-direction:column;gap:5px;font-size:var(--fs-caption);color:var(--text2)}
.plan-field>span{font-weight:600;letter-spacing:.01em}
.plan-field input,.plan-field select{padding:8px 10px;border-radius:10px;border:.5px solid var(--sep);
  background:var(--bg-elev);color:var(--text);font-family:inherit;font-size:var(--fs-subhead)}
.plan-field.f-size input{width:104px} .plan-field.f-unit select{width:96px} .plan-field.f-price input{width:132px}
/* Settings -> Glossary term-override table */
.gloss-tbl{table-layout:fixed}
.gloss-tbl col,.gloss-tbl th:first-child,.gloss-tbl td:first-child{width:34%}
.gloss-tbl input{width:100%}
/* Whole-basket footer: grand total(s) + Order all, ALWAYS in view (chef feedback 2026-07-14: "the total at
   the end should be static, like a footer"). position:fixed — NOT sticky — because the app scroller is the
   window (main.content grows to its content, so sticky:bottom pinned to an off-screen edge and never floated).
   left:248px == the sidebar column width (grid-template-columns:248px 1fr); the phone rule below drops it. */
.plan-foot{position:fixed;left:248px;right:0;bottom:0;z-index:15;display:flex;gap:14px;align-items:center;
  flex-wrap:wrap;justify-content:space-between;background:var(--bar-bg);backdrop-filter:saturate(180%) blur(20px);
  -webkit-backdrop-filter:saturate(180%) blur(20px);border-top:.5px solid var(--sep);
  padding:12px 30px;box-shadow:0 -6px 24px rgba(0,0,0,.28)}
.plan-foot-tot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.plan-foot-pad{height:36px}   /* reserve flow space so the fixed footer never covers the last supplier card */
@media (max-width:760px){
  .plan-foot{left:0;right:0;bottom:calc(64px + env(safe-area-inset-bottom,0px));padding:10px 16px}  /* sit clear above the 60px fixed bottom nav */
  .plan-foot-pad{height:132px}
  .plan-section .respcards tbody tr{border-left:3px solid hsl(var(--sup,220 8% 52%))}  /* a lone mid-scroll card still declares its supplier */
  .plan-sech-details{margin-left:0}
}
/* delivery calendar (cart phase 2): a Week strip + Month grid of draggable supplier chips; drag a chip to a
   day cell or pick a date on it to reschedule that supplier's whole order. List | Week | Month toggle. */
.plan-viewtoggle{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 12px}
.plan-viewtoggle-btns{display:inline-flex;gap:6px}
.plan-viewtoggle .btn.on{background:var(--accent-tint);color:var(--accent)}
.plan-cal{margin:0 0 18px}
.cal-week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px}   /* minmax(0,..) so day cells shrink & chips ellipsize instead of overflowing the row */
.cal-day{min-height:96px;background:var(--bg-elev);border:.5px solid var(--sep);border-radius:12px;padding:8px;display:flex;flex-direction:column;gap:6px}
.cal-day-h{font-size:var(--fs-caption);color:var(--text2)} .cal-day-h b{color:var(--text);font-size:var(--fs-subhead);margin-left:3px}
.cal-day.today,.cal-cell.today{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.cal-day.past,.cal-cell.past{opacity:.5}
.cal-month-label{font-weight:600;margin:0 0 8px}
.cal-month{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}
.cal-mh{font-size:var(--fs-caption2);color:var(--text3);text-transform:uppercase;letter-spacing:.5px;text-align:center;padding-bottom:2px}
.cal-cell{min-height:74px;background:var(--bg-elev);border:.5px solid var(--sep);border-radius:10px;padding:5px;display:flex;flex-direction:column;gap:4px}
.cal-cell-h{font-size:var(--fs-caption);color:var(--text2);text-align:right}
.cal-cell.dim{background:transparent;opacity:.5}
.cal-chip{background:hsl(var(--sup,220 8% 52%)/.14);border-left:3px solid hsl(var(--sup,220 8% 52%));border-radius:var(--r-sm);padding:4px 6px;font-size:var(--fs-caption);cursor:grab}
.cal-chip:active{cursor:grabbing}
.cal-chip-name{display:flex;align-items:center;gap:5px;font-weight:600;color:var(--text);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cal-dot{width:8px;height:8px;border-radius:50%;background:hsl(var(--sup,220 8% 52%));flex:0 0 auto}
.cal-chip-meta{display:block;color:var(--text2);font-size:var(--fs-caption2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
.cal-chip-date{width:100%;margin-top:3px;font-size:var(--fs-caption2);padding:2px 4px;border-radius:6px;border:.5px solid var(--sep);background:var(--bg-elev);color:var(--text);font-family:inherit}
.cal-other{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:.5px solid var(--sep)}
.cal-other .cal-chip{min-width:160px}
/* month cells are small — name-only draggable chips there; the week strip + Other bucket keep the date picker */
.cal-month .cal-chip-meta,.cal-month .cal-chip-date{display:none}
.cal-month .cal-chip{padding:3px 5px}
.cal-drop.drop-hot{outline:2px dashed var(--accent);outline-offset:-3px;background:var(--accent-tint)}
@media (max-width:600px){
  .cal-week{grid-template-columns:1fr} .cal-day{min-height:auto}
  .cal-month{gap:3px} .cal-cell{min-height:56px}
}

/* ============================================================================================
   CART REDESIGN 2026-07-16 — compact per-supplier cards: the product sits next to its price, no
   repeated column headers, no redundant supplier column (a "switch" chip shows only when a cheaper
   supplier exists). New class names; the old .plan-section/.plan-tbl rules above are now unused.
   ============================================================================================ */
.plan-cards{display:flex;flex-direction:column;gap:16px}
.plan-card{position:relative;background:var(--bg-elev);border:.5px solid var(--sep);
  border-left:4px solid hsl(var(--sup,220 8% 52%));border-radius:16px;box-shadow:var(--shadow);overflow:hidden}
.plan-card:target{box-shadow:0 0 0 2px hsl(var(--sup,220 8% 52%)),var(--shadow)}
.plan-head{display:flex;align-items:center;gap:12px;padding:14px 18px 12px}
.plan-cav{display:inline-grid;place-items:center;width:38px;height:38px;border-radius:11px;flex:0 0 auto;
  background:hsl(var(--sup,220 8% 52%));color:#fff;font-weight:800;font-size:16px}
.plan-head-b{flex:1;min-width:0}
.plan-head-nm{font-size:var(--fs-title3);font-weight:800;letter-spacing:-.01em;line-height:1.15}
.plan-head-meta{display:flex;flex-wrap:wrap;gap:3px 14px;margin-top:3px;font-size:var(--fs-caption);color:var(--text2)}
.plan-head-meta a{color:inherit;text-decoration:none}
.plan-head-meta .dow{color:var(--accent);font-weight:600}
.plan-head-meta .min{color:var(--warn-fg);font-weight:600}
.plan-head-r{display:flex;flex-direction:column;align-items:flex-end;gap:8px;flex:0 0 auto}
.plan-head-tot{font-size:var(--fs-title3);font-weight:800;letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap}
.plan-mov-row{padding:0 18px 12px;margin:0}
.plan-lines2{border-top:.5px solid var(--sep)}
.plan-line{display:grid;grid-template-columns:1fr auto auto auto;align-items:center;gap:14px;
  padding:11px 18px;border-bottom:.5px solid var(--sep)}
.plan-line:last-child{border-bottom:0}
.plan-line.row-save{background:var(--ok-bg)}
.plan-line-l{min-width:0}
.plan-line-nm{font-size:var(--fs-body);font-weight:600}
.plan-line-sub{font-size:var(--fs-caption);color:var(--text2);margin-top:2px}
.plan-line-sub b{color:var(--text);font-weight:600}
.plan-line-tot{font-size:var(--fs-subhead);font-weight:700;text-align:right;min-width:70px;font-variant-numeric:tabular-nums}
.plan-line-x{border:0;background:transparent;color:var(--text3);font-size:15px;cursor:pointer;width:28px;height:28px;border-radius:var(--r-sm);line-height:1;padding:0}
.plan-line-x:hover{background:var(--red);color:#fff}
.plan-line .plan-qty-ctl{background:var(--fill-2);border:.5px solid var(--sep);border-radius:9px;overflow:hidden;gap:0}
.plan-line .plan-qty-ctl .plan-step{width:30px;height:30px;border-radius:0;border:0;background:transparent;padding:0;font-size:16px}
.plan-line .plan-qty-ctl .plan-step:hover{background:var(--fill)}
.plan-line .plan-qty-input{width:42px;height:30px;border:0;background:transparent;text-align:center;padding:0;font-weight:600}
.plan-swchip{display:inline-flex;align-items:center;gap:5px;margin-top:6px;background:var(--ok-bg);border-radius:999px;padding:2px 6px 2px 9px}
.plan-swchip .sw-ic{color:var(--ok-fg);font-size:12px;font-weight:700}
.plan-swchip select{border:0;background:transparent;color:var(--ok-fg);font:inherit;font-size:var(--fs-caption);font-weight:700;cursor:pointer;max-width:230px}
.cal-grip{color:var(--text3);cursor:grab;font-size:11px;letter-spacing:-1px;margin-right:1px;touch-action:none}
.cal-chip{transition:box-shadow .12s,transform .1s}
.cal-chip:hover{box-shadow:var(--shadow);transform:translateY(-1px)}
.cal-chip.dragging{opacity:.4}
@media (max-width:600px){
  .plan-line{grid-template-columns:1fr auto;grid-template-areas:"prod total" "qty del";row-gap:8px}
  .plan-line-l{grid-area:prod}
  .plan-line-tot{grid-area:total;align-self:start}
  .plan-line>form.plan-qty{grid-area:qty}
  .plan-line>form.m-0:not(.plan-qty){grid-area:del;justify-self:end}
  .plan-head{flex-wrap:wrap}
}

/* ============================================================================================
   DISH PRICING & MARGIN panel (2026-07-16) — one row: selling price in -> live food cost % + gross
   margin out, a health bar carrying the target marker, and a one-tap suggested price.
   ============================================================================================ */
.rp{padding:0;overflow:hidden}
/* Cost/portion + selling price (auto) then food cost + gross margin (1fr) — cost in, price in, metrics out. */
.rp-main{display:grid;grid-template-columns:auto auto 1fr 1fr;gap:22px;align-items:center;padding:16px 18px}
.rp-zone{min-width:0}
.rp-lab{font-size:var(--fs-caption);font-weight:600;color:var(--text2);margin-bottom:7px}
.rp-pin{display:inline-flex;align-items:center;gap:2px;background:var(--fill);border:.5px solid var(--sep);border-radius:12px;padding:6px 10px}
.rp-pin .rp-cur{color:var(--text2);font-weight:700;font-size:15px}
.rp-pin input{width:100px;border:0;background:transparent;color:var(--text);font-size:25px;font-weight:800;letter-spacing:-.02em;font-variant-numeric:tabular-nums;padding:0 2px}
.rp-pin input:focus{outline:none}
.rp-pin input::-webkit-outer-spin-button,.rp-pin input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.rp-status{display:inline-block;margin-left:8px;font-size:12px;vertical-align:middle}
.rp-out .rp-num{font-size:26px;font-weight:800;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums;color:var(--text)}
.rp-out[data-fc-box] .rp-num{color:var(--ok-fg)}
.rp-out[data-fc-box].over .rp-num{color:var(--al-fg)}
.rp-out .rp-cap{font-size:var(--fs-caption2);color:var(--text3);margin-top:6px}
.rp-bar-wrap{padding:2px 18px 14px}
.rp-bar-top{display:flex;justify-content:space-between;font-size:var(--fs-caption2);color:var(--text3);margin-bottom:6px}
.rp-bar{position:relative;height:9px;border-radius:6px;background:var(--fill)}
.rp-bar-fill{position:absolute;left:0;top:0;bottom:0;border-radius:6px;background:var(--green);transition:width .18s,background .18s;max-width:100%}
.rp-bar-fill.over{background:var(--red)}
.rp-bar-mark{position:absolute;top:-4px;bottom:-4px;width:2px;background:var(--text2);border-radius:2px}
.rp-sug{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:11px 18px;border-top:.5px solid var(--sep);background:var(--accent-tint);font-size:var(--fs-body);color:var(--text2)}
.rp-sug b{color:var(--text)}
.rp-use{margin-left:auto;white-space:nowrap}
@media (max-width:860px){ .rp-main{grid-template-columns:1fr 1fr} }
@media (max-width:620px){ .rp-main{grid-template-columns:1fr;gap:16px} .rp-use{margin-left:0} }
/* "to taste" — a no-quantity ingredient line's qty cell (chef 2026-07-17: show the label, not a bare "—").
   RED (chef 2026-07-17b: "so users see visually there's still work to do") — reads as a to-do, not a value. */
.totaste{display:inline-block;font-size:11.5px;font-weight:700;font-style:italic;color:var(--al-fg);background:var(--al-bg);border:.5px solid transparent;border-radius:999px;padding:2px 10px;white-space:nowrap;letter-spacing:.01em}

/* po_detail order-sheet header + supplier-details grid (moved out of the template's inline <style>) */
.posheet-head{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:6px}
.posheet-head .cell b{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--text2)}

/* ============================================================================================
   GLOBAL PRINT STYLESHEET (chef feedback: "print looks like it's trying to print the webpage").
   Turns ANY page into a clean black-on-white document: hides the app shell + every interactive
   control, flattens panels, and renders tables as plain ledger tables. Page-specific rules
   (the order sheet, the cart) layer on top. One place, applied everywhere.
   ============================================================================================ */
@media print{
  @page{margin:16mm}
  html,body{background:#fff!important;color:#000!important;font-size:11pt;-webkit-print-color-adjust:exact;print-color-adjust:exact}
  *{box-shadow:none!important;text-shadow:none!important}
  /* hide the whole app shell + anything interactive or explicitly non-print */
  nav.side,nav.botnav,.botmore,.toasts,.skip,.cbx-panel,.pager,
  .no-print,[data-print],button,.btn,.plan-add,.plan-top,.plan-datebar,.plan-switch,.plan-send,
  .save-badge,.plan-qty-unit,form.formrow{display:none!important}
  main.content,.content{padding:0!important;margin:0!important;max-width:none!important;width:auto!important;display:block!important}
  a{color:#000!important;text-decoration:none}
  h1{font-size:19pt;margin:0 0 2mm} h2{font-size:13pt;margin:5mm 0 2mm} .sub{color:#333!important}
  /* an editable qty cell prints as its value, not an input box */
  .plan-qty-input{border:0!important;padding:0!important;background:transparent!important;
    color:#000!important;text-align:left!important;width:auto!important;font-size:10pt!important;-webkit-appearance:none;appearance:none}
  /* panels + section wrappers become plain document blocks */
  .panel,.plan-section,.tablewrap{background:#fff!important;border:0!important;border-radius:0!important;
    box-shadow:none!important;padding:0!important;margin:0 0 6mm!important;overflow:visible!important}
  /* tables print as clean ledger tables (screen uses .respcards card-stacking only under the phone media query) */
  table{width:100%!important;border-collapse:collapse!important}
  thead th{border-bottom:1pt solid #000!important;padding:2mm!important;text-align:left!important;
    font-size:8pt!important;text-transform:uppercase;letter-spacing:.4pt;color:#000!important;background:#fff!important}
  tbody td{border-bottom:.4pt solid #999!important;padding:1.6mm 2mm!important;font-size:9.5pt!important;
    color:#000!important;background:#fff!important}
  tr{break-inside:avoid}
  /* cart: one clean order sheet per supplier */
  .plan-section{break-inside:avoid}
  .plan-section+.plan-section{break-before:page}
  .plan-sech-details{color:#000!important;margin-bottom:2mm}
  .plan-sech-tot{color:#000!important}
}

/* ---- Supplier edit drawer + delivery days + serving areas (chef 2026-07-19, suppliers UX redesign).
   The drawer is .ao-dialog pinned right-edge full-height on desktop; on phone it inherits .ao-dialog's
   bottom-sheet rule. .area-seg is deliberately NOT .seg (that's the canonical segmented control). ---- */
.edit-drawer{max-width:460px;width:min(460px,100%);height:100%;max-height:100%;margin:0 0 0 auto;
  border-radius:16px 0 0 16px}
.edit-drawer[open]{display:flex;flex-direction:column}   /* only when open; else UA dialog:not([open]) hides it */
.edit-drawer form{display:flex;flex-direction:column;height:100%;min-height:0}
.se-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:.5px solid var(--sep)}
.se-head .ao-name{margin:0}
.se-x{font-size:18px;line-height:1;padding:4px 10px}
.se-body{flex:1;min-height:0;overflow-y:auto;padding:18px 20px;-webkit-overflow-scrolling:touch}
.se-foot{display:flex;gap:10px;justify-content:flex-end;padding:16px 20px;border-top:.5px solid var(--sep)}
.se-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.se-f{display:flex;flex-direction:column;gap:4px;min-width:0}
.se-f>span,fieldset.se-f>legend{font-size:var(--fs-footnote);font-weight:600;color:var(--text2)}
.se-f.full{grid-column:1 / -1}
.se-f input,.se-f textarea{width:100%}
.se-inline{display:flex;gap:6px}
.se-inline input:first-child{flex:1}
.se-inline input:last-child{width:80px;flex:0 0 auto}
fieldset.se-f{border:1px solid var(--sep);border-radius:12px;padding:12px;margin:0}
fieldset.se-f legend{padding:0 6px}
.se-note{margin-top:8px}
.day-presets{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.btn.xs{padding:3px 9px;font-size:var(--fs-caption)}
.day-chips{display:flex;flex-wrap:wrap;gap:6px}
.day-chip{position:relative;display:inline-flex;align-items:center;gap:4px;min-height:40px;padding:0 12px;
  border:1.5px solid var(--sep);border-radius:10px;cursor:pointer;user-select:none;font-size:var(--fs-footnote);
  font-weight:600;color:var(--text2);background:var(--bg-elev)}
.day-chip input{position:absolute;opacity:0;width:0;height:0}
.day-chip span{opacity:0;width:0;overflow:hidden}                 /* the ✓ glyph, revealed when checked */
.day-chip:has(input:checked){border-color:var(--accent);background:var(--accent-tint);color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.day-chip:has(input:checked) span{opacity:1;width:auto}
.day-chip:has(input:focus-visible){outline:2px solid var(--focus);outline-offset:2px}
.area-seg{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
.area-opt{flex:1;min-width:150px;display:flex;align-items:center;gap:6px;padding:8px 12px;border:1.5px solid var(--sep);
  border-radius:10px;cursor:pointer;font-size:var(--fs-footnote);background:var(--bg-elev)}
.area-opt input{accent-color:var(--accent)}
.area-opt:has(input:checked){border-color:var(--accent);background:var(--accent-tint)}
.tagwrap{display:flex;flex-direction:column;gap:8px}
.tags{display:flex;flex-wrap:wrap;gap:6px}
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 6px 4px 10px;border-radius:999px;background:var(--fill-2);font-size:var(--fs-footnote);color:var(--text)}
.tag-x{border:0;background:transparent;color:var(--text2);cursor:pointer;font-size:12px;line-height:1;padding:2px 5px;border-radius:50%}
.tag-x:hover{background:var(--sep);color:var(--text)}
.dow-strip{display:inline-flex;gap:3px}
.dow-cell{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5px;font-size:10px;font-weight:700;font-style:normal;color:var(--text2);background:var(--fill-2)}
.dow-cell.on{color:#fff;background:var(--accent)}
@media(max-width:560px){
  .edit-drawer{max-width:100%;width:100%;height:90dvh;border-radius:16px 16px 0 0}
  .se-grid{grid-template-columns:1fr}
}
@media(pointer:coarse){.tag-x{min-width:30px;min-height:30px}}

/* ---- Chef Space (chef 2026-07-19) — cross-Site workspace hub. Tabs + unified search + site badges ---- */
.cs-tabs{display:flex;gap:2px;border-bottom:.5px solid var(--sep);margin:18px 0 20px;overflow-x:auto}
.cs-tab{padding:9px 16px;font-weight:600;color:var(--text2);border-bottom:2px solid transparent;white-space:nowrap;text-decoration:none}
.cs-tab:hover{color:var(--text)}
.cs-tab.on{color:var(--accent);border-bottom-color:var(--accent)}
.cs-search{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:18px}
.cs-search .sinput{flex:1;min-width:180px}
.cs-search select.sinput{flex:0 0 auto;min-width:150px}
.site-badge{display:inline-block;font-size:var(--fs-caption2);font-weight:600;border-radius:var(--r-sm);padding:3px 9px;background:var(--fill);color:var(--text2);white-space:nowrap}
.site-badge.shared{background:var(--ok-bg);color:var(--ok-fg)}
/* Kept-but-removed dish in Chef Space: soft-deleted from the restaurant but retained in the Cook book. */
.site-badge.removed{background:var(--warn-bg);color:var(--warn-fg)}
.pager{display:flex;gap:12px;align-items:center;justify-content:center;margin:16px 0;flex-wrap:wrap}
/* Top-bar Chef Space workspace switch — sits beside the site switcher (a workspace, not a site scope). */
.cs-switch{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;padding:5px 12px;border-radius:999px;border:1px solid var(--sep);color:var(--text2);background:var(--bg-elev);white-space:nowrap;text-decoration:none;line-height:1.4}
.cs-switch:hover{color:var(--text);border-color:var(--accent)}
.cs-switch.on{color:#fff;background:var(--accent);border-color:var(--accent)}

/* ---- Recipe line INLINE cell edit (chef 2026-07-21: "I don't like the current edit, I would like the
   inline edit"). Replaces the <details class="rowedit"> whose revealed form widened the cell and shoved
   the table apart — the collision in the chef's screenshot. Read state is a .cellval button; .row-editing
   on the <tr> swaps in the real inputs IN PLACE. The Notes and Unit COLUMNS are gone: the note is a
   sub-line under the name, the unit a suffix in the qty cell. Without .cl-js on <html> the same partial
   shows inputs + Save + Delete permanently, so there is one source of truth. ---- */
.cellval{background:none;border:0;font:inherit;color:inherit;text-align:inherit;cursor:text;
  display:inline-flex;align-items:center;gap:var(--sp-1);padding:var(--sp-1) var(--sp-2);
  margin:calc(var(--sp-1) * -1) calc(var(--sp-2) * -1);border-radius:var(--r-sm);max-width:100%;
  transition:background var(--dur-fast) var(--ease)}
.cellval:hover{background:var(--accent-tint)}
.num .cellval{justify-content:flex-end}
.cellval .qv{font-variant-numeric:tabular-nums;font-weight:600}
/* The pencil is ALWAYS faintly visible, like the market list's .price .pen, so the cell signals it is
   editable at rest rather than only revealing on hover (chef 2026-07-21: "make the same"). It sits
   BEFORE the value in the markup. */
.cellval .pen{opacity:.4;flex:0 0 auto;font-size:var(--fs-caption);
  transition:opacity var(--dur-fast) var(--ease)}
.cellval:hover .pen,.cellval:focus-visible .pen{opacity:.75}
.lnu{color:var(--text2);font-size:var(--fs-caption)}      /* unit suffix — replaces the Unit COLUMN */
.lnname{font-weight:500}
/* note as a muted sub-line under the name — replaces the Notes COLUMN and its per-row em-dash */
.lnnote-btn{display:flex;font-size:var(--fs-caption);color:var(--text2);margin-top:2px}
.lnnote-btn.empty{opacity:0}
/* Barely-there (chef 2026-07-26: "note to be barely visible for the best user experience"). The
   empty "+ note" is an affordance, not content: it should whisper on hover and only come fully
   forward on keyboard focus, so a column of note-less rows stays visually quiet. */
tr:hover .lnnote-btn.empty{opacity:.28}
tr:focus-within .lnnote-btn.empty,.lnnote-btn.empty:focus-visible{opacity:.75}
/* Read and edit states share a min-width so revealing the input never jumps the column. */
.qty-cell{min-width:104px}
.lnedit{display:inline-flex;align-items:center;gap:var(--sp-1)}
.lnqin{width:88px;text-align:right;margin:0;font-variant-numeric:tabular-nums;font-size:var(--fs-callout)}
.lnnin{width:100%;min-width:140px;margin:0;font-size:var(--fs-callout)}   /* 16px: no iOS zoom-on-focus */
.rowact{white-space:nowrap}
.rowact form{display:inline-flex;gap:var(--sp-1);margin:0}
/* DRAG TO REORDER (chef 2026-07-28: "I want the rows to be reorderable simply by just dragging").
   The drag starts from this GRIP, never from the <tr>: the row carries the qty and note inputs, and a
   draggable row breaks text selection inside them — the exact reason the event-menu drag uses a grip
   too. touch-action:none lets the pointer fallback drag on a tablet without the page scrolling away
   underneath it. The ▲▼ buttons in the actions cell are NOT a lesser fallback: they are the keyboard
   and scripting-off path, and nothing here is reachable only by dragging. */
.ln-grip{cursor:grab;color:var(--text3);font-size:var(--fs-footnote);letter-spacing:-1px;
  touch-action:none;padding-right:4px;user-select:none;-webkit-user-select:none}
.ln-grip:active{cursor:grabbing}
/* OPEN THE PREP → (chef 2026-07-28: "we'll need the arrow to visit the Preparation from this table").
   A quiet inline link, not a button: it lives in the NAME cell and a third button shape there would
   compete with the row's own ▲▼ / 🗑 controls. The padding IS the tap target — a bare "→" glyph is ~10px
   wide, which a gloved thumb on the pass cannot hit — and it stays within the line box, so row height is
   unchanged. Always visible, never hover-only: this table is worked on a tablet, where hover does not
   exist. It renders at the FAR END of the name line, away from the ⠿ grip, so a mis-grab mid-drag can't
   navigate away and lose the order being dragged. */
.ln-open{display:inline-block;margin-left:2px;padding:2px 8px;border-radius:var(--r-sm);
  color:var(--text3);text-decoration:none;font-weight:600;line-height:1.4}
.ln-open:hover{color:var(--accent);background:var(--fill)}
.ln-open:focus-visible{outline:2px solid var(--accent);outline-offset:1px;color:var(--accent)}
@media print{.ln-open{display:none!important}}
tr.ln-dragging>td{opacity:.4}
tbody.ln-over{outline:2px dashed var(--accent);outline-offset:-2px}
@media print{.ln-grip{display:none!important}}
/* COLUMN SORTING, AS A VIEW ONLY (chef 2026-07-28: "can we add the column sorting to the table").
   The visual idiom is the recipes-list header (.rsort + .on + a ▲/▼ suffix) rather than a second
   invented one; .ingsort only adds the button reset .rsort never needed as an <a>. */
.ingsort{background:none;border:0;padding:0;margin:0;font:inherit;cursor:pointer}
.ingsort:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm)}
/* Scripting off, the header ships `disabled` (there is nothing behind a client-side sort without JS),
   so it has to read as the plain column label it replaced rather than as a broken control. */
.ingsort[disabled]{cursor:default;color:inherit}
.sortcue{font-size:var(--fs-caption)}
/* WHILE SORTED, REORDERING IS OFF. A drag inside a sorted view would save an order the chef never
   chose, and this order is what the spec sheet, service mode and the QR card print. pointer-events
   kills the drag at its source — no dragstart can fire from a grip nothing can grab — the ▲▼ are
   disabled from app.js, and persist() refuses on top of both. */
.ingtbl.ing-sorted .ln-grip{opacity:.25;cursor:not-allowed;pointer-events:none}
.lnmove .btn[disabled]{opacity:.3;cursor:default}
/* The strip that says so on screen, and carries the way back to manual order. The global
   `[hidden]{display:none!important}` normalize rule already beats this display, so app.js toggles the
   attribute and never writes an inline style. */
.ing-sortnote{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;margin:0 0 var(--sp-2);
  padding:var(--sp-2) var(--sp-3);border-radius:var(--r-card);background:var(--accent-tint);
  border:.5px solid var(--sep);font-size:var(--fs-caption);color:var(--text2)}
.ing-sortnote > span{flex:1 1 260px}
/* JS on: the read value leads; the inputs are summoned by .row-editing. JS off: the inputs ARE the UI.
   .rowdel is NOT in this set (chef 2026-07-28: "the remove button is not visible. it's visible only on
   ingredient edit. Please show it always"). Removing a line was reachable only by first ARMING the row —
   i.e. you had to look like you were editing a quantity to be offered a delete, which nothing on screen
   said. It inherits .rowact form's inline-flex, and the coarse-pointer rule below still gives it 44px. */
.cl-js .celled,.cl-js .lnedit,.cl-js .lnsave{display:none}
.cl-js tr.row-editing .cellval{display:none}
.cl-js tr.row-editing .celled{display:inline-block}
.cl-js tr.row-editing .lnedit{display:inline-flex}
tr.row-editing>td{background:var(--fill-2)}
tr.row-editing>td:first-child{box-shadow:inset 3px 0 0 var(--accent)}   /* not colour alone */
/* Saved-row tint so the chef sees WHICH line changed. Kept as a keyframe animation so the global
   prefers-reduced-motion block already stills it — never reimplement as a JS setTimeout style change,
   which would escape that block. */
@keyframes rowflash{from{background:var(--accent-tint)}to{background:transparent}}
tr.row-flash>td{animation:rowflash var(--dur-slow) var(--ease)}
@media (pointer:coarse){
  .cellval{min-height:44px;min-width:44px}
  .cellval .pen{opacity:.5}          /* touch has no hover — the affordance stands permanently */
  .lnnote-btn.empty{opacity:.3}   /* touch has no hover, so it stands — but only just */
  .lnqin,.lnnin{min-height:44px}
  .rowact .btn.sm{min-height:44px;min-width:44px;justify-content:center}
}
@media (max-width:760px){
  .qty-cell{min-width:0}
  .respcards .cellval,.respcards .celled{width:100%;justify-content:flex-start}
  .respcards .rowact{display:flex;gap:var(--sp-2);flex-wrap:wrap}
}
/* Print: the global sheet hides button/.btn/form.formrow but knows nothing about bare inputs in cells,
   so an edited recipe would print input boxes instead of a document. */
@media print{
  .celled{border:0!important;padding:0!important;background:transparent!important;color:#000!important;
    text-align:left!important;width:auto!important;font-size:10pt!important;-webkit-appearance:none;appearance:none}
  .cellval .pen{display:none!important}
}

/* ---- Method steps: INLINE edit (chef 2026-07-21: "Steps/Method have unnecessary buttons. Lets
   implement the inline edit"). The step IS the field — the instruction is a textarea that autosaves and
   the section an inline chip input. What this DELETES: the ✎ disclosure, the "Save step" button, and the
   standing ▲ / ▼ / ✕ Delete wall on every step. The reorder + delete controls now appear only on hover
   or keyboard focus, so at rest a 12-step method shows zero buttons instead of 48. ---- */
.step-row{display:flex;gap:var(--sp-3);align-items:flex-start;position:relative}
.step-body{flex:1;min-width:0;display:block}
.step-block + .step-block{border-top:.5px solid var(--hair)}   /* hairline between steps (was .step-sep) */
/* The instruction reads as text until you touch it: no border, no box, just the words. */
.step-instr.celled-step{width:100%;display:block;resize:none;overflow:hidden;font:inherit;
  line-height:1.5;color:var(--text);background:transparent;border:.5px solid transparent;
  border-radius:var(--r-sm);padding:var(--sp-1) var(--sp-2);margin:calc(var(--sp-1) * -1) calc(var(--sp-2) * -1);
  transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
.step-instr.celled-step:hover{border-color:var(--sep)}
.step-instr.celled-step:focus{background:var(--bg-elev);border-color:var(--accent);outline:none}
.step-chips{display:flex;flex-wrap:wrap;gap:var(--sp-1);align-items:center;margin-top:var(--sp-1)}
/* The section chip is an input that looks like a chip until focused. */
.step-station{font-size:var(--fs-caption);font-weight:500;border-radius:var(--r-sm);padding:3px 9px;
  width:auto;max-width:150px;background:var(--fill);color:var(--text2);border:.5px solid transparent;margin:0}
.step-station:hover{border-color:var(--sep)}
.step-station:focus{background:var(--bg-elev);border-color:var(--accent);color:var(--text);outline:none}
/* An UNSET station collapses to a small hint instead of parking a big empty box on every step — the
   chef asked for fewer things on screen, and a step with no station should cost nothing. It opens to
   full width on hover or focus, and a station that HAS a value always shows at full size. */
.step-station:placeholder-shown{opacity:.5;width:96px;background:transparent}
.step-block:hover .step-station:placeholder-shown,
.step-block:focus-within .step-station:placeholder-shown{opacity:1;width:auto;background:var(--fill)}
.step-station:placeholder-shown:focus{width:auto;opacity:1}
.chip.ccp{background:var(--al-bg);color:var(--al-fg);font-weight:600}   /* food-safety checkpoint */
/* Reorder + delete: absent at rest, revealed on hover or focus-within. Never display:none — that would
   take them out of the tab order and strand keyboard users. */
.step-acts{display:flex;gap:var(--sp-1);flex:0 0 auto;opacity:0;
  transition:opacity var(--dur-fast) var(--ease)}
.step-block:hover .step-acts,.step-block:focus-within .step-acts{opacity:1}
.step-acts .btn:focus-visible{opacity:1}
.step-del:hover{color:var(--red)}
.step-status{flex:0 0 auto;align-self:center}
.cl-js .step-save{display:none}          /* JS on: autosave owns the commit. JS off: this is the commit. */
@media (pointer:coarse){
  /* No hover on touch: the controls must stand permanently, and be thumb-sized. */
  .step-acts{opacity:.75}
  .step-acts .btn.sm{min-height:44px;min-width:44px;justify-content:center}
  .step-station{min-height:36px}
}
@media print{
  .step-acts,.step-status{display:none!important}
  .step-instr.celled-step,.step-station{border:0!important;background:transparent!important;
    color:#000!important;padding:0!important;margin:0!important;-webkit-appearance:none;appearance:none}
}

/* ---- Nutrition panel (chef 2026-07-21: "Nutrition is too big"). It used to render its EDITOR as the
   default state — 16 (EU) to 30 (US) number inputs, a %RI column, two AI buttons, an ingredient
   disclosure, a paste box and an Apply button, all expanded, always. Chefs READ nutrition far more often
   than they edit it, so the default is a four-figure strip and the rest folds away. These rules moved
   here from an inline <style> in recipe_detail.html so the panel is styled from one place. ---- */
.nut-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(84px,1fr));gap:var(--sp-2)}
.nut-fig{text-align:center;background:var(--fill-2);border-radius:var(--r-sm);padding:var(--sp-2)}
.nut-val{font-size:var(--fs-title3);font-weight:600;line-height:1.1;font-variant-numeric:tabular-nums}
.nut-key{font-size:var(--fs-caption);color:var(--text2);margin-top:2px}
.nut-key .nut-u{opacity:.75}
/* The disclosure reads as a quiet control, not a heading. */
.nut-edit > summary{cursor:pointer;font-size:var(--fs-caption);font-weight:600;color:var(--text2);
  display:inline-flex;align-items:center;gap:var(--sp-1);padding:var(--sp-1) var(--sp-2);
  border-radius:var(--r-sm);list-style:none}
.nut-edit > summary::-webkit-details-marker{display:none}
.nut-edit > summary:hover{background:var(--accent-tint);color:var(--accent)}
.nut-tbl{width:100%;border-collapse:collapse;font-size:var(--fs-footnote)}
.nut-tbl th,.nut-tbl td{padding:3px 8px;border-bottom:1px solid var(--sep);text-align:left;vertical-align:middle}
.nut-tbl th:not(:first-child),.nut-tbl td:not(:first-child){text-align:right;white-space:nowrap}
.nut-tbl thead th{font-size:var(--fs-caption);text-transform:uppercase;letter-spacing:.3px;color:var(--text2);border-bottom:2px solid var(--sep)}
.nut-in{width:70px;text-align:right;padding:2px 6px}
/* Input + its derived kJ stack right-aligned (chef 2026-07-22): the kJ sits BELOW the input so it can't
   push the energy input out of the column, and the whole editor stays compact. */
.nut-cell{display:inline-flex;flex-direction:column;align-items:flex-end;gap:1px}
.nut-kj{font-size:var(--fs-caption2);color:var(--text3);line-height:1;font-variant-numeric:tabular-nums}
.nut-i1 td:first-child{padding-left:22px;color:var(--text2)}
.nut-i2 td:first-child{padding-left:38px;color:var(--text2);font-style:italic}
.nut-ov{margin:6px 0 0;padding-left:18px;columns:2;font-size:var(--fs-caption);color:var(--text2)}
@media(max-width:640px){.nut-ov{columns:1}}
@media (pointer:coarse){.nut-edit > summary{min-height:44px}.nut-in{min-height:36px}}
/* Print: the label is a document, so open the full table and drop the editing chrome. */
@media print{
  .nut-edit > summary{display:none!important}
  .nut-edit > *{display:revert!important}
  .nut-in{border:0!important;padding:0!important;background:transparent!important;color:#000!important;
    -webkit-appearance:none;appearance:none}
}

/* ---- .joined — a stack of .panel children that reads as ONE card, with every panel after the first
   as a tinted footer attached to it. Generalises the .ing-block treatment the ingredients table already
   uses (chef: "the add section looks like a separate table"), applied to Method for the same reason
   (chef 2026-07-21: "The method and adding instructions are 2 sections").

   .ing-block keeps its own rules because it also styles a combobox popup and a segmented toggle; this
   is the plain version for any list + its add-form. Note overflow:hidden — safe here because nothing
   inside pops out of the box (the ingredients combobox is position:fixed for exactly that reason), so
   do NOT put a dropdown in a .joined footer without checking. ---- */
.joined{border:.5px solid var(--sep);border-radius:var(--r-card);box-shadow:var(--shadow);
  background:var(--bg-elev);overflow:hidden;margin-bottom:22px}
.joined > .panel{border:0;border-radius:0;box-shadow:none;margin:0;background:transparent}
.joined > .panel + .panel{background:var(--fill-2);border-top:1px solid var(--sep);padding:14px 16px}
.joined-ftitle{font-size:var(--fs-caption);font-weight:600;color:var(--text2);text-transform:uppercase;
  letter-spacing:.4px;margin:0 0 8px}

/* ---- PHONE LAYOUT for the recipe page (chef 2026-07-21, on an actual handset).
   Two things broke at 375px, both from flex rows that were fine on a laptop:

   1. A method step's action buttons took 119px of a 301px row — 40% — leaving the instruction 120px,
      about 15 characters, so a sentence wrapped to one or two words per line. The number and the
      controls now share the first line and the instruction gets the full width beneath them.
   2. The add-a-line fields sat 2-up at 127px each, which clips a placeholder mid-word; "Search or add
      an ingi" and "blank = to taste" then read as one run-on string. They stack instead.

   600px, not the 760px used by respcards: above ~600 the controls are a small share of the row and
   two-up fields still fit, so stacking earlier would waste space on a tablet. ---- */
@media (max-width:600px){
  .step-row{flex-wrap:wrap}
  .step-row .stepnum{order:1}
  .step-acts{order:2;margin-left:auto;opacity:1}     /* no hover on touch — always visible */
  .step-body{order:3;flex:1 1 100%;margin-top:var(--sp-1)}
  .step-status{order:4;flex:1 1 100%}
  /* Add-forms: one field per line. Scoped to these two panels rather than every .formrow in the app,
     which would restyle forms nobody has complained about. */
  .add-line-panel .formrow > div,
  .joined .formrow > div{flex:1 1 100%;min-width:0}
}

/* ---- EU-14 ALLERGEN PICKER (chef 2026-07-21: "looks bad Allergens UI").
   The old picker was a grid of 14 tap-to-cycle tiles at repeat(auto-fill,minmax(178px,1fr)). On the
   Add-product .formrow it sat in a minw-200 flex item, so the grid could only resolve to ONE column —
   14 tiles stacked ~900px tall — and .formrow's align-items:end then shoved Name/Category/Supplier/
   Pack/price/submit down to the bottom of that column. Hence the dead page in the screenshot.

   Two containers now, ONE set of rows:
     compact  = a collapsed chip + a <dialog>. The dialog lives in the TOP LAYER, so its content can
                never grow the flex item — the reported bug is structurally unable to recur. It must be
                a dialog rather than an absolutely-positioned panel because main.content is
                overflow:auto and clips absolute panels (the same reason every combobox re-anchors with
                position:fixed).
     default  = the 14 rows in flow, for the roomy ingredient detail page.
   ---- */
.algwrap{min-width:0}
/* ALLERGEN BOXES (chef 2026-07-26: "add the allergens in boxes so it would be visually visible").
   One box per recorded allergen: SOLID red = contains, HOLLOW dashed + "~" = may contain — the same
   contains/may vocabulary the allergen matrix uses, so the editor and the inspection sheet teach each
   other. Colour is never load-bearing: each box also carries its glyph, its label text and a title.
   Nothing recorded collapses to a faint ＋, so a table column of mostly-empty rows stays quiet. */
.alg-boxrow{display:flex;flex-wrap:wrap;gap:var(--sp-2)}
/* A PREP LINE'S ALLERGENS, READ-ONLY (chef 2026-07-28: "we should display them, with no ability to
   edit"). Deliberately the SAME .alg-box badges an ingredient row uses — a prep's milk and an
   ingredient's milk are the same fact and must not look like two different ones — with only a ↳ to mark
   it derived. NO hover state, NO pointer, NO focus ring: nothing in here is clickable, and giving it a
   press affordance would promise an editor that cannot exist (a prep's allergens are recomputed from its
   own ingredients). The way to change them is the row's "open the prep →" link. */
.alg-ro{align-items:center}
.alg-ro-ic{color:var(--text3);font-size:var(--fs-caption);flex:0 0 auto}
.alg-boxbtn{display:flex;flex-wrap:wrap;align-items:center;gap:4px;width:100%;background:none;border:0;
  padding:2px 0;cursor:pointer;text-align:left;min-height:30px}
/* CHIPS FIELD — multi-value text edited as removable badges (see _chips_field.html). The box mimics a
   normal input so it sits naturally in a .formrow next to real inputs; the text input inside it is
   borderless and grows to fill whatever the chips leave. */
.chipsel{min-width:0}
.chipsel-box{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:6px 8px;min-height:38px;
  background:var(--fill);border:1px solid var(--sep);border-radius:var(--r-input,10px)}
.chipsel-box:focus-within{border-color:var(--accent);outline:2px solid var(--accent);outline-offset:-1px}
.chipsel-list{display:contents}
.chipsel-chip{display:inline-flex;align-items:center;gap:6px;padding-right:4px}
.chipsel-x{background:none;border:0;padding:0 4px;cursor:pointer;color:inherit;opacity:.6;
  font-size:15px;line-height:1;border-radius:4px}
.chipsel-x:hover{opacity:1;background:var(--fill-2)}
.chipsel-x:focus-visible{outline:2px solid var(--accent);outline-offset:1px;opacity:1}
.chipsel-in{flex:1 1 90px;min-width:90px;border:0;background:transparent;padding:2px 0;color:var(--text);
  font-size:var(--fs-body,14px)}
.chipsel-in:focus{outline:none}

/* A button mid-request: it keeps its own label (swapped to data-busy) and pulses, so a two-second AI
   round-trip visibly reads as "working", not as a dead click. */
.btn.is-busy{opacity:.85;cursor:progress;animation:btnpulse 1.1s ease-in-out infinite}
@keyframes btnpulse{0%,100%{opacity:.85}50%{opacity:.55}}
@media (prefers-reduced-motion:reduce){ .btn.is-busy{animation:none} }

/* SIZE + SHAPE ONLY. Colour, the contains/may distinction and the B&W print rules already live on
   .badge.allergen (see _allergen_badge.html) — duplicating them here is exactly the drift that partial
   was created to stop. .alg-box just makes the same badge read as a chunkier box. */
.alg-box{border-radius:8px;padding:5px 10px;font-size:var(--fs-caption);line-height:1.2}
.alg-box.sm{padding:3px 8px;font-size:11px}
.alg-box.add{background:transparent;color:var(--text3);border:1px dashed var(--sep);font-weight:400;
  opacity:.5;padding:3px 8px;border-radius:8px;font-size:11px}
.alg-boxbtn:hover .alg-box.add{opacity:1;border-color:var(--text3)}
.alg-boxbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:8px}
/* In the ingredients table the boxes must not widen the column. */
.alg-cell{max-width:240px}
@media print{ .alg-box.add{display:none!important} }
.alg-chipwrap{min-width:0}
.alg-chip{display:inline-flex;align-items:center;gap:var(--sp-2);width:100%;min-height:38px;
  padding:var(--sp-2) var(--sp-3);border:.5px solid var(--sep);border-radius:var(--r-md);
  background:var(--bg-elev);color:var(--text);font:inherit;font-size:var(--fs-footnote);
  cursor:pointer;text-align:left}
.alg-chip:hover{border-color:var(--text3)}
.alg-chip-txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text2)}
.alg-chip-act{flex:0 0 auto;font-size:var(--fs-caption);font-weight:600;color:var(--accent)}
/* [open] IS LOAD-BEARING. An unconditional `display:flex` here overrides the UA's
   `dialog:not([open]){display:none}`, so the dialog renders permanently in the page flow, never becomes
   modal, and Done cannot hide it — close() removes the attribute but the display rule keeps it on
   screen. Scope every display rule for a <dialog> to [open]. (chef 2026-07-21: "this window is broken
   it doesnt close".) */
/* 460px was too narrow (chef 2026-07-28: "the window (pop-up spaces are too narrow)"): a 28px glyph +
   the name + the 3-option .alg-seg left ~95px for the label, so "Cereals containing gluten" and
   "Sulphur dioxide / sulphites" wrapped onto three lines each and the 14 rows read as a wall. 680px
   leaves ~340px for the name — one line for every EU-14 label — while the 94vw cap keeps a phone
   unchanged. */
.alg-dialog[open]{max-width:680px;width:min(680px,94vw);
  display:flex;flex-direction:column;max-height:80vh;overflow:hidden;
  border:.5px solid var(--sep);border-radius:18px;padding:0;background:var(--bg-elev);
  box-shadow:var(--shadow-lift)}
/* Header + filter are a STICKY cap; only .alg-rows scrolls (two nested scrollers gave two scrollbars). */
.alg-dialog .ao-head{display:flex;align-items:baseline;gap:var(--sp-2);
  padding:var(--sp-4) var(--sp-4) var(--sp-3);position:sticky;top:0;background:var(--bg-elev);z-index:2}
.alg-dialog .ao-head b{font-size:var(--fs-callout);font-weight:700;letter-spacing:-.01em}
.alg-dialog .alg-filter{margin:0 var(--sp-4) var(--sp-2)}
.alg-dialog .alg-sum{margin:0 var(--sp-4) var(--sp-2)}
.alg-dialog .alg-rows{flex:1;min-height:0;overflow:auto;padding:0 var(--sp-4)}
.alg-dialog .ao-actions{padding:var(--sp-3) var(--sp-4);border-top:.5px solid var(--sep);
  display:flex;gap:var(--sp-2);justify-content:flex-end;background:var(--bg-elev)}
/* NEW-PRODUCT sheet on the recipe page (chef 2026-07-28: the add-ingredient popup must carry the Market
   list's Category / Supplier / Purchase Unit / Purchase Price / Allergens). Same [open]-scoped display
   discipline as .alg-dialog above — read that comment before touching this one. A sticky head and a
   sticky action bar with only the body scrolling, because on a tablet in landscape the sheet is taller
   than the viewport and "Create & add" must never scroll out of reach. */
.np-dialog[open]{max-width:560px;width:min(560px,94vw);
  display:flex;flex-direction:column;max-height:88vh;overflow:hidden;
  border:.5px solid var(--sep);border-radius:18px;padding:0;background:var(--bg-elev);
  box-shadow:var(--shadow-lift)}
.np-dialog .ao-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-2);
  padding:var(--sp-4) var(--sp-4) var(--sp-3);border-bottom:.5px solid var(--sep);background:var(--bg-elev)}
.np-dialog .ao-head b{font-size:var(--fs-callout);font-weight:700;letter-spacing:-.01em}
/* The FORM is the scroller. Deliberately not a sticky action bar: the submit button has to live inside
   the form, so a sticky footer would have to overlay its own scroll container — and a sheet whose only
   commit control can be covered by a soft keyboard is worse than one you scroll. */
.np-body{flex:1;min-height:0;overflow:auto;padding:var(--sp-4);margin:0}
.np-dialog .ao-actions{padding-top:var(--sp-3);border-top:.5px solid var(--sep)}
/* One field per line by default; .np-row pairs two where they read as one decision (size+unit, price+
   readout). It COLLAPSES below 520px rather than shrinking — a 44px tap target beats a tidy grid on a
   line, and this sheet is used with a thumb. */
.np-field{margin-bottom:var(--sp-3);min-width:0}
.np-field>label{display:block;font-size:var(--fs-footnote);font-weight:600;color:var(--text2);
  margin-bottom:6px}
.np-field input[type=text],.np-field input[type=number]{width:100%;margin:0}
.np-row{display:flex;flex-wrap:wrap;gap:var(--sp-3)}
.np-row>.np-field{flex:1 1 220px}
/* The recipe half of the entry, set apart so it is obvious that these two fields describe THIS dish and
   the ones above describe the product everywhere. */
.np-inrecipe{border-top:.5px solid var(--sep);padding-top:var(--sp-3);margin-top:var(--sp-2)}
@media (max-width:520px){ .np-row>.np-field{flex:1 1 100%} }
.alg-filter{width:100%}
.alg-sum{font-size:var(--fs-caption);color:var(--text2);margin:0 0 var(--sp-2)}
.alg-none{font-size:var(--fs-caption);color:var(--text2);margin:var(--sp-2) 0}
/* One allergen. A <fieldset>'s UA min-inline-size is min-content and WILL refuse to shrink — this whole
   ticket exists because a container refused to shrink, so pin it to 0 explicitly. */
/* ONE LINE per allergen: name left, a compact 3-way control right. It was previously a name line PLUS
   a row of three 161px buttons = 88px per allergen, 1232px of scrolling content for 14 — a wall of 42
   big buttons for a field most products never use. */
/* flex-wrap is the pressure valve for the min-width below: where the name floor plus the (deliberately
   uncrushable) segments no longer fit, the segments drop to their own line rather than squeezing the
   name back into a three-line wrap — which is the bug this whole block exists to fix. */
.alg-row{min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);
  padding:7px 0;border-top:.5px solid var(--hair);border-radius:10px}
.alg-row:first-child{border-top:0}
.alg-row.alg-hide{display:none}
/* min-width, not min-width:0: the name is the content, the segments are the control. Without a floor the
   segments (flex:0 0 auto) take what they want and the label wraps; 200px is the widest EU-14 name at
   --fs-footnote plus its 28px glyph coin. */
.alg-leg{flex:1 1 200px;min-width:200px;display:flex;align-items:center;gap:10px;
  font-size:var(--fs-footnote);font-weight:600;color:var(--text)}
/* glyph in a soft coin so the pictograms read as deliberate, not clip-art dropped on a row */
.alg-leg .alg-g{flex:0 0 auto;width:28px;height:28px;border-radius:50%;background:var(--fill-2);
  display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1}
/* Three real radios. NOT .seg — its aria-pressed idiom is boolean and wrong for three states, and its
   selected colour is accent green at 2.22:1. */
/* CONNECTED SEGMENTED CONTROL — the house .seg idiom (trough + raised thumb), so three loose outlined
   buttons per row become one unit. Radios stay (accessibility unchanged); only the paint changes. */
.alg-seg{flex:0 0 auto;display:inline-flex;gap:2px;padding:2px;background:var(--fill);
  border-radius:var(--r-md)}
.alg-r{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.alg-opt{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:28px;
  padding:4px 11px;border:0;border-radius:8px;background:transparent;color:var(--text3);
  font-size:var(--fs-caption2);font-weight:600;cursor:pointer;white-space:nowrap;line-height:1.15;
  transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.alg-opt:hover{color:var(--text)}     /* just the text lifts; the thumb belongs to the selected one */
/* Channel 2 — SHAPE, reusing the inspection sheet's own vocabulary so the two surfaces teach each
   other. currentColor means the mark inherits its label's contrast for free. */
.alg-mk{display:inline-block;flex:0 0 auto}
.alg-mk.c{width:11px;height:11px;border-radius:3px;background:currentColor}      /* filled square */
.alg-mk.m{width:10px;height:10px;border-radius:50%;border:2px solid currentColor}/* hollow ring   */
.alg-mk.n{width:4px;height:4px;border-radius:50%;background:currentColor}        /* dot           */
/* Selected states. Channel 3 = border STYLE: solid for a hard declaration, dashed for traces, so the
   distinction survives greyscale and the B&W print sheet. */
/* The selected segment is a raised thumb (bg + shadow), tinted per state. The SHAPE mark (square /
   ring / dot) and the always-visible WORD keep the state legible without colour, so colour stays the
   third channel, not the only one. */
.alg-r:checked + .alg-opt{font-weight:700;box-shadow:var(--shadow)}
.alg-r:checked + .alg-opt.n{background:var(--bg-elev);color:var(--text)}
.alg-r:checked + .alg-opt.m{background:var(--warn-bg);color:var(--warn-fg)}
.alg-r:checked + .alg-opt.c{background:var(--al-bg);color:var(--al-fg)}
/* --warn-fg, never --orange, for TEXT: #FF9500 on a pale wash is ~2.1:1 and fails 1.4.3. --orange is
   allowed on the ring border only, where the 3:1 non-text threshold applies. */
.alg-r:focus-visible + .alg-opt{outline:2px solid var(--focus);outline-offset:2px}
/* Touch: fall back to the stacked layout so every target keeps its 44px, which a 3-up row of small
   segments cannot give on a phone. */
@media (pointer:coarse){
  .alg-row{gap:var(--sp-1)}
  .alg-leg{flex:1 1 100%;min-width:0}
  .alg-seg{flex:1 1 100%;grid-template-columns:repeat(3,1fr)}
  .alg-opt{flex:1;min-height:44px;font-size:var(--fs-caption)}
  .alg-chip{min-height:44px}
}
/* Narrow VIEWPORT, not just a touch pointer. The rule above keys off the input device, so a 375px
   window on a laptop (and a desktop browser's device toolbar, which is how this gets checked) kept the
   one-line layout and pushed the segments past the 94vw dialog edge. Same stacking, width-driven. */
@media (max-width:560px){
  .alg-leg{flex:1 1 100%;min-width:0}
  .alg-seg{flex:1 1 100%}
  .alg-opt{flex:1;padding:4px 6px}
}
/* JS off: no chip, and the dialog renders as a plain in-flow block on its own full-width row, so the
   picker is still reachable and still posts. Deliberately taller than the enhanced version — on legal
   data, reachable beats compact. */
html:not(.cl-js) .alg-chipwrap{display:none}
html:not(.cl-js) .alg-dialog{display:block!important;position:static;max-width:none;width:auto;border:0;
  box-shadow:none;background:transparent;padding:0}
html:not(.cl-js) .alg-dialog .alg-filter,
html:not(.cl-js) .alg-dialog .ao-actions,
html:not(.cl-js) .alg-dialog .ao-head{display:none}
html:not(.cl-js) .algwrap{flex:1 0 100%}
/* The picker's body is a real <dialog> in BOTH contexts so one element serves both (see the partial).
   On the detail page it renders with `open` — a non-modal dialog in normal flow — so strip the UA's
   dialog chrome (absolute positioning, border, padding, auto margins) and let it read as a plain block. */
.alg-body[open]:not(.alg-dialog){position:static;display:block;border:0;padding:0;margin:0;
  max-width:none;width:auto;background:transparent;color:inherit;inset:auto}
/* Stock count sheet: with JS the sheet autosaves per row, so the whole-sheet Save button is the no-JS
   floor only (chef 2026-07-21: "I don't like the save button"). Without .cl-js the note is hidden and
   the button stands, so the sheet is still savable. */
.cl-js .stock-savebar{display:none}
html:not(.cl-js) .stock-autonote{display:none}
/* Product unit, editable after creation. A plain form + Save is the no-JS floor; with JS the select
   saves on pick and the Save button goes (chef 2026-07-21: the unit was write-once, so a kg/L/unit
   mistake could never be corrected). */
.unitrow{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;margin:0 0 var(--sp-3)}
.unitrow-lab{font-size:var(--fs-caption);font-weight:600;color:var(--text2);margin:0}
.cl-js .unitrow-save{display:none}
@media (pointer:coarse){.unitrow select{min-height:44px}}

/* ---- Rota: add a shift from the cell (chef 2026-07-21: "very hard to add a schedule the current way
   ... addeble from the table Weekly rota. With few clicks under the person").

   The ＋ is a SEPARATE .no-print button, never wrapped around the "OFF" text — so the existing
   .no-print rule does all the work and the PRINTED rota is byte-identical to before. That is the whole
   reason this shape was chosen over wrapping the cell in a link. ---- */
.rota-add{border:0;background:none;cursor:pointer;color:var(--text3);font-size:var(--fs-footnote);
  line-height:1;padding:2px 6px;border-radius:var(--r-sm);opacity:0;
  transition:opacity var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
.grid-cell:hover .rota-add,.grid-cell:focus-within .rota-add,.rota-add:focus-visible{opacity:1}
.rota-add:hover{background:var(--accent-tint);color:var(--accent)}
@media (pointer:coarse){.rota-add{opacity:.6;min-height:44px;min-width:44px}}  /* touch has no hover */
/* The dialog IS the old flat panel. With JS it becomes modal; without <dialog> support it stays exactly
   what it was, so there is one element, one form, no duplicate ids and no divergent no-JS path. */
.cl-js .rota-dialog[open]{position:fixed;inset:0;margin:auto;max-width:640px;width:min(640px,94vw);
  max-height:86vh;overflow:auto;z-index:60}
.cl-js .rota-dialog:not([open]){display:none}
/* NO-JS FLOOR. The dialog no longer ships with `open`: with .cl-js applied in <head>, an open dialog
   would paint a full-screen modal over the rota on EVERY page load until app.js got round to closing it.
   Without JS this rule un-dialogs it back into the flat panel it has always been. */
html:not(.cl-js) .rota-dialog{display:block!important;position:static;max-width:none;width:auto}
html:not(.cl-js) .rota-addsech{display:block}
.cl-js .rota-addsech{display:none}
.rota-dialog::backdrop{background:rgba(0,0,0,.45)}
.rota-days{margin-top:var(--sp-2)}
.rota-chips{display:flex;flex-wrap:wrap;gap:var(--sp-1);align-items:center}
.rota-chip{display:inline-flex;align-items:center;gap:5px;padding:6px 10px;border:1px solid var(--sep);
  border-radius:999px;font-size:var(--fs-caption);font-weight:600;color:var(--text2);cursor:pointer}
.rota-chip:hover{border-color:var(--text3)}
.rota-chip:has(input:checked){background:var(--accent-tint);border-color:var(--accent);color:var(--accent)}
.rota-chip input{margin:0}
@media (pointer:coarse){.rota-chip{min-height:44px}}
/* Add/Cancel sit together, sized to content, and align to the bottom of the field row (chef 2026-07-22:
   the old inline flex:0 collapsed the basis to 0 so the two buttons wrapped onto two misaligned lines). */
.rota-actions{flex:0 0 auto;display:flex;gap:8px;align-items:end}
[data-rota-staff]{min-width:200px}   /* the staff combobox needs room to type + show the dropdown */
/* ---- Rota: the wage bill, and the calculation behind each person's share (chef 2026-07-28: "We need
   the Total Week labour / Total Full Timers / Total Part timers / and on hoover the detailed
   calculation per person").

   THE DISCLOSURE IS A <details>, NOT A TOOLTIP. A title= or a :hover popover answers the literal
   request and serves nobody on the tablet propped up on the pass, or anybody driving the page from a
   keyboard. <details> gives tap and Enter/Space natively and survives JS being off; app.js layers
   hover (and keyboard-focus) opening on top for devices that have a fine pointer. So the same markup
   covers all three ways in, and the fallback is the fully-expanded truth rather than nothing.

   NOT the house `.disclose`: that is a section HEADING that folds a panel away (width:max-content,
   uppercase, its own margins). This is a full-width ROW in a list of people, with three columns that
   have to line up down the page, so it shares the idea and none of the geometry.

   The three columns are FIXED-WIDTH, not auto: every row is its own grid, so `auto` columns would
   size per row and the hours/cost figures would stagger down the panel instead of forming a column
   the chef can read down and add up. ---- */
.labgap{align-items:flex-start}
.labgap div+div{margin-top:var(--sp-2)}
.labp-wrap{margin-bottom:var(--sp-5)}
.labp-head,.labp>summary{display:grid;grid-template-columns:minmax(0,1fr) 92px 132px 14px;
  gap:var(--sp-2) var(--sp-3);align-items:center;padding:10px var(--sp-4)}
.labp-head{font-size:var(--fs-caption);font-weight:600;text-transform:uppercase;letter-spacing:.4px;
  color:var(--text2);border-bottom:.5px solid var(--sep);background:var(--fill-2)}
.labp-head span+span{text-align:right}
.labp{border-top:.5px solid var(--hair)}
.labp:first-of-type{border-top:0}
.labp>summary{cursor:pointer;list-style:none}
.labp>summary::-webkit-details-marker{display:none}
/* The chevron is the 4th grid column — a real affordance that the row opens, since the UA marker is
   dropped by `list-style:none` (a grid <summary> would not paint it anyway). */
.labp>summary::after{content:"›";color:var(--text3);font-size:var(--fs-callout);line-height:1;
  text-align:center;transition:transform var(--dur-fast) var(--ease)}
.labp[open]>summary::after{transform:rotate(90deg)}
.labp>summary:hover{background:var(--fill-2)}
.labp[open]>summary{background:var(--fill-2)}
.labp>summary:focus-visible{outline:2px solid var(--focus);outline-offset:-2px}
.labp-name{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-weight:600;min-width:0}
.labp-h{color:var(--text2);font-size:var(--fs-footnote);text-align:right;white-space:nowrap;
  font-variant-numeric:tabular-nums}
.labp-c{font-weight:700;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}
.labp-calc{padding:0 var(--sp-4) var(--sp-4);font-size:var(--fs-footnote);color:var(--text2)}
.labp-calc p{margin:0 0 var(--sp-2)}
/* THE ARITHMETIC ITSELF is the point of opening the row, so it is set larger than the prose that
   explains it — the chef is checking a sum, not reading a paragraph. */
.labp-eq{font-size:var(--fs-callout);font-weight:600;color:var(--text);letter-spacing:-.01em;
  font-variant-numeric:tabular-nums}
.labp-lines{width:100%;border-collapse:collapse;margin-top:var(--sp-2)}
.labp-lines th{font-size:var(--fs-caption2);text-transform:uppercase;letter-spacing:.4px;
  color:var(--text3);text-align:left;font-weight:600;padding:4px 8px;border-bottom:.5px solid var(--hair)}
.labp-lines td{padding:4px 8px;border-bottom:.5px solid var(--hair);font-variant-numeric:tabular-nums}
.labp-lines tr:last-child td{border-bottom:0}
.labp-none{padding:var(--sp-4)}
@media (pointer:coarse){.labp>summary{min-height:44px}}
/* Phone: the two number columns shrink rather than squeezing the name to nothing (a name is the only
   thing that identifies the row; a cost that wraps is still readable). */
@media (max-width:520px){
  .labp-head,.labp>summary{grid-template-columns:minmax(0,1fr) 62px 104px 12px;padding:10px var(--sp-3)}
  .labp-h{font-size:var(--fs-caption)}
  .labp-calc{padding:0 var(--sp-3) var(--sp-3)}
}
/* Opening-balance control on a stock sheet. */
.openrow{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;margin:0 0 var(--sp-3)}
/* Recipe table: sortable headers + the column picker. */
.rsort{color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
.rsort:hover{color:var(--accent)}
.rsort.on{color:var(--accent);font-weight:700}
.colpick{position:relative;display:inline-block}
.colpick > summary{cursor:pointer;list-style:none}
.colpick > summary::-webkit-details-marker{display:none}
.colpick-panel{position:absolute;z-index:40;top:calc(100% + 6px);right:0;min-width:220px;
  background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);
  box-shadow:var(--shadow);padding:var(--sp-3);display:flex;flex-direction:column;gap:var(--sp-1)}
.colpick-opt{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-footnote);
  color:var(--text2);cursor:pointer}
.colpick-opt input{width:auto;margin:0}
@media (pointer:coarse){.colpick-opt{min-height:44px}}
/* Allergen sheet: the printed grid states that screen filters were not applied to it. */
.am-printnote{caption-side:top;text-align:left;font-size:var(--fs-caption);color:var(--text2);
  padding:0 0 6px}
@media screen{.am-printnote{display:none}}
/* Ingredients advanced-search bar: one row, the search box takes the slack. */
.ing-search{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap}
.ing-search .ing-q{flex:1 1 260px;min-width:200px;max-width:420px;margin:0}
.ing-search select{margin:0}
.ing-search .ing-clear{margin-left:auto}
@media (pointer:coarse){.ing-search select,.ing-search .ing-q{min-height:44px}}

/* ===================================================================================================
   CATERING EVENT SHEET  (/calendar/event/<id>/)  — UX rebuild 2026-07-27
   Chef: "the current interface for adding the dishes and managing the guests with allergens is very
   bad. Brainstorm how to make it smooth with the best user experience on any devices."

   THE DEFECT THIS LAYOUT FIXES is not decoration. The allergens a chef is cooking around used to live
   in a panel ABOVE the menu, so on any screen narrow enough to matter they were off-screen at the exact
   moment a dish was being chosen — the clash could only ever be reported afterwards. Three layouts,
   one job: keep the constraint next to the decision.

     < 760px   phone      one pane at a time (Details | Guests | Menu), pure-CSS segmented control,
                          summary bar sticky above it. 760 is the app's own phone breakpoint — the
                          same one that swaps nav.side for .botnav — so the page never disagrees with
                          the chrome around it.
     760-1079  tablet     every pane stacked, no tab control, summary still sticky.
     >= 1080   desktop    two columns: menu left, guests + allergen tally sticky right.

   Everything below is tokens only — no literal colour anywhere in this block — so it themes in light
   and OLED dark for free. Moved out of the page's old <style> block because this page now ships JS
   too, and app.css/app.js are versioned together in base.html.
   =================================================================================================== */
.evwrap{position:relative}
/* WITHOUT THIS RULE NOTHING ON THIS PAGE IS STICKY — and "sticky" is the whole layout.
   main.content is overflow:auto, but .app is min-height:100vh, so its 1fr row grows to fit the content
   and main.content never actually scrolls: the WINDOW does. It is still a scroll CONTAINER though, and
   a position:sticky descendant sticks to that container's scrollport — which never moves — so it does
   not stick at all. Measured: the summary cap scrolled clean off screen (top -735px at 900px of
   scroll); with overflow:visible it pins at exactly --appbar-h.
   :has() scopes the fix to this page, so no other screen's clipping behaviour changes (the combobox's
   position:fixed re-anchoring, the tablewraps). A browser without :has() ignores the rule and simply
   gets a non-sticky page that still works — the right way round to fail. */
main.content:has(.evwrap){overflow:visible}
.evmeta{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-5);align-items:center;color:var(--text2);
  font-size:var(--fs-footnote);margin:0 0 var(--sp-3)}
.evmeta .evfact{display:inline-flex;align-items:center;gap:6px}
.evmeta b{color:var(--text)}

/* ---- the sticky cap: phone tabs + the constraint summary ---- */
/* The radios are visually hidden but must stay FOCUSABLE and in the tab order — position:absolute with
   1px/opacity:0 (not display:none, not visibility:hidden) is the only way a keyboard user can still
   reach the segmented control. */
.evtab-r{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
/* NOT top:0. main.content declares overflow:auto but its grid row auto-sizes to the content, so it
   never actually becomes the scroller — the WINDOW scrolls, and a sticky child pins to the viewport,
   i.e. straight under the app bar (.top, sticky top:0 z-index:20). At top:0 the whole cap slid behind
   it and the clash banner was invisible while scrolled.

   --ev-top is the app bar's MEASURED height, published by app.js; --appbar-h is the nominal fallback
   used before any script runs and forever with scripting off. Measured because .top is not a fixed
   height: 57px on a phone, 59px at 1280, and 73px around 1024 where its pills wrap to a second line —
   at a hardcoded 56px the counts row was clipped by 17px at that width. */
.evsticky{position:sticky;top:var(--ev-top,var(--appbar-h));z-index:6;background:var(--bg);
  padding:var(--sp-2) 0}
.evtabs{display:flex;gap:2px;padding:3px;background:var(--fill);border-radius:var(--r-md);
  margin:0 0 var(--sp-2)}
.evtab{flex:1;margin:0;min-height:44px;display:flex;align-items:center;justify-content:center;
  cursor:pointer;padding:var(--sp-2);border-radius:var(--r-sm);
  font-size:var(--fs-footnote);font-weight:600;color:var(--text2);user-select:none;white-space:nowrap;
  transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
#evtab-details:checked ~ .evsticky .evtab[for="evtab-details"],
#evtab-guests:checked ~ .evsticky .evtab[for="evtab-guests"],
#evtab-menu:checked ~ .evsticky .evtab[for="evtab-menu"]{background:var(--bg-elev);color:var(--accent);
  box-shadow:var(--shadow)}
/* Ring only on the segment that actually has focus — outlining all three reads as "everything is
   focused". */
#evtab-details:focus-visible ~ .evsticky .evtab[for="evtab-details"],
#evtab-guests:focus-visible ~ .evsticky .evtab[for="evtab-guests"],
#evtab-menu:focus-visible ~ .evsticky .evtab[for="evtab-menu"]{outline:2px solid var(--focus);
  outline-offset:2px}

/* The counts. Tabular numerals so a 9 -> 10 tick does not shuffle the row sideways. */
.evbar{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4);align-items:center;
  background:var(--bg-elev);border:.5px solid var(--sep);border-radius:var(--r-card);
  padding:var(--sp-2) var(--sp-4);box-shadow:var(--shadow)}
.evstat{display:inline-flex;align-items:baseline;gap:6px;font-size:var(--fs-caption);color:var(--text2);
  white-space:nowrap}
.evstat b{font-size:var(--fs-callout);color:var(--text);font-variant-numeric:tabular-nums;
  letter-spacing:-.02em}
.evstat.warn b{color:var(--warn-fg)}
.evstat.bad b{color:var(--al-fg)}
/* The clash banner rides inside the sticky cap: it is the one thing on this page that must never
   scroll away. Compact (it is permanent), never dismissible (see _event_summary.html). */
.evsticky .hint{margin:var(--sp-2) 0 0;padding:9px 14px;align-items:flex-start}
.evclash{align-items:flex-start}

/* ---- panes ---- */
.evpane{min-width:0}
.evdet{margin:0}
.evdet-sum{cursor:pointer;display:flex;align-items:baseline;gap:var(--sp-2);flex-wrap:wrap;
  list-style:none;min-height:44px;padding:var(--sp-1) 0}
.evdet-sum::-webkit-details-marker{display:none}
.evdet-sum::after{content:"\25BE";color:var(--text3);font-size:var(--fs-caption)}
.evdet[open] .evdet-sum::after{content:"\25B4"}

/* ---- guests ---- */
.gadd-wrap{border-top:.5px solid var(--sep);padding-top:var(--sp-3)}
.gadd-sum{cursor:pointer;list-style:none;min-height:44px;display:flex;align-items:center;
  font-weight:600;font-size:var(--fs-footnote);color:var(--ok-fg)}
.gadd-sum::-webkit-details-marker{display:none}
.gadd{background:var(--fill-2);border:.5px solid var(--sep);border-radius:var(--r-md);
  padding:var(--sp-3);margin:0}
.gadd-row{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap}
.gadd-row + .gadd-row{margin-top:var(--sp-2)}
.gadd-f{flex:1 1 160px;min-width:0}
.gadd-f input{margin:0}
.gadd-al{flex:1 1 200px}
.gadd-go{flex:0 0 auto;min-height:44px}
/* "3 of 8 guests have a recorded allergen - gluten x2, nuts x1" — the constraint in one line. */
.gtally{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:var(--fs-footnote);
  color:var(--text2);margin:0 0 var(--sp-3)}
.gtally b{color:var(--text)}
.gtally-x{display:inline-flex;align-items:center;gap:3px}
.gtally-n{font-weight:700;font-size:var(--fs-caption2);color:var(--text2);font-variant-numeric:tabular-nums}
.glist{list-style:none;margin:0;padding:0}
.gitem{border-top:.5px solid var(--sep);padding:var(--sp-3) 0}
.glist>li:first-child{border-top:0;padding-top:0}
.ghead{display:flex;flex-wrap:wrap;gap:6px var(--sp-2);align-items:center}
.gname{font-weight:600}
.gbadges{display:inline-flex;flex-wrap:wrap;gap:4px;align-items:center}
.gnote{color:var(--text2);font-size:var(--fs-footnote);margin:6px 0 0}
.gsum{cursor:pointer;font-size:var(--fs-caption);color:var(--ok-fg);font-weight:600;
  list-style:none;min-height:44px;display:flex;align-items:center}
.gsum::-webkit-details-marker{display:none}
.gform{margin-top:var(--sp-1)}
.gactions{display:flex;gap:var(--sp-2);align-items:center}

/* ---- the guest allergen picker (searchable; a bottom sheet on a phone) ---- */
.galwrap{min-width:0}
.gal-btn{display:flex;align-items:center;gap:var(--sp-2);width:100%;min-height:44px;
  padding:var(--sp-2) var(--sp-3);border:.5px solid var(--sep);border-radius:var(--r-md);
  background:var(--bg-elev);color:var(--text);font:inherit;font-size:var(--fs-footnote);
  cursor:pointer;text-align:left}
.gal-btn:hover{border-color:var(--text3)}
.gal-chips{flex:1;min-width:0;display:flex;flex-wrap:wrap;gap:4px;align-items:center}
.gal-chip{display:inline-flex}
.gal-none{color:var(--text2)}
/* --ok-fg, never --accent, for small green TEXT: #34C759 is ~2.2:1 on --bg-elev and fails WCAG 1.4.3,
   which is the same reason this sheet's --focus ring is #0b6b2c and its warning text is --warn-fg
   rather than --orange. --ok-fg is 5.5:1 on white and 9:1 on OLED black. */
.gal-act{flex:0 0 auto;font-size:var(--fs-caption);font-weight:600;color:var(--ok-fg)}
/* [open] IS LOAD-BEARING: an unconditional display rule here beats the UA's
   `dialog:not([open]){display:none}`, and the dialog then renders permanently in flow and can never be
   closed. That shipped once on the product allergen picker; the template-lint suite pins it. */
.gal-dialog[open]{display:flex;flex-direction:column;max-width:460px;width:min(460px,94vw);
  max-height:80vh;overflow:hidden;border:.5px solid var(--sep);border-radius:18px;padding:0;
  background:var(--bg-elev);color:var(--text);box-shadow:var(--shadow-lift)}
.gal-dialog .ao-head{display:flex;align-items:baseline;gap:var(--sp-2);
  padding:var(--sp-4) var(--sp-4) var(--sp-3);background:var(--bg-elev)}
.gal-dialog .ao-head b{font-size:var(--fs-callout);font-weight:700;letter-spacing:-.01em}
.gal-filter{margin:0 var(--sp-4) var(--sp-2)}
.gal-rows{flex:1;min-height:0;overflow:auto;padding:0 var(--sp-4)}
.gal-dialog .ao-actions{padding:var(--sp-3) var(--sp-4);border-top:.5px solid var(--sep);
  margin:0;background:var(--bg-elev)}
.gal-nomatch{padding:0 var(--sp-4) var(--sp-2)}
/* One allergen per line, the whole line a 48px tap target — a glove on a phone must not have to find a
   14px checkbox. */
.gal-row{display:flex;align-items:center;gap:10px;min-height:48px;margin:0;padding:var(--sp-1) 0;
  border-top:.5px solid var(--hair);font-size:var(--fs-subhead);font-weight:500;color:var(--text);
  cursor:pointer}
.gal-row:first-child{border-top:0}
.gal-row.gal-hide{display:none}
.gal-cb{width:22px;height:22px;flex:0 0 auto;margin:0;accent-color:var(--accent)}
.gal-g{flex:0 0 auto;width:30px;height:30px;border-radius:50%;background:var(--fill-2);
  display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1}
.gal-lab{flex:1;min-width:0}
@media (max-width:560px){
  /* Bottom sheet: a thumb reaches the bottom of a phone, not the middle of it. */
  .gal-dialog[open]{position:fixed;inset:auto 0 0 0;margin:0;width:100%;max-width:100%;
    border-radius:18px 18px 0 0;max-height:86vh}
}
/* Scripting off: no chip button, and the dialog renders as a plain in-flow block so the fourteen
   checkboxes are still reachable and still post. Deliberately taller than the enhanced version — on
   allergen data, reachable beats compact. Same shape as the product picker's no-JS fallback. */
html:not(.cl-js) .gal-btn{display:none}
html:not(.cl-js) .gal-dialog{display:block!important;position:static;max-width:none;width:auto;
  border:0;box-shadow:none;background:transparent;padding:0;max-height:none;overflow:visible}
html:not(.cl-js) .gal-dialog .ao-head,
html:not(.cl-js) .gal-dialog .gal-filter,
html:not(.cl-js) .gal-dialog .ao-actions{display:none}

/* ---- menu: courses + dishes (carried over from the page's old <style> block) ---- */
.mnsec{border-top:.5px solid var(--sep);padding:var(--sp-4) 0 var(--sp-1)}
.mnsec:first-of-type{border-top:0;padding-top:0}
.mnsec-head{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:var(--sp-2)}
.mnsec-form{display:flex;gap:6px;align-items:center;margin:0;flex-wrap:wrap}
/* A course name READS AS A HEADING and only becomes an editable box on hover/focus — the same "no
   border, no box, just the words" treatment app.css gives an editable recipe step. Without it the top
   of every course is a form field and the menu looks like a settings screen instead of a menu. */
input.mnsec-name{font:inherit;font-size:var(--fs-callout);font-weight:600;color:var(--text);width:auto;
  min-width:150px;margin:0;background:transparent;border:.5px solid transparent;
  border-radius:var(--r-sm);padding:var(--sp-1) var(--sp-2);
  transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
input.mnsec-name:hover{border-color:var(--sep)}
input.mnsec-name:focus{background:var(--bg-elev);border-color:var(--accent);outline:none}
h3.mnsec-name.ro{font-size:var(--fs-callout);margin:0}
.mnlist{list-style:none;margin:0;padding:0;min-height:38px;border-radius:var(--r-sm)}
.mnlist.mn-over{outline:2px dashed var(--accent);outline-offset:2px;background:var(--accent-tint)}
.mnitem{border:.5px solid var(--sep);border-radius:var(--r-sm);padding:var(--sp-2) 10px;
  margin-bottom:6px;background:var(--bg-elev)}
.mnitem.mn-dragging{opacity:.4}
/* A clashing dish is marked by a THICK LEFT RULE as well as colour, so it survives a colour-blind
   reader and a black-and-white print (the same rule the allergen badges follow). */
.mnitem.clash{border-left:4px solid var(--al-fg);background:var(--al-bg)}
/* A just-added row flashes once so the eye lands on it after a no-reload add. Animation only — no
   layout shift, and no colour that carries meaning on its own. */
@keyframes mn-landed{from{background:var(--accent-tint)}to{background:var(--bg-elev)}}
.mnitem.mn-landed{animation:mn-landed var(--dur-slow) var(--ease)}
@media (prefers-reduced-motion:reduce){.mnitem.mn-landed{animation:none}}
.mnrow{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:baseline}
.mn-grip{cursor:grab;color:var(--text3);font-size:var(--fs-footnote);letter-spacing:-1px;
  touch-action:none;align-self:center}
.mn-grip:active{cursor:grabbing}
.mn-name{font-weight:600}
.mn-qty{margin-left:auto;white-space:nowrap}
.mn-al{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
.mn-note{font-size:var(--fs-footnote);margin-top:6px}
.mnclash{margin-top:var(--sp-2);padding-top:7px;border-top:.5px solid var(--sep)}
.mnclash-h{font-weight:600;color:var(--al-fg);font-size:var(--fs-footnote)}
.mnclash-l{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:var(--fs-footnote);
  margin-top:5px}
.mnedit{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:var(--sp-2)}
.mnedit-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:0}
.mn-portions{width:96px;margin:0}
.mn-inote{width:190px;margin:0}
.mn-sec{width:auto;min-width:120px;margin:0;font-size:var(--fs-caption)}
.mnempty{margin-bottom:var(--sp-2)}
.mnadd{margin:0}
.mnaddsec{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:var(--sp-4);
  border-top:.5px solid var(--sep);padding-top:var(--sp-4)}
.mnaddsec input{width:auto;min-width:180px;margin:0}
/* Touch: the reorder / move-to-course controls are the ones a drag cannot serve, so they get the full
   44px. Never rely on drag alone. */
@media (pointer:coarse){
  .mnmove{min-width:44px;min-height:44px;font-size:var(--fs-footnote)}
  .mn-sec{min-height:44px}
  .mnedit .btn{min-height:44px}
  .mn-grip{font-size:var(--fs-callout)}
}
/* The scripting-off dish <select>: invisible to anyone who has JS, i.e. to everyone who can see the
   search box above it. */
.cl-js .evnojs{display:none}
.evnojs{margin-top:var(--sp-4);border-top:.5px solid var(--sep);padding-top:var(--sp-4)}

/* ---- THE DISH SEARCH — the moment of decision ----
   Inline results, deliberately, not a floating panel: the app's combobox needs position:fixed
   re-anchoring on every scroll to escape main.content's overflow, and a panel that cannot be clipped
   beats one that has to be rescued. It also means the results push the page down instead of covering
   the course you are filling. */
.dsearch{margin-top:var(--sp-2)}
.dsearch-in{margin:0;min-height:44px}
.dsearch-out{margin-top:var(--sp-2);border:.5px solid var(--sep);border-radius:var(--r-md);
  background:var(--bg-elev);max-height:min(52vh,440px);overflow:auto;box-shadow:var(--shadow)}
.dsr-head{display:flex;align-items:center;gap:var(--sp-2);position:sticky;top:0;z-index:1;
  background:var(--bg-elev);padding:var(--sp-2) var(--sp-3);border-bottom:.5px solid var(--hair)}
.dsr-count{flex:1;min-width:0;font-size:var(--fs-caption);font-weight:600;color:var(--text2)}
.dsr-list{list-style:none;margin:0;padding:0}
.dsr{display:flex;gap:var(--sp-2);align-items:flex-start;padding:var(--sp-2) var(--sp-3);
  border-top:.5px solid var(--hair)}
.dsr:first-child{border-top:0}
/* A clashing result: thick left rule AND tint AND the words — three channels, so it survives
   colour-blindness and greyscale. It is NEVER disabled (owner decision B: warn, never block). */
.dsr.clash{border-left:4px solid var(--al-fg);background:var(--al-bg)}
/* The ＋ is the house .btn.primary, sized to a 44x44 square: it introduces no colour of its own (the
   fill and its text colour come from the existing component), and the primary action of the search
   deserves the primary control. Once tapped it flips to a ✓ in the ok tokens — the same row can be
   tapped again, because the same dish legitimately appears in two courses. */
.dsr-add{flex:0 0 auto;width:44px;height:44px;border-radius:var(--r-md);
  font-size:var(--fs-title3);line-height:1;justify-content:center;padding:0}
.dsr-add.added{background:var(--ok-bg);color:var(--ok-fg)}
.dsr-body{flex:1;min-width:0}
.dsr-name{font-weight:600;font-size:var(--fs-subhead);display:flex;align-items:center;gap:6px}
.dsr-often{color:var(--ok-fg);font-size:var(--fs-caption)}
.dsr-al{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}
.dsr-clash{margin-top:6px;font-size:var(--fs-caption);color:var(--al-fg);
  display:flex;flex-wrap:wrap;gap:4px var(--sp-2);align-items:center}
.dsr-cl{display:inline-flex;align-items:center;gap:4px;color:var(--text2)}
.dsr-empty{padding:var(--sp-3);font-size:var(--fs-footnote)}
@media (pointer:coarse){.dsr{padding:var(--sp-3)}}

/* ---- print & plan ---- */
.evprints{display:flex;flex-wrap:wrap;gap:var(--sp-2);align-items:center}

/* ---- LAYOUT: phone tabs -> stacked -> two columns ---- */
@media (max-width:759.98px){
  .evpane{display:none}
  #evtab-details:checked ~ .evcols .evpane-details,
  #evtab-guests:checked ~ .evcols .evpane-guests,
  #evtab-menu:checked ~ .evcols .evpane-menu{display:block}
  .mn-qty{margin-left:0}
  .mn-inote{width:100%}
  .mn-portions{width:88px}
  .evstat{font-size:var(--fs-caption2)}
  .evbar{gap:6px var(--sp-3);padding:var(--sp-2) var(--sp-3)}
  /* THE CAP MUST EARN ITS SCREEN. It is permanently on an ~812px phone, so it carries only the
     constraint: covers + dish count go (both are one tap away), and the banner keeps its warning and
     its action but drops the detail the flagged rows repeat anyway. Measured: 248px -> ~145px.
     max-height is a floor-of-last-resort so a fourteen-allergen event can never swallow the page. */
  .evstat-side{display:none}
  .evclash-more{display:none}
  .evsticky{max-height:40vh;overflow:auto}
}
@media (min-width:760px){
  .evtabs{display:none}                 /* every pane is on screen; the control would be a lie */
}
@media (min-width:1080px){
  .evcols{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:var(--sp-5);
    grid-template-areas:"det det" "menu guests";align-items:start}
  .evpane-details{grid-area:det}
  .evpane-menu{grid-area:menu}
  /* THE POINT OF THE WHOLE LAYOUT: the guests and their allergen tally stay on screen while dishes are
     picked. `top` has to clear the app bar AND the sticky cap above it — measured at 123px with the
     clash banner showing, so 136px leaves a margin. Deliberately generous: overshooting costs a strip
     of empty column while scrolled, undershooting means the cap paints over the top of the guest list
     (it is z-index 6), which would hide the very rows this column exists to keep on screen. */
  .evpane-guests{grid-area:guests;position:sticky;
    top:calc(var(--ev-top,var(--appbar-h)) + 136px);align-self:start;
    max-height:calc(100vh - var(--ev-top,var(--appbar-h)) - 160px);overflow:auto}
}
@media print{
  .evsticky,.evtabs,.dsearch,.evnojs,.gadd{display:none!important}
  .evpane{display:block!important}
}

/* ============================================================================================
   STAFF FOOD PROGRAM — the canteen calendar (stage 3, core/stafffood.py).

   A DELIBERATE SECOND GRID, not a reuse of /calendar/'s. That page's `.caltbl` rules still live in a
   <style> block inside core/templates/core/calendar.html, so they are not reachable from here; lifting
   them out would have meant editing a committed template on the way past. The GRID MATHS is shared
   (core/utils.py) — which is the half that can silently disagree — while the paint is local and named
   `sf-*` so the two can be restyled independently. If calendar.html's block ever moves into this file,
   merge them then.

   The state a cell has to carry is not "does it have dishes" but PROJECTED vs MATERIALISED (R1), and
   they must be distinguishable at a glance across a whole month: a projected day is tinted and its
   dishes are outlined (the plan as the rules predict it), a materialised day is plain with solid chips
   (what the kitchen was actually told to cook). Colour is never the only carrier — the outline/fill
   difference survives a colour-blind reader and a black-and-white print of the month.
   ============================================================================================ */
.sf-nav{display:flex;align-items:center;gap:10px;margin:0 0 14px;flex-wrap:wrap}
.sf-nav .sf-label{font-weight:700;font-size:var(--fs-subhead)}
.sf-seg{display:inline-flex;gap:4px;background:var(--fill);border:1px solid var(--sep);
  border-radius:var(--r-md);padding:4px}
.sf-seg a{display:inline-flex;align-items:center;gap:6px;color:var(--text2);font-size:14px;
  font-weight:600;border-radius:9px;padding:7px 14px;transition:background .14s,color .14s}
.sf-seg a:hover{color:var(--text)}
.sf-seg a.on{background:var(--accent);color:#fff}
.sf-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
.sf-tbl th{padding:6px 5px;font-size:var(--fs-caption2);text-transform:uppercase;letter-spacing:.4px;
  color:var(--text3);text-align:left;border-bottom:.5px solid var(--sep)}
.sf-tbl td.sf-cell{border:.5px solid var(--hair);vertical-align:top;padding:5px 6px;height:104px;
  position:relative}
.sf-tbl.sf-week td.sf-cell{height:150px}
/* No hover override here on purpose: the global `tbody tr:hover` tint sits BEHIND these cells (it paints
   the row, they paint themselves), so it reads as a gentle row highlight exactly as it does on
   /calendar/. An earlier `td.sf-cell:hover{background:transparent}` was worse than useless — being more
   specific than `.sf-cell.sf-today`, it stripped today's accent tint the moment you moved the mouse. */
.sf-tbl td.sf-cell.sf-out{background:var(--fill-2);opacity:.5}
.sf-tbl td.sf-cell.sf-today{background:var(--accent-tint)}
/* PROJECTED = the rules' prediction. Tinted so a month of untouched days reads as one block. */
.sf-tbl td.sf-cell.sf-proj{background:var(--fill-2)}
.sf-tbl td.sf-cell.sf-proj.sf-today{background:var(--accent-tint)}
.sf-date{font-size:var(--fs-caption);font-weight:700;color:var(--text2);margin-bottom:3px;
  display:flex;align-items:center;gap:4px}
.sf-date a{color:inherit}
.sf-dish{display:block;font-size:var(--fs-caption2);border-radius:var(--r-xs);padding:2px 6px;
  margin:2px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  background:var(--ok-bg);color:var(--ok-fg)}
/* Outlined = projected. Shape, not just colour, so the distinction survives a mono printout. */
.sf-dish.sf-dish-proj{background:transparent;color:var(--text2);border:1px dashed var(--text3)}
.sf-none{font-size:var(--fs-caption2);color:var(--text3)}
.sf-flag{font-size:var(--fs-caption2);color:var(--text3);font-weight:600}
.sf-rulerow{display:flex;gap:var(--sp-3);align-items:flex-end;flex-wrap:wrap;margin-top:var(--sp-2)}
.sf-rulerow label{display:block;font-size:var(--fs-caption);color:var(--text2);margin-bottom:3px}
.sf-wd{font-weight:700;font-size:var(--fs-subhead);margin:var(--sp-4) 0 var(--sp-1)}
@media(max-width:760px){
  .sf-tbl td.sf-cell{height:auto;min-height:60px;padding:3px}
  .sf-tbl.sf-week td.sf-cell{height:auto;min-height:70px}
  .sf-tbl th,.sf-date{font-size:var(--fs-caption2)}
}
