/* ---------- Design tokens ----------
   Every colour in this file now comes from here. Before this, there were 55
   distinct hex values scattered across 1,222 lines and no variables, so
   "change the theme" meant find-and-replace and hoping.

   Palette is "Fresh" - a light nav instead of a dark bar, green accent -
   chosen by QX from rendered mockups of the real pages.

   To retheme the whole app, change these values and nothing else. To add a
   dark mode later, it's a second block of the same names under a media query
   or a body class. */
:root {
  /* surfaces */
  --bg: #ffffff;
  --surface: #ffffff;
  --surface-2: #f6f9f7;

  /* text */
  --ink: #10241a;
  --ink-2: #33453b;
  --muted: #5f6368;   /* design audit 16 Oct 2026: was #6b7d74 (4.4:1, failed WCAG AA) */
  --muted-2: #70757a; /* was #93a39b (2.6:1) */

  /* lines */
  --border: #e2ebe6;
  --border-strong: #cbdad2;

  /* the nav bar. Fresh makes this light, so --brand-ink is DARK - if you
     swap back to a dark nav, both need to change together. */
  --brand: #ffffff;
  --brand-ink: #10241a;

  /* the one colour that means "this is actionable" */
  --accent: #0f7b48;  /* was #0f7b48: 4.1:1 on the soft green pills, now 4.7:1 */
  --accent-ink: #ffffff;
  --accent-soft: #e8f5ee;
  --accent-border: #a9d9c1;

  /* status. Kept separate from --accent even though ok/accent are the same
     green today: they mean different things, and a future palette may want
     them apart. */
  --ok: #0f7b48;       --ok-soft: #e8f5ee;      --ok-border: #a9d9c1;
  --warn: #96580f;     --warn-soft: #fdf4e8;    --warn-border: #f0d4a8;
  --warn-strong: #8a4e0c;
  --bad: #bb2d3b;      --bad-soft: #fceced;     --bad-border: #f2b8bd;
  --violet: #5b4bb8;   --violet-soft: #eeecfb;

  /* Design system (batch 1, 16 Oct 2026) - the ONLY sizes new work should use.
     Type: 12 secondary, 14 body, 16 phone fields / emphasis, 20 section, 24 page title.
     Space: 4px steps. Radius: 4 small, 8 cards/fields, pill for chips/buttons.
     Shadow: 1 = cards, 2 = pop-ups and menus. */
  --fs-12: 12px; --fs-14: 14px; --fs-16: 16px; --fs-20: 20px; --fs-24: 24px;
  --radius-4: 4px; --radius-8: 8px; --radius-pill: 999px;
  --shadow-1: 0 1px 2px rgba(60,64,67,.12), 0 1px 3px 1px rgba(60,64,67,.06);
  --shadow-2: 0 1px 3px rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15);

  /* ---- Design library (Nov 2026): names for values the shared components already use. ----
     Nothing here changes how a page looks: each token holds a value that was already written
     out in the rules below. Shown live at /admin/design-library (lib/designTokens.js reads
     this file), ratcheted by test/designLibrary.test.js.
     Space: the 4px grid. Radius 12 = cards, 16 = stat tiles. --line = control and button
     edges, --line-soft = card edges. --info = "in progress" teal (same as --pal-teal). */
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; --space-12: 48px;
  --radius-12: 12px; --radius-16: 16px;
  --line: #dadce0; --line-soft: #e3e5e8;
  --info: #0e7c86; --info-soft: #e0f2f2;
  /* ---- end Design library tokens ---- */

  /* focus ring, derived from the accent so it follows a retheme */
  --focus-ring: rgba(15, 123, 72, 0.18);
}

* { box-sizing: border-box; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
}
.topnav {
  background: var(--brand);
  color: var(--brand-ink);
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 24px;
  /* A dark bar separated itself from the page by contrast. A light one needs
     an actual line, or the nav and the content run together. */
  border-bottom: 1px solid var(--border);
}
/* WT CMMS (see partials/head.ejs) gets its own color so it reads as visibly
   different from FM CMMS at a glance, not just a relabeled bar - same shape
   as .topnav otherwise, just a different background.

   This override used to live on `.topnav.topnav-wt`, which scoped it to the
   nav ELEMENT: custom properties inherit down the tree, so a redefinition on
   the nav reached the wordmark and nothing else. Everything below the nav -
   the active tab underline, every button, the card stripes, the focus ring -
   kept resolving --accent from :root and stayed FM green, so WT was the one
   place in the app where the theme contradicted itself (violet wordmark,
   green everything). Redefining on <body> is the same declaration one level
   up, where it actually covers the page.

   Status colours (--ok / --warn / --bad) are deliberately NOT remapped. They
   mean "fine / careful / broken", not "this is our brand", and a violet
   overdue badge would be worse than an off-palette red one.

   .topnav-wt is kept as a hook (the body class drives the colour now) so the
   nav can still be targeted on its own. */
body.wt-theme {
  --accent: var(--violet);
  --accent-soft: var(--violet-soft);
  --accent-border: #c3bced;
  /* derived from --violet the same way :root's is from --accent, or the ring
     would flash green on a violet field. */
  --focus-ring: rgba(91, 75, 184, 0.16);
}
/* ---------- Phone menu toggle ----------
   Above 700px neither of these exists; the nav is the same single row it has
   always been. See the phone media query near the bottom for what they do and
   why. The checkbox is kept focusable (not display:none) so Tab + Space still
   opens the menu without a mouse. */
.navtoggle { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.navtoggle-btn { display: none; }

.topnav .brand {
  /* QX: "I think the header need to be larger / the FM CMMS?" - and on a light
     nav the wordmark had nothing holding it, so it read as body text. A rule
     down its left side gives the corner something deliberate without the
     weight of a filled badge. 22px chosen from a rendered size ladder.
     Padding/border in em so the whole mark scales from font-size alone. */
  font-size: 22px;
  font-weight: 720;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--brand-ink);
  border-left: 0.22em solid var(--accent);
  padding-left: 0.6em;
}
/* The phone nav rules used to sit HERE, immediately below the wordmark rules
   they belong with. They have moved down to the phone block at the bottom of
   this file, because position in the file is the whole game: a media query
   adds no specificity, so a phone rule placed ABOVE an equally specific
   desktop rule loses to it and does nothing at all.
   That is what happened. Twenty-one declarations in this block were dead on
   every phone - the search box kept its desktop `width: 140px` inside a
   full-width frame (which is how QX found this), the links kept their 20px
   left indent and 14px text, and the dropdowns stayed absolutely positioned
   white cards instead of indenting inline. See the phone block for the rest.
   The rule of thumb worth carrying: a max-width override belongs at the END,
   after everything it overrides. Reading it beside what it modifies is worth
   less than having it actually apply. */
/* Brand + search, as one left-hand group.

   The search box used to sit inside .links, wedged between the Settings
   dropdown and Loans. Being the only form control among text links, it read as
   a divider and split the bar at a meaningless seam - which is how Loans, a
   module, ended up visually grouped with Help and Logout. Moving it here does
   three things at once: it uses the empty left third of the bar, it gives the
   app's fastest path the prominence it had earned, and it leaves the module
   links as one uninterrupted run.

   It stays a SIBLING of .navtoggle and .links, never a wrapper around them -
   the phone menu is `.navtoggle:checked ~ .links`, a sibling selector with no
   JavaScript behind it, and nesting would silently break the only navigation
   a phone has. */
.nav-left { display: flex; align-items: center; gap: 18px; min-width: 0; }

/* The nav links align by CENTRE, not by text baseline.
   
   Baseline alignment is fragile here: an inline-block's baseline comes from its
   last line box, EXCEPT when overflow is anything other than visible, when it
   becomes the bottom margin edge instead. The account menu needs
   overflow: hidden to truncate a long name - so the moment it got that, it
   stopped sitting on the same line as Storage/Inventory/Maintenance while
   every other item carried on using the baseline. That is a CSS trap rather
   than a mistake anyone would spot by reading the rule.
   
   Flex with align-items: center takes baselines out of the question entirely,
   so no future item can knock itself out of line by needing overflow, a
   different font-size, or a border. */
.topnav .links { display: flex; align-items: center; }

/* The account menu (Help / Change password / Log out) named after the person.
   max-width so a long name cannot push the rest of the bar around; the name
   itself is never truncated in the markup, so a screen reader still reads it
   whole. */
.topnav .links .navgroup-account > summary {
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topnav .links .navgroup-account .navgroup-menu { right: 0; left: auto; }

.topnav .links a, .topnav .links button.linklike {
  color: var(--brand-ink); text-decoration: none; margin-left: 20px; font-size: var(--fs-14);
  background: none; border: none; cursor: pointer; font-family: inherit;
}
.topnav .links a:hover, .topnav .links button.linklike:hover { text-decoration: underline; }

/* Storage / Maintenance nav groups — plain <details>/<summary> so the
   dropdown opens on click with no JS, and works the same on touch as it
   does with a mouse. */
.topnav .links .navgroup { position: relative; display: inline-block; margin-left: 20px; }
.topnav .links .navgroup summary {
  color: var(--brand-ink); cursor: pointer; font-size: var(--fs-14); list-style: none; padding: 2px 0;
}
.topnav .links .navgroup summary::-webkit-details-marker { display: none; }
.topnav .links .navgroup summary::after { content: ' \25BE'; font-size: 11px; }
.topnav .links .navgroup[open] summary::after { content: ' \25B4'; }
.topnav .links .navgroup summary:hover { text-decoration: underline; }
.topnav .links .navgroup-menu {
  position: absolute; top: 100%; left: 0; margin-top: 8px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-8);
  box-shadow: 0 8px 22px rgba(16,40,28,0.12);
  padding: 6px 0; min-width: 190px; z-index: 30; display: flex; flex-direction: column;
}
/* Higher specificity than ".topnav .links a" (which sets color: white) so
   these dropdown links actually show up as dark text on the white menu
   background, instead of invisible white-on-white. */
/* Anchors AND the logout button, together and identically.
   
   This rule used to name only `a`. Log out is a <button class="linklike"> in a
   <form>, so it kept `margin-left: 20px` from the nav-wide button rule and sat
   visibly indented from the two links above it in the account menu. The lesson
   is in the selector: a dropdown row is a dropdown row regardless of which
   element happens to implement it, and listing one of the two is how they
   drift apart. text-align: left matters because a <button> centres its text by
   default while an <a> does not. */
.topnav .links .navgroup-menu a,
.topnav .links .navgroup-menu button.linklike {
  color: var(--ink); margin-left: 0; padding: 8px 16px; font-size: var(--fs-14);
  text-decoration: none; white-space: nowrap; text-align: left; width: 100%;
}
.topnav .links .navgroup-menu a:hover,
.topnav .links .navgroup-menu button.linklike:hover { background: var(--surface-2); text-decoration: none; }
/* The <form> wrapping the logout button must not add a box of its own, or the
   button it contains stops lining up with the anchors beside it. */
.topnav .links .navgroup-menu form.inline { display: block; margin: 0; }
/* Design library (Nov 2026) - START: one quiet sub-heading inside a dropdown (Settings -> "System",
   over the rarely used owner pages). Lives here, before the phone block, because test/phoneNav.test.js
   requires every nav rule to come before it. */
.topnav .links .navgroup-menu .navgroup-sub { display: block; margin: var(--space-1) 0 0; padding: var(--space-2) var(--space-4) var(--space-1); border-top: 1px solid var(--border); font-size: var(--fs-12); font-weight: 600; color: var(--muted); cursor: default; white-space: nowrap; }
/* Design library (Nov 2026) - END */

/* Global nav search box — sized/weighted to sit quietly among the plain-text
   nav links rather than reading as a big, disconnected form control (it was
   originally a bare <input style="width:160px"> with the browser's default
   white box + heavy padding, which looked oversized/out of place next to
   14px text links). Kept translucent against the dark bar at rest; brightens
   to solid white only on focus, when it's actually the thing being used. */
/* The border, background and rounded corners live on the FORM, not on the
   input - so the text field and the camera button read as one control with a
   single outline around both. They were on the input itself until Task #272:
   the input kept its own 5px radius on all four corners and its own border,
   so however tightly the button was tucked against it there was always a
   wedge of navy showing where the input's rounded right edge met the
   button's straight left edge - and the moment the input was focused it
   turned solid white while the button stayed translucent, making them look
   like two unrelated things sitting next to each other (which is exactly
   what QX's screenshot showed). */
.topnav form.nav-search {
  display: inline-flex; align-items: stretch; vertical-align: middle;
  border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--surface-2);
  overflow: hidden; /* clips both children to the shared rounded corners */
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.topnav form.nav-search input[type="search"] {
  width: 140px; font-size: 13px; padding: 5px 10px;
  border: none; background: none; border-radius: 0;
  color: var(--ink); font-family: inherit;
}
.topnav form.nav-search input[type="search"]::placeholder { color: var(--muted); }
.topnav form.nav-search input[type="search"]:focus { outline: none; }
/* :focus-within so the whole control lights up together when the text field
   is focused, rather than only the input turning white inside a still-dark
   frame. */
.topnav form.nav-search:focus-within {
  background: var(--surface); border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-ring);
}
.topnav form.nav-search:focus-within input[type="search"] { color: var(--ink); }
.topnav form.nav-search:focus-within input[type="search"]::placeholder { color: var(--muted-2); }
/* Task #268: photo search's entry point - a camera button tucked against the
   right edge of the nav search field, so "search by typing" and "search by
   photo" read as one control rather than the separate dropdown entry it used
   to be. Sized to a comfortable tap target on a phone (where this gets used
   most, standing in a storeroom) without making the nav bar taller.
   The frame around it comes from the form (see above), so this carries no
   border or radius of its own - just a divider line separating it from the
   text field, and a background that stays legible whether the control is
   resting (translucent over navy) or focused (solid white). */
.nav-photo-btn {
  background: transparent;
  border: none;
  border-left: 1px solid var(--border-strong);
  border-radius: 0;
  color: var(--muted);
  cursor: pointer;
  line-height: 0;
  padding: 0 11px;
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  transition: background-color 0.12s ease;
}
/* Task #274: the icon is an inline SVG drawn in currentColor rather than a
   colour emoji. The emoji rendered as a different picture on every platform
   (a photorealistic camera on iOS, a flat one on Android, a tiny grey one on
   Windows), always in its own fixed colours, so it could never match the
   navy bar or switch to dark ink when the field is focused - it just sat
   there looking like a sticker on the nav. An SVG inherits the button's
   colour, so it follows every state below for free.
   display:block because an inline SVG sits on the text baseline and leaves a
   few px of descender space underneath, which knocked it off-centre inside
   the frame. */
.nav-photo-btn svg { display: block; }
.nav-photo-btn:hover { background: var(--accent-soft); color: var(--accent); }
/* Once the field is focused the frame is white, so the button's translucent
   white-on-navy treatment would read as a grey smear against it. */
.topnav form.nav-search:focus-within .nav-photo-btn {
  background: var(--border);
  border-left-color: var(--border-strong);
  color: var(--ink-2);
}
.topnav form.nav-search:focus-within .nav-photo-btn:hover { background: var(--border); }
/* Drag a photo anywhere over the page and the control highlights as a drop
   target (see public/photo-search.js). */
.topnav form.nav-search.drop-active {
  background: var(--accent); border-color: var(--accent-border);
}
/* Task #274: a WT CMMS account has no "Search everything" field (its nav is
   deliberately cut down), so its camera button has nothing to be tucked
   against - every rule above is scoped to form.nav-search and none of them
   reached it, leaving a bare browser-default button in the middle of the
   nav. This gives it the same frame the search control has, on its own. */
.nav-photo-btn.standalone {
  margin-left: 20px;
  vertical-align: middle;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  padding: 6px 11px;
}
.nav-photo-btn.standalone:hover { background: var(--accent-soft); color: var(--accent); }
.nav-photo-btn.standalone.drop-active { background: var(--accent); border-color: var(--accent-border); }

/* Set by the script while a lookup is in flight. A deliberate pulse rather
   than a fake spinner: the request takes as long as it takes (one vision
   call plus a catalog query) and there's no progress to report, so animating
   something that looks like a progress indicator would be a lie. */
.nav-photo-btn.is-busy { animation: nav-photo-pulse 0.9s ease-in-out infinite; cursor: progress; }
@keyframes nav-photo-pulse { 50% { opacity: 0.45; } }

/* Photo-search results overlay (public/photo-search.js). Results appear over
   whatever page you were on, since the camera button lives in the nav and is
   reachable from everywhere - navigating away to a dedicated results page
   would lose the user's place for what is usually a quick "where is this?"
   lookup. */
.photo-search-overlay {
  display: none;
  position: fixed; inset: 0; z-index: 200;
  background: rgba(15,23,42,0.55);
  padding: 20px;
  overflow-y: auto;
}
.photo-search-overlay.open { display: block; }
.photo-search-panel {
  background: var(--surface); border-radius: 14px; max-width: 560px; margin: 40px auto;
  box-shadow: 0 20px 50px -12px rgba(15,23,42,0.4);
  overflow: hidden;
}
.photo-search-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 18px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.photo-search-close {
  background: none; border: none; font-size: var(--fs-24); line-height: 1; cursor: pointer;
  color: var(--muted); padding: 0 4px; min-height: 32px;
}
.photo-search-close:hover { color: var(--ink); }
.photo-search-body { padding: 16px 18px 18px; }

/* Task #274: QX on the first cut - "still looks funny and not impressive".
   The results were correct but presented as a cramped 36px thumbnail beside
   three lines of similarly-weighted text, so nothing told you at a glance
   which result mattered or - the actual question being asked - where the
   thing is. Rebuilt below around that question: a photo big enough to
   recognise the item from, the location as the largest line after the name,
   and everything else demoted to small grey supporting text.
   Styled in the CMMS's own vocabulary (the card radius, .badge, the grey
   #6b7280 muted text used everywhere else) rather than copying the layout of
   the app QX showed me - it should look like part of this system. */

/* "Photo looks like: a blue folding chair" - the model's read of the photo,
   stated up front so a wrong result is immediately explicable ("it thought
   this was a bin") instead of just looking broken. */
.photo-search-caption {
  margin: 0 0 14px; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-8);
  font-size: 13px; color: var(--ink-2);
}
.photo-search-caption strong { color: var(--ink); }

.photo-search-result {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 14px; margin-bottom: 10px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.photo-search-result:hover {
  border-color: var(--border-strong); box-shadow: 0 4px 12px -4px rgba(15,23,42,0.18);
}
.photo-search-result:last-of-type { margin-bottom: 0; }
/* Deliberately much bigger than the 36px .photo-thumb-md used in list
   tables: here the photo IS the identification - you are checking whether
   the thing on screen is the thing in your hand. */
.photo-search-thumb {
  width: 84px; height: 84px; flex: 0 0 84px;
  object-fit: cover; border-radius: var(--radius-8); background: var(--surface-2);
}
/* Same footprint when there's no photo on file, so a mixed list of results
   doesn't stagger left and right down the panel. */
.photo-search-thumb.empty {
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--border-strong); color: var(--muted-2); font-size: 11px;
  text-align: center; padding: 6px; line-height: 1.3;
}
.photo-search-main { flex: 1; min-width: 0; }
.photo-search-title {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 6px;
}
.photo-search-title a {
  font-size: 16px; font-weight: 600; color: var(--accent); text-decoration: none;
}
.photo-search-title a:hover { text-decoration: underline; }
/* The answer to the question the person actually asked. */
.photo-search-loc {
  display: flex; align-items: flex-start; gap: 6px;
  font-size: 15px; color: var(--ink); margin-bottom: 4px;
}
.photo-search-loc svg { flex: 0 0 auto; margin-top: 2px; color: var(--muted); }
.photo-search-loc.none { color: var(--muted-2); font-style: italic; font-size: var(--fs-14); }
.photo-search-meta { font-size: var(--fs-12); color: var(--muted); line-height: 1.5; }
.photo-search-foot {
  margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--border);
  font-size: var(--fs-12); color: var(--muted);
}
.photo-search-foot span { display: block; }
/* Also used on its own on the no-results and error paths, where there's no
   .photo-search-foot around it to inherit from. */
.photo-search-remaining {
  display: block; margin-top: 4px; font-size: var(--fs-12); color: var(--muted);
}

/* Task #272: QX moved to the camera button and immediately hit "i cant drop
   and drag it inside now" - the page that got deleted had a proper dropzone,
   and a button that only opens a file picker is a step backwards on a
   desktop where dragging a photo in is the natural action. Dropping an image
   anywhere on the page now runs a photo search, and this is the banner that
   says so while a file is being dragged over. Deliberately page-wide rather
   than a small target in the nav: a 30px button is an unreasonably fiddly
   thing to aim a dragged file at.
   Drops onto the app's existing photo dropzones (item form, consumables,
   fault tickets) are left alone by the script - those are uploads, not
   searches. */
.photo-drop-hint {
  display: none;
  position: fixed; inset: 0; z-index: 190;
  background: rgba(30,58,95,0.55);
  pointer-events: none; /* never swallow the drop event itself */
}
.photo-drop-hint.open { display: flex; align-items: center; justify-content: center; }
.photo-drop-hint span {
  background: var(--surface); color: var(--accent); font-size: 17px; font-weight: 600;
  padding: 18px 26px; border-radius: 14px;
  border: 2px dashed var(--accent);
  box-shadow: 0 20px 50px -12px rgba(15,23,42,0.4);
}

/* Task #273: brief highlight on a form section that has just appeared
   because of something the person ticked elsewhere on the page - see
   partials/access_toggles.ejs. Without it, ticking "can work Fault Tickets"
   revealed the Trade access checkboxes silently, several hundred pixels up
   the form, and read as nothing having happened. */
.section-flash {
  animation: section-flash-fade 1.6s ease-out;
  border-radius: var(--radius-8);
}
@keyframes section-flash-fade {
  0%, 35% { background: var(--warn-soft); box-shadow: 0 0 0 8px var(--warn-soft); }
  100% { background: transparent; box-shadow: 0 0 0 8px transparent; }
}


form.inline { display: inline; }
.container { max-width: 1080px; margin: 0 auto; padding: 24px; }

/* Section tabs (e.g. Dashboard / Fault Tickets / PPM on the Maintenance
   pages) - an underline style, since these sit right under an <h1> and are
   meant to read as "which page of this section am I on", not a control. */
.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 16px; border-bottom: 2px solid var(--border); }
.tabs a {
  padding: 8px 14px; font-size: var(--fs-14); color: var(--muted); text-decoration: none;
  border-bottom: 2px solid transparent; margin-bottom: -2px; font-weight: 500;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.tabs a:hover { color: var(--ink); }
.tabs a.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 700; }

/* Segmented view-switch tabs (e.g. List / Calendar on the PPM page) - a
   pill-style control instead of the underline style above, since these
   toggle between two renderings of the same data rather than different
   pages. */
.seg-tabs { display: inline-flex; background: var(--surface-2); border-radius: 10px; padding: 3px; gap: 2px; margin-bottom: 14px; }
.seg-tabs a {
  padding: 6px 16px; font-size: 13px; font-weight: 600; color: var(--ink-2); text-decoration: none;
  border-radius: 7px; transition: color 0.15s ease;
}
.seg-tabs a:hover { color: var(--ink); }
.seg-tabs a.active { background: var(--surface); color: var(--accent); box-shadow: 0 1px 2px rgba(15,23,42,0.08), 0 1px 1px rgba(15,23,42,0.04); }
h1 { font-size: 26px; margin-bottom: 4px; }
h2 { font-size: 19px; margin-top: 28px; }
.subtitle { color: var(--muted); margin-top: 0; margin-bottom: 20px; }
/* "Clean & modern" pass (QX, PPM page feedback): a softer, more layered
   shadow instead of a flat 1px border + faint shadow reads less like a
   plain boxed-in panel and more like a card actually sitting above the
   page background. Slightly larger radius and a touch more inner padding
   for breathing room. Kept intentionally subtle/neutral (no color) since
   this is a shared class used on every page, not just PPM. */
.card {
  background: var(--surface); border-radius: 14px; padding: 20px 22px; margin-bottom: 18px;
  box-shadow: 0 1px 2px rgba(15,23,42,0.04), 0 8px 24px -14px rgba(15,23,42,0.12);
  border: 1px solid var(--border);
}
.alert-section h3 { margin: 0 0 10px 0; font-size: 15px; }
.alert-row {
  display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--surface-2); font-size: var(--fs-14);
}
.alert-row:last-child { border-bottom: none; }
.badge {
  display: inline-block; padding: 2px 8px; border-radius: var(--radius-12); font-size: var(--fs-12); font-weight: 600;
}
.badge.red { background: var(--bad-soft); color: var(--bad); }
.badge.orange { background: var(--warn-soft); color: var(--warn-strong); }
.badge.yellow { background: var(--warn-soft); color: var(--warn); }
.badge.gray { background: var(--border); color: var(--ink-2); }
.badge.green { background: var(--ok-soft); color: var(--ok); }
.badge.blue { background: var(--accent-soft); color: var(--accent); }
/* Restricted-area fault ticket rows (see routes/maintenance.js isRestricted,
   lib/auth.js isTicketAreaRestricted) - only owner/admin ever see these rows
   at all (everyone else has them filtered out of the list entirely), so this
   is purely a "notice me" cue for that row's Area cell rather than an access
   control of its own. A soft red tint on the row plus a small badge next to
   the Area text - not color alone, since a status/priority pill in the same
   row can already be red for an unrelated reason. */
tr.restricted-row { background: var(--bad-soft); }
tr.restricted-row:hover { background: var(--bad-soft); }
/* The generic tr:hover td rule elsewhere paints its own background on each
   td, which (being a separate element) would otherwise sit on top of and
   hide this row's tint during hover - override it here so hovering a
   restricted row stays visibly red instead of flashing back to the default
   hover gray. */
tr.restricted-row:hover td { background: var(--bad-soft); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Storage items list - Loanable column. Centered on desktop, under its own
   narrow header column (see the mobile override for why this changes on a
   phone). Was an inline style="text-align:center" on the <td> itself until
   Task #265 - moved to a class so the mobile media query below can actually
   override it (an inline style always beats an external stylesheet rule
   regardless of specificity, so there was no way to change this per-
   breakpoint while it stayed inline). */
.loanable-cell { text-align: center; }
/* Task #266: list-row photo thumbnails (Storage items list, Assets list) -
   moved off inline width/height styles and onto these two classes so the
   mobile media query below can size them differently than desktop (an
   inline style always wins over an external stylesheet rule, same reason
   .loanable-cell had to move off an inline style earlier - see that
   comment). Two sizes, matching what each list already used: items_list is
   less dense (fewer columns) so its thumbnail was already bigger than
   Assets' own denser table. */
.photo-thumb-md { width: 36px; height: 36px; object-fit: cover; border-radius: var(--radius-4); cursor: pointer; }
.photo-thumb-sm { width: 24px; height: 24px; object-fit: cover; border-radius: var(--radius-4); cursor: pointer; }
.status-select {
  border-radius: var(--radius-12); padding: 3px 22px 3px 8px; font-size: 12px; font-weight: 600; border: 1px solid transparent; cursor: pointer;
  /* A native <select>'s own browser-drawn dropdown arrow was rendering on
     top of the pill's last letter (this padding was copied from .badge,
     which never needed room for one) - e.g. "Closed" reading as "Close"
     plus a stray glyph. appearance:none removes that native arrow, and this
     small inline chevron plus the extra right padding above replaces it
     with one that actually fits, so the pill still reads as a dropdown
     without clipping into the text. */
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5l3.5 3.5 3.5-3.5' stroke='%23374151' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 7px center; background-size: 9px;
  /* Belt-and-braces on top of appearance:none: some mobile browsers still
     size a <select> to some internal minimum widget width rather than its
     own text + the padding above, which was still clipping "In Progress"/
     "Closed" on a phone even after the arrow-overlap fix. A generous
     min-width (comfortably wider than "In Progress", the longest option
     used in either the Status or Priority quick-editors) guarantees the
     text always has room regardless of what a given browser does on its
     own, and text-align:center keeps a shorter value like "Low" looking
     deliberate rather than stranded on one side of the extra width. */
  min-width: 104px; text-align: center;
  /* QX: "maybe the priority and status can be smaller so that the assignee
     can look better?" They were not sized at all - .status-select set padding
     and colour but never a width, so the app-wide form rule further down
     (the one setting select width to 100%, around line 825) stretched each
     pill across its whole column. Braces deliberately not quoted in this
     comment: a stray closing brace inside a rule body is invisible to a
     browser but ends the rule as far as any tool reading the file is
     concerned - the test guarding this rule broke on exactly that. A 190px "Low" pill is why those two columns dominated the
     table and left Assignee squeezed.

     width:auto lets the pill hug its own text again, and min-width keeps a
     short value like "Low" from looking stunted beside "In Progress".
     Dropped 118 -> 104: 118 was padded for a mobile quirk (see above), and
     "In Progress" at 12px semibold measures about 70px, plus this rule's
     8px + 22px of padding - so 104 still clears it with room to spare on a
     desktop. The phone block restores 118, where the original clipping was
     actually seen. */
  width: auto;
}
.status-select.status-orange { background: var(--warn-soft); color: var(--warn-strong); }
.status-select.status-yellow { background: var(--warn-soft); color: var(--warn); }
.status-select.status-gray { background: var(--border); color: var(--ink-2); }
.status-select.status-red { background: var(--bad-soft); color: var(--bad); }
/* Assignee cell on the Fault Tickets list.

   QX on the first version: "looks very clunky". Every row carried a bordered
   dropdown, so a column of unassigned tickets read as a stack of form
   controls with an occasional name among them - the control was heavier than
   the data it sat beside, and there is one per row.

   So the picker is dressed as a quiet link rather than a field: no border, no
   box, no browser chevron. It is still a real <select>, which matters more on
   a phone than any custom menu would - tapping it opens the native picker,
   with the native scrolling and search that comes with it. Only its weight
   changed, not what it is.

   Deliberately NOT .status-select's treatment (used by Priority and Status):
   those show a current value that someone changes, so they should look like
   controls. This one shows nothing - it is an action, and an action that most
   rows will never use. */
.assignee-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.assignee-add { display: block; margin-top: 2px; }
.assignee-chips + .assignee-add { margin-top: 4px; }
/* It is now an <input list> rather than a <select>. QX: "The scrolling for the
   name is very hard if I have 90 people. Can there be a search function." A
   native dropdown has no search on a desktop browser, and ninety names is a
   long way to scroll on a phone.

   Resting state is unchanged - it still reads as the quiet "+ Add…" link
   described above, with no box around it - because the argument for that has
   not changed: most rows never use it. The box only appears on focus, once
   the person has committed to typing and needs to see where the text goes. */
.assignee-add .assignee-search {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  border: none; background: none; box-shadow: none;
  padding: 0; margin: 0; height: auto; min-height: 0;
  /* Wide enough for "+ Add…" at rest; it grows on focus, below. Left at a
     fixed width rather than sized to content, because an input cannot size to
     its placeholder without script. */
  width: 68px;
  font-size: var(--fs-12); font-weight: 600; color: var(--accent);
  font-family: inherit; cursor: pointer;
}
.assignee-add .assignee-search::placeholder { color: var(--accent); opacity: 1; font-weight: 600; }
.assignee-add .assignee-search:hover::placeholder { text-decoration: underline; }
.assignee-add .assignee-search:focus {
  /* Now it is a field, so it looks like one - and it needs room for a name,
     not for "+ Add…". */
  width: 150px; cursor: text;
  border: 1px solid var(--border); border-radius: 6px;
  padding: 4px 6px; background: var(--card);
  color: var(--text); font-weight: 400;
  outline: 2px solid var(--focus-ring); outline-offset: 1px;
}
.assignee-add .assignee-search:focus::placeholder { color: var(--muted-2); font-weight: 400; }
.empty { color: var(--muted); font-size: var(--fs-14); padding: 6px 0; }
table { width: 100%; border-collapse: collapse; font-size: var(--fs-14); }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); }
/* Assets list: table-layout:auto (the default above) sizes every column
   from its own content, which for this table meant short-content columns
   like Trade/Classification/Status ended up carrying leftover width as
   blank padding, while Name and Location - the two columns that actually
   run long - got squeezed into wrapping across several lines. Scoped to
   just this table (not the base `table` rule) via table-layout:fixed plus
   the <colgroup> widths in views/assets_list.ejs, which hand each column a
   deliberate share instead of leaving the browser to guess. word-break
   guards against a single long unbroken token (e.g. a location code with no
   spaces) overflowing its now-fixed column instead of wrapping. */
.assets-table { table-layout: fixed; }
.assets-table td { word-break: break-word; }
/* The Trade tag is display:inline-block + white-space:nowrap (see .tag), and
   this table is table-layout:fixed with colgroup shares - so a trade name
   wider than its column share does not wrap, it OVERHANGS into Location.
   That is the #181 bug, and it was still present: measured at 28px of
   overhang for "Lifts & Escalators" before this sizing pass, on real-shaped
   data (trade names are stored "SHORT (Long)" and displayed short - see the
   split(' (')[0] in the views).
   Clipping to the column with an ellipsis keeps the tag inside its own cell
   at any font size. Truncating a label is a smaller loss than overlapping the
   next column, and the full trade is on the asset's own page. Scoped to this
   table: every other .tag in the app sits in flexible space and must not
   clip. */
.assets-table td .tag { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
.assets-table td form { max-width: 100%; }
/* QX: too much empty space between the checkbox and the Name column. The
   base th/td rule above gives every cell 12px of horizontal padding on
   each side regardless of how wide its column actually is - fine for a
   16%-wide text column, but on the 36px checkbox column and the (now 28px,
   was 44px - see the <colgroup> comment in assets_list.ejs) photo column,
   usually blank since most assets have no photo, that's 24px of padding
   added on top of an already-narrow column, which is most of what reads as
   "a gap" before the Name text even starts. Tightening just these two
   narrow utility columns (via an explicit class rather than
   :first-child/:nth-child, since the checkbox column only exists for
   canManage and would shift which position is "first" depending on role)
   closes that gap without touching the padding text columns actually need
   for readability. QX pushed on this a second time after 6px still left a
   visible gap (most of what remained was the photo column's own width, not
   padding - see the colgroup comment) - dropped to 4px here too since
   there's no other content in these cells competing for breathing room. */
.assets-table .tight-cell { padding-left: 4px; padding-right: 4px; }
/* The word-break rule above only ever covered td - but a column header can
   be a single long unbroken word too ("CLASSIFICATION" - 14 characters,
   uppercase, no spaces to break at), and th had no equivalent protection.
   Narrowing that column (see the <colgroup> comment in assets_list.ejs)
   made this a real, visible bug: the header text couldn't wrap, wasn't
   clipped, and overflowed sideways into the "STATUS" header next to it -
   the exact same failure mode the .tag fix above addresses for td content,
   just one row up, in a cell this rule never reached. */
.assets-table th { word-break: break-word; }
/* A trade tag (.tag) is nowrap everywhere else in the app on purpose - see
   the comment on .tag itself - so its colored background reads as one
   unbroken pill instead of splitting across a line wrap. That's fine when a
   tag's container can grow to fit it, but the Assets table is
   table-layout:fixed with a deliberately narrow 7% Trade column (see the
   comment above), and there's no overflow:hidden on these cells - so a
   longer trade name than "HVAC"/"O&M" (e.g. "Logistics / Storage / BM")
   can't wrap, doesn't get clipped either, and instead visibly overflows
   past the column's edge, overlapping the Location text next to it.
   table-layout:fixed only matters above the 700px breakpoint - below that,
   the mobile media query further down turns every td into a full-width
   block (its own line, no neighboring column to spill into), so the narrow-
   column problem this exists for simply doesn't occur there. Learned the
   hard way: putting this at every width instead of gating it to desktop
   made even short one-word tags like "HVAC" wrap (some phones size an
   inline-block's line box off word-break's relaxed break opportunities
   even when there's plenty of room, not just when content would actually
   overflow) - QX caught this as "HVA" / "C" splitting across two lines on
   the phone card layout, where the tag previously always fit on one line.
   Scoping to min-width:701px keeps the overflow fix for the one place it's
   needed without touching how any tag behaves on a phone. */
@media (min-width: 701px) {
  .assets-table td .tag { white-space: normal; word-break: break-word; }
}
/* QX: the Class pill (manager's quick-edit <select>) was reading much wider
   than its "Class 1"/"Class 3" text on the desktop table, not just on the
   phone card layout this was already fixed for. Root cause is the same
   generic form-control rule everywhere else in the app - `select { width:
   100%; ... }` (see the base input/select rule near the top of this file) -
   which the mobile fix only ever overrode inside its own @media (max-width:
   700px) block, so above that breakpoint this select still stretched to
   fill its whole table cell (12% of a wide desktop table is a lot more
   pixels than "Class 1" needs.

   width:auto alone (the first attempt at this) turned out not to be enough,
   still leaving the pill visibly wider than its text - because width:auto
   only controls the box's own natural size, and .status-select separately
   carries a min-width:118px floor (added for Status/Priority, whose longest
   value is "In Progress" - see .status-select's own comment). That floor
   applies here too regardless of width:auto, so the box never actually got
   to shrink past 118px. Clearing min-width to 0 - the same fix the mobile
   version of this pill already needed for the same reason - is what
   actually lets it shrink to "Class 1"'s real width. Also carrying over the
   mobile pill's other two fixes for the same visual consistency: dropping
   the reserved dropdown-chevron space (22px right padding down to 8px,
   background-image:none) so the box isn't holding room for an arrow this
   compact, obviously-tappable pill doesn't need, and the same asymmetric
   10px/6px padding nudge that corrects for "Class 1" reading right-heavy
   due to its trailing thin digit not filling its character box the way a
   leading "C" does (see the mobile version of this same comment). Unlike
   mobile, plain width:auto (not fit-content) is enough once min-width is
   cleared - the "some browsers won't shrink a bare select below an
   intrinsic default box" quirk that forced a JS-measured width on the phone
   (see fitClassificationSelect in assets_list.ejs) is a mobile-browser
   thing, desktop browsers don't share it, so no script is needed here. */
@media (min-width: 701px) {
  .assets-table td[data-label="Class"] select,
  .assets-table td[data-label="Class"] .badge {
    width: auto;
    min-width: 0;
    padding-left: 10px;
    padding-right: 6px;
  }
  .assets-table td[data-label="Class"] select {
    background-image: none;
  }
}
/* Task #393. QX: "Also the rows looks funny. All different sizes".
   The PPM list alternated between 56px and 89px rows, because a row's height
   depended on how many optional extra lines it happened to carry: the asset
   name (Task #390), and "Last serviced". Measured on the live page at 1280px
   there were exactly two heights, and the whole gap came from the
   last-serviced line wrapping - fixed with white-space:nowrap in
   ppm_tasks.ejs. That alone brought the tall rows to 72px; this floor lifts
   the short ones to meet them, so the list has ONE rhythm rather than two.

   height on a table row is a MINIMUM, not a fixed size (CSS tables grow to
   fit their content), so a genuinely long task name that wraps still gets the
   space it needs - the row is taller because there is more to read, which is
   information rather than noise. min-height would have been the obvious
   choice and does nothing at all on a td; that was the first thing tried.

   Scoped to #ppm-table, and to min-width:701px, because below that the ONE
   @media (max-width: 700px) block turns every row into a stacked card - where
   a 72px floor would squash the card, not tidy it. 701px is the same
   breakpoint the rest of this file uses. */
@media (min-width: 701px) {
  #ppm-table tbody tr { height: 72px; }
  /* ...but not the inline Edit / Mark done rows, which are full-width forms
     revealed under a task. Those size to their own content and a floor on
     them would just add dead space under a collapsed one. */
  #ppm-table tbody tr.edit-task-row,
  #ppm-table tbody tr.mark-done-row { height: auto; }
}
/* Light header shading so the header row reads as a distinct band at a
   glance instead of just being slightly smaller gray text sitting flush
   with the data below it - a common "clean SaaS table" cue. */
th { color: var(--muted); font-weight: 600; font-size: 13px; text-transform: uppercase; background: var(--surface-2); }
tr:hover td { background: var(--surface-2); transition: background 0.12s ease; }
/* Tried letting #users-table overflow sideways and scroll instead of
   wrapping (width:auto + white-space:nowrap on every cell) so no cell text
   ever wrapped, but QX preferred the previous behavior - everything visible
   at once on one screen, without having to scroll to see the rest of a
   row. Reverted back to the plain width:100% table above. Cells can still
   wrap if a name is long, but the Venues/Trades/Departments "+N more"
   summarizing (see views/users.ejs summarizeNames()) and the shortened
   trade names already cut most of what used to force multi-line wrapping,
   so this should stay reasonably tidy without needing horizontal scroll. */
/* Users table can run wide (long Venues/Departments lists), and its
   action buttons (Reset password, Edit - now both in one Actions column,
   see views/users.ejs) live in the very last column, so on a normal-width
   screen that column scrolls out of view inside the table's own
   overflow-x:auto wrapper, with nothing on screen hinting there's more to
   scroll to. Pinning it to the right of the scroll area keeps both
   buttons always visible, the same idea as a frozen column in a
   spreadsheet. A fixed min-width comfortably fits both buttons side by
   side without wrapping. */
#users-table th:last-child, #users-table td:last-child {
  position: sticky;
  right: 0;
  min-width: 90px;
  box-shadow: -6px 0 6px -6px rgba(0,0,0,0.15);
}
/* The background is set SEPARATELY for the header cell and the body cells,
   and that separation is the whole point. QX: "the bar color is off. Doesnt
   cover the last column" - the grey header bar stopped short of ACTIONS,
   leaving one white cell on the end of an otherwise solid strip.

   Cause: this rule used to set `background: var(--surface)` on
   `th:last-child, td:last-child` together. A pinned cell MUST be opaque, or
   the row it is floating over shows through it while the table scrolls
   sideways - so the background is not optional. But `#users-table
   th:last-child` outranks the generic `th { background: var(--surface-2) }`
   on specificity, so painting both in one rule quietly repainted the header
   cell white along with the data cells that actually needed it.

   Both still get an opaque background; they just get the RIGHT one. */
#users-table th:last-child { background: var(--surface-2); }
#users-table td:last-child { background: var(--surface); }
#users-table tr:hover td:last-child { background: var(--surface-2); }
/* Dropping the Email column (see views/users.ejs) freed up width, but the
   Name column was still wrapping two-word names like "Lim Yew Siong" onto
   two lines - the table's auto layout was handing that freed-up space to
   the other columns instead. Giving Name a floor and keeping it on one
   line claims that space back for it first; the other columns (Venues,
   Trades, Departments, etc.) still wrap as needed same as before. */
#users-table th:first-child, #users-table td:first-child {
  min-width: 140px;
  white-space: nowrap;
}
/* Task #383: the team-tag filter row on the Users page. Reuses .tabs for its
   shape (see the Users page tab row right above it) so the two rows read as
   the same kind of control, with two differences: no bottom border, because
   two stacked underlined rows look like two sets of tabs rather than tabs and
   a filter; and smaller text, because this one is a refinement of the page
   the tabs above already chose. */
/* A tag pill that is also a filter. Looks exactly like the plain .tag beside
   it - it IS the same label - and only the cursor and a hover tint say it
   does something, because a row of loud buttons down the Name column is the
   "clunky" problem from the assignee picker in another form. */
a.tag-pill-filter { text-decoration: none; cursor: pointer; }
a.tag-pill-filter:hover { filter: brightness(0.94); }

a.btn, button.btn, summary.btn {
  display: inline-block; background: var(--accent); color: var(--accent-ink); padding: 8px 14px; border-radius: 7px;
  text-decoration: none; border: none; cursor: pointer; font-size: var(--fs-14); font-family: inherit;
  /* Without this, a button squeezed inside a flex row with flex-wrap:nowrap
     (e.g. the PPM list's Edit/Mark done/Delete row) can still shrink narrower
     than its own text, wrapping "Mark done" onto two lines and making that
     one button look oversized next to its single-line neighbors. */
  white-space: nowrap;
  transition: filter 0.12s ease;
}
a.btn:hover, button.btn:hover { filter: brightness(1.08); }
a.btn:active, button.btn:active { filter: brightness(0.96); }
/* UI audit, 10 Oct 2026 ("think Google"): secondary buttons are outlined -
   white, grey border, green text - so the one green primary button on a page
   is the obvious next step, instead of a row of heavy dark-grey blocks. */
a.btn.secondary, button.btn.secondary, summary.btn.secondary { background: #fff; color: var(--accent); box-shadow: inset 0 0 0 1px #dadce0; }
a.btn.secondary:hover, button.btn.secondary:hover, summary.btn.secondary:hover { filter: none; background: var(--accent-soft); }
a.btn.danger, button.btn.danger { background: var(--bad); }
a.btn.small, button.btn.small { padding: 5px 10px; font-size: 13px; }
/* Task #263 (phone workflow audit): a disabled button (e.g. "Identify from
   photo" before any photo is picked - see item_form.ejs) previously had no
   disabled styling of its own, so it fell back to each browser's own
   default - inconsistent, and on some mobile browsers barely different from
   the enabled look. QX had already run into "why can't I click this"
   confusion with this exact button once (Task #261/#262's script-ordering
   bug); a clearly dimmed, non-interactive appearance makes "this isn't
   ready yet" obvious at a glance instead of only discoverable by tapping
   it. filter:none overrides the :hover/:active brightness rules above,
   which would otherwise still fire on a disabled button on devices that
   emulate hover on tap. */
button.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  filter: none;
}
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.between { justify-content: space-between; }
/* Task #264: every list/detail page's header - title+subtitle on one side,
   action buttons (Cost by department / Export / + Add item, etc.) on the
   other - is this same "row between" pattern (21 views use it), immediately
   followed by that page's filter bar or content, which had no top margin of
   its own. On desktop the two sit close but each reads as its own line; on
   a narrow phone screen .row's flex-wrap drops the button group onto its
   own line right under the title, and then the filter row right under
   that with zero breathing room - QX flagged this as "buttons touching the
   fields" on the Storage items list, but it's the same header pattern
   everywhere this class is used, not a one-off. One shared margin instead
   of patching each page's own markup separately. */
.row.between { margin-bottom: 16px; }
label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; color: var(--ink-2); }
input[type=text], input[type=number], input[type=date], input[type=time], input[type=email], input[type=password], input[type=search], select, textarea {
  width: 100%; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 7px; font-size: var(--fs-14); font-family: inherit;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* A visible focus ring wasn't defined anywhere before (so it fell back to
   each browser's own default outline) - this gives every text field,
   dropdown, and textarea in the app the same deliberate, on-brand focus
   state instead of an inconsistent default. */
input[type=text]:focus, input[type=number]:focus, input[type=date]:focus, input[type=time]:focus, input[type=email]:focus,
input[type=password]:focus, input[type=search]:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-ring);
}
/* ---------- Filter bars ----------
   Every list page (Storage items, Consumables, Fault Tickets, PPM, Assets)
   puts its dropdowns in a `<form class="row filter-form">`, and .row is a
   flex row - so they were meant to sit side by side. They never did. The
   generic form-control rule directly above sets `select { width: 100% }`,
   and a flex item's width beats its content when flex-basis is auto, so each
   dropdown demanded the full line and wrapped onto its own row. On the
   Storage items list that was five stacked full-width dropdowns, ~220px of
   chrome, before the first item appeared - on every page, for every account.

   Sizing them to their own content puts the bar back on one line. max-width
   keeps a dropdown with one very long option (a venue or department name)
   from pushing the row wide, and flex-wrap on .row still stacks them
   gracefully when the viewport really is too narrow.

   Deliberately scoped to .filter-form and not to `.row select` generally:
   plenty of other rows hold a select that is SUPPOSED to fill its line. */
.filter-form select,
.filter-form input[type=search],
.filter-form input[type=text] {
  width: auto;
  min-width: 128px;
  max-width: 260px;
}
/* The standalone search box next to a filter bar reads as part of the same
   control group, so it gets the same treatment - it just wants more room
   than a dropdown, since its placeholder is a sentence. */
.filter-search input[type=search] { width: auto; min-width: 200px; max-width: 320px; }
/* The phone override for these lives in the ONE @media (max-width: 700px)
   block at the bottom of this file, not here. A second phone block was the
   obvious place to put it and is exactly what test/phoneNav.test.js exists to
   stop: with two, which rule wins depends on which block you happened to write
   in, and that is invisible from either one. */

.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 20px; }
.check-row { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.check-row input { width: auto; }
/* Quantity stepper (see partials/foot.ejs) - large tap targets on either side
   of the number input so a phone in a storeroom doesn't need the on-screen
   keyboard for a quick +1/-1. 44px is the commonly-cited minimum comfortable
   touch-target size. */
.stepper { display: flex; align-items: stretch; gap: 0; }
.stepper input[type=number] { width: 100%; text-align: center; border-radius: 0; }
.stepper-btn {
  flex: 0 0 44px; min-height: 44px; border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--accent);
  font-size: var(--fs-20); font-weight: 600; cursor: pointer; line-height: 1; padding: 0;
}
.stepper-btn:first-child { border-radius: 6px 0 0 6px; border-right: none; }
.stepper-btn:last-child { border-radius: 0 6px 6px 0; border-left: none; }
.stepper-btn:hover { background: var(--border); }
.stepper-btn:active { background: var(--border-strong); }
/* Saved filter views (see partials/foot.ejs) */
.saved-view-banner {
  background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent-border); border-radius: 6px;
  padding: 8px 12px; margin-bottom: 14px; font-size: 13px;
}
.saved-view-banner a { color: var(--accent); font-weight: 600; }
/* Long checkbox lists (venue/trade/department access on the Users page) -
   used to be a single wrapping row, which turned into a ragged, hard-to-scan
   block once department lists grew past ~10 items. Multi-column layout lets
   the browser lay them out into neat, aligned columns instead; the bordered
   box gives the group a clear visual boundary so it doesn't blur into
   surrounding form fields. */
.checkbox-columns {
  /* "220px 3" = at most 3 columns, but never narrower than ~220px each -
     the browser drops to 2 or 1 columns on its own as the viewport narrows,
     so long department names (e.g. "Identity & Resource Ministry (Design)")
     never get squeezed, without needing extra breakpoints for this alone. */
  columns: 220px 3; column-gap: 24px; border: 1px solid var(--border); border-radius: var(--radius-8);
  padding: 10px 16px; margin-top: 6px; background: var(--surface-2);
}
.checkbox-item {
  display: flex; align-items: center; gap: 6px; font-weight: 400; font-size: var(--fs-14);
  color: var(--ink); margin: 6px 0; break-inside: avoid;
}
.checkbox-item input[type=checkbox] { width: auto; }
/* Task #258: bumped from font-size 14px / no border to this - an upload
   failure (oversized photo, unsupported file type) used to land here as
   plain small red text easy to miss right above a form full of other
   fields, which made a specific, fixable problem look like "the save just
   didn't work." Bigger, bolder, and a solid border (not just a tinted
   background) so it reads as a distinct alert rather than blending into
   the page. */
.error { background: var(--bad-soft); color: var(--bad); padding: 12px 16px; border-radius: 6px; border: 1px solid var(--bad-border); margin-bottom: 14px; font-size: 15px; font-weight: 600; }

/* Task #260: inserted (by foot.ejs) right next to the Save button the
   moment a form with an attached photo is submitted - see that file's
   comment for why a relabelled button alone wasn't enough. Amber rather
   than the red .error above: this isn't a problem, just a "please wait". */
.upload-progress-banner {
  display: flex; align-items: center; gap: 10px;
  background: var(--warn-soft); color: var(--warn); padding: 12px 16px;
  border-radius: 6px; border: 1px solid var(--warn-border);
  margin-bottom: 14px; font-size: 15px; font-weight: 600;
}
.upload-progress-spinner {
  width: 16px; height: 16px; flex: 0 0 auto;
  border: 3px solid var(--warn-border); border-top-color: var(--warn);
  border-radius: 50%; display: inline-block;
  animation: upload-progress-spin 0.8s linear infinite;
}
@keyframes upload-progress-spin { to { transform: rotate(360deg); } }

.login-wrap { max-width: 400px; margin: 72px auto; padding: 0 16px; }
/* Login refresh (QX, 18 Oct 2026): one card, the app's teal-to-green band on top (same as the greeting band). */
.card.login-card { padding: 0; overflow: hidden; border-radius: var(--radius-16); box-shadow: var(--shadow-2); }
.login-band { display: flex; align-items: center; gap: 12px; padding: 20px 24px; color: #fff;
  background: linear-gradient(110deg, #0e6f78 0%, #0e7c86 45%, #138a5e 100%); }
.login-band h1 { margin: 0; color: #fff; font-size: var(--fs-20); }
.login-band .subtitle { margin: 2px 0 0; color: #e2f4f2; font-size: var(--fs-14); }
.login-logo { width: 44px; height: 44px; border-radius: var(--radius-12); background: rgba(255,255,255,.18); display: grid; place-items: center;
  font-weight: 800; letter-spacing: .02em; }
.login-body { padding: 20px 24px 24px; }
.login-hi { margin: 0 0 12px; font-size: var(--fs-20); font-weight: 700; }
.login-ok { margin-bottom: 12px; padding: 8px 12px; border-radius: var(--radius-8); background: var(--ok-soft); color: var(--ok); font-size: var(--fs-14); }
.btn.login-btn { width: 100%; margin-top: 16px; height: 40px; border-radius: var(--radius-pill); }
.login-links { margin: 12px 0 0; text-align: center; font-size: var(--fs-14); }
.login-or { margin: 16px 0 0; text-align: center; color: var(--muted); font-size: var(--fs-12); }
a.btn.login-one { display: flex; align-items: center; justify-content: center; margin-top: 8px; text-decoration: none; }
.login-foot { text-align: center; color: var(--muted); font-size: var(--fs-12); margin-top: 16px; }
/* display:inline-block + white-space:nowrap so a tag's own background pill
   never breaks mid-text (e.g. "+ Tickets" splitting into "+" on one line and
   "Tickets" on the next, each with its own disjoint bit of pill background,
   overlapping the row above) - a narrow column just wraps the whole tag as
   one unit onto the next line instead. */
.tag { font-size: 13px; background: var(--violet-soft); color: var(--violet); padding: 2px 8px; border-radius: 10px; display: inline-block; white-space: nowrap; }

/* Chip with its own remove control, for lists of people (loan approvers).
   The point is that the name and its × are ONE element: the previous version
   put a separate "remove" button after each .tag, which meant that once the
   row wrapped, a name could end up on one line and its remove button on the
   next - and the resulting mis-click removed the wrong person's rights.
   inline-flex keeps them welded together; the row below wraps between chips
   rather than inside one. */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; background: var(--violet-soft); color: var(--violet);
  padding: 2px 4px 2px 8px; border-radius: 10px; white-space: nowrap;
}
.chip form { display: inline-flex; margin: 0; }
/* 22px square: below the 44px tap target used for primary controls, which is
   deliberate - this is a destructive action sitting inside a dense list, and a
   large easy-to-hit × next to a name is how the wrong person gets removed. It
   is paired with a data-confirm for the same reason. */
.chip-x {
  border: 0; background: transparent; color: var(--violet); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0; width: 22px; height: 22px;
  border-radius: var(--radius-8);
}
.chip-x:hover { background: var(--bad-soft); color: var(--bad); }
.muted { color: var(--muted); font-size: 13px; }

/* The Help page reads as one continuous explanation, so it gets ONE type size
   rather than each element carrying its own.

   The problem this fixes: .muted above sets 13px, and the Help page used it
   for the descriptive line under every heading ("Something broken, leaking,
   not working, unsafe."). Against an unstyled 16px body that is a three-pixel
   drop on text which is not a caption - it is the sentence telling you what
   the section is for. Elsewhere in the app .muted genuinely is secondary and
   the smaller size is right, so this resets the SIZE here only and keeps the
   colour, rather than changing .muted everywhere.

   line-height lives here too. The lists previously carried an inline 1.85
   while the paragraphs inherited the default, so prose and bullets sat on
   different rhythms down the page. */
.help-page p,
.help-page li { font-size: 16px; line-height: 1.75; }
.help-page .muted { font-size: 16px; }
.help-page h3 { font-size: 16px; }
/* Help refresh (QX, 18 Oct 2026): topic tiles that jump to each section, then ONE quiet list of sections
   (hairlines, a coloured icon tile per topic) instead of a stack of boxes. Palette colours only. */
.hp-topics { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin: 0 0 16px; }
.hp-topic { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #fff; border: 1px solid var(--line-soft);
  border-radius: var(--radius-12); color: var(--ink); text-decoration: none; }
.hp-topic:hover { border-color: var(--pal-teal); box-shadow: var(--shadow-1); text-decoration: none; }
.hp-topic b { display: block; font-size: var(--fs-14); font-weight: 600; }
.hp-topic small { display: block; font-size: var(--fs-12); color: var(--muted); margin-top: 2px; }
.hp-ico { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 10px; display: inline-grid; place-items: center; font-size: 18px; }
.hp-ico.is-teal { background: var(--pal-teal-soft); }
.hp-ico.is-green { background: var(--pal-green-soft); }
.hp-ico.is-amber { background: var(--pal-amber-soft); }
.hp-ico.is-slate { background: var(--pal-slate-soft); }
.hp-list { padding: 8px 24px; }
.hp-sec { padding: 20px 0; border-top: 1px solid var(--border); scroll-margin-top: 80px; }
.hp-sec:first-child { border-top: none; }
.help-page .hp-sec h3 { display: flex; align-items: center; gap: 12px; margin: 0 0 8px; font-size: var(--fs-16); font-weight: 700; }
.help-page .hp-sec h3 .hp-ico { width: 32px; height: 32px; font-size: 16px; }
.hp-sec h4 { margin: 12px 0 0; font-size: var(--fs-14); font-weight: 600; }
.hp-sec p { margin: 4px 0 8px; }
.hp-sec ul, .hp-sec ol { margin: 0; padding-left: 20px; }
.hp-sec p.muted { margin-top: 0; }
hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
/* align-items:start, or every card in a row grows to match the tallest one in
   it. "Drums Cone (Upper)" wraps its title onto a second line and pushes its
   tags onto a third, which was silently adding that height to the two cards
   beside it - empty space that looked like a bug in those cards rather than a
   consequence of their neighbour. Each card is now as tall as its own content;
   the grid no longer equalises them. */
.venue-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; align-items: start; }
.venue-card {
  background: var(--surface); border-radius: 10px; padding: 18px 20px; border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(0,0,0,0.06); display: flex; flex-direction: column;
}
.venue-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.venue-card-head h3 { margin: 0; font-size: 17px; color: var(--ink); }
/* Two tags beside a long name ("Drums Cone (Upper)  Hidden  In-house") do not
   fit on one line in a 260px card. Wrapped and right-aligned they stack neatly
   under each other instead of dragging the title's line height around. */
.venue-card-head .row { flex-wrap: wrap; justify-content: flex-end; }
.venue-card-sub { margin: 2px 0 14px; color: var(--muted-2); font-size: var(--fs-12); }
/* The FM card's one line: count on the left, rate on the right, baselines
   aligned so the small grey count sits level with the big figure rather than
   floating above it. Replaces the stat grid on that card - see views/venues.ejs
   for why one figure in a four-cell grid had to go. */
.venue-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 4px 0 14px; }
.venue-meta-count { font-size: 13px; color: var(--muted-2); }
.venue-meta-rate { font-size: 21px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.venue-meta-unit { font-size: var(--fs-12); font-weight: 600; color: var(--muted-2); letter-spacing: 0; }
.venue-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; margin-bottom: 16px; }
.venue-stat { display: flex; flex-direction: column; gap: 2px; }
.venue-stat-label { font-size: var(--fs-12); text-transform: uppercase; letter-spacing: 0.02em; color: var(--muted-2); }
.venue-stat-value { font-size: 15px; font-weight: 600; color: var(--ink); }
.venue-card-actions { margin-top: auto; display: flex; gap: 8px; padding-top: 4px; border-top: 1px solid var(--surface-2); padding-top: 12px; }
.tag-green { background: var(--ok-soft); color: var(--ok); }
.dash-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 16px; margin-bottom: 20px; }
.dash-stat {
  background: var(--surface); border-radius: 14px; padding: 18px 20px; border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(15,23,42,0.04), 0 8px 24px -14px rgba(15,23,42,0.12);
}
.dash-stat-label { font-size: var(--fs-12); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-2); }
.dash-stat-value { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; color: var(--ink); margin-top: 4px; }
/* Caveat that belongs to the number above it (e.g. "covers 6 of 200 with a
   unit cost"). Deliberately sits inside the card rather than as a footnote
   elsewhere on the page - a qualifier people have to go looking for is one
   they will not read, and the big number gets believed on its own. 13px, not
   12px, per the text-sizing pass (Task #294). */
.dash-stat-note { font-size: 13px; color: var(--muted-2); margin-top: 6px; line-height: 1.4; }
.alert-section h3 { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.count-pill { font-size: var(--fs-12); font-weight: 700; padding: 2px 10px; border-radius: 10px; background: var(--surface-2); color: var(--ink-2); }
.count-pill.warn { background: var(--bad-soft); color: var(--bad); }
.alert-section { border-top: 3px solid var(--border); }
.alert-section.has-items { border-top-color: var(--warn); }
.alert-section.worklist { border-top-color: var(--accent-border); }
/* ---------- Mobile (phone) layout ---------- */
/* ---------- Fresh look (QX, 16 Oct 2026: "give some freshness to the system") ----------
   Sits BEFORE the one phone block on purpose, so every phone rule still wins.
   1 wider pages · 2 cleaner tables · 3 tidy filter rows · 4 top bar. */

/* 1. Use the screen: tables stop wrapping into 3-4 line cells. */
.container { max-width: 1280px; padding-left: 32px; padding-right: 32px; }

/* 2. Cleaner tables: quiet sentence-case headers, names in ink (not browser
   blue + underline), pills that never break in two. */
a { color: var(--accent); text-underline-offset: 2px; }
th { text-transform: none; letter-spacing: 0; font-size: var(--fs-12); font-weight: 500; color: var(--muted); }
td > a:not([class]), td > div > a:not([class]) { color: var(--ink); text-decoration: none; font-weight: 500; }
td > a:not([class]):hover, td > div > a:not([class]):hover { color: var(--accent); text-decoration: underline; }
select { background-color: #fff; }
.dash-stat-label, .venue-stat-label { text-transform: none; letter-spacing: 0; font-size: 13px; font-weight: 500; color: var(--muted); }
th a { color: inherit; text-decoration: none; }
th a:hover { color: var(--accent); }
.badge, .tag { white-space: nowrap; }
/* Design audit: every fold-out line is a comfortable tap target (WCAG 2.2 target size). */
details > summary { min-height: 24px; }
.cal-hidden > summary { padding: 4px 0; }
.badge { font-weight: 500; padding: 2px 10px; line-height: 18px; }

/* 3. Filter rows: one even row, every control 36px, white, ticks as chips. */
.filter-form select, .filter-form input[type=search], .filter-form input[type=text] {
  height: 36px; padding-top: 0; padding-bottom: 0; background-color: #fff; border-color: var(--line); border-radius: var(--radius-8);
}
.filter-form { align-items: center; }
.filter-form label.check-row {
  margin: 0; height: 36px; padding: 0 12px; border: 1px solid var(--line); border-radius: 18px;
  font-size: 13px; font-weight: 400; color: var(--ink-2); background: #fff; white-space: nowrap; user-select: none;
}
.filter-form label.check-row:hover { background: var(--surface-2); }
.filter-form label.check-row:has(input:checked) { background: var(--accent-soft); border-color: var(--accent-border); color: var(--accent); font-weight: 500; }
.filter-form label.check-row input { margin: 0; accent-color: var(--accent); }
@media (min-width: 701px) {
  /* Search first and roomy, like Google - the dropdowns follow it. */
  .filter-form > input[type=search] { order: -1; min-width: 260px; max-width: 340px !important; }
}

/* Loading bar (QX, 16 Oct 2026): thin green line along the very top while
   the next page loads. Creeps to ~85% (never pretends to finish), snaps to
   100% and fades when the page arrives. */
.pagebar { position: fixed; top: 0; left: 0; height: 3px; width: 0; z-index: 1000; background: var(--accent); pointer-events: none; opacity: 0; }
.pagebar.on { opacity: 1; animation: pagebar-creep 8s cubic-bezier(.1,.7,.2,1) forwards; }
.pagebar.done { width: 100% !important; animation: none; opacity: 0; transition: width .2s ease, opacity .25s ease .1s; }
@keyframes pagebar-creep { 0% { width: 0; } 10% { width: 35%; } 40% { width: 65%; } 100% { width: 85%; } }
@media (prefers-reduced-motion: reduce) { .pagebar.on { animation: none; width: 70%; } }

/* Design system batch 1 (16 Oct 2026): small shared classes that replace
   one-off inline styles (Users, Planned maintenance first). Built only from
   the tokens in :root. */
.hint { margin: 4px 0 0; font-size: var(--fs-12); line-height: 16px; color: var(--muted); }
.hint-indent { margin-left: 24px; }
.section-split { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
.subtle-box { background: var(--surface-2); border-radius: var(--radius-8); padding: 12px; margin: 4px 0; }
.actions-end { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.modal-backdrop { position: fixed; inset: 0; background: rgba(32,33,36,.5); z-index: 999; align-items: center; justify-content: center; }
.modal-card { background: var(--surface); padding: 24px; border-radius: var(--radius-8); max-width: 420px; width: 90%; box-shadow: var(--shadow-2); }
.scroll-x { overflow-x: auto; }
.clickable { cursor: pointer; }
.nowrap { white-space: nowrap; }
.items-center { align-items: center; }
.fw-600 { font-weight: 600; }
.fs-13 { font-size: 13px; }
.gap-6 { gap: 6px; } .gap-8 { gap: 8px; }
.m-0 { margin: 0; } .mt-0 { margin-top: 0; } .mt-4 { margin-top: 4px; } .mt-8 { margin-top: 8px; } .mt-12 { margin-top: 12px; } .mt-16 { margin-top: 16px; } .mt-n8 { margin-top: -8px; }
.mb-0 { margin-bottom: 0; } .mb-8 { margin-bottom: 8px; } .mb-16 { margin-bottom: 16px; }
/* Design system batch 2 (28 Oct 2026): shared bits that used to be inline style= on the item page,
   Assets list and Restricted locations. */
.mb-12 { margin-bottom: 12px; } .my-8 { margin: 8px 0; } .w-90 { width: 90px; } .inline-block { display: inline-block; }
.card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 0; }
.card-head.is-top { align-items: flex-start; }
.card-sub { margin: 2px 0 0; }
.back-btn { display: inline-block; margin-bottom: 16px; }
.btn.is-danger { color: var(--bad); }
.btn.is-danger:hover { background: var(--bad-soft); }
.flag-card { background: var(--pal-amber-soft, #fdf1e3); border: 1px solid #f3d6a8; margin-bottom: 16px; }
.flag-card .flag-title { margin: 0; font-weight: 600; color: var(--warn); }
.flag-card .flag-note { margin: 8px 0 0; white-space: pre-wrap; }
.modal-card.is-wide { max-width: 480px; max-height: 90vh; overflow-y: auto; }
/* Design system batch 3 (28 Oct 2026): ticket page, vendor page, Planned maintenance calendar, Storage items list. */
.mb-4 { margin-bottom: 4px; } .fw-400 { font-weight: 400; } .pre-wrap { white-space: pre-wrap; }
.fs-12 { font-size: var(--fs-12); } .fs-14 { font-size: var(--fs-14); } .fs-20 { font-size: var(--fs-20); }
.gap-4 { gap: 4px; } .gap-12 { gap: 12px; } .gap-24 { gap: 24px; }
.flex { display: flex; } .inline-flex { display: inline-flex; } .d-inline { display: inline; } .flex-1 { flex: 1; }
.wrap { flex-wrap: wrap; } .nowrap-flex { flex-wrap: nowrap; } .items-start { align-items: flex-start; } .justify-between { justify-content: space-between; }
.no-underline, .no-underline:hover { text-decoration: none; } .list-none { list-style: none; } .list-none::-webkit-details-marker { display: none; }
.w-auto { width: auto; } .w-40 { width: 40px; } .maxw-220 { max-width: 220px; } .maxw-320 { max-width: 320px; }
.text-center { text-align: center; } .text-right { text-align: right; } .self-start { justify-self: start; }
.scroll-y-220 { max-height: 220px; overflow-y: auto; }
.row-line { padding: 12px 0; border-bottom: 1px solid var(--border); }
.row-line.is-top { border-bottom: 0; border-top: 1px solid var(--border); }
.card.edge-bad { border-left: 4px solid var(--bad); }
.modal-card.is-500 { max-width: 500px; }
.as-batch.is-sticky { position: sticky; bottom: 0; z-index: 10; background: var(--pal-teal-soft, #e6f4f5); border: 1px solid var(--border); }
.cal-meta { margin-bottom: 12px; font-size: var(--fs-12); color: var(--muted); flex-wrap: wrap; }
.cal-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
/* Account safety + Usage (28 Oct 2026): two-step login, Settings → Security, Settings → Usage. */
.mr-4 { margin-right: 4px; } .d-block { display: block; }
.tag.t-ok { background: var(--pal-green-soft, #e6f4ea); color: var(--pal-green); }
.ts-card { max-width: 640px; }
.ts-steps { margin: 0; padding-left: 20px; display: grid; gap: 16px; }
.ts-qr { width: 184px; margin: 12px 0 8px; padding: 8px; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-8); }
.ts-qr svg { display: block; width: 100%; height: auto; }
.ts-key { font-size: var(--fs-14); letter-spacing: .04em; word-break: break-all; }
input.ts-code { width: 160px; max-width: 100%; font-size: var(--fs-20); letter-spacing: .15em; text-align: center; }
.ts-codes { list-style: none; padding: 0; margin: 12px 0 16px; display: grid; grid-template-columns: repeat(2, max-content); gap: 8px 32px; }
.ts-codes code { font-size: var(--fs-16); }
.ts-lbl { display: block; font-weight: 600; margin-bottom: 4px; }
.ts-off > summary { cursor: pointer; color: var(--muted); }
/* My work (30 Oct 2026): outstanding work for trade people + trade heads. */
.mw-card { margin-bottom: 16px; }
.mw-card h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.mw-list, .mw-people { list-style: none; margin: 0; padding: 0; }
.mw-row { display: flex; align-items: center; gap: 12px; padding: 12px 8px; border-top: 1px solid var(--border); color: var(--ink); text-decoration: none; border-radius: var(--radius-8); }
.mw-list > li:first-child .mw-row, .mw-list > li.mw-row:first-child { border-top: 0; }
a.mw-row:hover { background: var(--surface-2); text-decoration: none; }
a.mw-row:hover .mw-title { color: var(--accent); }
.mw-main { flex: 1; min-width: 0; display: flex; flex-direction: column; color: var(--ink); text-decoration: none; }
a.mw-main:hover .mw-title { color: var(--accent); }
.mw-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mw-sub { font-size: var(--fs-12); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mw-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; }
.mw-dot.hi { background: var(--bad); } .mw-dot.md { background: var(--pal-amber); } .mw-dot.lo { background: var(--pal-green); }
.mw-ic { flex: none; width: 10px; text-align: center; font-size: var(--fs-12); }
.mw-go { color: var(--muted); }
.mw-row.is-grab form { margin: 0; flex: none; }
.mw-empty { margin: 0; color: var(--muted); }
.mw-people { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px 24px; }
.mw-people li { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.mw-av { flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--pal-teal-soft, #e6f4f5); color: var(--hero); display: inline-flex; align-items: center; justify-content: center; font-size: var(--fs-12); font-weight: 600; }
.mw-sec { margin: 16px 0 0; font-size: var(--fs-12); font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
/* My work, less busy (QX, 30 Oct 2026): Next up first, rest short, HVAC maps one tap away */
li.mw-row:hover { background: var(--surface-2); }
.mw-pin { flex: none; font-size: var(--fs-12); font-weight: 600; color: var(--pal-teal); text-decoration: none; padding: 4px 8px; border: 1px solid var(--border); border-radius: var(--radius-pill); background: #fff; white-space: nowrap; }
.mw-pin:hover { border-color: var(--pal-teal); text-decoration: none; }
.mw-pin.is-pick { color: var(--muted); border-style: dashed; }
.unit-pick { display: flex; align-items: center; gap: 12px; margin: 0 0 16px; padding: 12px 16px; border-radius: var(--radius-8); background: var(--pal-teal-soft, #e6f4f5); border-left: 4px solid var(--pal-teal); }
.unit-pick-t { flex: 1; min-width: 0; }
.fp-access { margin-top: 20px; }
.fp-access > summary { cursor: pointer; }
.fp-access-list { list-style: none; margin: 8px 0 0; padding: 0; columns: 2 240px; }
.fp-access-list li { padding: 4px 0; break-inside: avoid; }
.mw-more > summary { cursor: pointer; padding: 8px; font-size: var(--fs-12); font-weight: 600; color: var(--accent); list-style: none; }
.mw-more > summary::-webkit-details-marker { display: none; }
.mw-more > summary::after { content: ' ›'; }
.mw-more[open] > summary::after { content: ' ⌄'; }
.mw-more > .mw-list > li:first-child .mw-row, .mw-more > .mw-list > li.mw-row:first-child { border-top: 1px solid var(--border); }
.mw-stuck { margin-top: 8px; }
.mw-stuck > summary { padding-left: 0; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.mw-mapsum { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; }
.mw-mapsum::-webkit-details-marker { display: none; }
.mw-mapsum::after { content: '›'; margin-left: auto; color: var(--muted); font-size: var(--fs-20); transition: transform .15s; }
.mw-mapcard[open] .mw-mapsum::after { transform: rotate(90deg); }
.mw-maphead { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 12px 0 8px; }
.mw-floors { display: flex; gap: 4px; flex-wrap: wrap; }
.mw-floor-btn { font: inherit; font-size: var(--fs-12); font-weight: 600; padding: 4px 12px; border-radius: var(--radius-pill); border: 1px solid var(--border); background: #fff; color: var(--ink); cursor: pointer; }
.mw-floor-btn[aria-selected="true"] { background: var(--pal-teal); border-color: var(--pal-teal); color: #fff; }
.mw-fcount { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: var(--radius-pill); background: var(--bad); color: #fff; font-size: 11px; text-align: center; }
.mw-mapwrap { position: relative; display: inline-block; max-width: 100%; border-radius: var(--radius-8); overflow: hidden; background: var(--surface-2); vertical-align: top; }
.mw-mapwrap img { display: block; max-width: 100%; max-height: 360px; width: auto; height: auto; }
.mw-pt { position: absolute; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; background: var(--pal-green); border: 2px solid #fff; box-shadow: 0 1px 2px rgba(0,0,0,.35); }
.mw-pt.is-bad { background: var(--bad); width: 16px; height: 16px; margin: -8px 0 0 -8px; z-index: 1; }
.mw-pt.is-mine { outline: 3px solid var(--pal-amber); outline-offset: 1px; z-index: 2; }
.mw-pt:hover, .mw-pt:focus { transform: scale(1.4); }
.mw-maplegend { margin: 8px 0 0; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.mw-key { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--pal-green); margin-left: 8px; }
.mw-key:first-child { margin-left: 0; }
.mw-key.is-bad { background: var(--bad); }
.mw-key.is-mine { background: #fff; outline: 2px solid var(--pal-amber); }
.mw-bad { color: var(--bad); font-weight: 600; }
.rt-list { display: none; }
.mw-up { flex: none; font-size: var(--fs-12); color: var(--muted); }
/* Add storage places from the Storage Updates sheet (28 Oct 2026). */
.shp-group { margin-bottom: 16px; }
.shp-list, .shp-units { list-style: none; margin: 0; padding: 0; }
.shp-row { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0; border-top: 1px solid var(--border); }
.shp-row:first-child { border-top: 0; }
.shp-row > input { margin-top: 4px; width: 18px; height: 18px; flex: none; }
.shp-row.is-done { opacity: .6; }
.shp-main { flex: 1; min-width: 0; }
.shp-name { font-weight: 600; margin-right: 8px; }
.shp-units { margin-top: 8px; display: grid; gap: 4px; font-size: var(--fs-14); }
.shp-size { color: var(--muted); white-space: nowrap; }
.tag.t-warn { background: var(--pal-amber-soft, #fdf1e3); color: var(--warn); }
.shp-go { align-items: center; flex-wrap: wrap; }
.shp-go .hint { flex: 1; min-width: 240px; }
.sec-list, .us-unopened { list-style: none; margin: 0; padding: 0; }
.sec-list li { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--border); }
.sec-list li:first-child { border-top: 0; }
.sec-who { flex: 1; min-width: 0; }
.us-table td { vertical-align: middle; }
.us-bar { display: block; height: 3px; width: var(--w); background: var(--pal-teal); border-radius: 2px; margin-top: 4px; opacity: .5; }
.us-unopened { columns: 2; margin-top: 8px; } .us-unopened li { padding: 2px 0; }
.cal-addcard { border: 1px solid var(--border); border-radius: 10px; padding: 16px; margin-bottom: 12px; background: #fffdf7; }
.as-search { max-width: 320px; }
.as-batch { margin-bottom: 16px; padding: 12px; background: var(--surface-2); border-radius: var(--radius-8); gap: 8px; align-items: center; flex-wrap: wrap; }
.assets-table col.as-c-tick, .assets-table th.as-c-tick { width: 36px; }
.assets-table col.as-c-photo { width: 28px; }
.assets-table col.as-c-code { width: 8%; } .assets-table col.as-c-cat { width: 11%; } .assets-table col.as-c-trade { width: 10%; }
.assets-table col.as-c-loc { width: 28%; } .assets-table col.as-c-class { width: 12%; }
.assets-table th.sortable { cursor: pointer; }
.tag.t-bad { background: var(--bad-soft); color: var(--bad); }
.tag.t-neutral { background: var(--surface-2); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--border); }
.tag.t-low { background: var(--warn-soft); color: var(--warn); }   /* "Low stock" - amber, like Supplies */
.tag.t-flag { background: var(--pal-amber-soft, #fdf1e3); color: var(--warn); }
#items-table input[type=checkbox] { cursor: pointer; }
/* Storage refresh (18 Oct 2026): a quiet "‹ Venue" back link, and form folds as one tidy list (hairline, 16px title, chevron). */
.back-link { margin: 0 0 8px; font-size: var(--fs-14); }
.back-link a { color: var(--muted); text-decoration: none; }
.back-link a:hover { color: var(--accent); }
summary.form-fold { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: var(--fs-16); font-weight: 600;
  margin: 0; padding: 16px 0; border-top: 1px solid var(--border); color: var(--ink); }
summary.form-fold::-webkit-details-marker { display: none; }
summary.form-fold::after { content: '›'; margin-left: auto; color: var(--muted); font-size: var(--fs-20); transition: transform .15s; }
details[open] > summary.form-fold::after { transform: rotate(90deg); }
summary.form-fold:hover { color: var(--accent); }

/* 4. Top bar: stays in reach while you scroll (soft shadow once you do),
   shows which section you are in, a cleaner menu arrow, a roomier search. */
@media (min-width: 701px) {
  .topnav { position: sticky; top: 0; z-index: 40; transition: box-shadow .15s ease; }
  .topnav.is-scrolled { box-shadow: 0 1px 2px rgba(60,64,67,.15), 0 2px 6px rgba(60,64,67,.08); border-bottom-color: transparent; }
  .dash-side, .loan-side { top: 72px; }
  .topnav form.nav-search { border-radius: 18px; border-color: transparent; background: #f1f3f4; }
  .topnav form.nav-search input[type="search"] { width: 220px; height: 34px; padding: 0 14px; font-size: var(--fs-14); }
}
.topnav .links .navgroup summary::after {
  content: ''; display: inline-block; width: 5px; height: 5px; margin: 0 2px 3px 7px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); opacity: .6;
}
.topnav .links .navgroup[open] summary::after { content: ''; transform: rotate(-135deg); margin-bottom: -1px; }
.topnav .links .navgroup summary, .topnav .links > a { padding: 6px 2px; border-radius: var(--radius-4); }
.topnav .links .navgroup summary:hover, .topnav .links > a:hover { text-decoration: none; color: var(--accent); }
.topnav .links .navgroup.here > summary, .topnav .links > a.here { color: var(--accent); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); border-radius: 0; }

/* ---------- Warm look (QX, 17 Oct 2026: "still looks boring and uninviting" - picked a mix of
   Warm (greeting, soft colour on cards, icons) and Bold (a slim green band at the very top)).
   test/warmLook.test.js. Lists and forms stay clean; colour only where it means something. ---------- */
:root { --bg: #f4f5f7; --hero: #1f2733; }
.topnav { border-top: 4px solid var(--hero); background: #fff; }
.card { border-radius: var(--radius-12); border-color: var(--line-soft); }
/* Greeting band (partials/hello.ejs) */
.hello { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 16px; padding: 16px 20px; border-radius: var(--radius-12); background: var(--page-tint); border: 1px solid #cfe9db; }
.hello-hi { font-size: var(--fs-20); font-weight: 600; color: #0b5a35; }
.hello-sub { font-size: var(--fs-14); color: #2f6b4c; margin-top: 2px; }
.btn.hello-btn { border-radius: var(--radius-pill); white-space: nowrap; }
/* Stat tiles: a soft colour each, so the three numbers read at a glance. */
.dash-stats .dash-stat { border: none; box-shadow: none; border-radius: var(--radius-12); }
.dash-stats .dash-stat:nth-child(1) { background: #fff4e5; }
.dash-stats .dash-stat:nth-child(1) .dash-stat-label, .dash-stats .dash-stat:nth-child(1) .dash-stat-value { color: #7a4500; }
.dash-stats .dash-stat:nth-child(2) { background: #e8f0fe; }
.dash-stats .dash-stat:nth-child(2) .dash-stat-label, .dash-stats .dash-stat:nth-child(2) .dash-stat-value { color: #174ea6; }
.dash-stats .dash-stat:nth-child(3) { background: #fff; border: 1px solid var(--line-soft); }
.dash-stats .dash-stat:nth-child(3) .dash-stat-label, .dash-stats .dash-stat:nth-child(3) .dash-stat-value { color: #0d652d; }
/* Storage dashboard colour (QX, 17 Oct 2026: "looks sad, lack of colours"). */
.dash-stats.is-store .dash-stat:nth-child(1) { background: #f3e8fd; box-shadow: inset 4px 0 0 #8430ce; }
.dash-stats.is-store .dash-stat:nth-child(1) .dash-stat-label, .dash-stats.is-store .dash-stat:nth-child(1) .dash-stat-value { color: #5b1e8f; }
.dash-stats.is-store .dash-stat:nth-child(2) { background: #e8f0fe; box-shadow: inset 4px 0 0 #1a73e8; }
.dash-stats.is-store .dash-stat:nth-child(2) .dash-stat-label, .dash-stats.is-store .dash-stat:nth-child(2) .dash-stat-value { color: #174ea6; }
.dash-stats.is-store .dash-stat:nth-child(3) { background: #e6f4ea; border: none; box-shadow: inset 4px 0 0 #188038; }
.dash-stats.is-store .dash-stat:nth-child(3) .dash-stat-label, .dash-stats.is-store .dash-stat:nth-child(3) .dash-stat-value { color: #0d652d; }
.alert-section.tint-orange { border-top: 4px solid #e8710a; }
.alert-section.tint-blue { border-top: 4px solid #1a73e8; }
.alert-section.tint-purple { border-top: 4px solid #8430ce; }
.alert-section.tint-teal { border-top: 4px solid #12b5cb; }
.alert-section.tint-orange h3::first-letter, .alert-section.tint-blue h3::first-letter { font-size: 1.1em; }
.dash-supplies-pill { display: inline-flex; align-items: center; gap: 6px; margin: 0 0 16px; padding: 6px 14px; border-radius: var(--radius-pill); font-size: var(--fs-14); }
.dash-supplies-pill.is-low { background: #fff4e5; color: #7a4500; }
.dash-supplies-pill.is-ok { background: #e6f4ea; color: #0d652d; }
.dash-supplies-pill a { color: inherit; font-weight: 600; }
.venue-card { border-radius: var(--radius-12); }
.venue-card-head { align-items: center; gap: 12px; }
.venue-card-head h3 { flex: 1; min-width: 0; }
.venue-av { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; font-weight: 700; font-size: 16px; color: hsl(var(--h) 55% 30%); background: hsl(var(--h) 70% 92%); }
.venue-card .venue-card-sub { margin-left: 48px; }
.dash-stats .dash-stat.is-warn { background: #fff4e5; }
.dash-stats.is-store .dash-stat.is-warn:nth-child(3) { background: #fff4e5; box-shadow: inset 4px 0 0 #e8710a; }
.dash-stats.is-store .dash-stat.is-warn:nth-child(3) .dash-stat-label, .dash-stats.is-store .dash-stat.is-warn:nth-child(3) .dash-stat-value { color: #7a4500; }
.dash-stats .dash-stat.is-warn .dash-stat-label, .dash-stats .dash-stat.is-warn .dash-stat-value { color: #7a4500; }
.dash-stat-label::before { margin-right: 6px; }
/* Bolder (QX, 17 Oct 2026: "need a bit more bold"): the greeting is a solid green band with white text,
   numbers are bigger, the top band is thicker. */
.topnav { border-top-width: 6px; }
/* QX: "too much green all around" - the bold band is dark slate; green stays for buttons, links and good news. */
.hello { background: var(--hero); border: none; padding: 20px 24px; border-radius: var(--radius-16); box-shadow: 0 2px 8px rgba(31,39,51,.25); }
.hello-hi { color: #fff; font-size: var(--fs-24); font-weight: 700; letter-spacing: -0.01em; }
.hello-sub { color: #d5dae1; font-size: var(--fs-14); }
.btn.hello-btn { background: #fff; color: var(--hero); border: none; font-weight: 600; }
.dash-stats .dash-stat { padding: 16px 20px; border-radius: var(--radius-16); }
.dash-stats .dash-stat-value { font-size: 36px; font-weight: 800; letter-spacing: -0.02em; }
.dash-stats .dash-stat-label { font-size: var(--fs-14); font-weight: 600; }
.dash-stats.is-maint .dash-stat:nth-child(1) { box-shadow: inset 4px 0 0 #e8710a; }
.dash-stats.is-maint .dash-stat:nth-child(2) { box-shadow: inset 4px 0 0 #1a73e8; }
.dash-stats.is-maint .dash-stat:nth-child(3) { box-shadow: inset 4px 0 0 #188038; }
.dash-stats .dash-stat.is-warn { box-shadow: inset 4px 0 0 #e8710a; }
h1 { letter-spacing: -0.01em; }
.dash-stats.is-maint .dash-stat:nth-child(1) .dash-stat-label::before { content: '🛠️'; }
.dash-stats.is-maint .dash-stat:nth-child(2) .dash-stat-label::before { content: '⏳'; }
.dash-stats.is-maint .dash-stat:nth-child(3) .dash-stat-label::before { content: '✅'; }
.dash-stats.is-store .dash-stat:nth-child(1) .dash-stat-label::before { content: '🏢'; }
.dash-stats.is-store .dash-stat:nth-child(2) .dash-stat-label::before { content: '📦'; }
.dash-stats.is-store .dash-stat:nth-child(3) .dash-stat-label::before { content: '👀'; }

/* ---------- Harmony (QX, 17 Oct 2026: "the colours need to complement each other more, especially the
   overall theme"). One palette, built from the slate band and the brand green:
     slate  #1f2733 / #3d4a5c (structure)   green #0f7b48 (good, brand)   teal #0e7c86 (in progress, info)
     amber  #b45309 (needs a look) - the ONLY warm colour, and only when something is waiting.
   Tiles are white; colour lives in a small icon chip and the number. No purple, no bright blue. ---------- */
:root { --pal-slate: #3d4a5c; --pal-slate-soft: #e9edf2; --pal-teal: #0e7c86; --pal-teal-soft: #e0f2f2; --pal-green: #0f7b48; --pal-green-soft: #e6f4ea; --pal-amber: #b45309; --pal-amber-soft: #fdf1e3; }
/* QX: "this black is an eye sore" - the greeting band and the top band use the palette's teal, a soft sweep into green. */
.topnav { border-top-color: var(--pal-teal); }
.hello { background: linear-gradient(110deg, #0e6f78 0%, #0e7c86 45%, #138a5e 100%); box-shadow: 0 2px 10px rgba(14,124,134,.22); }
.hello-sub { color: #e2f4f2; }
.btn.hello-btn { color: var(--pal-teal); }
.dash-stats .dash-stat, .dash-stats.is-store .dash-stat:nth-child(n), .dash-stats.is-maint .dash-stat:nth-child(n) { background: #fff; border: 1px solid var(--line-soft); box-shadow: none; }
.dash-stats .dash-stat .dash-stat-label { color: var(--muted) !important; display: flex; align-items: center; gap: 8px; }
.dash-stats .dash-stat .dash-stat-value { color: var(--hero) !important; }
.dash-stat-label::before { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: var(--radius-8); font-size: 15px; margin-right: 0; }
.dash-stats .dash-stat:nth-child(1) .dash-stat-label::before { background: var(--pal-slate-soft); }
.dash-stats .dash-stat:nth-child(2) .dash-stat-label::before { background: var(--pal-teal-soft); }
.dash-stats .dash-stat:nth-child(3) .dash-stat-label::before { background: var(--pal-green-soft); }
.dash-stats.is-maint .dash-stat:nth-child(1) .dash-stat-label::before { background: var(--pal-amber-soft); }
.dash-stats.is-maint .dash-stat:nth-child(1) .dash-stat-value { color: var(--pal-amber) !important; }
.dash-stats.is-maint .dash-stat:nth-child(2) .dash-stat-value { color: var(--pal-teal) !important; }
.dash-stats.is-maint .dash-stat:nth-child(3) .dash-stat-value { color: var(--pal-green) !important; }
.dash-stats.is-store .dash-stat:nth-child(3) .dash-stat-value { color: var(--pal-green) !important; }
.dash-stats .dash-stat.is-warn:nth-child(n) { background: var(--pal-amber-soft); border-color: #f3d9b8; }
.dash-stats .dash-stat.is-warn .dash-stat-label::before { background: #fff; }
.dash-stats .dash-stat.is-warn .dash-stat-value { color: var(--pal-amber) !important; }
/* Alert cards: one quiet look; the top turns amber only when something is waiting. */
.alert-section.tint-orange, .alert-section.tint-blue, .alert-section.tint-purple, .alert-section.tint-teal { border-top: 1px solid var(--line-soft); }
.alert-section.has-items, .alert-section.tint-teal.has-items { border-top: 3px solid var(--pal-amber); }
.dash-supplies-pill.is-low { background: var(--pal-amber-soft); color: #7c3d06; }
.dash-supplies-pill.is-ok { background: var(--pal-green-soft); color: #0d5c33; }
/* Venue tiles: from the same palette, not the whole rainbow. */
.venue-av { color: var(--pal-slate); background: var(--pal-slate-soft); }
.venue-grid .venue-card:nth-child(3n+2) .venue-av { color: var(--pal-teal); background: var(--pal-teal-soft); }
.venue-grid .venue-card:nth-child(3n+3) .venue-av { color: var(--pal-green); background: var(--pal-green-soft); }

/* ---------- Bold, in the palette (QX, 17 Oct 2026: "but the bold all gone?") - tinted tiles and coloured edges
   come back, using only the Harmony colours (slate / teal / green, amber only when waiting). ---------- */
.dash-stats .dash-stat:nth-child(n) { border: none; }
.dash-stats.is-maint .dash-stat:nth-child(1) { background: var(--pal-amber-soft); box-shadow: inset 5px 0 0 var(--pal-amber); }
.dash-stats.is-maint .dash-stat:nth-child(2) { background: var(--pal-teal-soft); box-shadow: inset 5px 0 0 var(--pal-teal); }
.dash-stats.is-maint .dash-stat:nth-child(3) { background: var(--pal-green-soft); box-shadow: inset 5px 0 0 var(--pal-green); }
.dash-stats.is-store .dash-stat:nth-child(1) { background: var(--pal-slate-soft); box-shadow: inset 5px 0 0 var(--pal-slate); }
.dash-stats.is-store .dash-stat:nth-child(1) .dash-stat-value { color: var(--pal-slate) !important; }
.dash-stats.is-store .dash-stat:nth-child(2) { background: var(--pal-teal-soft); box-shadow: inset 5px 0 0 var(--pal-teal); }
.dash-stats.is-store .dash-stat:nth-child(2) .dash-stat-value { color: var(--pal-teal) !important; }
.dash-stats.is-store .dash-stat:nth-child(3) { background: var(--pal-green-soft); box-shadow: inset 5px 0 0 var(--pal-green); }
.dash-stats .dash-stat.is-warn:nth-child(n) { background: var(--pal-amber-soft); box-shadow: inset 5px 0 0 var(--pal-amber); }
.dash-stats .dash-stat .dash-stat-label { color: var(--hero) !important; }
.dash-stat-label::before { background: rgba(255,255,255,.75) !important; }
.alert-section.tint-orange { border-top: 4px solid var(--pal-slate); }
.alert-section.tint-blue { border-top: 4px solid var(--pal-teal); }
.alert-section.tint-purple { border-top: 4px solid var(--pal-green); }
.alert-section.tint-teal { border-top: 4px solid var(--pal-teal); }
.alert-section.has-items:nth-child(n) { border-top: 4px solid var(--pal-amber); }

/* ---------- Tiles, option A (QX, 17 Oct 2026: "this color combination won't work" -> picked A).
   White tiles, dark numbers, one teal icon chip each. The ONE tile that needs action (Open tickets / Needs
   attention, when > 0) is solid amber with white text (QX: "need attention should be another color" - amber = waiting). Overrides every earlier tile rule. ---------- */
.dash-stats.dash-stats.dash-stats .dash-stat:nth-child(n), .dash-stats.is-maint .dash-stat:nth-child(n), .dash-stats.is-store .dash-stat:nth-child(n) {
  background: #fff; border: 1px solid var(--line-soft); box-shadow: none; }
.dash-stats.dash-stats.dash-stats .dash-stat:nth-child(n) .dash-stat-label { color: var(--muted) !important; }
.dash-stats.dash-stats.dash-stats .dash-stat:nth-child(n) .dash-stat-value { color: var(--hero) !important; }
.dash-stats.dash-stats.dash-stats .dash-stat:nth-child(n) .dash-stat-label::before { background: var(--pal-teal-soft) !important; }
.dash-stats.dash-stats.dash-stats .dash-stat.is-warn:nth-child(n) { background: var(--pal-amber); border-color: var(--pal-amber); box-shadow: 0 2px 8px rgba(180,83,9,.25); }
.dash-stats.dash-stats.dash-stats .dash-stat.is-warn:nth-child(n) .dash-stat-label { color: #fff !important; }
.dash-stats.dash-stats.dash-stats .dash-stat.is-warn:nth-child(n) .dash-stat-value { color: #fff !important; }
.dash-stats.dash-stats.dash-stats .dash-stat.is-warn:nth-child(n) .dash-stat-label::before { background: rgba(255,255,255,.18) !important; }

/* ---------- Storage areas (QX, 27 Oct 2026: Runway Stage first, then everywhere) ----------
   Section buttons on a venue's items page (one row per letter), List | 3D switch, phone list links,
   the phone "side" page, and the Venues page grouped by site. Palette variables only. */
.sa-bar { margin: 0 0 16px; }
.sa-views { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 2px; background: #fff; margin-bottom: 8px; }
.sa-views a { padding: 4px 16px; border-radius: var(--radius-pill); color: var(--muted); font-size: var(--fs-14); text-decoration: none; }
.sa-views a.is-on { background: var(--pal-teal); color: #fff; }
.sa-sections { display: flex; flex-direction: column; gap: 6px; }
.sa-row { display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 6px; padding-bottom: 2px; scrollbar-width: thin; }
.sa-chip { flex: 0 0 auto; }
.sa-chip.is-run { cursor: default; border-style: dashed; }
.sa-chip.is-run:hover { border-color: var(--border); }
/* "More" menu for less-used page actions (venue list). */
.more-menu { position: relative; }
.more-menu > summary { list-style: none; cursor: pointer; }
.more-menu > summary::-webkit-details-marker { display: none; }
.more-menu-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 200px; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-8); box-shadow: var(--shadow-2); padding: 4px 0; display: flex; flex-direction: column; }
.more-menu-pop a { padding: 8px 16px; font-size: var(--fs-14); color: var(--ink); text-decoration: none; }
.more-menu-pop a:hover { background: var(--surface-2); }
.sa-gap { width: 44px; }
.sa-chip { display: inline-flex; align-items: center; gap: 4px; min-width: 44px; justify-content: center; height: 32px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: var(--radius-pill); background: #fff; color: var(--ink); font-size: var(--fs-14);
  text-decoration: none; white-space: nowrap; }
.sa-chip small { font-size: var(--fs-12); color: var(--muted); }
.sa-chip:hover { border-color: var(--pal-teal); text-decoration: none; }
.sa-chip.is-on { background: var(--pal-teal); border-color: var(--pal-teal); color: #fff; }
.sa-chip.is-on small { color: #fff; }
.sa-phone { margin: 8px 0 0; }
.it-sits { margin: 8px 0 0; }
.sa-site { font-size: var(--fs-16); font-weight: 700; margin: 24px 0 8px; color: var(--hero); }
.sa-site:first-of-type { margin-top: 8px; }
.sa-chip.is-empty { color: var(--muted); border-style: dashed; }
.sa-chip.is-bad { color: var(--bad); border-color: var(--bad-border); background: var(--bad-soft); }
/* Import Usher map (lib/mapImport.js) */
.mi-top { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.mi-top label { margin: 0; white-space: nowrap; }
.mi-top select { width: auto; max-width: 280px; }
.mi-answer { font-size: var(--fs-16); margin: 0 0 12px; }
.mi-table { width: 100%; }
.mi-table td { vertical-align: top; }
.mi-pick { max-width: 360px; }
.mi-table tr.is-bad td { background: var(--bad-soft); }
.mi-bad { color: var(--bad); font-weight: 600; }
/* 3D Show filter: other departments' rows leave the list */
.v3-list .is-other { display: none; }
.tag.t-view { background: var(--pal-teal-soft); color: color-mix(in srgb, var(--pal-teal) 75%, #000); }
/* Phone side list (QR): big section headers, one quiet row per item. */
.sd-sec { margin: 0 0 16px; }
.sd-sec h2 { font-size: var(--fs-20); margin: 0 0 4px; display: flex; align-items: baseline; gap: 8px; }
.sd-sec h2 small { font-size: var(--fs-12); color: var(--muted); font-weight: 400; }
.sd-list { list-style: none; margin: 0; padding: 0; }
.sd-list li { padding: 8px 0; border-top: 1px solid var(--border); }
.sd-list li:first-child { border-top: none; }
.sd-name { font-weight: 600; color: var(--ink); text-decoration: none; }
.sd-name:hover { color: var(--accent); }
.sd-sub { font-size: var(--fs-12); color: var(--muted); margin-top: 2px; }
.sd-jump { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 16px; }
/* Space check page (venue_space.ejs). */
.card.sp-card h3 { margin: 0 0 8px; }
.sp-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.sp-answer { display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border-radius: var(--radius-pill); background: var(--pal-slate-soft); color: var(--hero); font-weight: 600; font-size: var(--fs-14); }
.sp-answer.is-good { background: var(--pal-green-soft); color: var(--pal-green); }
.sp-tabs { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 2px; background: #fff; }
.sp-tabs button { border: none; background: none; padding: 4px 14px; border-radius: var(--radius-pill); font-size: var(--fs-14); color: var(--muted); cursor: pointer; min-height: 28px; }
.sp-tabs button.is-on { background: var(--pal-slate); color: #fff; }
.sp-ctrl { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px 24px; margin-bottom: 8px; }
.sp-ctrl label { font-size: var(--fs-12); font-weight: 400; color: var(--muted); display: flex; flex-wrap: wrap; justify-content: space-between; margin: 0; }
.sp-ctrl label b { color: var(--ink); font-weight: 600; }
.sp-ctrl input[type=range] { width: 100%; margin: 4px 0 0; min-height: 0; padding: 0; }
.sp-grid { display: flex; flex-direction: column; gap: 4px; margin-top: 16px; overflow-x: auto; }
.sp-row { display: flex; gap: 4px; align-items: stretch; }
.sp-rl { flex: 0 0 20px; display: flex; align-items: center; font-size: var(--fs-12); color: var(--muted); font-weight: 600; }
.sp-c { flex: 1 0 52px; max-width: 96px; height: 64px; position: relative; border: 1px solid var(--border); border-radius: var(--radius-4); background: #fff; overflow: hidden; }
.sp-c.is-over { border-color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
.sp-c.is-free { background: repeating-linear-gradient(45deg, #fff, #fff 4px, var(--pal-green-soft) 4px, var(--pal-green-soft) 8px); }
.sp-lab { position: absolute; top: 2px; left: 4px; font-size: 11px; font-weight: 600; z-index: 2; color: var(--ink); background: rgba(255,255,255,.85); border-radius: 3px; padding: 0 2px; }
.sp-pct { position: absolute; bottom: 2px; right: 4px; font-size: 11px; z-index: 2; color: var(--muted); background: rgba(255,255,255,.8); border-radius: 3px; padding: 0 2px; }
.sp-bars { position: absolute; inset: 0; display: flex; flex-direction: column-reverse; }
.sp-bars i { display: block; width: 100%; }
.sp-legend { margin: 8px 0 0; }
table.sp-table th.num, table.sp-table td.num { text-align: right; white-space: nowrap; }
table.sp-table tr.is-on td { background: var(--pal-teal-soft); }
.sp-dot { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 8px; vertical-align: -1px; }
.sp-link { color: var(--ink); font-weight: 600; text-decoration: none; }
.sp-hero .sp-big { font-size: var(--fs-24); font-weight: 700; color: var(--pal-green); margin-bottom: 4px; }
.sp-list { list-style: none; margin: 0; padding: 0; }
.sp-list > li { padding: 12px 0; border-top: 1px solid var(--border); }
.sp-list > li:first-child { border-top: none; padding-top: 4px; }
.sp-li-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.sp-bar { height: 10px; border-radius: var(--radius-pill); overflow: hidden; display: flex; margin: 0 0 6px;
  background: repeating-linear-gradient(45deg, var(--pal-green-soft), var(--pal-green-soft) 4px, #fff 4px, #fff 8px); border: 1px solid var(--border); }
.sp-bar i { display: block; height: 100%; }
.sp-bar i.is-need { background: var(--pal-teal); }
.sp-key { margin: 8px 0 0; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sp-k { display: inline-block; width: 12px; height: 10px; border-radius: 3px; border: 1px solid var(--border); }
.sp-k.is-need { background: var(--pal-teal); }
.sp-k.is-spare { background: repeating-linear-gradient(45deg, var(--pal-green-soft), var(--pal-green-soft) 3px, #fff 3px, #fff 6px); }
.sp-todo { margin: 0; padding-left: 20px; }
.sp-say { font-size: var(--fs-16); }
.sp-good { color: var(--pal-green); }
.sp-pct.is-free { color: var(--pal-green); font-weight: 600; }
.sp-pct.is-over { color: var(--bad); font-weight: 600; }
/* Kept for a department, nothing recorded there yet (K1-K6 = Events) - theirs, not empty. */
.sp-c.is-kept { background: repeating-linear-gradient(45deg, #fff, #fff 4px, var(--pal-slate-soft, #eceff3) 4px, var(--pal-slate-soft, #eceff3) 8px); border-style: dashed; }
.sp-c.is-kept .sp-bars { opacity: .45; }
.sp-kept { position: absolute; left: 4px; right: 4px; bottom: 2px; z-index: 2; font-size: 11px; color: var(--ink, #202124); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sa-chip.is-kept small { color: var(--muted); }
/* Storage map (views/storage_map.ejs, public/storageMap.js): Level 4 as plain rooms + a spot per storage. */
.fp-card { margin-bottom: 16px; }
.fp-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-bottom: 12px; }
.fp-find input { width: 260px; max-width: 100%; height: 36px; border: 1px solid var(--line); border-radius: var(--radius-pill); padding: 0 14px; }
#fp-say { flex: 1 1 200px; }
.fp-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: var(--fs-12); color: var(--muted); }
.fp-legend i, .fp-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: -1px; }
.fp-c-room { background: var(--pal-green); fill: var(--pal-green); }
.fp-c-busy { background: var(--pal-amber); fill: var(--pal-amber); }
.fp-c-full { background: var(--bad); fill: var(--bad); }
.fp-c-none { background: var(--pal-teal); fill: var(--pal-teal); }
.fp-dot.is-off { background: #c4c7cc; }
.fp-wrap { border: 1px solid var(--line-soft); border-radius: var(--radius-8); background: #fbfbfc; }
.fp-svg { display: block; width: 100%; min-width: 900px; height: auto; font-family: inherit; }
.fp-room { fill: #fff; stroke: #c4c7cc; stroke-width: 1.5; }
.fp-room.is-used { fill: var(--pal-teal-soft); stroke: #9fc9cc; }
.fp-rlab { fill: #70757a; font-size: 17px; }
.fp-stage { fill: #e9edf2; stroke: #c4c7cc; }
.fp-area { opacity: .35; }
.fp-spot { cursor: pointer; }
.fp-pill { fill: #fff; stroke: var(--line); filter: drop-shadow(0 1px 2px rgba(60,64,67,.25)); }
.fp-spot:hover .fp-pill, .fp-spot:focus .fp-pill, .fp-spot.is-hit .fp-pill { stroke: var(--pal-teal); stroke-width: 2; }
.fp-spot.is-hit .fp-pill { fill: #fff8e1; stroke: var(--pal-amber); stroke-width: 3; }
.fp-spot.is-dim { opacity: .3; }
.fp-name { font-size: 18px; font-weight: 600; fill: #202124; }
.fp-sub { font-size: 15px; fill: var(--muted); }
.fp-list { list-style: none; margin: 0; padding: 0; }
.fp-list li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid #eceef0; }
.fp-list li:first-child { border-top: 0; }
.fp-list li.is-dim { opacity: .4; }
.fp-li-main { flex: 1; min-width: 0; }
.fp-pop { position: absolute; z-index: 50; width: 300px; margin: 0; padding: 16px; box-shadow: var(--shadow-2); }
.fp-pop[hidden] { display: none; }
.fp-x { position: absolute; top: 8px; right: 8px; border: 0; background: none; font-size: var(--fs-20); line-height: 1; cursor: pointer; color: var(--muted); width: 32px; height: 32px; border-radius: 50%; }
.fp-x:hover { background: #f1f3f4; }
.fp-facts { list-style: none; margin: 12px 0; padding: 0; display: grid; gap: 6px; }
/* Cupboard check (views/venue_cupboards.ejs, public/cupboards3d.js). Grey = not checked yet (never "empty"). */
.cb-answer { margin-bottom: 16px; }
.cb-pills { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: 8px; vertical-align: middle; }
.cb-pill { display: inline-block; padding: 2px 10px; border-radius: var(--radius-pill); font-size: var(--fs-12); font-weight: 600; white-space: nowrap; }
.cb-pill.t-empty { background: var(--pal-green-soft); color: var(--pal-green); }
.cb-pill.t-some { background: var(--pal-amber-soft); color: #8f550e; }
.cb-pill.t-full { background: var(--bad-soft); color: var(--bad); }
.cb-pill.t-none { background: #f1f3f4; color: var(--muted); }
.cb-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; margin-bottom: 16px; }
.cb-view-card { padding: 12px; }
.cb-view { border-radius: var(--radius-8); overflow: hidden; background: #f4f5f7; min-height: 320px; }
.cb-view canvas { display: block; }
.cb-card label, .cb-card legend { display: block; font-size: var(--fs-12); color: var(--muted); margin: 12px 0 4px; }
.cb-card input[type=text] { width: 100%; }
.cb-full { border: 0; padding: 0; margin: 0; }
.cb-full .cb-fbtn { display: inline-flex; align-items: center; gap: 6px; margin: 0 4px 4px 0; padding: 6px 12px; border: 1px solid var(--line); border-radius: var(--radius-pill); font-size: var(--fs-14); color: #202124; cursor: pointer; }
.cb-full .cb-fbtn:has(input:checked).t-empty { background: var(--pal-green-soft); border-color: var(--pal-green); }
.cb-full .cb-fbtn:has(input:checked).t-some { background: var(--pal-amber-soft); border-color: var(--pal-amber); }
.cb-full .cb-fbtn:has(input:checked).t-full { background: var(--bad-soft); border-color: var(--bad); }
.cb-img { display: block; width: 100%; margin-top: 12px; border-radius: var(--radius-8); }
.cb-items { list-style: none; margin: 4px 0 0; padding: 0; max-height: 240px; overflow: auto; }
.cb-items li { padding: 4px 0; border-top: 1px solid #eceef0; font-size: var(--fs-14); }
.cb-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0 24px; }
.cb-row { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px; padding: 6px 0; border: 0; border-top: 1px solid #eceef0; background: none; text-align: left; font: inherit; cursor: pointer; color: inherit; }
.cb-row:hover .cb-code, .cb-row.is-on .cb-code { color: var(--accent); }
.cb-code { font-weight: 600; white-space: nowrap; }
.cb-row .hint { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cb-go { color: var(--muted); }
.cb-pill[hidden] { display: none; }
.cb-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 8px 0 0; font-size: var(--fs-12); color: var(--muted); }
.cb-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
.cb-legend i.t-none { background: #bdc1c6; } .cb-legend i.t-empty { background: var(--pal-green); }
.cb-legend i.t-some { background: var(--pal-amber); } .cb-legend i.t-full { background: var(--bad); }
.cb-empty { text-align: center; padding: 32px 8px; }
.cb-empty[hidden] { display: none; }
.cb-empty-ic { display: block; font-size: var(--fs-24); margin-bottom: 8px; }
.cb-all h3 { margin-top: 0; }
.cb-all > .hint { margin-bottom: 8px; }
@media (max-width: 900px) { .cb-grid { grid-template-columns: 1fr; } .cb-pills { margin: 6px 0 0; display: flex; } }
.sp-depts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.sp-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--radius-pill); background: #fff; font-size: var(--fs-12); color: var(--ink); cursor: pointer; }
.sp-chip.is-on { border-color: var(--pal-teal); background: var(--pal-teal-soft); }
.sp-chip .sp-dot { margin-right: 0; }
/* Space check = one list of departments (28 Oct 2026). */
.sp-tags { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.sp-dmove { margin-top: 8px; }
.sp-dmove > summary { cursor: pointer; color: var(--accent); font-weight: 600; font-size: var(--fs-14); }
.sp-dmove[open] > summary { margin-bottom: 4px; }
.sp-from { color: var(--muted); font-size: var(--fs-12); }
.sp-chip.is-static { cursor: default; color: var(--muted); }
.sp-c.is-freeable { border-color: var(--pal-green); box-shadow: inset 0 0 0 1px var(--pal-green); }
.sp-todo-list { list-style: none; margin: 0; padding: 0; }
.sp-todo-list li { display: flex; gap: 10px; align-items: baseline; padding: 8px 0; border-top: 1px solid var(--border); font-size: var(--fs-14); line-height: 1.6; }
.sp-todo-list li:first-child { border-top: none; padding-top: 0; }
.sp-todo-list li .sp-dot { flex: 0 0 10px; transform: translateY(1px); }
.sp-todo-list a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--border); }
.sp-todo-list a:hover { color: var(--accent); }
.sp-then { margin: 8px 0 0; }
.sp-fold > summary { cursor: pointer; font-weight: 600; font-size: var(--fs-14); }
.sp-fold[open] > summary { margin-bottom: 12px; }
.sp-steps { margin: 0; padding-left: 20px; }
.sp-steps > li { padding: 8px 0; border-top: 1px solid var(--border); }
.sp-steps > li:first-child { border-top: none; }
.sp-step-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sp-moves { list-style: none; margin: 4px 0 0; padding: 0; columns: 2 280px; column-gap: 24px; }
.sp-moves li { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 2px 0; break-inside: avoid; font-size: var(--fs-14); }
.sp-moves a { color: var(--ink); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; min-height: 24px; line-height: 24px; }
.sp-moves a:hover { color: var(--accent); }
.sp-mv { white-space: nowrap; color: var(--muted); font-size: var(--fs-12); }
.sp-todo li { margin: 4px 0; }
.sp-link:hover { color: var(--accent); }
.sp-fix summary { cursor: pointer; font-weight: 600; }
.sp-fix ul { margin: 8px 0 0; padding-left: 20px; }
/* 3D view: model left, the picked item + the list right; stacked on a phone. */
/* Name menu: Connect Telegram first, green and bold until connected (QX, 27 Oct 2026). */
.topnav .navgroup .navgroup-menu a.na-tg { color: var(--accent); font-weight: 600; }
.topnav .navgroup .navgroup-menu a.na-tg.is-on { color: var(--muted); font-weight: 400; }
.v3-toast { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 70; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 16px; background: #202124; color: #fff; border-radius: 24px; box-shadow: var(--shadow-2); font-size: var(--fs-14); max-width: calc(100vw - 24px); }
.v3-toast[hidden] { display: none; }
.v3-toast button { border: none; background: transparent; color: #8ab4f8; font: inherit; font-weight: 600; padding: 6px 10px; border-radius: var(--radius-16); cursor: pointer; }
.v3-toast button[hidden] { display: none; }
.v3-draghint { margin-top: 6px; }
.v3-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }
.card.v3-stage { padding: 12px; margin: 0; }
/* Tidied 28 Oct 2026: one toolbar, an answer row only when needed, one side card with folded sections. */
.v3-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.v3-bar-gap { flex: 1; }
.v3-bar .v3-select { height: 32px; margin: 0; border-radius: var(--radius-pill); padding: 0 12px; max-width: 260px; }
.v3-more { position: relative; }
.v3-more > summary { list-style: none; width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 50%; display: grid; place-items: center; cursor: pointer; color: var(--muted); background: #fff; font-size: var(--fs-16); }
.v3-more > summary::-webkit-details-marker { display: none; }
.v3-more[open] > summary { color: var(--pal-teal); border-color: var(--pal-teal); }
.v3-menu { position: absolute; right: 0; top: 40px; z-index: 5; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-8); box-shadow: var(--shadow-2); padding: 12px; width: 240px; display: flex; flex-direction: column; gap: 8px; }
.v3-sayrow { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; min-height: 32px; }
.v3-sayrow[hidden] { display: none; }
.card.v3-side { margin: 0; padding: 12px; }
.v3-side input[type=search] { margin: 0 0 8px; }
.v3-pick { position: relative; background: var(--pal-teal-soft); border-radius: var(--radius-8); padding: 12px 36px 12px 12px; margin-bottom: 8px; }
.v3-pick[hidden] { display: none; }
.v3-x { position: absolute; top: 6px; right: 6px; width: 28px; height: 28px; border: none; background: none; border-radius: 50%; font-size: var(--fs-20); line-height: 1; color: var(--muted); cursor: pointer; }
.v3-x:hover { background: rgba(0,0,0,.06); }
.v3-sec { border-bottom: 1px solid var(--border); }
.v3-sec > summary { list-style: none; cursor: pointer; padding: 8px 4px; font-size: var(--fs-14); font-weight: 600; display: flex; gap: 6px; align-items: baseline; }
.v3-sec > summary::-webkit-details-marker { display: none; }
.v3-sec > summary::after { content: '›'; margin-left: auto; color: var(--muted); transition: transform .15s; }
.v3-sec[open] > summary::after { transform: rotate(90deg); }
.v3-sec > summary small { color: var(--muted); font-weight: 400; font-size: var(--fs-12); }
.v3-sec.is-bad > summary { color: var(--bad); }
.v3-cams { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 2px; background: #fff; }
.v3-cams button { border: none; background: none; padding: 4px 12px; border-radius: var(--radius-pill); font-size: var(--fs-14); color: var(--muted); cursor: pointer; min-height: 28px; }
.v3-cams button.is-on { background: var(--pal-slate); color: #fff; }
.v3-tidy [data-tidy="after"].is-on { background: var(--pal-green); }
.v3-tidy-say { font-size: var(--fs-14); }
.v3-move { color: var(--warn); font-weight: 600; }
.v3-stuck { color: var(--bad); }
.sp-check { display: flex; align-items: center; gap: 8px; }
/* Measure page (views/venue_measure.ejs) */
.ms-depts { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.ms-answer { font-size: var(--fs-16); margin-bottom: 8px; }
.ms-list { list-style: none; margin: 0 0 8px; padding: 0; }
.ms-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.ms-row.is-done .ms-now { color: var(--ok); }
.ms-what { display: flex; flex-direction: column; gap: 2px; min-width: 200px; flex: 1; }
.ms-form { display: flex; gap: 6px; align-items: center; }
.ms-form input { width: 72px; height: 36px; font-size: var(--fs-16); text-align: center; }
.ms-bad { color: var(--bad); }
.sp-how { margin: 0 0 8px; padding-left: 20px; }
.sp-how li { margin-bottom: 4px; }
.v3-step { display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-14); }
.v3-step[hidden] { display: none; }
.v3-stepbtn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border); background: #fff; color: var(--pal-slate); font-size: var(--fs-16); cursor: pointer; }
.v3-stepbtn:hover { border-color: var(--pal-amber); color: var(--pal-amber); }
.v3-checks { display: inline-flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.v3-select { width: auto; height: 28px; min-height: 0; padding: 0 8px; margin: 0 0 0 4px; font-size: var(--fs-14); }
.v3-check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-14); font-weight: 400; margin: 0; }
.v3-check input { width: auto; min-height: 0; margin: 0; }
.v3-viewport { position: relative; }
.v3-ctrl { position: absolute; top: 8px; right: 8px; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.v3-goto { height: 32px; font-size: var(--fs-14); border-radius: var(--radius-pill); padding: 0 10px; background: #fff; border: 1px solid var(--border); }
.v3-pad { display: grid; grid-template-columns: repeat(2, 36px); gap: 4px; background: rgba(255,255,255,.92); border: 1px solid var(--border); border-radius: var(--radius-8); padding: 4px; box-shadow: var(--shadow-1); }
.v3-pad button { width: 36px; height: 36px; border: none; border-radius: var(--radius-4); background: none; color: var(--pal-slate); font-size: var(--fs-20); line-height: 1; cursor: pointer; }
.v3-pad button:hover, .v3-pad button:focus-visible { background: var(--bg); color: var(--pal-teal); }
.v3-pad [data-nav="reset"] { grid-column: span 2; width: auto; font-size: var(--fs-16); }
.v3-canvas { position: relative; height: 540px; border-radius: var(--radius-8); overflow: hidden; background: var(--bg); touch-action: none; }
.v3-fallback { padding: 16px; }
.v3-help { margin: 8px 0 0; }
.v3-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 8px; font-size: var(--fs-12); color: var(--muted); }
.v3-legend .v3-est { font-style: italic; }
.v3-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--border); margin-right: 4px; vertical-align: -1px; }
.v3-pick h3 { font-size: var(--fs-16); }
.v3-where { margin: 4px 0; font-size: var(--fs-14); color: #0b5f66; font-weight: 600; }
.v3-note { margin: 8px 0 0; font-size: var(--fs-14); }
.v3-list { max-height: 560px; overflow-y: auto; }
.v3-sec ul { list-style: none; margin: 0; padding: 0 0 6px; }
.v3-item { display: block; width: 100%; text-align: left; border: none; background: none; padding: 6px 8px; border-radius: var(--radius-8);
  font-size: var(--fs-14); color: var(--ink); cursor: pointer; text-decoration: none; }
.v3-item small { display: block; font-size: var(--fs-12); color: var(--muted); }
.v3-item:hover { background: var(--surface-2); text-decoration: none; }
.v3-item.is-on { background: var(--pal-teal-soft); }
.v3-item.is-loose { color: var(--muted); }

/* Users page, simpler (QX, 30 Oct 2026: "very ugly and messy"): ONE filter row like Fault Tickets, ONE Access
   column instead of five, Telegram as quiet text. test/usersRoutes.test.js. */
.u-filters { gap: 8px; margin: 16px 0; }
.u-filters input[type=search] { flex: 1 1 260px; max-width: 340px; margin: 0; }
.u-filters select { width: auto; margin: 0; }
.u-email { display: none; }
.u-role { display: flex; flex-wrap: wrap; gap: 4px; }
.u-acc { font-size: var(--fs-12); }
.u-line { line-height: 20px; white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
.u-k { color: var(--muted); display: inline-block; min-width: 48px; }
.u-tg { font-size: var(--fs-12); color: var(--muted); white-space: nowrap; }
.u-tg::before { content: '○ '; }
.u-tg.is-on { color: var(--ok); }
.u-tg.is-on::before { content: '● '; }
.u-assign { display: inline-flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; font-size: var(--fs-12); font-weight: 400; }

/* Vendor contracts (QX, 3 Nov 2026; restyled same day - "needs to look nicer"). Palette only: amber = needs a
   decision, teal = running, slate/grey = ended. Tiles like the dashboards, rows with a date tile like Loans,
   a hero card with a start → end timeline, fact tiles, a numbered form. test/contracts.test.js. */
.ct-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 16px 0; }
.ct-tile { display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; background: #fff; border: 1px solid var(--line-soft); border-radius: var(--radius-16); box-shadow: var(--shadow-1); }
.ct-tile-n { font-size: 32px; font-weight: 800; line-height: 1; color: var(--hero, #1f2733); }
.ct-tile-l { font-size: var(--fs-14); font-weight: 600; color: var(--muted); }
.ct-tile.is-warn { background: var(--pal-amber); border-color: var(--pal-amber); }
.ct-tile.is-warn .ct-tile-n, .ct-tile.is-warn .ct-tile-l { color: #fff; }
.ct-tile.is-quiet .ct-tile-n { color: var(--muted); }
.ct-card { padding: 8px 20px 12px; }
.ct-group + .ct-group { border-top: 1px solid var(--line-soft); margin-top: 4px; }
.ct-gh { margin: 16px 0 4px; font-size: var(--fs-14); font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; display: flex; align-items: center; gap: 8px; }
.ct-list { list-style: none; margin: 4px 0 0; padding: 0; }
.ct-list li + li { border-top: 1px solid #eef0f2; }
.ct-row { display: flex; align-items: center; gap: 16px; padding: 12px 8px; margin: 0 -8px; border-radius: var(--radius-12); color: var(--ink); text-decoration: none; }
.ct-row:hover { background: var(--surface-2); }
.ct-row:hover .ct-title { color: var(--accent); }
.ct-cal { flex: 0 0 52px; width: 52px; height: 58px; border-radius: var(--radius-12); display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1; background: var(--pal-teal); color: #fff; }
.ct-cal-m { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.ct-cal-d { font-size: 22px; font-weight: 800; margin: 2px 0 1px; }
.ct-cal-y { font-size: 10px; }
.ct-cal.is-soon { background: var(--pal-amber); }
.ct-cal.is-ended { background: var(--pal-slate-soft); color: var(--pal-slate); }
.ct-cal.is-open { background: var(--pal-teal-soft); color: var(--pal-teal); }
.ct-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.ct-title { font-weight: 600; font-size: var(--fs-16); }
.ct-sub { font-size: var(--fs-12); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ct-side { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.ct-when { font-size: var(--fs-12); font-weight: 600; color: var(--pal-teal); white-space: nowrap; }
.ct-when.is-soon { color: #fff; background: var(--pal-amber); padding: 2px 10px; border-radius: var(--radius-pill); }
.ct-when.is-ended, .ct-when.is-open { color: var(--muted); font-weight: 500; }
.ct-pdf { font-size: 11px; font-weight: 600; color: var(--pal-slate); background: var(--pal-slate-soft); border-radius: var(--radius-pill); padding: 2px 8px; white-space: nowrap; }
.ct-pdf.is-none { background: none; color: var(--muted); font-weight: 400; padding: 0; }
.ct-chev { color: var(--muted); font-size: var(--fs-20); line-height: 1; }
.ct-empty { text-align: center; padding: 40px 24px; }
.ct-empty-ic { font-size: 32px; width: 64px; height: 64px; margin: 0 auto 12px; border-radius: 50%; background: var(--pal-teal-soft); display: flex; align-items: center; justify-content: center; }
.ct-empty p { color: var(--muted); max-width: 440px; margin: 8px auto 16px; }
/* detail */
.ct-pill { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 4px 14px; border-radius: var(--radius-pill); background: var(--pal-teal-soft); color: var(--pal-teal); font-size: var(--fs-16); font-weight: 700; }
.ct-pill.is-soon { background: var(--pal-amber); color: #fff; }
.ct-pill.is-ended { background: var(--pal-slate-soft); color: var(--pal-slate); }
.ct-hero { border-top: 4px solid var(--pal-teal); padding: 20px 24px; }
.ct-hero.is-soon { border-top-color: var(--pal-amber); }
.ct-hero.is-ended, .ct-hero.is-open { border-top-color: var(--pal-slate); }
.ct-hero-top { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.ct-hero-sub { font-size: var(--fs-14); color: var(--muted); }
.ct-line { position: relative; margin: 36px 4px 8px; }
.ct-line-bar { height: 8px; border-radius: var(--radius-4); background: #e8eaed; overflow: hidden; }
.ct-line-done { display: block; height: 100%; background: var(--pal-teal); border-radius: var(--radius-4); }
.ct-hero.is-soon .ct-line-done { background: var(--pal-amber); }
.ct-hero.is-ended .ct-line-done { background: var(--pal-slate); }
.ct-line-mark { position: absolute; top: -4px; width: 2px; height: 16px; background: var(--pal-slate); transform: translateX(-1px); }
.ct-line-mark span { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); font-size: 11px; font-weight: 700; white-space: nowrap; color: var(--pal-slate); }
.ct-line-mark.is-notice { background: var(--pal-amber); }
.ct-line-mark.is-notice span { color: var(--pal-amber); }
.ct-line-mark.is-today { width: 12px; height: 12px; top: -2px; border-radius: 50%; background: #fff; border: 3px solid var(--hero, #1f2733); transform: translateX(-6px); }
.ct-line-mark.is-today span { bottom: 17px; color: var(--hero, #1f2733); }
.ct-line-ends { display: flex; justify-content: space-between; font-size: var(--fs-12); color: var(--muted); }
.ct-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin: 16px 0; }
.ct-fact { display: flex; flex-direction: column; gap: 6px; padding: 14px 16px; background: #fff; border: 1px solid var(--line-soft); border-radius: var(--radius-12); min-width: 0; }
.ct-fact-l { font-size: var(--fs-12); font-weight: 600; color: var(--muted); }
.ct-fact-v { font-size: var(--fs-16); font-weight: 600; color: var(--ink); }
.ct-add { color: var(--accent); text-decoration: none; font-size: var(--fs-14); }
.ct-file { display: inline-flex; align-items: center; gap: 8px; min-width: 0; color: var(--ink); text-decoration: none; }
.ct-file:hover .ct-file-n { color: var(--accent); text-decoration: underline; }
.ct-file-ic { flex: 0 0 auto; font-size: 10px; font-weight: 800; color: #fff; background: var(--bad); border-radius: var(--radius-4); padding: 4px 5px; letter-spacing: .03em; }
.ct-file-n { font-size: var(--fs-14); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ct-docs { list-style: none; margin: 0; padding: 0; }
.ct-docs li { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; }
.ct-docs li + li { border-top: 1px solid #eef0f2; }
.ct-docs.is-edit { border: 1px solid var(--line-soft); border-radius: var(--radius-8); padding: 0 12px; margin-bottom: 8px; }
.ct-docs label.check-row { margin: 0; font-weight: 400; }
.ct-picked { list-style: none; margin: 8px 0 0; padding: 0; font-size: var(--fs-12); color: var(--muted); }
.ct-picked li::before { content: '📄 '; }
.ct-picked[hidden] { display: none; }
.ct-scope { margin: 0 0 8px; color: var(--ink-2, var(--ink)); }
.ct-job-ic { flex: 0 0 36px; height: 36px; border-radius: 10px; background: var(--pal-teal-soft); display: flex; align-items: center; justify-content: center; }
.btn.secondary.ct-del { color: var(--bad); }
.btn.secondary.ct-del:hover { background: var(--bad-soft); }
/* form */
.ct-form { padding: 8px 24px 20px; }
.ct-step { padding: 16px 0 8px; }
.ct-step + .ct-step { border-top: 1px solid #eef0f2; }
.ct-step-h { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; font-size: var(--fs-16); }
.ct-step-n { width: 24px; height: 24px; border-radius: 50%; background: var(--pal-teal); color: #fff; font-size: var(--fs-12); font-weight: 700; display: inline-flex; align-items: center; justify-content: center; }
.ct-form textarea { width: 100%; }
.ct-prefix, .ct-suffix { display: flex; align-items: stretch; border: 1px solid var(--line); border-radius: var(--radius-8); background: #fff; overflow: hidden; }
.ct-prefix:focus-within, .ct-suffix:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.ct-prefix input, .ct-suffix input { border: 0 !important; box-shadow: none !important; margin: 0; flex: 1; min-width: 0; }
.ct-prefix span, .ct-suffix span { display: flex; align-items: center; padding: 0 12px; background: var(--surface-2); color: var(--muted); font-size: var(--fs-14); }
.ct-check { display: flex; align-items: flex-start; padding-top: 28px; }
.ct-switch { display: inline-flex; align-items: center; gap: 10px; margin: 0; font-weight: 400; cursor: pointer; }
.ct-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.ct-switch-ui { flex: 0 0 40px; height: 24px; border-radius: var(--radius-12); background: var(--line); position: relative; transition: background .15s; }
.ct-switch-ui::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: left .15s; }
.ct-switch input:checked + .ct-switch-ui { background: var(--accent); }
.ct-switch input:checked + .ct-switch-ui::after { left: 19px; }
.ct-switch input:focus-visible + .ct-switch-ui { outline: 2px solid var(--accent); outline-offset: 2px; }
.ct-ppm { border: 1px solid var(--line-soft); border-radius: var(--radius-8); padding: 8px 12px 12px; margin: 16px 0; background: var(--surface-2); }
.ct-ppm legend { font-weight: 600; font-size: var(--fs-14); padding: 0 4px; }
.ct-ppm-list { max-height: 240px; overflow-y: auto; }
.ct-ppm-row { display: flex; align-items: flex-start; gap: 8px; margin: 0; padding: 6px 0; font-weight: 400; cursor: pointer; }
.ct-ppm-row input { width: auto; margin: 2px 0 0; }
.ct-ppm-row .hint { display: block; }
.ct-ppm-row[hidden], .ct-ppm[hidden], .ct-ppm-none[hidden] { display: none; }
.ct-flabel { display: block; font-weight: 600; font-size: var(--fs-14); margin: 8px 0 8px; }
.ct-saved { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border: 1px solid var(--line-soft); border-radius: var(--radius-8); margin-bottom: 8px; }
.ct-saved label.check-row { margin: 0; font-weight: 400; }
.ct-drop { position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 20px; border: 2px dashed var(--line); border-radius: var(--radius-12); background: #fff; cursor: pointer; text-align: center; font-weight: 400; margin: 0; }
.ct-drop:hover, .ct-drop:focus-within { border-color: var(--accent); background: var(--accent-soft); }
.ct-drop.is-picked { border-style: solid; border-color: var(--accent); background: var(--accent-soft); }
.ct-drop-ic { font-size: var(--fs-24); }
.ct-drop-t { font-weight: 600; color: var(--accent); }
.ct-drop input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
.ct-actions { margin-top: 16px; padding-top: 16px; border-top: 1px solid #eef0f2; }

/* Tap-the-stars rating (QX, 3 Nov 2026) - partials/star_pick.ejs. Radios 5→1 + row-reverse: hover/checked
   lights that star and the ones before it. Amber (palette) stars, 28px targets. test/vendorStars.test.js. */
.star-pick { display: inline-flex; align-items: center; gap: 10px; }
.star-row { display: inline-flex; flex-direction: row-reverse; justify-content: flex-end; }
.star-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.star-row label { margin: 0; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; font-size: 26px; line-height: 1; color: var(--line); cursor: pointer; transition: color .1s, transform .1s; font-weight: 400; }
.star-row label:hover, .star-row label:hover ~ label, .star-row input:checked ~ label { color: var(--pal-amber); }
.star-row:hover input:checked ~ label { color: var(--line); }
.star-row:hover label:hover, .star-row:hover label:hover ~ label { color: var(--pal-amber); }
.star-row label:active { transform: scale(.9); }
.star-row input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.star-word { font-size: var(--fs-14); font-weight: 600; color: var(--pal-amber); min-width: 64px; }
.rate-card.is-ask { border-left: 4px solid var(--pal-amber); }
.rate-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.rate-form { margin: 0; }
.rate-q { display: block; font-weight: 600; font-size: var(--fs-14); margin-bottom: 4px; }
.rate-note { display: flex; gap: 8px; align-items: center; margin-top: 10px; max-width: 560px; }
.rate-note input { flex: 1; margin: 0; }
.rate-said { display: block; white-space: normal; font-size: var(--fs-12); color: var(--muted); font-style: italic; }
.rate-saids { list-style: none; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px solid #eef0f2; }
.rate-saids li { padding: 4px 0; font-size: var(--fs-14); }
.rate-stars { color: var(--pal-amber); letter-spacing: 1px; }
.rate-list { border-top: 4px solid var(--pal-amber); }
.rate-rows { list-style: none; margin: 0; padding: 0; }
.rate-rows li { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
.rate-rows li + li { border-top: 1px solid #eef0f2; }
.rate-rows form { margin: 0; flex: 0 0 auto; }
.rate-job { min-width: 0; display: flex; flex-direction: column; gap: 2px; color: var(--ink); text-decoration: none; }
.rate-job:hover .rate-title { color: var(--accent); }
.rate-title { font-weight: 600; font-size: var(--fs-14); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Ask - the assistant panel (public/assistant.js, partials/ask_button + ask_panel) ----------
   Design tokens only. A pill beside the search box opens one card: messages, an input, and - when the helper
   offers a change - a card with ONE green button and a quiet "Not this one". Phone rules are in the phone block. */
.nav-ask { margin-left: 8px; height: 34px; padding: 0 16px; border: 1px solid var(--accent-border); border-radius: var(--radius-pill); background: var(--surface); color: var(--accent); font-family: inherit; font-size: var(--fs-14); font-weight: 600; cursor: pointer; vertical-align: middle; white-space: nowrap; }
.nav-ask:hover, .nav-ask[aria-expanded="true"] { background: var(--accent-soft); }
.nav-ask:focus-visible, .ask-x:focus-visible, .ask-quiet:focus-visible, .ask-log:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ask-panel { position: fixed; right: 16px; top: 72px; z-index: 60; width: 380px; max-width: calc(100vw - 32px); max-height: calc(100vh - 96px); display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-12); box-shadow: var(--shadow-2); }
.ask-panel[hidden] { display: none; }
.ask-head { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--border); }
.ask-title { flex: 1 1 auto; font-size: var(--fs-16); font-weight: 600; color: var(--ink); }
.ask-x { flex: none; width: 32px; height: 32px; border: none; background: none; border-radius: var(--radius-pill); font-size: var(--fs-20); line-height: 1; color: var(--muted); cursor: pointer; }
.ask-x:hover { background: var(--surface-2); color: var(--ink); }
.ask-quiet { border: none; background: none; padding: 8px; font-family: inherit; font-size: var(--fs-12); color: var(--muted); text-decoration: underline; cursor: pointer; }
.ask-quiet:hover { color: var(--ink); }
.ask-log { flex: 1 1 auto; min-height: 120px; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.ask-msg { max-width: 88%; padding: 8px 12px; border-radius: var(--radius-12); font-size: var(--fs-14); line-height: 1.45; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.ask-msg.is-me { align-self: flex-end; background: var(--accent-soft); }
.ask-msg.is-bot { align-self: flex-start; background: var(--surface-2); }
.ask-msg.is-wait { color: var(--muted); }
.ask-msg a { color: var(--accent); }
.ask-card { align-self: stretch; padding: 12px; border: 1px solid var(--border-strong); border-radius: var(--radius-8); background: var(--surface); }
.ask-card-title { font-size: var(--fs-14); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.ask-card-ref { margin-top: 4px; font-size: var(--fs-12); color: var(--muted); }
.ask-card-note { margin-top: 4px; font-size: var(--fs-12); color: var(--muted); overflow-wrap: anywhere; }
.ask-card-acts { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.ask-card-acts .btn { border-radius: var(--radius-pill); white-space: normal; }
.ask-card-done { margin-top: 8px; font-size: var(--fs-14); color: var(--ok); }
.ask-card-done.is-bad { color: var(--bad); }
.ask-form { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--border); }
.ask-form input { flex: 1 1 auto; min-width: 0; height: 36px; padding: 0 12px; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); font-family: inherit; font-size: var(--fs-14); color: var(--ink); background: var(--surface); }
.ask-form input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-ring); }
.ask-form .btn { flex: none; border-radius: var(--radius-pill); }

@media (max-width: 700px) {
  /* Ask on a phone: the pill shares the search row; the panel is a card near the top (so the keyboard never
     covers the box), typing at 16px (no iOS zoom), 44px targets. */
  .nav-left:has(.nav-ask) form.nav-search { flex: 1 1 calc(100% - 80px); min-width: 0; }
  .nav-ask { order: 2; flex: none; width: 72px; padding: 0; margin: 8px 0 0 8px; height: 40px; font-size: var(--fs-16); }
  .ask-panel { left: 8px; right: 8px; top: 8px; width: auto; max-width: none; max-height: 70vh; }
  .ask-form input { height: 44px; font-size: var(--fs-16); }
  .ask-form .btn, .ask-card-acts .btn { min-height: 44px; }
  .ask-quiet { min-height: 44px; padding: 8px 12px; }
  .ask-x { width: 44px; height: 44px; }
  /* Stars on a phone: the To rate rows stack (job on top, stars under it); stars stay 32px to tap. */
  .rate-rows li { flex-direction: column; align-items: flex-start; gap: 6px; }
  .rate-job { width: 100%; }
  /* Contracts on a phone: tiles stay 3 across but smaller; rows keep the date tile, the date tag drops under the name. */
  .ct-tiles { gap: 8px; }
  .ct-tile { padding: 12px; border-radius: var(--radius-12); }
  .ct-tile-n { font-size: var(--fs-24); }
  .ct-tile-l { font-size: var(--fs-12); }
  .ct-row { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; column-gap: 12px; row-gap: 6px; }
  .ct-row .ct-cal { grid-row: 1 / span 2; }
  .ct-row .ct-side { grid-column: 2; flex-direction: row; align-items: center; justify-content: flex-start; }
  .ct-row .ct-chev { grid-column: 3; grid-row: 1 / span 2; align-self: center; }
  .ct-row .ct-job-ic { grid-row: 1; width: 36px; justify-self: start; }
  .ct-pill { font-size: var(--fs-14); border-radius: var(--radius-16); }
  .ct-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .ct-form { padding: 4px 16px 16px; }
  .ct-check { padding-top: 0; }
  .mw-title, .mw-sub { white-space: normal; }
  .rt-list { display: block; }
  .rt-table { display: none; }
  .us-unopened { columns: 1; } .ts-codes { grid-template-columns: 1fr 1fr; gap: 8px 16px; }
  /* Storage areas: section rows scroll sideways instead of wrapping into a wall of chips. */
  .v3-wrap { grid-template-columns: 1fr; }
  .v3-canvas { height: 360px; }
  .v3-list { max-height: none; }
  .v3-bar .v3-select { flex: 1 1 100%; max-width: none; font-size: var(--fs-16); }
  .v3-bar-gap { display: none; }
  .sa-row { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; }
  .sa-gap { flex: 0 0 44px; }
  /* Help refresh: topic tiles two across on a phone; login card fills the width. */
  .hp-topics { grid-template-columns: 1fr 1fr; gap: 8px; }
  .hp-topic { padding: 10px 12px; gap: 8px; }
  .hp-topic small { display: none; }
  .hp-list { padding: 4px 16px; }
  .login-wrap { margin: 32px auto; }
  .card.login-card { padding: 0; }
  .login-band, .login-body { padding-left: 20px; padding-right: 20px; }
  .hello { flex-direction: column; align-items: flex-start; padding: 14px 16px; }
  .hello-hi { font-size: var(--fs-20); }
  .dash-stats .dash-stat-value { font-size: 26px; }
  .dash-stats .dash-stat { padding: 10px 12px; }
  /* The borrow guide is a SIDE panel only where there is a side. On a phone
     it becomes one column with the guide FIRST (order:-1), because the
     alternative - leaving it after the list - buries it under two hundred
     cards where nobody scrolls to it, and the whole point of the panel is to
     be read before you start picking things. Un-sticks too: a sticky block in
     a single column just eats the top of the screen. */
  .loan-guide { position: static; order: -1; }

  /* Every page number is on screen with the current one highlighted, so
     "Page 2 of 8" repeats what the buttons already say - and dropping it
     keeps the pager from wrapping onto a third line on a phone. */
  .pager-count { display: none; }
  /* 40px tall so a page number is a reliable thumb target. The mobile audit
     flagged 25px controls elsewhere in the app; no reason to add more. */
  .pager-num { min-height: 40px; display: inline-flex; align-items: center; justify-content: center; }

  .container { padding: 14px; }
  h1 { font-size: 21px; }
  h2 { font-size: 17px; }
  /* ---------- The phone nav ----------
     Every phone nav rule lives here, and this is the ONLY phone block in the
     file. It sits after all the desktop .topnav rules, which is the point:
     a media query contributes no specificity, so an override only wins by
     coming later. The previous arrangement put these 750 lines EARLIER, up
     beside .topnav .brand, where twenty-one of them silently lost to the
     desktop rules below and did nothing on any phone.
     Twice now the same trap, in opposite directions - #289 lost `display:
     none` to a later block, this one lost a whole block to later rules. Hence
     one block, last. test/phoneNav.test.js pins both properties. */
  .topnav .brand { font-size: 18px; }
  /* The nav was costing 141px of a 667px screen before any content, on every
     page - measured in the mobile audit - because all ten-odd links wrapped
     onto three rows under the wordmark. And the group headers (Storage,
     Inventory, Maintenance), the most-tapped controls in the whole app, were
     25px tall: roughly half the 44px a thumb hits reliably.
     Collapsing to a toggle fixes both at once. Closed, the bar is one row -
     wordmark and button - so every page starts ~100px higher. Open, each
     entry is a full-width 44px row instead of a 25px word squeezed between
     two others. */
  /* Row, not column: the wordmark and the button share line one, and
     space-between pushes the button to the right edge where a thumb is. The
     menu gets line two only when it is open, via flex-basis: 100% below.
     (The old rule here was flex-direction: column, which is what put the
     button on its own row and cost most of the saving.) */
  .topnav { flex-wrap: wrap; padding: 12px 16px; }
  .navtoggle-btn {
    display: inline-flex; flex-direction: column; justify-content: center; align-items: center;
    gap: 5px; width: 44px; height: 44px; cursor: pointer;
    border-radius: var(--radius-sm); flex: 0 0 auto;
    /* Anchors the .pm-on presentation-mode dot, which is absolutely
       positioned against this button. Without it the dot would escape to the
       nearest positioned ancestor and land somewhere arbitrary. */
    position: relative;
    /* .nav-left is two rows tall on a phone now (wordmark, then the search
       box), and a flex item centres itself against the tallest sibling - so
       the burger drifted down level with the search instead of the wordmark.
       Pin it to the top row, where it reads as belonging to the title. */
    align-self: flex-start;
    /* The burger is a <label>, so the global form rule
       `label { margin: 12px 0 4px }` applies to it and quietly added 16px to
       the height of the bar on every page - the button measured 44px but
       occupied 60. Resetting it here is the difference between a 84px bar and
       a 68px one. Anything styled as a control but written as a <label> needs
       this; it is not obvious from looking at the rule. */
    margin: 0;
  }
  .navtoggle-btn span {
    display: block; width: 22px; height: 2px; background: var(--brand-ink); border-radius: 2px;
  }
  .navtoggle:focus-visible + .navtoggle-btn { box-shadow: 0 0 0 3px var(--focus-ring); }
  /* Presentation-mode dot on the hamburger. Phone-only by necessity: this is
     where the account name (and its own dot) is hidden behind a tap, so
     without this the indicator would be invisible on a phone until the menu
     was opened. Lives inside the phone block rather than above it because
     .navtoggle-btn is display:none on desktop - a rule for it up there is
     dead code, and the phoneNav guard rightly objects to one appearing after
     the block that overrides it. */
  .navtoggle-btn.pm-on::after {
    content: ''; position: absolute; top: 6px; right: 6px;
    width: 7px; height: 7px; border-radius: 50%; background: var(--warn);
  }
  /* Checked becomes "menu open" with no script at all - the sibling
     combinator is the entire mechanism. */
  .topnav .links { display: none; flex-basis: 100%; flex-direction: column; align-items: stretch; padding-bottom: 6px; }
  .navtoggle:checked ~ .links { display: flex; }
  /* Every entry becomes a full-width row. min-height 44px on the anchors AND
     on the group summaries - the summaries are the ones that measured 25px. */
  .topnav .links a,
  .topnav .links button.linklike,
  .topnav .links .navgroup > summary {
    margin-left: 0; padding: 11px 2px; font-size: 15px; min-height: 44px;
    display: flex; align-items: center; border-bottom: 1px solid var(--border);
  }
  .topnav .links .navgroup { display: block; margin-left: 0; position: static; }
  /* A dropdown floating over the page makes no sense once the menu is already
     a vertical list - the group's items just indent underneath it. */
  .topnav .links .navgroup-menu {
    position: static; box-shadow: none; border: none; border-radius: 0;
    margin: 0 0 0 14px; padding: 0; min-width: 0; background: none;
  }
  /* Both, for the same reason as the desktop rule - Log out is a button and
     would otherwise keep the desktop padding inside the phone menu. */
  .topnav .links .navgroup-menu a,
  .topnav .links .navgroup-menu button.linklike { padding: 11px 2px; }
  /* Design library (Nov 2026): the "System" sub-heading lines up with the rows on a phone; the row above
     already draws the hairline. */
  .topnav .links .navgroup-menu .navgroup-sub { margin: 0; padding: var(--space-3) 2px var(--space-1); border-top: 0; }
  /* QX: "It looks funny on the phone with the search function in the middle
     of the drop down." It was landing fifth of nine, wedged between Settings
     and Help, because the markup order that reads well across a desktop bar
     (groups | search | account) turns into an odd interruption once the bar
     is a vertical list. order:-1 lifts it to the top without touching the
     markup, so the desktop bar is unaffected - and the top is where it wants
     to be anyway, since searching is the most common reason to open this menu
     at all. margin-bottom rather than margin-top now that nothing precedes it. */
  /* Search moved out of .links and into .nav-left (see the desktop rules), so
     these had to move with it or the box would keep its 140px desktop width
     inside a full-width bar - the exact bug QX found the first time round.
     On a phone it now sits ALWAYS VISIBLE on its own line under the wordmark
     rather than hidden behind the hamburger, which is the right trade for the
     control people reach for most and the one the Help page tells them beats
     the menus. */
  /* QX: "on the phone ... feels stretched". The bar was THREE rows - the
     wordmark, the search box, then the burger alone on a third line, because
     .nav-left took the full width and pushed the burger under it. Now
     .nav-left steps aside (display: contents) so its two children sit in the
     bar itself: wordmark + burger on row one, search full width on row two,
     the menu below when open. ~110px instead of ~240px on every page. */
  .nav-left { display: contents; }
  .topnav .brand { flex: 1 1 auto; order: 0; }
  .navtoggle-btn { order: 1; }
  .nav-left form.nav-search { flex: 1 1 100%; order: 2; margin: 8px 0 0; }
  .topnav .links { order: 3; }
  /* 16px is not a style choice: iOS Safari zooms the whole page in when a
     focused input's text is smaller than 16px, and then leaves it zoomed. The
     desktop 13px was invisible until the box moved out of the hamburger and
     became the first thing a thumb reaches - now tapping search would shove
     the layout sideways on every phone in the church. The padding brings the
     control to ~40px tall at the same time, which is the other half of being
     usable with a thumb. */
  .nav-left form.nav-search input[type="search"] {
    width: 100%; min-width: 0; font-size: 16px; padding: 9px 12px;
  }
  .topnav .links form.inline { display: block; }
  /* Task #274: this is the phone, where photo search is actually used -
     standing in a storeroom with the item in one hand. Widen the button to a
     real thumb target. The surrounding frame already sets the height from
     the 16px search input beside it (see the iOS auto-zoom rule below), so
     this only needs the horizontal room. */
  .nav-photo-btn { padding: 0 16px; }
  .nav-photo-btn.standalone { margin-left: 0; padding: 9px 16px; }
  /* Single-column layouts instead of squeezed 2-3 column grids */
  .grid2, .grid3 { grid-template-columns: 1fr; gap: 0; }
  .checkbox-columns { columns: 1; }
  /* Three small figures on one line rather than two big cards and an orphan. */
  .dash-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-bottom: 16px; }
  .dash-stat { padding: 10px 12px; border-radius: 10px; box-shadow: none; }
  .dash-stat-label { font-size: 11px; letter-spacing: 0.02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Three tiles side by side on a phone: no room for the icon chip - it cut "In Progress" short (30 Oct 2026). */
  .dash-stats.dash-stats.dash-stats .dash-stat .dash-stat-label::before { display: none !important; }
  .dash-stat-value { font-size: 22px; margin-top: 2px; }
  p.subtitle { font-size: 13px; line-height: 18px; margin-bottom: 12px; }
  .venue-stats { grid-template-columns: 1fr 1fr; }
  .row.between { align-items: flex-start; }
  .alert-row { flex-wrap: wrap; gap: 6px; }
  /* Task #282: columns marked <th class="sm-hide"> disappear on phones. The
     class is copied onto each cell by partials/foot.ejs - see the comment
     there for the measurements that prompted this.
     Nothing is deleted: the desktop table is untouched, and every hidden
     field is still on the record's own detail page, one tap away. The rule
     is deliberately specific enough to beat `table td { display: block }`
     right below it. */
  table th.sm-hide,
  table td.sm-hide { display: none !important; }

  /* Tables become stacked "cards" (one per row) instead of a wide grid that
     gets cut off on a phone screen. Each cell shows its column header as a
     label above the value (data-label is set automatically by foot.ejs). */
  table thead { display: none; }
  table, table tbody, table tfoot, table tr, table td { display: block; width: 100%; }
  table {
    border: none;
  }
  table tr {
    border: 1px solid var(--border); border-radius: var(--radius-8); margin-bottom: 10px; padding: 4px 12px;
    background: var(--surface);
    /* Task #283: flex-wrap rather than plain block, so a cell can opt into
       sharing a line via .sm-pair below. Every cell still defaults to
       width:100% and therefore its own line, so this changes nothing on its
       own - flex was chosen over inline-block (which adds an unpredictable
       whitespace gap between cells, so two at 50% wrap) and over floats
       (which then need clearing on every following cell). */
    display: flex; flex-wrap: wrap; align-items: flex-start;
  }
  table td {
    border-bottom: 1px solid var(--surface-2); padding: 8px 0; text-align: left; word-break: break-word;
  }
  table td:last-child { border-bottom: none; }
  /* Task #283: two short cells share a line. Marked once on the <th> and
     copied down by partials/foot.ejs, same mechanism as .sm-hide.
     Only worth using where BOTH values are short - a pill or a number. A long
     value here would wrap to three lines in half the width and end up taller
     than it was as a full-width row. */
  /* flex: 0 0 50% rather than width: 50% - as flex items these would
     otherwise shrink to share the line with any other auto-width cell, which
     measured 137px each instead of half the row. Fixed basis, no shrink,
     means exactly half each and anything else wraps to its own line. */
  table td.sm-pair { flex: 0 0 50%; max-width: 50%; }
  /* Adjacent-sibling rather than :nth-of-type(odd): nth-of-type counts EVERY
     cell in the row including the ones .sm-hide has hidden, so whether a pair
     landed on an odd index was luck. "an .sm-pair directly after an .sm-pair"
     is exactly the right-hand half, whatever its column index. */
  table td.sm-pair + td.sm-pair { padding-left: 10px; }
  /* The pair sits on one line, so the divider belongs under the pair rather
     than under each half - otherwise the two halves draw separate underlines
     at different heights when one wraps. */
  table td.sm-pair + td.sm-pair { border-bottom-color: transparent; }
  /* Task #264: the row-select checkbox and photo-thumbnail columns (Storage
     items list, Assets list) have no column header text, so foot.ejs's
     table-labeling script never gives their <td> a data-label (see that
     script's own comment - it only sets one when the header cell has real
     text). Without a label, each one still got the same "own full-width
     row with a bottom border" treatment as every labeled field above,
     which is exactly what QX flagged as the checkbox looking "out of
     place" - a bare, unlabeled checkbox sitting alone on its own line
     above the photo, instead of reading as a compact header strip for the
     row. Marked explicitly with class="cell-compact" in the two templates
     that pair these columns (rather than a blanket ":not([data-label])"
     selector, which would also catch the trailing action-button column
     several other tables have with an equally blank header - that one's a
     single full-width tap target and isn't broken, so it's deliberately
     left alone). Side-by-side, compact, no row divider between them. */
  table td.cell-compact {
    display: inline-block;
    width: auto;
    padding: 4px 14px 8px 0;
    border-bottom: none;
    vertical-align: middle;
  }
  table td[data-label]:not([data-label=""])::before {
    content: attr(data-label);
    display: block;
    font-weight: 600; color: var(--muted-2); font-size: 11px; text-transform: uppercase;
    letter-spacing: 0.02em; margin-bottom: 2px;
  }
  /* Task #265: QX's follow-up screenshot showed the new cell-compact
     checkbox rendering tiny with a large trailing dead zone, and the photo
     thumbnail forced down onto its own line below it instead of sitting
     next to it as intended. Root cause: this rule applied width:100% to
     EVERY <input> in a table cell, including checkboxes - it was written
     for real value inputs (an inline-editable number/text field), but a
     checkbox matched the same selector. Inside the checkbox <td>'s
     shrink-to-fit inline-block box (see .cell-compact above), that 100%
     resolved against the row's full width instead - so the checkbox's own
     clickable box silently stretched to the entire row's width (looking
     like empty dead space next to a normal-sized checkmark), and used up
     the whole line, pushing the adjacent inline-block photo <td> onto the
     next line. Excluding checkboxes here and sizing them explicitly below
     fixes both the phantom blank space and the checkbox/photo split. */
  table td select, table td input:not([type=checkbox]) { width: 100%; }
  /* The assignee picker is a link, not a field, so it must escape the
     full-width rule above - stretched across the card it would look exactly
     like the boxed control this change removed. Given vertical padding
     instead, so the tap target is still a comfortable height without drawing
     a box around it. */
  /* 16px, not 13px, and not negotiable: iOS Safari zooms the whole page in
     when a focused control's computed font-size is under 16px and does not
     zoom back out (Task #263 - the reason for the select/input rule further
     down this block). My first version of this rule set 13px, and
     `.assignee-add .assignee-search` outranks that generic rule on specificity, so it
     would have re-introduced exactly that bug - on the one control QX said
     everyone would be using from a phone.
     min-height matches .stepper-btn's 44px, this file's stated tap target.
     Line-height carries the text down the middle of it without a border to
     centre against. */
  /* width:auto is load-bearing: `table td input:not([type=checkbox])` a few
     lines up sets width:100% on every input in a stacked card cell, and
     without this the resting "+ Add…" link would stretch the full width of
     the card - the boxed look QX called clunky, back again. */
  .assignee-add .assignee-search { width: auto; font-size: 16px; min-height: 44px; line-height: 44px; }
  /* Focused, it is a real text box on a phone, so it gets the phone-sized one:
     full width of the stacked card cell and a 44px tap target. line-height is
     reset because the padding now does that job and the two together would
     make a 60px-tall box. */
  .assignee-add .assignee-search:focus { width: 100%; line-height: normal; padding: 10px; }
  /* width:auto on .status-select (a class) now outranks the generic
     table-cell select width rule above, so these pills hug their text on a card
     too. Restoring the original 118px floor here rather than app-wide: the
     clipping this guards against ("In Progress" cut short by a browser
     sizing a <select> to its own internal minimum) was only ever seen on a
     phone, and paying for it on every desktop row is what made the pills
     bigger than the data. */
  /* Two phone rules, for the same reason as .assignee-add select above, and
     found the same way - by checking every class-scoped control against them
     rather than by looking at a screen.
     
     This one is NOT a regression from tonight: .status-select has carried
     font-size 12px since the pills were built, and a class outranks the
     generic `select { font-size: 16px }` further down this block. So tapping
     Priority or Status on an iPhone has always zoomed the page in and never
     zoomed back - on the Fault Tickets list, which is the page most people
     open on a phone. It survived a previous phone audit because that pass
     looked at forms, and these read as pills rather than as form controls.
     
     16px on phones only: the desktop pill keeps its 12px, which is what QX
     asked for when she said Priority and Status were too big. A phone card has
     the room. min-height 44px matches this file's stated tap target. */
  .status-select { min-width: 118px; font-size: 16px; min-height: 44px; }
  /* Task #430: 20px -> 24px, with margin. Changed HERE rather than adding a
     .ppm-checkbox/.item-checkbox rule lower down, which is what I tried
     first: this selector is (0,1,2) and a bare class is (0,1,0), so the class
     rule lost silently and the audit still measured 20px. One rule beats two
     fighting.
     A checkbox's hit area cannot be padded out reliably across browsers, so
     the margin does half the work - at 20px in a tight stacked card, the miss
     that costs you is ticking the row above. */
  table td input[type=checkbox] { width: 24px; height: 24px; margin: 8px 6px 8px 0; }
  /* Task #266: QX asked for the list photo bigger on the phone card layout.
     Desktop keeps each table's own size (36px/24px, set above) since a
     dense multi-row table benefits from a small thumbnail - but the mobile
     card has plenty of room and was cramping both down to nearly the same
     tiny size as the checkbox next to them. Same bigger size for both
     lists once stacked into cards, since there's no column-density reason
     to keep Assets' version smaller here. */
  .photo-thumb-md, .photo-thumb-sm { width: 64px; height: 64px; }
  /* Task #265: centered under a narrow desktop column, this checkbox reads
     fine there, but once the mobile layout stacks it into its own
     full-width row with "LOANABLE" as the label above it, centering leaves
     a wide, seemingly empty gap on both sides of a small checkbox instead
     of sitting directly under its label like every other field on the
     card - part of what QX flagged as checkboxes surrounded by odd blank
     space. Left-aligning here matches how every other stacked field reads. */
  .loanable-cell { text-align: left; }
  /* Classification column (Assets list): first pass here just added
     text-align:center, but that didn't actually center anything for
     managers - the quick-edit select was still catching the generic
     "table td select { width: 100% }" rule above, stretching the pill
     across the full row width. Once it's that wide, text-align:center
     centers the text within the select's content box, but the content box
     itself is off-center: .status-select reserves more padding on the
     right (22px, for the dropdown chevron) than the left (8px), so the
     visual middle of the text sits left of the pill's actual middle - the
     clipped-looking result QX flagged, and also just a much bigger colored
     highlight than the same pill shows anywhere else in the app.
     Fixing it properly means not stretching the pill to begin with: laying
     the cell out as a small flex column (so the data-label above still
     stacks correctly) and centering that column's items, then reverting
     the select/badge back to its own natural width instead of the
     generic 100% rule. That leaves a normal button-sized pill, genuinely
     centered under its label, matching how it looks everywhere else.

     Task #266: this whole block was unscoped ("table td[...]" instead of
     ".assets-table td[...]") even though the desktop version of this same
     fix (above, in the non-mobile rules) was correctly scoped to
     .assets-table. "Class" isn't a unique label - the Storage items list
     has its own, unrelated "Class" column (Item class: Equipment/Supplies/
     Consumable), and this bare-table selector matched that column too,
     centering its little tag pill and its "CLASS" label inside a flex
     column - exactly the lopsided empty-space-on-both-sides look QX
     flagged on the items list. Scoping every rule in this block to
     .assets-table (matching the desktop version) keeps the fix only where
     it was ever meant to apply. */
  .assets-table td[data-label="Class"] {
    display: flex; flex-direction: column; align-items: center; text-align: center;
  }
  .assets-table td[data-label="Class"] select,
  .assets-table td[data-label="Class"] .badge {
    width: auto;
  }
  /* "width:auto" on a <select> doesn't reliably mean "shrink to fit its
     text" the way it does on other elements - several mobile browsers still
     give a bare select some larger intrinsic default box regardless, which
     is why the pill was still visibly wider than "Class 1"/"Class 3" even
     after min-width was cleared below. fit-content is the explicit way to
     say "size this box to its content", which actually shrink-wraps it. */
  .assets-table td[data-label="Class"] select {
    width: fit-content;
  }
  /* Shrinking the select back to auto width (above) fixed the stretching,
     but the text inside still reads left-shifted: .status-select's padding
     is deliberately asymmetric everywhere else (8px left, 22px right - the
     extra room on the right is reserved for the dropdown chevron drawn via
     background-image, see the .status-select rule itself for the full
     history of why). text-align:center only centers within that padding's
     content box, so a 14px gap between the two paddings is still there
     even once the box itself isn't stretched - the same 14px the pill in
     QX's screenshot is visibly still off by. This compact mobile pill has
     no real need for a visible dropdown affordance (it's an obviously
     tappable colored pill either way, same as the read-only badge next to
     it), so here specifically: drop the reserved chevron and its padding,
     and go back to equal padding on both sides, so text-align:center
     actually lands the text in the middle of the pill you see. */
  .assets-table td[data-label="Class"] select {
    padding-right: 8px;
    background-image: none;
    /* .status-select's min-width:118px exists so Status/Priority pills
       (which get stretched full-width on mobile, and whose longest value
       is "In Progress") always have room - but it also means this select
       never actually shrinks to its own "Class 1"/"Class 3" text, so the
       colored highlight stays a fixed 118px wide even after everything
       above stopped stretching the box itself. Overriding it back to none
       here lets this one pill shrink to match its own short text, same as
       the read-only badge next to it. */
    min-width: 0;
  }
  /* Even with equal 8px/8px padding, QX still saw the text reading
     right-heavy - specifically on "Class 1", which ends in a thin digit
     that doesn't fill its own character box the way "C" does at the start.
     That's an optical illusion of the glyph shape, not the box model (the
     padding either side genuinely is equal), but it's a real, visible
     effect - a couple of px in the other direction corrects for it without
     being noticeably lopsided on Class 2/3, whose last digit fills its box
     better already. Applies to both the select and the read-only badge,
     since the same glyph-shape effect applies to plain text just as much
     as it does inside a select. */
  .assets-table td[data-label="Class"] select,
  .assets-table td[data-label="Class"] .badge {
    padding-left: 10px;
    padding-right: 6px;
  }
  /* The PPM calendar month grid is an actual grid, not a list of records —
     stacking each cell into its own full-width card (like every other table
     above) would turn it into unreadable rubble. Keep it a real table and
     let the page's existing horizontal-scroll wrapper handle narrow screens
     instead. */
  table.cal-grid, table.cal-grid tbody, table.cal-grid tr, table.cal-grid td, table.cal-grid th {
    display: revert; width: revert;
  }
  table.cal-grid thead { display: table-header-group; }
  table.cal-grid { border: none; }
  table.cal-grid tr { border: none; border-radius: 0; margin-bottom: 0; padding: 0; background: none; }
  table.cal-grid td { border-bottom: none; padding: revert; }
  table.cal-grid .cal-daynum-row td { border-top: 1px solid var(--border); }
  /* Same for the cleaning planner's week grid (it scrolls sideways). */
  table.cl-grid, table.cl-grid thead, table.cl-grid tbody, table.cl-grid tr, table.cl-grid td, table.cl-grid th { display: revert; width: revert; }
  table.cl-grid thead { display: table-header-group; }
  table.cl-grid tr { border: none; border-radius: 0; margin: 0; padding: 0; background: none; }
  table.cl-grid td::before { content: none !important; }
  /* And the Calendar's Week view (rooms x days). */
  table.wk-grid, table.wk-grid thead, table.wk-grid tbody, table.wk-grid tr, table.wk-grid td, table.wk-grid th { display: revert; width: revert; }
  table.wk-grid thead { display: table-header-group; }
  table.wk-grid tr { border: none; border-radius: 0; margin: 0; padding: 0; background: none; }
  table.wk-grid td::before { content: none !important; }
  table.wk-grid tr.wk-row.empty { display: none; }
  table.wk-grid.dragging tr.wk-row.empty { display: table-row; }

  /* Task #263 (phone workflow audit): Safari on iOS auto-zooms the whole
     page in when a focused text input's computed font-size is under 16px -
     these fields were 14px, so tapping into "Item name" (or any other field)
     on an iPhone zoomed the viewport in, and the user had to manually pinch
     back out before the next field, on every single field in a long form
     like Add Item. 16px is the documented threshold that keeps Safari from
     doing this at all - scoped to mobile only so desktop keeps its existing
     14px sizing. */
  input[type=text], input[type=number], input[type=date], input[type=time], input[type=email],
  input[type=password], input[type=search], select, textarea {
    font-size: 16px;
  }

  /* Task #263: 44px is the commonly-cited minimum comfortable touch target
     (already the standard this app used for the quantity stepper buttons -
     see .stepper-btn above). Regular buttons were sized for a mouse pointer
     (a ~32px tall hit area from 8px/14px padding at 14px font) - fine on
     desktop, tight for a thumb. Scoped to the regular .btn, not .small,
     since .small is deliberately compact for dense inline row actions
     (table Edit/Delete buttons etc.) where growing every one of them would
     be a much bigger, riskier change than this phone-workflow pass calls
     for.
     Task #265 fix: the selector below originally read "a.btn, button.btn"
     with no ":not(.small)" - since an element can carry both classes at
     once (e.g. the items list's <a class="btn small secondary">View</a>),
     that still matched every .small button too, forcing min-height:44px
     onto it. .small's own padding:5px 10px (higher specificity, so it won,
     unaffected) meant the text stayed compact-sized while the box was
     stretched tall around it - exactly the "View button is too fat" QX
     flagged. Excluding .small here is what the comment above already
     described intending to do. */
  a.btn:not(.small), button.btn:not(.small) {
    min-height: 44px;
    padding-top: 11px;
    padding-bottom: 11px;
  }

  /* Task #263: the photo dropzone is now the very first thing tapped on the
     Add Item form (Task #262) - a bigger target and icon make it read as
     the obvious starting point of the phone workflow rather than a small
     generic upload widget. */
  .photo-dropzone { padding: 26px 18px; }
  .photo-dropzone-icon { width: 40px; height: 40px; font-size: var(--fs-20); }

  /* ---------- Borrowing cards (Task #322) ----------
     Stack them: photo and name on one line, then the add form full width with
     a tappable button rather than a 72px box squeezed against the edge. */
  .loan-card { flex-wrap: wrap; }
  /* Stays on the first row with the name - it is the number they came for, and
     pushing it onto the stacked action row would bury it again. */
  .loan-card-stock { min-width: 0; }
  .loan-card-action { width: 100%; text-align: left; }
  .loan-card-action form { width: 100%; }
  .loan-card-action input[type=number] { width: 80px; }
  .loan-card-action button { flex: 1 1 auto; }
  /* The bar has to clear the phone's own browser chrome, and a button that
     wraps under the count is the difference between one tap and a scroll. */
  .cart-bar { flex-wrap: wrap; }
  .cart-bar .btn { flex: 1 1 auto; text-align: center; }

  /* ---------- Filter bars ----------
     The desktop rules (search for .filter-form above) size these to their own
     content so the bar sits on one line. On a phone the row has to stack
     whatever we do, and a half-width dropdown next to dead space reads as
     broken - so each control goes back to filling its line, which is what
     every other form in the app does at this width.
     max-width has to be cleared explicitly: the desktop cap would otherwise
     hold them at 260px on a 375px screen. */
  .filter-form select,
  .filter-form input[type=search],
  .filter-form input[type=text],
  .filter-search input[type=search] { width: 100%; max-width: none; }

  /* ================= Task #430: phone tap targets and text =================
     QX: "can u fix all of the UIUX on the phone."

     Measured across 24 pages at 390px with a headless browser rather than
     guessed. Two things came back clean and are deliberately NOT touched:
     nothing scrolls sideways, and no form field is under 16px (so iOS never
     zooms the page on focus). What follows is only what actually measured
     wrong. All of it lives inside this phone-only block; desktop is untouched.

     ---- 1. .btn.small was 25px tall (~73 of them) ----
     The most-tapped controls in the app: Add photo, Mark done, Edit, Remove,
     Back to tickets. A fingertip is ~44px, so this was a little over half.

     Task #263 deliberately EXCLUDED .small from the 44px rule above, and
     Task #265 then had to fix a related bug - read both comments before
     changing this. The reason .small was excluded: raising min-height alone
     left .small's own padding:5px 10px winning on specificity, so the box
     stretched while the text stayed small and top-aligned. QX called the
     result "too fat".

     So this grows the box AND the padding AND centres the label, rather than
     stretching a box around unchanged text. inline-flex + centring is what
     makes that hold no matter how the label wraps. */
  a.btn.small, button.btn.small {
    min-height: 44px;
    padding: 10px 14px;
    font-size: var(--fs-14);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ---- 2. Bulk-select checkboxes: see the `table td input[type=checkbox]`
     rule earlier in this block. Sized there, not here, because that selector
     outranks a bare class and a second rule would just lose to it. ---- */

  /* ---- 3. Help text under fields was 12px (209 instances) ----
     !important is load-bearing here and not laziness: .muted is already 13px
     in this stylesheet, but 79 views carry an inline style="font-size:12px"
     that beats any class selector. The inline styles are the thing being
     overridden. Scoped to this phone block so desktop keeps its density.

     13px -> 14px rather than 16px: these are secondary hints sitting under a
     16px field, and matching the field would stop them reading as secondary. */
  p.muted, .muted p, p.help, .field-hint {
    font-size: var(--fs-14) !important;
    line-height: 1.45;
  }

  /* ---- 4. Status pills and stat labels were 12px ----
     Short uppercase strings, so they survive being small better than prose -
     13px, not 14px. These are the words someone reads at a glance to decide
     whether a ticket needs them: HIGH, In Progress, "442h over". */
  .badge, .dash-stat-label, .count-pill,
  .venue-card-sub, .venue-meta-unit { font-size: 13px !important; }

  /* ---- 5. Dropdowns were 37px ----
     Just under comfortable, and unlike the buttons above there is no
     competing padding rule to fight. */
  select { min-height: 44px; }

  /* ---- 6. Standalone action links were 15-18px tall ----
     Scoped to links that stand alone as an action. Deliberately NOT a blanket
     `a { min-height }`: most of the 145 short links measured are inline links
     inside a sentence, and stretching those would put gaps through the middle
     of prose. */
  a.filter-summary-clear, a.filter-chip, .row > a:not(.btn):not(.tab) {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
  }

  /* ---- 7. Section tabs were 34px, and loose checkboxes 13px ----
     Found by probing the live pages rather than by reading: `a.active` could
     have been any of several things, and the 13px checkboxes turned out to be
     the ones OUTSIDE a table cell (the "still looking for a vendor" style
     tick, and the access-group labels) - which the table rule above never
     reached. A 13px checkbox is a third of a fingertip.

     The tab rule targets .tabs a rather than a.active: the SELECTED tab is
     not the one you are trying to hit. */
  .tabs a { min-height: 44px; display: inline-flex; align-items: center; }
  .check-row input[type=checkbox],
  label > input[type=checkbox] { width: 22px; height: 22px; }

  /* QX: "on phone doesn't look good. Feels stretched". Section tabs were
     wrapping into three rows; they now sit on ONE line that scrolls
     sideways (like Google's tab bars), and cards lose some desktop padding
     so content gets the width. */
  .tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .tabs::-webkit-scrollbar { display: none; }
  /* Batch 4 (14 Oct 2026): a tab row that scrolls sideways fades at the edge that has more
     (classes set by public/filters.js), so "Lending setup" / "Spend" aren't hidden by surprise. */
  .tabs.tabs-more-right { -webkit-mask-image: linear-gradient(to right, #000 82%, transparent); mask-image: linear-gradient(to right, #000 82%, transparent); }
  .tabs.tabs-more-left { -webkit-mask-image: linear-gradient(to left, #000 82%, transparent); mask-image: linear-gradient(to left, #000 82%, transparent); }
  .tabs.tabs-more-left.tabs-more-right { -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 82%, transparent); mask-image: linear-gradient(to right, transparent, #000 12%, #000 82%, transparent); }
  .tabs a { flex: none; white-space: nowrap; }
  /* "← Back to …" is a quiet text link on a phone, not a big grey button. */
  .back-link { background: none !important; border: none !important; box-shadow: none !important; color: var(--accent) !important; padding: 0 !important; min-height: 0 !important; height: auto !important; font-weight: 500; margin-bottom: 8px !important; }
  body .photo-dropzone { padding: 14px 12px; }
  body .photo-dropzone-icon { width: 32px; height: 32px; font-size: 18px; }
  body .photo-dropzone-hint { display: none; }
  body .photo-dropzone-prompt { display: flex; align-items: center; justify-content: center; gap: 10px; }
  body .photo-dropzone-prompt .photo-dropzone-icon { margin: 0; width: 28px; height: 28px; font-size: 16px; }
  /* Shared phone patterns (QX: phone UI/UX pass):
     .hide-phone   desktop-only actions (exports, cost reports)
     .fab-phone    the page's main "+" action as a floating button
     .pf           search first, other filters behind a "Filters" chip
     .m-list/.m-row  a tidy one-row-per-record list instead of a table */
  .hide-phone { display: none !important; }
  /* Room under the floating + so it never sits on the last row. */
  body:has(.fab-phone), body:has(#add-work-btn) { padding-bottom: 88px; }
  /* An alert card with nothing in it shrinks to one slim line ("Assigned to
     you · 0") instead of a full card saying "Nothing ..." on a phone. */
  body .alert-section:not(.has-items) { padding: 10px 14px; margin-bottom: 10px !important; box-shadow: none; }
  body .alert-section:not(.has-items) h3 { margin: 0; font-size: var(--fs-14); }
  body .alert-section:not(.has-items) .empty { display: none; }
  /* A form's main button spans the width - an easy thumb target at the end. */
  body .card form > button.btn[type="submit"]:not(.small):not(.secondary) { width: 100%; height: 48px; justify-content: center; }
  body .fab-phone { position: fixed; right: 16px; bottom: 20px; z-index: 50; width: 56px; height: 56px; padding: 0; border-radius: var(--radius-16); font-size: 0; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 1px 3px rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15); }
  body .fab-phone::before { content: '+'; font-size: 30px; font-weight: 400; }
  .pf { display: flex; flex-wrap: wrap; align-items: center; width: 100%; }
  .pf .pf-search { order: -2; flex: 1 1 0 !important; width: auto !important; max-width: none !important; min-width: 0; }
  .pf .pf-btn { order: -1; display: inline-flex; align-items: center; height: 40px; padding: 0 14px; margin: 0; border: 1px solid var(--border-strong); border-radius: 20px; font-size: var(--fs-14); font-weight: 500; cursor: pointer; }
  .pf > select { display: none; }
  .pf .pf-toggle:checked ~ select { display: block; flex: 1 1 100%; }
  .pf .pf-toggle:checked + .pf-btn { background: #e8f0fe; border-color: #d2e3fc; color: #1967d2; }
  body .m-list { display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-12); overflow: hidden; margin-bottom: 16px; }
  .m-table { display: none; }
  .m-row { display: flex; gap: 12px; align-items: center; padding: 12px 14px; border-top: 1px solid var(--surface-2); color: var(--ink); text-decoration: none; }
  .m-row:first-child { border-top: none; }
  .m-row:active { background: var(--surface-2); }
  .m-body { flex: 1; min-width: 0; display: grid; gap: 2px; }
  .m-title { font-size: 15px; line-height: 20px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .m-meta { font-size: 13px; line-height: 18px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .m-meta b { font-weight: 500; color: var(--ink); }
  .m-sub { font-size: var(--fs-12); line-height: 16px; color: var(--muted-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .m-sub:empty { display: none; }
  .m-low { display: inline-block; padding: 0 6px; border-radius: var(--radius-8); background: #fce8e6; color: #c5221f; font-size: 11px; font-weight: 600; line-height: 16px; }
  .m-low.m-low-amber { background: var(--warn-soft); color: var(--warn); }
  .m-thumb { flex: none; width: 48px; height: 48px; border-radius: var(--radius-8); object-fit: cover; }
  .m-empty { padding: 24px; text-align: center; color: var(--muted-2); margin: 0; }
  .card { padding: 16px; border-radius: var(--radius-12); }

  /* UI audit, 10 Oct 2026: page-level styles (calendar filters, the item
     page's low-stock box) set some fields to 13-14px, and iOS zooms the
     whole page when one is tapped. One rule, strong enough to win. */
  body input:not([type=file]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]), body select, body textarea {
    font-size: 16px;
  }
  /* Three stats per row on a phone; minmax(0,…) so a long label can't push
     the row wider than the screen (Inventory dashboard scrolled sideways). */
  .dash-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* One-time success/error banner - see lib/flash.js + the app.use() flash
   middleware in server.js for how res.locals.flash gets set, and
   partials/head.ejs for where this markup is rendered. */
/* Presentation mode indicator. This was a full-width amber banner under the
   nav reading "Presenting - restricted areas are hidden from your screen".
   QX, seeing it on the projector: "can this not be at the top, every can
   still see. Can it be abit more discrete" - a banner announcing that
   restricted areas exist tells the room the thing the mode exists to hide.

   Now a 7px dot beside her own name, and a matching one on the phone
   hamburger (where the name lives inside the menu and would otherwise need a
   tap to reveal). Warn amber, because it is a state someone chose rather than
   a fault, and because it reads as decoration to anyone who does not know
   what it means. No text, no tooltip: a title attribute would pop the
   explanation up on hover, in front of everyone. */
.pm-dot {
  display: inline-block; width: 7px; height: 7px; margin-left: 6px;
  border-radius: 50%; background: var(--warn); vertical-align: middle;
}
/* Off-screen but still announced. The dot carries no text, so this is the
   only thing a screen reader has to go on. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.flash {
  position: relative;
  padding: 10px 36px 10px 14px;
  border-radius: var(--radius-8);
  margin-bottom: 14px;
  font-size: var(--fs-14);
  line-height: 1.4;
}
.flash-success { background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-border); }
.flash-error { background: var(--bad-soft); color: var(--bad); border: 1px solid var(--bad-border); }
.flash-close {
  position: absolute;
  top: 6px;
  right: 8px;
  background: none;
  border: none;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  color: inherit;
  opacity: 0.55;
  padding: 4px;
}
.flash-close:hover { opacity: 1; }

/* Photo upload dropzone — click-or-drag file upload, used everywhere a photo
   can be attached (storage items, consumables, fault tickets). One shared
   implementation instead of one-off versions per form: see the
   ".photo-upload-input" enhancement script in partials/foot.ejs, which turns
   any file input carrying that class into one of these at page load. */
.photo-dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius-8);
  padding: 18px;
  text-align: center;
  cursor: pointer;
  color: var(--ink-2);
  transition: border-color .15s ease, background-color .15s ease;
}
.photo-dropzone:hover,
.photo-dropzone:focus,
.photo-dropzone.dragover {
  border-color: var(--accent);
  background-color: var(--accent-soft);
  outline: none;
}
.photo-dropzone-icon {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 8px;
  font-size: 17px;
  line-height: 1;
}
.photo-dropzone-hint { font-size: var(--fs-12); margin-top: 2px; }
.photo-dropzone-filename { font-size: var(--fs-12); margin-top: 6px; word-break: break-word; }
.photo-dropzone-existing { margin-bottom: 8px; }
.photo-dropzone-existing img,
.photo-dropzone-preview img {
  max-width: 140px;
  max-height: 140px;
  border-radius: 6px;
  display: block;
  margin: 0 auto 8px;
}
/* Task #263: "Identify from photo" (item_form.ejs) is now the first action
   on the Add Item form (Task #262 moved Photos to the top so the workflow
   is photo -> identify -> rest of the form fills in), so it needs to read
   as the form's primary call-to-action rather than a small secondary
   afterthought tucked under the file picker. Full-width and sized like a
   real primary button - easy to hit one-handed with a phone in the other
   hand while standing in a storeroom - instead of the old cramped
   `btn secondary small`. Disabled look comes from the generic
   button.btn:disabled rule above.  */
/* Task #273: the photo block that leads the Add Consumable form. Full width
   rather than sitting in a .grid2 half, because on a phone this is the first
   and most important action on the page (photograph the product, tap
   Identify, check what came back) and a half-width dropzone beside an empty
   column looked like an afterthought. */
.photo-first {
  margin-bottom: 18px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
}

/* Task #276: photo stock-take panel on item_detail / consumable_detail.
   Styled as a quiet review panel rather than a result banner - the whole
   point is that the person reads the two numbers, disagrees if they want to,
   and then confirms, so nothing here should look like a finished action. */
.photo-count-result {
  margin-top: 14px; padding: 14px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.photo-count-summary {
  display: flex; gap: 28px; flex-wrap: wrap; margin-bottom: 10px;
}
.photo-count-summary strong { font-size: var(--fs-20); display: block; }
.photo-count-label {
  display: block; font-size: var(--fs-12); color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.04em;
}
.photo-count-verdict { margin: 0 0 10px; font-size: 15px; }
.photo-count-verdict.match { color: var(--ok); }
.photo-count-verdict.up { color: var(--accent); }
.photo-count-verdict.down { color: var(--warn-strong); }
.photo-count-fields {
  margin: 0 0 10px; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-8);
}
.photo-count-fieldshead { margin: 0 0 6px; font-size: 13px; color: var(--ink-2); }
.photo-count-field { display: block; margin-bottom: 4px; }
/* Not a disclaimer for its own sake: counting a deep or overlapping shelf
   from one photo genuinely is unreliable, and the person is about to write
   the result into a stock ledger. */
.photo-count-caveat { margin: 0 0 12px; font-size: var(--fs-12); color: var(--muted); }

/* Task #277: multi-photo counting. The per-photo list is the point of the
   whole design - it's what makes a duplicate angle visible ("Photo 1: 8,
   Photo 2: 8" for one shelf) instead of hiding inside an authoritative-
   looking total. */
/* Task #278: the "What the photo shows" block. A definition list because
   that is what it is - label/value pairs read off a product - and it keeps
   the labels aligned without a table. */
.photo-count-read {
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-8);
}
.photo-count-readhead {
  margin: 0 0 6px; font-size: var(--fs-12); color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.photo-count-read dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 2px 14px; }
.photo-count-read dt { font-size: 13px; color: var(--muted); }
.photo-count-read dd { margin: 0; font-size: var(--fs-14); color: var(--ink); }
/* Neutral, not alarming - see the comment in public/photo-count.js. A name
   read slightly differently from the record is common and harmless, so this
   states the fact and lets the person judge it. */
.photo-count-differs { font-size: var(--fs-12); color: var(--warn); }
.photo-count-isblank { font-size: var(--fs-12); color: var(--muted); font-style: italic; }

.photo-count-list { list-style: none; margin: 0 0 10px; padding: 0; }
.photo-count-list li { margin-bottom: 4px; }
.photo-count-photo { display: block; }
.photo-count-failed { font-size: 13px; color: var(--warn-strong); }
.photo-count-note {
  margin: 0 0 10px; padding: 8px 10px; font-size: 13px;
  background: var(--warn-soft); border: 1px solid var(--warn-border); border-radius: 6px; color: var(--warn);
}
/* Sits directly above the tick boxes that fix the problem it describes. */
.photo-count-overlap {
  margin: 0 0 12px; padding: 8px 10px; font-size: 13px;
  background: var(--warn-soft); border: 1px solid var(--warn-border); border-radius: 6px; color: var(--warn-strong);
}
/* The total is editable on purpose: the person owns the final figure, not
   the model. Sized to match the big read-only balance beside it. */
.photo-count-total {
  font-size: var(--fs-20); font-weight: 600; width: 120px; padding: 4px 8px;
}

.identify-btn {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 12px 16px;
  font-size: 15px;
  font-weight: 600;
}

/* ---------- Pager (views/partials/pager.ejs) ----------
   QX: "can u do up the option for us to click from any of the pages 1-8
   instead of only being able to click next". Eight lists each had their own
   Prev/Next markup; they now all include one shared partial that renders
   numbered links, so these styles are the only place the look is defined. */
.pager {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  flex-wrap: wrap;
}
/* min-width so a single digit isn't a noticeably smaller target than a
   double digit, and so the row doesn't jitter as you move between page 9
   and page 10. */
/* Plain numerals, not buttons. Eight filled pills in a row drew more of the
   eye than the table they belong to; numbers with one underlined read as
   "where am I" instead of "press one of these eight things". Padding is kept
   so each number is still a comfortable target even without a visible box. */
.pager-num {
  min-width: 34px;
  text-align: center;
  padding: 6px 9px;
  font-size: var(--fs-14);
  font-weight: 560;
  color: var(--ink-2);
  text-decoration: none;
  border-radius: var(--radius-sm);
  /* transparent by default so the box doesn't shift by 2px when a page
     becomes the current one */
  border-bottom: 2px solid transparent;
}
a.pager-num:hover {
  color: var(--accent);
  background: var(--accent-soft);
  text-decoration: none;
}
/* The current page is a <span>, not a link - it should not look clickable. */
.pager-num[aria-current="page"] {
  cursor: default;
  color: var(--accent);
  font-weight: 700;
  border-bottom-color: var(--accent);
  border-radius: 0;
}
.pager-gap {
  padding: 0 2px;
}
/* QX: "the page 3 of 8 can it be below and centralise it". A full-width flex
   basis pushes it onto its own line (the row already wraps), and the parent's
   justify-content:center centres it there - so this needs no extra wrapper
   element and stays centred however many numbers are on the line above. */
.pager-count {
  flex: 0 0 100%;
  text-align: center;
  margin-top: 8px;
}

/* ---------- Loan request as a document ("something like a PO for the event") ----------
   QX asked for the loan request to read like a purchase order rather than a
   web page, because that is the form this information has always taken on
   paper and it gets handed to whoever is at the store. So: reference
   top-right, the event as a labelled header block, items as the body.
   Deliberately reuses the existing .card shell rather than inventing a second
   panel style - only the INSIDE of the card is document-shaped. */
.doc-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 20px; padding-bottom: 16px; border-bottom: 2px solid var(--border-strong);
}
.doc-kicker {
  font-size: var(--fs-12); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; margin-bottom: 4px;
}
.doc-title { margin: 0; font-size: 26px; line-height: 1.2; }
.doc-sub { margin: 4px 0 0; color: var(--muted); }
.doc-ref { text-align: right; flex: 0 0 auto; }
.doc-ref-num {
  font-size: var(--fs-20); font-weight: 700; font-variant-numeric: tabular-nums;
  margin-bottom: 6px;
}
/* Label-above-value pairs, the way a form on paper reads. auto-fit rather
   than a fixed column count so it collapses to one column on a phone without
   needing its own media query. */
.doc-fields {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 14px 24px; margin: 18px 0 0;
}
.doc-fields dt {
  font-size: var(--fs-12); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; margin-bottom: 2px;
}
.doc-fields dd { margin: 0; font-size: 15px; color: var(--ink); }
.doc-note {
  margin: 18px 0 0; padding: 12px 14px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
}

/* ---------- The borrowing cart (Task #322) ----------
   QX: "I imagine like a store check out." The old borrow page was the whole
   lendable catalog as one table with a number box on every row. These styles
   are what turn it into cards you add from, plus the cart bar and the cart
   table on checkout. */

/* One row per item: photo, details, add form. Not a grid of tiles - the names
   are long ("Trestle Table 6ft, white"), the stock line has two parts, and a
   tile wide enough for both is a tile you only fit two of on a phone. A row
   scans faster and stacks cleanly. */
/* TIGHT ON PURPOSE. The first version gave every fact its own line and four
   items filled a phone screen - QX: "too hard to see the info". Same facts,
   one line each, so a dozen fit where four did. */
/* The borrow page: catalogue on the left, the how-it-works guide beside it.
   QX asked for the guide "at the side", which is a desktop idea - the phone
   rule is in the ONE max-width:700px block, where this goes back to a single
   column and the guide comes FIRST, so it is read before the list rather than
   stranded under two hundred cards.
   minmax(0, 1fr) rather than 1fr because a grid column defaults to min-content
   width, and the longest item name would otherwise refuse to wrap and push the
   guide off the side of the screen. */
/* ONE COLUMN BY DEFAULT, two only when there is genuinely room. Measured on
   the live page rather than assumed: the first version split at the app's
   usual 700px phone breakpoint, and at an 800px window that left the
   catalogue column 249px wide - every card crushed into a stack beside a
   300px panel, on exactly the tablets and small laptops people use. The guide
   is 300px fixed, so the split has to wait until the list still has ~650px of
   its own. */
.loan-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; align-items: start; }
@media (min-width: 1000px) {
  .loan-layout { grid-template-columns: minmax(0, 1fr) 300px; }
  /* QX: "i dont need 2 review and checkout right" - correct, two of the same
     button on one screen.
     The sticky bar is NOT deleted, because it is doing a real job on a phone:
     there the cart panel sits above the list and scrolls away, so the bar is
     the only Checkout within reach after a few screens of cards. At this width
     the panel is beside the list and sticky, so the bar is the redundant one
     and it is the bar that goes. Hidden at the breakpoint where the panel
     appears, so exactly one of them is ever on screen. */
  .cart-bar { display: none; }
}
/* The side column: the cart (what you are doing) above the guide (how it
   works). Sticky as a unit, so the cart stays on screen while a long
   catalogue scrolls past - which is the whole point of having it there. */
.loan-side { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 12px; }
.loan-cart, .loan-guide {
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
}
.loan-cart { padding: 14px 16px; }
.loan-cart h2 { margin: 0 0 10px; font-size: 16px; }
.loan-cart-lines { list-style: none; margin: 0 0 12px; padding: 0; max-height: 44vh; overflow-y: auto; }
.loan-cart-lines li { display: flex; align-items: baseline; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--surface-2); font-size: 13px; }
.loan-cart-lines li:last-child { border-bottom: 0; }
.loan-cart-qty { font-weight: 700; font-variant-numeric: tabular-nums; min-width: 22px; }
.loan-cart-name { flex: 1 1 auto; min-width: 0; }
/* The remove control is a form, so it needs un-forming to sit on one line. */
.loan-cart-remove { margin: 0; flex: 0 0 auto; }
.loan-cart-remove button {
  background: none; border: 0; cursor: pointer; padding: 0 2px;
  color: var(--muted-2); font-size: 18px; line-height: 1;
}
.loan-cart-remove button:hover { color: var(--bad); }
.loan-cart-go { width: 100%; text-align: center; }
.loan-cart-empty { font-size: 13px; margin: 0; }

/* QX: "maybe it should be a psuh button, when they press it appears at th
   side." A <details> IS that button, with no script and no state to keep in
   sync - it opens on its own, remembers itself, and announces itself properly
   to a screen reader. */
.loan-guide { padding: 0; }
.loan-guide > summary {
  cursor: pointer; padding: 12px 16px; font-weight: 600; font-size: var(--fs-14);
  list-style: none; display: flex; justify-content: space-between; align-items: center;
}
.loan-guide > summary::-webkit-details-marker { display: none; }
.loan-guide > summary::after { content: '+'; color: var(--muted-2); font-size: 17px; }
.loan-guide[open] > summary::after { content: '\2212'; }
.loan-guide[open] > summary { border-bottom: 1px solid var(--surface-2); }
.loan-guide ol { margin: 12px 0 0; padding: 0 16px 0 34px; }
.loan-guide li { font-size: 13px; line-height: 1.45; margin-bottom: 6px; color: var(--ink); }
.loan-guide p { padding: 0 16px; }
.loan-guide > *:last-child { padding-bottom: 14px; }

.loan-card {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; margin-bottom: 4px;
  border: 1px solid var(--border); border-radius: var(--radius-8); background: var(--surface);
}
/* Something already in the cart is marked, so scrolling back past it answers
   "did I add that one?" without opening the cart. */
.loan-card.in-cart { border-color: var(--accent-border); background: var(--accent-soft); }
.loan-card-photo { flex: 0 0 auto; }
/* Same footprint when there is no photo, so a mixed list does not stagger
   left and right down the page. */
.loan-card-nophoto {
  display: block; width: 36px; height: 36px; border-radius: var(--radius-4);
  background: var(--surface-2); border: 1px solid var(--border);
}
.loan-card-body { flex: 1 1 auto; min-width: 0; }
.loan-card-body h3 { margin: 0; font-size: 15px; line-height: 1.3; }
.loan-card-meta { margin: 1px 0 0; font-size: 13px; line-height: 1.35; }

/* The count gets its own block rather than a clause in a grey sentence.
   "Facilities Management · 2 pcs available", under a heading already reading
   FACILITIES MANAGEMENT, was three facts fighting in one line - QX: "the text
   for the number of items left is still very messy". Big figure, small label,
   sitting immediately before the Add control where the eye is already going. */
.loan-card-stock {
  flex: 0 0 auto; text-align: right; line-height: 1.15; min-width: 72px;
}
/* Sized to sit level with the item name rather than shout over it. The first
   pass ran the figure at 17px with a small-caps label under it, which QX read
   as "a bit too big" - the count is the answer to the question, not the
   headline of the card. */
.loan-card-stock strong { display: block; font-size: var(--fs-14); white-space: nowrap; }
.loan-card-stock-label { display: block; font-size: 11px; color: var(--muted); }
.loan-card-action { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.loan-card-action form { display: flex; gap: 6px; align-items: center; }
.loan-card-action input[type=number] { width: 64px; }
.loan-card-held { font-size: 13px; color: var(--accent); white-space: nowrap; }

/* Department heading between groups of cards. Lighter than a normal h2 since
   it separates rather than opens a section. */
.loan-group {
  font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); margin: 16px 0 6px; font-weight: 700;
}
.loan-group:first-of-type { margin-top: 0; }

/* Sticks to the bottom of the screen: on a phone the Checkout button would
   otherwise be two hundred cards away from wherever you just added something.
   Rendered only when the cart has anything in it. */
/* ---------- "Showing 12 of 340 · [Decor ×] · Clear all filters" ----------
   Task #418. One bar, every list, so what you learn on one page works on the
   next. Renders only when something is actually filtered - see
   partials/filter_summary.ejs. */
.filter-summary {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin: 8px 0 4px; font-size: 13px;
}
.filter-summary-count { color: var(--muted); }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 10px; border-radius: var(--radius-pill);
  background: var(--accent-soft); border: 1px solid var(--accent-border);
  color: var(--ink); text-decoration: none; line-height: 1.4;
}
.filter-chip:hover { border-color: var(--accent); }
.filter-chip-x { color: var(--muted-2); font-size: 15px; }
.filter-chip:hover .filter-chip-x { color: var(--bad); }
.filter-summary-clear { color: var(--muted); text-decoration: underline; }
/* Visually hidden but read aloud: a chip's link text is the filter's NAME,
   which on its own does not tell a screen-reader user that following it
   removes the filter rather than applying it. */
.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;
}

.cart-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 14px; margin-bottom: 16px;
  background: var(--accent-soft); border: 1px solid var(--accent-border); border-radius: 10px;
  box-shadow: 0 -2px 8px rgba(15, 23, 42, 0.06);
}

.cart-qty { display: flex; gap: 6px; align-items: center; }
/* A line asking for more than is now available. The cart never reserved
   anything - approving does - so this is normal, not an error state, and it
   is marked on the line that needs fixing rather than as a banner at the top
   that does not say which. */
.cart-line.over { background: var(--bad-soft); }

/* The phone rules for these live in the ONE @media (max-width: 700px) block
   further down. There is exactly one, and it comes last, so that "which rule
   wins on a phone" always has one answer - a second block here would fight it
   depending on which came first. test/phoneNav.test.js pins both. */

/* ---------- Printing ----------
   The first print rules in this app. A loan request is the one page people
   will genuinely put on paper - to carry to the store, or to have somebody
   sign. Everything that cannot be pressed on paper comes off: nav, buttons,
   forms, flash banners. .no-print marks the rest case by case. */
@media print {
  .topnav, .flash, .tabs, .no-print, form.inline { display: none !important; }
  body { background: #fff; }
  .container { max-width: none; padding: 0; }
  .card {
    box-shadow: none; border: 1px solid #999; border-radius: 0;
    margin-bottom: 12px; break-inside: avoid;
  }
  /* Badges are colour-coded on screen; printed (often in mono) the colour
     carries nothing, so they fall back to an outline that still reads. */
  .badge { border: 1px solid #666 !important; background: #fff !important; color: #000 !important; }
  a { text-decoration: none; color: #000; }
  table { font-size: var(--fs-12); }
  thead { display: table-header-group; }  /* repeat headers across pages */
  tr { break-inside: avoid; }
}


/* Phone-only pieces stay out of the way on larger screens. */
.m-list, .pf-btn { display: none; }

/* QX (14 Oct 2026): while a filter change loads, the list dims instead of the page just freezing. */
body.is-filtering main, body.is-filtering .container { opacity: .55; transition: opacity .12s; pointer-events: none; }

/* Batch 4: the venue picker sits under the tabs, left-aligned, compact. */
form.venue-filter { margin: 0 0 14px; }
form.venue-filter select { width: auto; min-width: 180px; }
/* Batch 4 (14 Oct 2026): the Users table fits a laptop without sliding under the sticky Actions column. */
#users-table th, #users-table td { padding-left: 8px; padding-right: 8px; }
#users-table th { white-space: normal; }

/* Cupboard list (QX, 4 Nov 2026: "looks messy" -> "still messy") - views/partials/cupboard_list.ejs.
   One line per wall, cupboards flowing along it as tiles. */
.cb-walls { margin-top: 12px; }
.cb-wall { display: flex; align-items: flex-start; gap: 16px; padding: 10px 0; border-top: 1px solid #eceef0; }
.cb-wall:first-child { border-top: none; }
.cb-gtitle { flex: none; width: 92px; margin: 7px 0 0; font-size: var(--fs-12); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.cb-tiles { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.cb-tile { display: inline-flex; align-items: center; gap: 2px; padding: 2px; border: 1px solid var(--line-soft); border-radius: 10px; background: #fff; }
.cb-tcode { padding: 0 6px 0 8px; font-size: var(--fs-14); }
.cb-row.cb-chip { display: inline-flex; align-items: center; width: auto; min-height: 30px; padding: 0 10px; gap: 6px; border: 0; border-top: 0; border-radius: var(--radius-8); background: none; font-size: 13px; color: inherit; }
.cb-tile.is-one .cb-chip b { font-size: var(--fs-14); }
.cb-row.cb-chip:hover, .cb-row.cb-chip.is-on { color: var(--accent); background: var(--accent-soft, #e8f5ee); }
.cb-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex: none; background: #c4c7ca; }
.cb-dot.t-empty { background: var(--pal-green); } .cb-dot.t-some { background: #d9822b; } .cb-dot.t-full { background: var(--bad); }
.cb-n { font-size: var(--fs-12); font-weight: 600; color: var(--muted); }
.cb-n:empty { display: none; }
.cb-key { margin-top: 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
.cb-key > span { white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
@media (max-width: 640px) { .cb-wall { flex-direction: column; gap: 8px; } .cb-gtitle { width: auto; margin: 0; } }

/* ==================== Design library (Nov 2026) - START ====================
   Shared pieces added with views/partials/ui/* and the reference page /admin/design-library.
   Tokens only: no raw colours, no sizes off the scale. Nothing in this block touches an
   existing class, so no existing page changes. (Kept at the end of the file, on its own,
   so it merges cleanly with other work on this stylesheet.) */

/* ui/answer_pill - the one answer first: ✓ ok · ⚠️ warn · ❓ ask · grey when there is nothing to say. */
.answer-pill { display: inline-flex; align-items: center; gap: var(--space-1); min-height: var(--space-6); padding: 0 var(--space-3); border-radius: var(--radius-pill); font-size: var(--fs-12); font-weight: 600; white-space: nowrap; }
.answer-pill.is-ok { background: var(--ok-soft); color: var(--ok); }
.answer-pill.is-warn { background: var(--bad-soft); color: var(--bad); }
.answer-pill.is-ask { background: var(--warn-soft); color: var(--warn); }
.answer-pill.is-off { background: var(--surface-2); color: var(--muted); font-weight: 500; }

/* The reference page. */
.dl-toc { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); margin: 0 0 var(--space-4); padding: 0; list-style: none; font-size: var(--fs-14); }
.dl-toc a { color: var(--accent); text-decoration: none; }
.dl-toc a:hover { text-decoration: underline; }
.dl-toc .dl-toc-head { color: var(--muted); font-weight: 600; }
.dl-sec { scroll-margin-top: 72px; }
.dl-sec > h3 { margin: 0 0 var(--space-1); }
.dl-sec > .dl-what { margin: 0 0 var(--space-4); color: var(--muted); font-size: var(--fs-14); max-width: 70ch; }
.dl-rule-list { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-2) var(--space-6); font-size: var(--fs-14); }
.dl-rule-list b { display: block; }
.dl-rule-list span { color: var(--muted); }
.dl-group { margin: var(--space-4) 0 var(--space-2); font-size: var(--fs-12); font-weight: 600; color: var(--muted); }
.dl-group:first-of-type { margin-top: 0; }
.dl-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: var(--space-3) var(--space-4); margin: 0; padding: 0; list-style: none; }
.dl-sw { display: flex; align-items: flex-start; gap: var(--space-3); min-width: 0; }
.dl-chip { flex: none; width: var(--space-10); height: var(--space-10); border-radius: var(--radius-8); box-shadow: inset 0 0 0 1px var(--line); }
.dl-sw-text { min-width: 0; font-size: var(--fs-12); line-height: 16px; color: var(--muted); }
.dl-sw-text code { display: block; font-size: var(--fs-14); line-height: 20px; color: var(--ink); overflow-wrap: anywhere; }
.dl-sw-text .dl-ratio { white-space: nowrap; }
.dl-sw-text .dl-fail { color: var(--bad); font-weight: 600; }
.dl-scale { margin: 0; padding: 0; list-style: none; }
.dl-scale > li { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-2) 0; border-top: 1px solid var(--border); min-width: 0; }
.dl-scale > li:first-child { border-top: 0; }
.dl-scale code { flex: none; width: 160px; font-size: var(--fs-12); color: var(--muted); }
.dl-scale .dl-sample { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dl-bar { flex: none; height: var(--space-4); background: var(--info-soft); box-shadow: inset 0 0 0 1px var(--info); border-radius: var(--radius-4); }
.dl-box { flex: none; width: 72px; height: var(--space-12); background: var(--surface); }
.dl-box.is-radius { background: var(--info-soft); box-shadow: inset 0 0 0 1px var(--info); }
.dl-box.is-shadow { border-radius: var(--radius-8); margin: var(--space-2) var(--space-2) var(--space-3); }
.dl-demo { padding: var(--space-4); border: 1px dashed var(--line); border-radius: var(--radius-8); background: var(--surface); min-width: 0; }
.dl-demo.is-page { background: var(--bg); }
.dl-demo + .dl-demo { margin-top: var(--space-3); }
.dl-demo > .dl-label { margin: 0 0 var(--space-2); font-size: var(--fs-12); color: var(--muted); }
.dl-demo .card:last-child, .dl-demo .dash-stats:last-child, .dl-demo .row.between:last-child, .dl-demo .subtitle:last-child, .dl-demo .m-list:last-child { margin-bottom: 0; }
.dl-demo h1 { margin-top: 0; }
.dl-demo .dl-inline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.dl-demo .dl-narrow { max-width: 360px; }
.dl-phone-note { display: block; margin: 0; font-size: var(--fs-12); color: var(--muted); }
.dl-code { margin: var(--space-3) 0 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-8); background: var(--surface-2); color: var(--ink-2); font-size: var(--fs-12); line-height: 20px; overflow-x: auto; white-space: pre; }
.dl-classes { margin: var(--space-2) 0 0; font-size: var(--fs-12); color: var(--muted); }
.dl-classes code { color: var(--ink-2); }
.dl-rules { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-2) var(--space-6); margin: var(--space-4) 0 0; }
.dl-rules h4 { margin: 0 0 var(--space-1); font-size: var(--fs-12); font-weight: 600; color: var(--muted); }
.dl-rules ul { margin: 0; padding: 0; list-style: none; font-size: var(--fs-14); }
.dl-rules li { position: relative; padding: 2px 0 2px var(--space-5); }
.dl-rules li::before { position: absolute; left: 0; font-weight: 700; }
.dl-rules .dl-do li::before { content: '✓'; color: var(--ok); }
.dl-rules .dl-dont li::before { content: '✕'; color: var(--bad); }
@media (max-width: 640px) {
  .dl-scale code { width: 120px; }
  .dl-demo { padding: var(--space-3); }
  .dl-demo.is-page { padding: var(--space-2); }
}
/* ==================== Design library (Nov 2026) - END ==================== */
