@import url('https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&family=IBM+Plex+Mono:wght@500;600&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');

/* ============================================================
   HOS Design System v3 — Release 1 mockups
   Aesthetic: "violet glass — AI as a coworker, at 2am calm"
   - Chrome: deep indigo-violet gradient, heavier frosted glass throughout
   - Content canvas: cool violet-white, solid cards (glass never under dense data)
   - AI moments: fuchsia-violet (295°), always marked with ✦ and a gradient rail.
     Moved off saffron because saffron sat 1° from --warn-soft: an AI draft and a
     warning were the same tint. See the --ai token block for the full reasoning.
   - Status colors kept semantic (good=green, warn=amber, crit=red) — UNCHANGED
   - Charts: validated CVD-safe triad — UNCHANGED: #0B8A68 / #2563A8 / #C25E0A
   (brand hue moved from teal/green → violet-indigo; chart & status hues stay
   put deliberately, so brand/AI/status/data are four distinct, legible families)
   ============================================================ */

:root {
  /* content canvas & ink (cool violet-white, was cool teal-white) */
  --bg: #F0EFF9;
  --panel: #FFFFFF;
  --panel-2: #F8F7FD;
  --line: #E4E1F2;
  --line-strong: #CFC9E6;
  --ink: #1A1730;
  --ink-2: #5B5775;
  /* --ink-3 was #8F8AA8, which measured 3.30:1 on white, 3.10:1 on --panel-2
     and 2.85:1 on --good-soft — every one of those below the 4.5:1 AA floor for
     text under 18.66px. It is the colour of the product's small print (`.tiny`,
     `.feed-t`, `.tl-date`, `.kpi-d.flat`, table headers, OT stage labels), and
     that small print carries real meaning here: "per 1,000 catheter-days",
     "sorted by SLA headroom", "37 min × ₹502". A contrast audit across 14
     workspaces found ~450 failing text nodes and ALL of them resolved to this
     one token plus --good-as-text below.
     #6A6584 clears the floor on all three surfaces it actually lands on:
       white 5.51:1 · --panel-2 5.21:1 · --good-soft 4.78:1
     tools/ui-check.mjs cannot catch this class of bug — it measures geometry and
     font size, never contrast. */
  --ink-3: #6A6584;

  /* ---- simulated printed-paper ink (Administration → Branding preview only) ----
     The letterhead / Rx / invoice / discharge-summary SVG mockups render what a
     printed page looks like, so they intentionally do NOT use --ink/--panel-2/
     --line: those are violet-tinted (this app's chrome), and a hospital's actual
     printout is plain black-on-white — tinting the "paper" preview violet would
     make it look like more app UI instead of the physical document it's standing
     in for. Verified numerically against the --ink family before naming these
     (e.g. --ink-2 #5B5775 vs this #55636E — different hue, not a rounding drift),
     confined to this one preview widget, so it's named, not merged. */
  --doc-ink: #16202B;
  --doc-ink-2: #55636E;
  --doc-ink-3: #8A97A0;
  --doc-line: #E3E8EB;
  --doc-line-2: #CFD7DC;
  --doc-bg: #F7F9FA;

  /* chrome — deep indigo-violet gradient (sidebar / topbar / tabbar) */
  --chrome-1: #170F30;
  --chrome-2: #2A1B5C;
  --chrome-3: #3B2178;
  --chrome-glass: rgba(23, 15, 48, 0.60);
  --chrome-line: rgba(255, 255, 255, 0.12);
  --chrome-ink: #F1EEFB;
  --chrome-ink-2: rgba(241, 238, 251, 0.66);
  --chrome-accent: #A78BFA;
  --chrome-accent-soft: rgba(167, 139, 250, 0.18);
  --chrome-glow-2: #60A5FA;

  /* brand (actions on the light canvas) — violet, replaces teal */
  --teal: #6D4FE0;
  --teal-strong: #5636B8;
  --teal-soft: #EFEAFC;
  --teal-ghost: #F8F5FE;

  /* ---- AI signature — fuchsia-violet (was saffron) ----
     The saffron was not just a taste problem, it was a SEMANTIC collision:
       --ai      #F2930C  hue 35°   |  --warn      #B45309  hue 26°
       --ai-soft #FCEEDA  hue 35°   |  --warn-soft #FBEEDD  hue 34°
     One degree apart. An AI draft panel and a warning banner were rendered in
     literally the same tint, on screens that now carry up to 30 AI blocks — so
     "the AI is thinking" and "something is wrong" looked identical.

     Statuses occupy 3° (crit), 26° (warn), 164° (good), 212° (info); the brand
     violet sits at 252°. The widest empty arc is 252°→360°, so AI takes 295°:
     43° clear of the brand, 68° clear of red, and unmistakably not a status.

     It also fixes a real accessibility failure — white on saffron was 2.35:1,
     below the 4.5:1 AA floor, on the primary approve button of every AI block.
     White on the deep cyan is 5.36:1, versus 2.35:1 for the old saffron.
     Fuchsia was the first attempt at that arc and read as PINK — wrong register
     for a clinical product. The answer is not another flat hue: AI's identity is
     now a **cyan → violet gradient**, and no status colour is ever a gradient, so
     the signature cannot be confused with info-blue (212°) even though the cyan
     solid sits at 193°. The gradient does the identifying; the solid only has to
     be legible. Deep teal-cyan also reads cool and calm, which is the stated
     aesthetic of this design system, where saffron read as an alarm. */
  --ai: #0E7490;         /* solid fills, approve buttons, chips — 5.36:1 white text */
  --ai-bright: #22D3EE;  /* light gradient stop + glows on the dark chrome */
  --ai-deep: #0B5567;    /* AI text on a light panel */
  --ai-soft: #DDF4FA;    /* chip tint — deliberately AQUA, so it cannot be mistaken
                            for good-soft (156°, green) or info-soft (210°, blue) */
  --ai-ghost: #F6FDFF;   /* panel wash: near-white, because at 30 blocks a page a
                            tinted fill stops being an accent and becomes the page */
  --ai-line: #B9E6F2;
  /* the signature itself — used on every spine, spark, orb and pill */
  --ai-grad: linear-gradient(135deg, var(--ai-bright) 0%, var(--ai) 48%, var(--teal) 105%);

  /* ---- the multicolour AI MARK ----
     Four-hue spark, in the register people now read as "AI". Scoped deliberately
     and narrowly: this appears on the MARK ONLY — the ✦ badge, the copilot orb,
     the brand — and never on a chip, a button, a panel tint or anything that
     carries state.

     The reason is not taste. This palette contains red, amber and green, which
     on these screens are --crit, --warn and --good: a triage vocabulary that
     already means something. As a small brand mark that is fine, the way a logo
     is not a status light. As a fill behind data it would put all three status
     colours into one element, which is worse than the saffron problem this
     design system already had to fix once.

     Angles match the reference: red at the top, blue right, green bottom,
     amber left. */
  --ai-mark: conic-gradient(from 0deg at 50% 50%,
              #EA4335 0deg, #4285F4 92deg, #34A853 184deg, #FBBC04 272deg, #EA4335 360deg);

  /* status — unchanged */
  --good: #0B8A68;
  --good-soft: #E3F2EC;
  --warn: #B45309;
  --warn-soft: #FBEEDD;
  --crit: #B3261E;
  --crit-soft: #F9E7E5;
  --info: #2563A8;
  --info-soft: #E7EFF8;

  /* status "-deep" text-on-tint — the accessible-contrast pairing for each
     -soft chip fill, the same job --ai-deep already does for --ai-soft.
     Every page that put text on a status chip had to pick this shade itself
     because no token existed; they'd already converged on one value each by
     accident (found via a page-wide sweep: #8E1D16 26×, #07634B 21×, #8A3E06
     15×, #1A4C85 6× — including 4 already inside this file's own chip and
     banner rules) — naming the value they'd already agreed on, not
     picking a new one. */
  --good-deep: #07634B;
  --warn-deep: #8A3E06;
  --crit-deep: #8E1D16;
  --info-deep: #1A4C85;

  /* charts — validated CVD-safe, unchanged */
  --c1: #0B8A68;
  --c2: #2563A8;
  --c3: #C25E0A;

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 18px;
  --r-xl: 22px;
  --r-full: 999px; /* pill/circle radius — was a raw 999px literal on ~35
                       chips, avatars and toggle switches across the pages */

  /* spacing scale — consolidates ~22 ad hoc padding/margin/gap values down to
     11 canonical steps. Every padding/margin/gap in this file and every page
     should reference one of these, never a bare px value. */
  --space-0: 2px;
  --space-1: 4px;
  --space-2: 6px;
  --space-3: 8px;
  --space-4: 10px;
  --space-5: 12px;
  --space-6: 16px;
  --space-7: 20px;
  --space-8: 24px;
  --space-9: 32px;
  --space-10: 48px;

  /* shadows — hue-tinted to violet (262°), never neutral black.
     Flat gray/black shadows on a violet product are the #1 "generic AI
     product" tell; tinting every shadow to the brand hue is the single
     highest-leverage fix (Comeau shadow method + Vercel/Stripe layering). */
  --shadow-hue: 262deg 45% 27%;
  /* --shadow-sm carries almost every card in the product, and at two 1-2px
     layers it was doing nothing perceptible: cards read as outlined rectangles
     rather than surfaces sitting on the canvas. Now a proper contact shadow
     (tight, slightly stronger) plus one soft diffuse layer — still restrained
     enough for a page holding 20 cards, but the surface actually lifts. */
  --shadow-sm: 0 1px 2px hsl(var(--shadow-hue) / .07), 0 2px 6px -1px hsl(var(--shadow-hue) / .07);
  --shadow-md: 0 2px 4px hsl(var(--shadow-hue) / .06), 0 8px 16px -4px hsl(var(--shadow-hue) / .09), 0 20px 32px -10px hsl(var(--shadow-hue) / .10);
  /* the hover/raised step — three layers so the falloff reads as one light
     source rather than a stack of blurs, same hue as every other shadow */
  --shadow-lg: 0 4px 8px -2px hsl(var(--shadow-hue) / .07), 0 12px 24px -6px hsl(var(--shadow-hue) / .11), 0 28px 48px -14px hsl(var(--shadow-hue) / .14);
  --shadow-glass: 0 8px 24px -8px hsl(var(--shadow-hue) / .45), 0 2px 10px hsl(var(--shadow-hue) / .28);
  --sidebar-w: 248px;

  /* fonts — Google Sans Flex, a single variable family (opsz 6-144, wght 1-1000)
     for both display and body: opsz auto-adapts glyph texture to size (headlines
     get a display cut, body text gets a text cut) so one family reads considered
     at every size instead of a static font scaled up/down. IBM Plex Mono stays for
     tabular clinical/financial figures — Sans Flex has no monospace cut. */
  --f-display: "Google Sans Flex", system-ui, -apple-system, sans-serif;
  --f-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --f-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, monospace;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body {
  font-family: var(--f-body);
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--bg);
  font-optical-sizing: auto;
}
img, svg { display: block; max-width: 100%; }
a { color: var(--teal); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font: inherit; cursor: pointer; }
/* ---- focus ring ----
   This was --chrome-accent (#A78BFA) everywhere. That token is designed to read
   against the DARK sidebar; on the white content canvas it is a pale lavender
   at ~2.1:1 against the panel, so keyboard focus was genuinely hard to see on
   the ~90% of interactive controls that live on the light canvas — the same
   class of bug as the invisible .avatar (a chrome token used on a light
   surface). The brand violet is the legible choice on light; the chrome accent
   stays where it belongs. */
:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 4px; }
.sidebar :focus-visible, .topbar :focus-visible, .ws-menu :focus-visible,
.glass-hero :focus-visible, .dark :focus-visible { outline-color: var(--chrome-accent); }

/* ---- interactive affordance, every tab at once ----
   hos-sim.js binds these declarative attributes through ONE delegated listener
   on document, so the elements themselves are never touched — nothing ever got
   `cursor: pointer`. Across all 55 tabs, anything interactive that wasn't a
   .btn looked inert: users hovered rows, cards and toggles and got no signal
   back. This selector list mirrors the one in hos-sim.js's dispatcher, plus the
   five row classes the pages wire by hand. */
[data-toast], [data-goto], [data-busy], [data-reveal], [data-hide], [data-toggle],
[data-append], [data-bump], [data-modal-title], [data-modal-body], [data-ai-target],
[data-select-group], [data-close-modal],
.q-card, .tx-body, .soap-sec, .ins-body, .rx-dur { cursor: pointer; }

/* …and a hover response for the ones that aren't buttons or chips, so the
   feedback is felt and not just inferred from the cursor.
   The border shift alone was too quiet to register on a card-sized target, so
   an interactive surface now also LIFTS. Scoped to elements actually wired to a
   demo action — a page full of static cards that all rise on hover reads as
   noise, and stops signalling which things you can press. */
:where([data-toast], [data-goto], [data-reveal], [data-toggle], [data-modal-title],
  [data-select-group], .q-card, .tx-body, .soap-sec, .ins-body):not(.btn):not(.chip):not(.sw):hover {
  border-color: var(--line-strong);
}
@media (prefers-reduced-motion: no-preference) {
  :where([data-toast], [data-goto], [data-reveal], [data-toggle], [data-modal-title],
    [data-select-group], .q-card, .tx-body, .soap-sec, .ins-body):not(.btn):not(.chip):not(.sw):hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
  }
}

h1, h2, h3, .display { font-family: var(--f-display); font-optical-sizing: auto; }
h1 { font-size: 23px; font-weight: 600; letter-spacing: -0.015em; }
h2 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
h3 { font-size: 14px; font-weight: 600; letter-spacing: -0.005em; }
.num, td.num, .kpi-v { font-variant-numeric: tabular-nums slashed-zero; font-feature-settings: 'tnum' 1, 'zero' 1; }
.mono { font-family: var(--f-mono); font-size: 12px; font-feature-settings: 'tnum' 1; }
.muted { color: var(--ink-2); }
.tiny { font-size: 12px; color: var(--ink-2); }

/* subtle grain — SVG feTurbulence, low opacity, chrome/hero surfaces ONLY.
   Never on tables or dense text (legibility risk + no payoff there). */
.grain { position: relative; }
.grain::after {
  content: "";
  position: absolute; inset: 0; border-radius: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: .05; mix-blend-mode: overlay; pointer-events: none;
}
@media (prefers-reduced-transparency: reduce) { .grain::after { display: none; } }

/* ---------------- glass utility (used deliberately: chrome + overlays only) ----------------
   Every glass surface gets a top-edge inset highlight + thin rim — this is what separates
   real "glass" from a flat blur patch (a plain backdrop-filter with no edge treatment is
   the flat/generic look). Shadows are the hue-tinted --shadow-* tokens, never neutral. */
.glass {
  background: var(--chrome-glass);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid var(--chrome-line);
  box-shadow: inset 0 1px 0 0 rgba(255,255,255,.14), inset 0 0 0 1px rgba(255,255,255,.04);
}
.glass-panel {
  background: rgba(255,255,255,.72);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid rgba(255,255,255,.5);
  box-shadow: var(--shadow-md), inset 0 1px 0 0 rgba(255,255,255,.7);
}
/* frosted card for elevated overlays: dropdowns, tooltips, hover previews —
   NEVER for dense data tables (legibility) */
.glass-card {
  background: rgba(255,255,255,.62);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255,255,255,.6);
  box-shadow: var(--shadow-md), inset 0 1px 0 0 rgba(255,255,255,.65);
  border-radius: var(--r-lg);
}
/* violet-blue gradient glass hero band — for page-top emphasis moments only */
.glass-hero {
  position: relative; overflow: hidden;
  background: linear-gradient(120deg, rgba(42,27,92,.92), rgba(96,165,250,.55)), var(--chrome-glass);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid var(--chrome-line);
  border-radius: var(--r-xl);
  color: #fff;
  box-shadow: var(--shadow-glass), inset 0 1px 0 0 rgba(255,255,255,.16);
}
.glass-hero::after {
  content: "";
  position: absolute; inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: .06; mix-blend-mode: overlay; pointer-events: none;
}
.glass-hero .tiny, .glass-hero .muted { color: rgba(255,255,255,.68); }
@supports not (backdrop-filter: blur(1px)) {
  .glass { background: var(--chrome-2); }
  .glass-panel { background: var(--panel); }
  .glass-card { background: var(--panel); }
  .glass-hero { background: linear-gradient(120deg, var(--chrome-2), var(--chrome-3)); }
}

/* premium gradient-edge border — for data/financial containers that must NOT go
   translucent (KPI tiles, table wrappers). Richness at the boundary, zero cost
   to interior legibility: fill stays fully opaque, only the 1px edge is gradient. */
.edge-premium { position: relative; }
.edge-premium::before {
  content: "";
  position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  background: linear-gradient(135deg, rgba(167,139,250,.55), rgba(34,211,238,.26) 60%, transparent 85%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  pointer-events: none;
}

/* ---------------- ambient AI aurora background (global, all pages) ----------------
   A slow-drifting multi-blob mesh behind everything. Cards are opaque, so this only
   breathes through the gutters/margins — richer/"more AI" without hurting legibility.
   html carries the flat base color; body::before paints the animated mesh above it
   but behind all content (z-index:-1). */
html { background: var(--bg); }
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(680px 520px at 8% -6%, rgba(167,139,250,.30), transparent 62%),
    radial-gradient(720px 560px at 96% 4%, rgba(96,165,250,.24), transparent 60%),
    radial-gradient(760px 620px at 78% 96%, rgba(109,79,224,.22), transparent 60%),
    radial-gradient(620px 520px at 18% 104%, rgba(34,211,238,.10), transparent 58%),
    var(--bg);
  background-size: 160% 160%;
  animation: aurora 26s ease-in-out infinite alternate;
}
@keyframes aurora {
  0%   { background-position: 0% 0%, 100% 0%, 80% 100%, 0% 100%, 0 0; }
  50%  { background-position: 20% 12%, 84% 10%, 64% 86%, 14% 88%, 0 0; }
  100% { background-position: 6% 4%, 96% 2%, 78% 98%, 4% 100%, 0 0; }
}
@media (prefers-reduced-motion: reduce) { body::before { animation: none; } }

/* ---------------- app shell ---------------- */
.app {
  display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh;
  background: transparent; /* let the body aurora show through the gutters */
}

.sidebar {
  /* grain layered INTO the background paint (never a pseudo-element on top of
     content) — background is always behind real children, so this can't
     interfere with nav-link legibility */
  /* Gradient DEEPENS downward. Going lighter toward the bottom (the previous
     direction) read as washed-out and muddy in the empty lower half. A violet
     lift behind the brand + a deep base below feels intentional instead. */
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.3'/%3E%3C/svg%3E"),
    radial-gradient(120% 42% at 0% 0%, rgba(109,79,224,.42), transparent 70%),
    linear-gradient(180deg, #26185A 0%, #1A0F42 42%, #150C34 100%);
  background-blend-mode: overlay, screen, normal;
  box-shadow: inset -1px 0 0 rgba(255,255,255,.05);
  border-right: 1px solid var(--chrome-line);
  padding: var(--space-7) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-1);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
  color: var(--chrome-ink);
}
.brand { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-1) var(--space-3) var(--space-6); }
/* The mark itself lives in assets/brand.svg — one file, every surface.
   This rule only sizes and floats it; colour, glyph and tile are the SVG's. */
.brand-mark {
  width: 36px; height: 36px; border-radius: 11px; display: block;
  flex-shrink: 0; box-shadow: 0 4px 14px rgba(167,139,250,.35);
}
.brand-name { font-family: var(--f-display); font-weight: 700; font-size: 16px; color: #fff; }
.brand-sub { font-size: 11px; color: var(--chrome-ink-2); line-height: 1.3; }

.nav-label { font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: rgba(241,238,251,.42); padding: var(--space-5) var(--space-4) var(--space-2); }
.nav a, .nav-item {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4); border-radius: 10px;
  color: var(--chrome-ink-2); font-weight: 500; font-size: 13.5px;
  text-decoration: none;
}
.nav a:hover { background: rgba(255,255,255,.06); color: #fff; text-decoration: none; }
.nav a.active { background: var(--chrome-accent-soft); color: #fff; font-weight: 600; box-shadow: inset 0 0 0 1px rgba(167,139,250,.35); }
.nav a.active .ic { background: var(--chrome-accent); color: #1B1042; border-color: var(--chrome-accent); }
/* nav icons — a boxed tile for workspace-level identity (switcher, dropdown),
   and a bare stroke glyph inside the section nav (see .sec-nav .ic below).
   Text inside .ic is the monogram fallback for an unmapped label. */
.ic {
  width: 24px; height: 24px; border-radius: 7px; flex-shrink: 0;
  border: 1px solid rgba(255,255,255,.14); background: rgba(255,255,255,.06);
  color: var(--chrome-ink-2); display: grid; place-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: .01em;
  font-family: var(--f-display);
}
.ic svg { width: 15px; height: 15px; display: block; }
.ic-ai { background: var(--ai-soft); border-color: var(--ai-line); color: var(--ai-deep); }
/* on the DARK sidebar the light-panel tokens invert badly — use the bright stop */
.sec-nav .ic-ai, .ws-item .ic-ai { background: none; border: none; color: var(--ai-bright); }
.nav a.active .ic-ai { background: var(--ai); border-color: var(--ai); color: #fff; }

.sidebar-foot { margin-top: auto; padding: var(--space-5) var(--space-4) var(--space-1); border-top: 1px solid var(--chrome-line); }
/* When .sb-aside is present IT owns the auto margin (see .sb-aside). Leaving
   `auto` on both makes flexbox split the free space between them, reopening the
   gap this was meant to close. */
.sb-aside + .sidebar-foot { margin-top: var(--space-5); }
.role-badge { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 11.5px; color: var(--chrome-ink-2); }
.role-badge b { color: #fff; font-weight: 600; }

.main { display: flex; flex-direction: column; min-width: 0; }

/* ---- breadcrumb bar — injected by shell.js as a sibling right after .topbar ----
   Sits in the light content flow (not the dark chrome), so it uses page tokens
   rather than chrome tokens. Not sticky: it lives in `.main`'s normal flow and
   scrolls with the content, avoiding any stacking interaction with the topbar's
   own `position: sticky`. */
.crumb-bar {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-3) var(--space-8);
  background: var(--panel); border-bottom: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.4;
}
.crumb-sep { color: var(--ink-3); }
.crumb-l { color: var(--ink-2); text-decoration: none; }
.crumb-l:hover { color: var(--teal); text-decoration: underline; }
.crumb-root { font-weight: 600; color: var(--ink); }
.crumb-cur { font-weight: 600; color: var(--ink); }

.topbar {
  display: flex; align-items: center; gap: var(--space-6);
  padding: var(--space-5) var(--space-8);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.3'/%3E%3C/svg%3E"),
    linear-gradient(120deg, rgba(23,15,48,.85), rgba(59,33,120,.72));
  background-blend-mode: overlay, normal;
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  border-bottom: 1px solid var(--chrome-line);
  box-shadow: inset 0 -1px 0 0 rgba(255,255,255,.06);
  position: sticky; top: 0; z-index: 50;
  color: #fff;
}
.topbar .hospital { font-family: var(--f-display); font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar .hospital .tiny { font-weight: 400; color: rgba(255,255,255,.6); }

/* ---- app bar search ----
   This was a plain <div> with the text "Search patients, bills, drugs…" and a
   `Ctrl K` hint. Two defects: it could not be clicked or typed into, and the
   shortcut it advertised opens the AI copilot, not search. Both were the first
   thing a demo viewer tried. It is now a real focusable input on `/`. */
.topbar-search {
  flex: 1; max-width: 440px; min-width: 0;
  display: flex; align-items: center; gap: var(--space-3);
  background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.14);
  border-radius: 10px; padding: 0 var(--space-4);
  color: rgba(255,255,255,.55); font-size: 13px;
  transition: background .16s ease, border-color .16s ease, box-shadow .16s ease;
}
.topbar-search:focus-within {
  background: rgba(255,255,255,.14); border-color: rgba(167,139,250,.6);
  box-shadow: 0 0 0 3px rgba(167,139,250,.18);
}
.topbar-search svg { width: 15px; height: 15px; flex-shrink: 0; opacity: .7; }
.topbar-search input {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  padding: var(--space-3) 0; color: #fff; font: inherit; font-family: var(--f-sans);
}
.topbar-search input::placeholder { color: rgba(255,255,255,.5); }
.topbar-search kbd, .tb-kbd {
  font-family: var(--f-mono); font-size: 10.5px; flex-shrink: 0;
  background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);
  border-radius: 5px; padding: 1px var(--space-2); color: rgba(255,255,255,.7);
}

/* ---- app bar right cluster ---- */
.tb-right { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; flex-shrink: 0; }
.tb-sep { width: 1px; height: 22px; background: rgba(255,255,255,.16); flex-shrink: 0; }

/* the AI entry point, promoted INTO the app bar. A floating orb in the corner
   is easy to miss and easy to mistake for a chat widget; a labelled, shining
   pill in the chrome says the AI is part of the product, not an add-on. */
.tb-ask {
  position: relative; overflow: hidden; flex-shrink: 0;
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4) var(--space-3) var(--space-3);
  border-radius: var(--r-full); cursor: pointer;
  background: linear-gradient(120deg, rgba(34,211,238,.24), rgba(109,79,224,.18));
  border: 1px solid rgba(34,211,238,.44);
  color: #CFF6FF; font-size: 12.5px; font-weight: 600; white-space: nowrap;
  transition: background .18s ease, border-color .18s ease, transform .18s ease;
}
.tb-ask::after {   /* the sheen — the app bar's share of the AI signature */
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent 0%, rgba(207,246,255,.42) 48%, transparent 100%);
  animation: ai-sheen 4.4s ease-in-out infinite;
}
.tb-ask:hover {
  background: linear-gradient(120deg, rgba(34,211,238,.38), rgba(109,79,224,.28));
  border-color: rgba(34,211,238,.7); color: #fff; transform: translateY(-1px);
}
.tb-ask .tb-star { font-size: 14px; line-height: 1; color: var(--ai-bright); }
.tb-ask .tb-kbd { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.22); color: rgba(255,255,255,.85); }

/* fallback: if shell.js hasn't wrapped the right cluster yet, the avatar must
   still push to the right edge rather than sitting against the search box */
.topbar > .avatar { margin-left: auto; }

/* icon button (notifications) */
.tb-ico {
  position: relative; width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
  display: grid; place-items: center; cursor: pointer;
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.14);
  color: rgba(255,255,255,.8); transition: background .16s ease, color .16s ease;
}
.tb-ico:hover { background: rgba(255,255,255,.15); color: #fff; }
.tb-ico svg { width: 16px; height: 16px; }
.tb-ico .tb-dot {
  position: absolute; top: -3px; right: -3px; min-width: 16px; height: 16px;
  border-radius: var(--r-full); background: var(--crit); color: #fff;
  font-size: 9.5px; font-weight: 700; line-height: 16px; text-align: center;
  padding: 0 3px; border: 2px solid #221448;
}
/* The user avatar lives ONLY in the topbar, on the violet chrome gradient.
   It used to use --chrome-accent-soft (violet at 18% alpha) as fill with
   --chrome-accent (#A78BFA) as text — tokens designed for the much darker
   SIDEBAR. On the topbar that is violet on violet: the initials were nearly
   invisible and the whole control read as a rendering fault rather than an
   avatar. Solid translucent-white fill + white text + a brighter ring, so it
   reads as a person's badge against the gradient it actually sits on.
   (Same class of bug as .door-t on 01-login: a token borrowed across surfaces.
   tools/ui-check.mjs cannot catch either — it measures geometry, not contrast.) */
.avatar {
  width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
  background: rgba(255,255,255,.16); color: #fff;
  border: 1px solid rgba(255,255,255,.34);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2);
  display: grid; place-items: center; font-weight: 700; font-size: 12.5px;
  font-family: var(--f-display); letter-spacing: .01em;
  cursor: pointer; transition: background .16s ease, border-color .16s ease;
}
.avatar:hover { background: rgba(255,255,255,.26); border-color: rgba(255,255,255,.5); }

.page { padding: var(--space-8) var(--space-8) var(--space-10); display: flex; flex-direction: column; gap: var(--space-7); }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-6); flex-wrap: wrap; }
.page-head p { color: var(--ink-2); font-size: 13px; margin-top: var(--space-0); }

/* ---------------- tabs (first-class component) ---------------- */
/* `top` must clear the TOPBAR, which is itself sticky at top:0 with z-index 50.
   At top:0 the tab bar stuck to the same line and — being z-index 10 — parked
   itself entirely BEHIND the topbar: measured on 17-tenant, scrolled, the bar
   sat at [0,50] under a topbar occupying [0,66], i.e. 100% covered. The ten
   tenant tabs simply vanished as soon as you scrolled, which is the exact
   moment a sticky bar is supposed to earn its keep.
   --topbar-h is measured and published by shell.js (the topbar's height is
   content-dependent: 63px on administration, 66px on tenant, so no hardcoded
   value is right for both). The 0px fallback keeps the old behaviour on any
   page that loads this stylesheet without the shell. */
.tabbar {
  display: flex; gap: var(--space-0); padding: var(--space-2);
  /* OPAQUE, and it has to be. The fill was rgba(42,27,92,.07) — a 7% tint,
     effectively transparent — which was invisible only because the bar was
     parked behind the topbar and never actually stuck. Once it does stick,
     page content scrolls underneath and read straight through the tab labels
     ("Usage & Cost" over "Tenant database"). The tint is preserved exactly by
     layering it over --bg instead of relying on the page behind it. */
  background: linear-gradient(rgba(42,27,92,.07), rgba(42,27,92,.07)), var(--bg);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow-x: auto;
  position: sticky; top: var(--topbar-h, 0px); z-index: 10;
}
.tab {
  appearance: none; border: none; background: transparent;
  padding: var(--space-3) var(--space-6); border-radius: 10px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
  white-space: nowrap; display: inline-flex; align-items: center; gap: var(--space-2);
}
.tab:hover { background: rgba(42,27,92,.06); color: var(--ink); }
.tab.active {
  background: var(--panel); color: var(--chrome-1);
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--line);
}
.tab.active::after { content: ""; }
.tab .tab-badge {
  font-size: 10px; font-weight: 700; padding: 1px var(--space-2); border-radius: var(--r-full);
  background: var(--ai-soft); color: var(--ai-deep);
}
.tabpanel { display: none; animation: fadein .15s ease; }
.tabpanel.active { display: flex; flex-direction: column; gap: var(--space-7); }
@keyframes fadein { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }

/* ---------------- primitives ---------------- */
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-sm);
}
/* A whisper of tint separates the header from the body without adding another
   hairline. One border was carrying the whole distinction, so on a page of 8
   cards the headers read as just another table row. Uses the existing surface
   tokens, top-lit to match the shadow direction. */
.card-h {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-5);
  padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border-radius: var(--r-md) var(--r-md) 0 0;
}
/* ============================================================
   CHARTS — shared primitives
   ------------------------------------------------------------
   These lived ONLY inside 09-analytics.html, which is why every other page's
   charts looked worse: the IPD sparks, the patient trajectories, the superadmin
   usage bars and the nursing vitals chart all rendered with browser defaults and
   no grid, no axis type scale and no series palette.

   Series colours stay the CVD-validated triad (--c1/--c2/--c3) — deuteranopia and
   protanopia are ~8% of men, and a clinical chart that fails them is a clinical
   defect, not an aesthetic one.
   ============================================================ */
/* NO width/height here. An <svg> with a viewBox already sizes itself
   intrinsically, and my first version set `width:100%; height:auto`, which
   collapsed the analytics revenue chart to 0x0 inside its overflow-x wrapper —
   the chart vanished and the enhancer then skipped it as too small. Pages own
   their own chart sizing (09-analytics uses `.chart-wrap svg { min-width }`);
   this layer owns only the visual language. */
/* `width: 100%` is folded in from the duplicate block that used to sit lower in
   this file and win. It is safe on its own — it was pairing it with
   `height: auto` that collapsed the analytics chart to 0x0, because that let a
   viewBox'd <svg> resolve BOTH axes from the parent instead of its own aspect
   ratio. Width alone still leaves the intrinsic height in place. */
.chart { overflow: visible; width: 100%; }

/* ---- FOUR SIZE BANDS ------------------------------------------------------
   Chart sizing used to be per-page and there were TEN different heights in the
   product (34, 60, 100, 130, 140, 180, 190, 200, 220, 260) across ELEVEN
   viewBox widths. Nothing looked related to anything else, and several charts
   were too small to read the number they existed to carry.

   These four bands cover every real chart. Pick by the job:
     spark  — inline trend beside a value; no axes, endpoint marker only
     card   — one metric inside a card
     panel  — half-width analytical chart, axes + legend
     wide   — full-width analytical chart, the ones a reader studies

   `aspect-ratio` with `width: 100%` is deliberate, and NOT `height: auto`:
   the note above records that height:auto collapsed the analytics chart to
   0x0, because it let a viewBox'd <svg> resolve both axes from the parent.
   aspect-ratio gives a definite height computed from the ratio instead, so a
   chart scales with its column without ever consulting the parent's height.

   min-height is the floor that stops a narrow column (or a phone) shrinking a
   chart below legibility — the actual complaint that started this pass. */
   The ratios are chosen to MATCH the viewBoxes already in the product rather
   than to impose new ones — a band whose ratio differs from the chart's own
   viewBox just letterboxes it, which shrinks the plot instead of enlarging it.
   Every real chart lands within ~8% of one of these four, so the padding is
   invisible while the sizing finally becomes consistent.

   This is also the fix for the actual complaint. A chart written as
   `width="100%" height="140"` with a viewBox does NOT fill its box: the
   default preserveAspectRatio letterboxes the content inside a 100%x140
   frame, so on a wide column the plot stays small and centred with dead air
   either side. Replacing the fixed height with a band lets it scale up. */
.chart-sz-spark { aspect-ratio: 10 / 3;  min-height: 54px; }  /* 200x60, 320x100, 480x140 */
.chart-sz-card  { aspect-ratio: 8 / 5;   min-height: 152px; } /* 320x200 */
.chart-sz-plot  { aspect-ratio: 36 / 19; min-height: 160px; } /* 360x190 — exact */
.chart-sz-panel { aspect-ratio: 12 / 5; min-height: 168px; }  /* 480x200, 460x180, 680x260 */
.chart-sz-wide  { aspect-ratio: 3 / 1;  min-height: 196px; }  /* 660x220, 640x220, 720x260 */

.chart .axis line, .chart .grid line { stroke: var(--line); stroke-width: 1; }
.chart .grid line.base { stroke: var(--line-strong); }
/* 11px/--ink-2, also folded in from the duplicate: axis labels are the numbers
   a reader interprets the plot with, and --ink-3 at 10.5px did not clear the
   contrast floor on a white plot area */
/* 12px, up from 11px. Axis text is the scale a reader interprets the plot
   with, and at 11px inside a chart that then scales DOWN in a narrow column it
   was the first thing to become unreadable. */
.chart text { font-family: var(--f-body); font-size: 12px; fill: var(--ink-2); }
.chart .num { font-variant-numeric: tabular-nums slashed-zero; }
.chart .dl { font-weight: 600; fill: var(--ink); font-size: 12.5px; }
/* Bar value / category labels, promoted from 09-analytics where they were
   page-local. `.dl-v` and `.bar-v` are direct labels — the skill's rule is to
   label the endpoint or the extreme, never every point, so these are applied
   selectively in markup rather than to every bar. */
.chart .dl-v, .chart .bar-v { font-weight: 600; fill: var(--ink); font-variant-numeric: tabular-nums; }
.chart .bar { fill: var(--c1); }
.chart .cat { fill: var(--ink-2); }
.chart .series { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chart .s1 { stroke: var(--c1); } .chart .s2 { stroke: var(--c2); } .chart .s3 { stroke: var(--c3); }
.chart .f1 { fill: var(--c1); }   .chart .f2 { fill: var(--c2); }   .chart .f3 { fill: var(--c3); }
.chart .area { stroke: none; opacity: .10; }
.chart .target-line { stroke: var(--ink-3); stroke-width: 1.2; stroke-dasharray: 4 4; }
.chart .crosshair { stroke: var(--ink-3); stroke-width: 1; stroke-dasharray: 3 4; }
.chart .end-dot { stroke: var(--panel); stroke-width: 1.5; }
.chart .hover-dot { stroke: var(--panel); stroke-width: 2; }
.chart .tip-card { fill: var(--panel); stroke: var(--line-strong); }
.chart .tip-t { font-weight: 600; fill: var(--ink); }
.chart .tip-v { font-variant-numeric: tabular-nums; fill: var(--ink); font-weight: 600; }

/* ---- point markers, added by enhanceCharts() in hos-sim.js ----
   A bare polyline hides WHERE the readings are, so a viewer cannot tell a
   measurement from interpolation. On a clinical trend that distinction matters:
   five HbA1c results over four years is a very different claim from a smooth
   curve. Markers are hollow (panel-filled) so overlapping series stay readable. */
.chart .pt { fill: var(--panel); stroke-width: 1.6; }
.chart .pt-last { stroke-width: 2; }
.chart .pt.p1, .chart .pt-last.p1 { stroke: var(--c1); } .chart .pt-last.p1 { fill: var(--c1); }
.chart .pt.p2, .chart .pt-last.p2 { stroke: var(--c2); } .chart .pt-last.p2 { fill: var(--c2); }
.chart .pt.p3, .chart .pt-last.p3 { stroke: var(--c3); } .chart .pt-last.p3 { fill: var(--c3); }
.chart .pt.pc, .chart .pt-last.pc { stroke: var(--crit); } .chart .pt-last.pc { fill: var(--crit); }
.chart .pt.pw, .chart .pt-last.pw { stroke: var(--warn); } .chart .pt-last.pw { fill: var(--warn); }
.chart .pt.pg, .chart .pt-last.pg { stroke: var(--good); } .chart .pt-last.pg { fill: var(--good); }
/* hit targets for the native <title> tooltip — invisible but hoverable, and sized
   for a finger rather than a mouse because these are used on ward tablets */
.chart .pt-hit { fill: transparent; stroke: none; cursor: help; }

/* legend — shared, so a chart never relies on colour alone */
.chart-legend { display: flex; align-items: center; gap: var(--space-6); flex-wrap: wrap; font-size: 12px; color: var(--ink-2); }
.chart-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: var(--space-2); vertical-align: -1px; }
.chart-legend .l1 i { background: var(--c1); } .chart-legend .l2 i { background: var(--c2); } .chart-legend .l3 i { background: var(--c3); }

@media (prefers-reduced-motion: no-preference) {
  .chart .series { transition: opacity .16s ease; }
}

/* ---- KPI tiles sitting ON the dark glass hero ----
   .kpi is designed for the light page: light panel, dark ink. Dropped into a
   .glass-hero it renders dark-on-dark — the headline numbers simply vanish, and
   tools/ui-check.mjs reports the page "clean" because it measures geometry and
   font size, not contrast.
   These six rules existed only as PAGE-LOCAL copies in 05-ipd.html and
   07-pharmacy.html, so every new workspace that reused the hero pattern lost its
   own headline figures until someone noticed by looking. Promoted here so the
   pattern works by default; the page-local copies are harmless duplicates. */
.glass-hero .kpi { background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.20); }
.glass-hero .kpi-l { color: rgba(255,255,255,.68); }
.glass-hero .kpi-v { color: #fff; }
.glass-hero .kpi-d.up { color: #7EE7C4; }
.glass-hero .kpi-d.down { color: #FFA79E; }
.glass-hero .kpi-d.flat { color: rgba(255,255,255,.6); }

/* right-hand cluster in a card header: status chips + the card's one action.
   NO flex-shrink:0 here — a long action label ("Start consultation · Lakshmi
   Devi →") then cannot shrink and pushes the whole page sideways on a phone. */
.card-h-act { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-4); flex-wrap: wrap; min-width: 0; }
.card-b { padding: var(--space-6); }
.card-b.tight { padding: 0; }

.btn {
  display: inline-flex; align-items: center; gap: var(--space-3);
  border-radius: 9px; padding: var(--space-3) var(--space-6);
  font-weight: 600; font-size: 13px; border: 1px solid transparent;
}
.btn-primary { background: var(--teal); color: #fff; }
.btn-primary:hover { background: var(--teal-strong); }
.btn-ghost { background: var(--panel); border-color: var(--line-strong); color: var(--ink); }
.btn-ghost:hover { background: var(--panel-2); }
/* ---- AI button: glowing neon pill with cosmic glass gradient and aura ---- */
@property --ai-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes ai-conic-spin {
  to { --ai-angle: 360deg; }
}

.btn-ai {
  background: linear-gradient(135deg, rgba(14,116,144,0.35) 0%, rgba(27,16,66,0.92) 55%, rgba(14,116,144,0.40) 100%);
  color: #fff;
  border-radius: var(--r-full);
  border: 1px solid transparent;
  position: relative;
  overflow: visible;
  box-shadow: 0 0 16px rgba(34,211,238,0.32), 0 2px 8px rgba(27,16,66,0.45), inset 0 1px 0 rgba(255,255,255,0.25);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease, background 0.2s ease;
}

.btn-ai::before {
  content: "";
  position: absolute;
  inset: -1.5px;
  border-radius: var(--r-full);
  padding: 1.5px;
  background: conic-gradient(from var(--ai-angle), var(--ai-bright), var(--chrome-accent), var(--chrome-glow-2), var(--ai-bright));
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  animation: ai-conic-spin 4s linear infinite;
}

.btn-ai:hover {
  background: linear-gradient(135deg, rgba(14,116,144,0.45) 0%, rgba(35,21,85,0.95) 55%, rgba(14,116,144,0.50) 100%);
  box-shadow: 0 0 24px rgba(34,211,238,0.50), 0 4px 12px rgba(27,16,66,0.55), inset 0 1px 0 rgba(255,255,255,0.35);
  transform: translateY(-0.5px);
}

.btn-ai:active {
  transform: scale(0.97);
}

@media (prefers-reduced-motion: reduce) {
  .btn-ai::before {
    animation: none;
    background: linear-gradient(135deg, var(--ai-bright), var(--chrome-accent));
  }
  .btn-ai {
    transition: none;
  }
}
.btn-sm { padding: var(--space-2) var(--space-4); font-size: 12px; border-radius: 7px; }
.btn-danger-ghost { background: var(--panel); border-color: var(--line-strong); color: var(--crit); }

/* ---- Universal button loading state ---- */
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

.btn.is-loading,
.btn[aria-busy="true"] {
  pointer-events: none;
  cursor: progress;
}

.btn.is-loading:not(.has-loading-label),
.btn[aria-busy="true"]:not(.has-loading-label) {
  position: relative;
  color: transparent !important;
}

.btn.is-loading:not(.has-loading-label) > *,
.btn[aria-busy="true"]:not(.has-loading-label) > * {
  opacity: 0;
}

.btn.is-loading:not(.has-loading-label)::after,
.btn[aria-busy="true"]:not(.has-loading-label)::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 14px;
  height: 14px;
  border: 2px solid var(--ink);
  border-top-color: transparent;
  border-radius: var(--r-full);
  animation: btn-spin 0.75s linear infinite;
}

.btn-primary.is-loading:not(.has-loading-label)::after,
.btn-primary[aria-busy="true"]:not(.has-loading-label)::after {
  border-color: var(--panel);
  border-top-color: transparent;
}

.btn-ai.is-loading:not(.has-loading-label)::after,
.btn-ai[aria-busy="true"]:not(.has-loading-label)::after {
  border-color: var(--ai-bright);
  border-top-color: transparent;
}

.btn-ghost.is-loading:not(.has-loading-label)::after,
.btn-ghost[aria-busy="true"]:not(.has-loading-label)::after {
  border-color: var(--teal);
  border-top-color: transparent;
}

.btn-sm.is-loading:not(.has-loading-label)::after,
.btn-sm[aria-busy="true"]:not(.has-loading-label)::after {
  width: 12px;
  height: 12px;
  border-width: 1.5px;
}

@media (prefers-reduced-motion: reduce) {
  .btn.is-loading::after,
  .btn[aria-busy="true"]::after {
    animation: none;
    border-top-color: currentColor;
    opacity: 0.7;
  }
}

/* ---- action hierarchy in repeated items ----
   A row action belongs to its row, not to the page. Repeating a FILLED primary
   down a list produced many equally-loud focal points — the lab queue had five
   solid "Start collection" buttons, the plan tiers three "Save changes", the
   order templates three "Use template" — and the page's real next step became
   invisible. In a repeated container a primary renders outlined: unmistakably
   that item's action, no longer competing with the one page-level primary.

   Containers, not individual buttons, so this holds as pages grow.
   `:where()` keeps specificity at zero so any explicit override still wins. */
:where(td, .row-act, .tier-actions, .tpl-foot) .btn-primary {
  background: transparent; color: var(--teal-strong); border-color: var(--teal);
}
:where(td, .row-act, .tier-actions, .tpl-foot) .btn-primary:hover { background: var(--teal); color: #fff; }
:where(td, .row-act, .tier-actions, .tpl-foot) .btn-ai {
  background: transparent; color: var(--ai-deep); border-color: var(--ai);
}
:where(td, .row-act, .tier-actions, .tpl-foot) .btn-ai:hover { background: var(--ai); color: #fff; }

.chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  border-radius: var(--r-full); padding: var(--space-0) var(--space-3);
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
}
.chip-good { background: var(--good-soft); color: var(--good-deep); }
.chip-warn { background: var(--warn-soft); color: var(--warn-deep); }
.chip-crit { background: var(--crit-soft); color: var(--crit-deep); }
.chip-info { background: var(--info-soft); color: var(--info-deep); }
.chip-neutral { background: var(--panel-2); color: var(--ink-2); border: 1px solid var(--line); }
.chip-ai { background: var(--ai-soft); color: var(--ai-deep); border: 1px solid var(--ai-line); }
.chip-ai.glow,
.badge-ai.glow {
  border-color: rgba(34, 211, 238, 0.6);
  background: color-mix(in srgb, var(--ai-bright) 14%, var(--panel));
  box-shadow: 0 0 10px rgba(34, 211, 238, 0.35);
}
.chip-teal { background: var(--teal-soft); color: var(--teal-strong); }

.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.dot-good { background: var(--good); } .dot-warn { background: var(--warn); }
.dot-crit { background: var(--crit); } .dot-info { background: var(--info); }

/* tables — dense but calm */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
/* Header cells take --ink-2, not --ink-3: a column header is the label you read
   to interpret every row under it, so it belongs one tier ABOVE the small print,
   not level with it. This also restores the tier gap that darkening --ink-3
   compressed (headers 6.4:1, small print 5.5:1). */
thead th {
  text-align: left; font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-2);
  padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--line);
  background: var(--panel-2);
}
tbody td { padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--teal-ghost); }
td.num, th.num { text-align: right; }

/* KPI tiles */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--space-5); }
/* KPI tiles hold the product's financial/clinical headline numbers — richness
   at the edge via a violet→amber gradient border, fill stays fully opaque
   (never glass) so dense figures never risk a contrast failure */
.kpi { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); padding: var(--space-6) var(--space-6); box-shadow: var(--shadow-sm); }
.kpi::before {
  content: "";
  position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  background: linear-gradient(135deg, rgba(167,139,250,.5), rgba(34,211,238,.24) 65%, transparent 85%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  pointer-events: none;
}
.kpi-l { font-size: 12px; color: var(--ink-2); font-weight: 500; }
.kpi-v { font-family: var(--f-display); font-size: 26px; font-weight: 700; margin-top: var(--space-0); }
.kpi-d { font-size: 11.5px; margin-top: var(--space-0); font-weight: 600; }
/* The delta line is TEXT on a light panel, so it takes the "-deep" pairing that
   exists for exactly this job — not the saturated status fill. --good as text
   measured 4.33:1 (below the 4.5:1 floor at 11.5px), and it is the single most
   repeated small figure in the product: every "▲ 12 vs Jun" on every KPI tile
   across quality, OT, analytics, tenant and administration. --good-deep is
   7.26:1. --crit already passed at 6.5:1, but it moves to --crit-deep too so
   both directions use the one documented text pairing instead of one each. */
.kpi-d.up { color: var(--good-deep); } .kpi-d.down { color: var(--crit-deep); } .kpi-d.flat { color: var(--ink-3); }

/* forms */
label.f-label { display: block; font-size: 12px; font-weight: 600; color: var(--ink-2); margin-bottom: var(--space-1); }
.f-input, select.f-input, textarea.f-input {
  width: 100%; border: 1px solid var(--line-strong); border-radius: 9px;
  padding: var(--space-3) var(--space-4); font: inherit; font-size: 13.5px; background: var(--panel);
  color: var(--ink);
}
.f-input:focus { outline: 2px solid var(--teal); outline-offset: 0; border-color: var(--teal); }
.f-row { display: grid; gap: var(--space-5); }

/* ---------------- AI signature components (validated — unchanged) ---------------- */
/* The spine is a GRADIENT from the brand violet into the AI fuchsia — it reads as
   "the product's own intelligence" rather than a fourth status colour, and a
   gradient cannot be mistaken for any flat status swatch. At up to 30 blocks on a
   page the wash stays near-white and the identity is carried entirely by the
   spine, the spark and the chip. */
.ai-block {
  position: relative;
  background: var(--ai-ghost);
  border: 1px solid var(--ai-line);
  border-left: none;
  border-radius: var(--r-md);
  padding: var(--space-6) var(--space-6);
  padding-left: calc(var(--space-6) + 3px);
  overflow: hidden;
}
.ai-block::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: linear-gradient(180deg, var(--teal) 0%, var(--ai) 52%, var(--ai-bright) 100%);
}
.ai-block-h { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); flex-wrap: wrap; }
.ai-spark {
  width: 22px; height: 22px; border-radius: 7px; flex-shrink: 0;
  background: var(--ai-mark);
  color: #fff;
  display: inline-grid; place-items: center; font-size: 12px;
  /* The glyph must survive passing over the amber quadrant, where white on
     #FBBC04 is far below any contrast floor. A tight dark shadow keeps it legible
     on all four hues without turning the badge grey.
     11 of these badges carry a SEMANTIC glyph — ✓ approved, 🔒 tamper-evident,
     ⏱ clock, 👁 preview — so the badge keeps its shape rather than being masked
     into a star: masking would delete those meanings. */
  text-shadow: 0 1px 2px rgba(20,10,0,.55), 0 0 3px rgba(20,10,0,.35);
  box-shadow: 0 1px 4px rgba(60,40,10,.28), inset 0 0 0 1px rgba(255,255,255,.28);
}
.ai-spark.glow {
  box-shadow: 0 0 12px rgba(34, 211, 238, 0.5), 0 1px 4px rgba(60, 40, 10, 0.28), inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.ai-block-h b { font-family: var(--f-display); font-size: 13.5px; }
.ai-actions { display: flex; gap: var(--space-3); margin-top: var(--space-5); flex-wrap: wrap; align-items: center; }
.ai-src { font-size: 11.5px; color: var(--ai-deep); }

/* ---- an approved draft must LOOK settled ----
   The saffron left edge and warm wash mean "AI output, pending your call".
   Once you've made that call the block has to stop shouting for attention, or
   an approved item is indistinguishable from one still waiting — which is the
   whole point of the approval step. Green edge, calmed background, done. */
.ai-block.is-approved {
  background: var(--good-soft);
  border-color: color-mix(in srgb, var(--good) 32%, transparent);
  border-left-color: var(--good);
}
.ai-block.is-approved::before { background: var(--good); }
.ai-block.is-approved .ai-spark { background: var(--good); box-shadow: 0 1px 4px rgba(11,138,104,.3), inset 0 1px 0 rgba(255,255,255,.35); }
.ai-block.is-approved .ai-src { color: var(--good-deep); }
.ai-approved-note { font-size: 12.5px; color: var(--good-deep); }
.ai-approved-note b { font-weight: 700; }
/* cards (not .ai-block) that hold a draft — pharmacy dispense, plan tiers */
.is-approved > .card-h .chip-ai, .is-approved .ai-spark { transition: background .2s ease; }

/* recording pulse — the scribe's live indicator */
.rec {
  display: inline-flex; align-items: center; gap: var(--space-4);
  background: var(--chrome-1); color: #fff; border-radius: var(--r-full);
  padding: var(--space-3) var(--space-6); font-size: 13px; font-weight: 600;
}
.rec .bars { display: inline-flex; align-items: flex-end; gap: var(--space-0); height: 16px; }
.rec .bars i {
  width: 3px; background: var(--chrome-accent); border-radius: 2px;
  animation: pulse 1.1s ease-in-out infinite;
}
.rec .bars i:nth-child(1) { height: 6px; animation-delay: 0s; }
.rec .bars i:nth-child(2) { height: 14px; animation-delay: .15s; }
.rec .bars i:nth-child(3) { height: 9px; animation-delay: .3s; }
.rec .bars i:nth-child(4) { height: 15px; animation-delay: .45s; }
.rec .bars i:nth-child(5) { height: 7px; animation-delay: .6s; }
@keyframes pulse { 0%,100% { transform: scaleY(.5);} 50% { transform: scaleY(1);} }
@media (prefers-reduced-motion: reduce) {
  .rec .bars i { animation: none; }
  .tabpanel { animation: none; }
  * { transition: none !important; }
}

/* ---------------- domain components ---------------- */
/* queue tokens */
.token {
  min-width: 66px; border-radius: var(--r-md); padding: var(--space-4) var(--space-5); text-align: center;
  background: var(--panel); border: 1px solid var(--line);
}
.token .t-no { font-family: var(--f-display); font-weight: 700; font-size: 20px; }
.token .t-l { font-size: 10.5px; color: var(--ink-2); }
.token.now { background: var(--chrome-2); border-color: var(--chrome-2); color: #fff; }
.token.now .t-l { color: rgba(255,255,255,.65); }

/* bed board */
.bed-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--space-3); }
.bed {
  border-radius: 10px; border: 1px solid var(--line); background: var(--panel);
  padding: var(--space-3) var(--space-3); font-size: 11px; min-height: 64px;
  display: flex; flex-direction: column; gap: var(--space-0);
}
.bed .b-no { font-family: var(--f-mono); font-size: 10px; color: var(--ink-3); }
.bed .b-name { font-weight: 600; font-size: 12px; color: var(--ink); }
.bed.occupied { background: var(--info-soft); border-color: #C4D7EC; }
.bed.free { background: var(--good-soft); border-color: #BFE0D2; }
.bed.cleaning { background: var(--warn-soft); border-color: #EFD3AE; }
.bed.critical { background: var(--crit-soft); border-color: #EFC5C1; }

/* timeline */
.timeline { position: relative; padding-left: var(--space-8); display: flex; flex-direction: column; gap: var(--space-6); }
.timeline::before { content: ""; position: absolute; left: 8px; top: 6px; bottom: 6px; width: 2px; background: var(--line-strong); border-radius: 2px; }
.tl-item { position: relative; }
.tl-item::before {
  content: ""; position: absolute; left: -24px; top: 5px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--panel); border: 2.5px solid var(--teal);
}
.tl-item.ai::before { border-color: var(--ai); }
.tl-date { font-size: 11px; font-weight: 600; color: var(--ink-3); letter-spacing: .04em; text-transform: uppercase; }
.tl-body { margin-top: var(--space-0); }

/* WhatsApp phone frame */
.phone {
  width: 100%; max-width: 320px; border-radius: var(--r-xl); overflow: hidden;
  border: 1px solid var(--line-strong); box-shadow: var(--shadow-md); background: #EFE7DC;
}
.phone-h { background: #075E54; color: #fff; padding: var(--space-4) var(--space-6); font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: var(--space-3); }
/* WhatsApp's brand teal #128C7E carries white initials at only 4.14:1. Rather
   than invent a colour or darken someone else's brand arbitrarily, this uses
   #075E54 — WhatsApp's OWN darker brand green, the one they use for headers —
   which reads about 7:1 with white. Still recognisably WhatsApp, and legible.
   Deliberately NOT a token: this is another company's brand, not our palette. */
.phone-h .p-av { width: 26px; height: 26px; border-radius: 50%; background: #075E54; display: grid; place-items: center; font-size: 11px; }
.phone-b { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); min-height: 200px;
  background-image: radial-gradient(#E4DACB 1px, transparent 1px); background-size: 18px 18px; }
.wa-msg { max-width: 86%; border-radius: 10px; padding: var(--space-3) var(--space-4); font-size: 12.5px; box-shadow: var(--shadow-sm); position: relative; }
.wa-in { background: #fff; align-self: flex-start; border-top-left-radius: 3px; }
.wa-out { background: #D9FDD3; align-self: flex-end; border-top-right-radius: 3px; }
/* 9.5px is the smallest text in the product, so it needs the MOST contrast, not
   the least. #7B8B84 measured 3.58:1 — WhatsApp's own timestamps are faint, but
   theirs are decoration and ours sit in a demo a buyer is asked to read. */
.wa-time { font-size: 9.5px; color: #5A6B63; text-align: right; margin-top: var(--space-0); }
.wa-btns { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-2); }
.wa-btn { border: none; background: #fff; color: #027EB5; font-size: 12px; font-weight: 600; padding: var(--space-2); border-radius: 7px; text-align: center; }

/* activity feed */
.feed { display: flex; flex-direction: column; }
.feed-item { display: flex; gap: var(--space-5); padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--line); font-size: 13px; }
.feed-item:last-child { border-bottom: none; }
.feed-t { font-family: var(--f-mono); font-size: 11px; color: var(--ink-3); white-space: nowrap; padding-top: var(--space-0); }

/* (A second `.chart` block used to sit here, redefining width/text/grid/.dl.
   Being later in the file it silently WON over the documented chart primitives
   above — including re-adding the `width: 100%` that block explicitly warns
   about. Its declarations are folded into the primitives now, preserving the
   values that were actually winning, so the file no longer contradicts itself.
   `.legend` below is a different component from `.chart-legend` and stays.) */
.legend { display: flex; gap: var(--space-6); flex-wrap: wrap; font-size: 12px; color: var(--ink-2); }
.legend .sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: var(--space-2); vertical-align: -1px; }

/* misc */
.split { display: grid; gap: var(--space-7); }
/* `display:flex` + a bare sentence = an ANONYMOUS flex item, which cannot be
   given min-width:0 and therefore refuses to shrink — so any banner carrying
   prose rather than a short phrase pushed the page sideways on a phone (found at
   390px, 60px of overflow, in two sections of a new workspace). flex-wrap plus
   min-width:0 on real children fixes the whole class centrally instead of each
   page rediscovering it. Wrapping banner prose in a <span> is still the better
   habit, because an anonymous item can't be targeted at all. */
.banner {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  border-radius: var(--r-md); padding: var(--space-4) var(--space-6); font-size: 13px; font-weight: 500;
}
.banner > * { min-width: 0; }
.banner-good { background: var(--good-soft); color: var(--good-deep); }
.banner-warn { background: var(--warn-soft); color: var(--warn-deep); }
.banner-ai { background: var(--ai-soft); color: var(--ai-deep); }
.divider { height: 1px; background: var(--line); border: none; margin: var(--space-1) 0; }
.tag-offline { font-family: var(--f-mono); font-size: 10.5px; background: var(--chrome-1); color: #E8E4D8; padding: var(--space-0) var(--space-3); border-radius: 6px; }

/* ---------------- workspace switcher (sidebar top) ----------------
   Replaces the old flat list of workspace links. Workspace = dropdown,
   sections = sidebar nav, horizontal .tabbar freed for sub-features. */
.ws-switch-wrap { position: relative; padding: 0 var(--space-2) var(--space-3); }
.ws-switch {
  width: 100%; display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-radius: var(--r-md);
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.14);
  color: #fff; text-align: left;
  transition: background .16s ease, border-color .16s ease;
}
.ws-switch:hover { background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.22); }
.ws-switch[aria-expanded="true"] { background: var(--chrome-accent-soft); border-color: rgba(167,139,250,.45); }
.ws-cur { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ws-cur-l { font-size: 9.5px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: rgba(241,238,251,.5); }
.ws-cur-n { font-family: var(--f-display); font-size: 13.5px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ws-chev { color: rgba(241,238,251,.55); font-size: 15px; line-height: 1; flex-shrink: 0; }

.ws-menu {
  position: absolute; left: var(--space-2); right: var(--space-2); top: calc(100% - var(--space-1));
  z-index: 200; padding: var(--space-2);
  background: rgba(23,15,48,.96);
  backdrop-filter: blur(20px) saturate(160%); -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255,255,255,.16); border-radius: 14px;
  box-shadow: var(--shadow-glass);
  max-height: 70vh; overflow-y: auto;
  animation: ws-menu-in .16s cubic-bezier(.2,.8,.2,1);
}
@keyframes ws-menu-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.ws-menu-hint { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); font-size: 10.5px; color: rgba(241,238,251,.45); padding: var(--space-2) var(--space-3) var(--space-3); }
.ws-menu-hint kbd { font-family: var(--f-mono); font-size: 9.5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.16); border-radius: 4px; padding: 1px 5px; }
.ws-group { font-size: 9.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: rgba(241,238,251,.38); padding: var(--space-3) var(--space-3) var(--space-1); }
.ws-item {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-3); border-radius: 9px;
  color: rgba(241,238,251,.72); font-size: 13px; font-weight: 500; text-decoration: none;
}
.ws-item:hover { background: rgba(255,255,255,.09); color: #fff; text-decoration: none; }
.ws-item.is-current { background: var(--chrome-accent-soft); color: #fff; font-weight: 600; }
.ws-item.is-current .ic { background: var(--chrome-accent); color: #1B1042; border-color: var(--chrome-accent); }
.ws-tick { margin-left: auto; color: var(--chrome-accent); font-size: 12px; }

/* HQ (control plane) has no workspace switcher — it shows its access scope instead.
   .chip forces nowrap, which overflows the 248px sidebar — allow wrapping here. */
.hq-scope { padding: 0 var(--space-2) var(--space-4); }
.hq-scope .chip {
  width: 100%; justify-content: center; text-align: center;
  white-space: normal; line-height: 1.35; padding: var(--space-3) var(--space-4);
}

/* ---------------- section nav (workspace sections, vertical) ----------------
   A boxed tile per row made the nav read as a stack of buttons and competed
   with the workspace switcher above it. Bare glyph + label, with an accent
   rail marking the active row, gives one clear focal point per screen. */
.sec-nav { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--space-2); }
.sec-item {
  position: relative;
  display: flex; align-items: center; gap: var(--space-4); width: 100%;
  padding: var(--space-3) var(--space-4); border-radius: 9px;
  background: transparent; border: none; text-align: left;
  color: var(--chrome-ink-2); font-size: 13.5px; font-weight: 500;
  transition: background .16s ease, color .16s ease;
}
/* the rail: 0-height when idle so it grows out of the row instead of popping in */
.sec-item::before {
  content: ''; position: absolute; left: -6px; top: 50%; transform: translateY(-50%);
  width: 2.5px; height: 0; border-radius: var(--r-full);
  background: var(--chrome-accent);
  transition: height .18s cubic-bezier(.4,0,.2,1);
}
.sec-nav .ic {
  width: 18px; height: 18px; border: none; background: none; border-radius: 0;
  color: rgba(241,238,251,.58);
  transition: color .16s ease;
}
.sec-nav .ic svg { width: 17px; height: 17px; }
.sec-nav .ic-ai { color: var(--ai); }
.sec-item:hover { background: rgba(255,255,255,.055); color: #fff; }
.sec-item:hover .ic { color: rgba(255,255,255,.9); }
.sec-item.active { background: rgba(167,139,250,.14); color: #fff; font-weight: 600; }
.sec-item.active::before { height: 17px; }
.sec-item.active .ic { color: var(--chrome-accent); }
.sec-item.active .ic-ai { color: var(--ai); }
.sec-l { flex: 1; min-width: 0; }
.sec-badge {
  font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: var(--r-full);
  background: rgba(255,255,255,.14); color: rgba(255,255,255,.85); flex-shrink: 0;
}
.sec-badge.ai { background: var(--ai-soft); color: var(--ai-deep); }

/* contextual card in the sidebar. `margin-top: auto` pins it (and the footer
   below it) to the bottom, so the leftover space collects as ONE gap under the
   nav rather than a ~200px void floating between three detached blocks. */
.sb-aside {
  margin: var(--space-6) var(--space-2) 0;
  margin-top: auto;
  padding: var(--space-5);
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06);
}
.sb-aside-l { font-size: 9.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: rgba(241,238,251,.5); margin-bottom: var(--space-3); }
.sb-stat { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: 12px; color: rgba(241,238,251,.72); padding: var(--space-1) 0; }
.sb-stat b { font-family: var(--f-display); font-size: 14px; font-weight: 700; color: #fff; }
.sb-bar { height: 5px; border-radius: var(--r-full); background: rgba(255,255,255,.12); overflow: hidden; margin-top: var(--space-3); }
.sb-bar > span { display: block; height: 100%; border-radius: var(--r-full); background: linear-gradient(90deg, var(--chrome-accent), var(--chrome-glow-2)); }
/* text-wrap: pretty stops the single-word widow ("…~2h 10m / left") that a
   248px column produces for most of these notes */
.sb-aside-n { font-size: 10.5px; line-height: 1.45; color: rgba(241,238,251,.5); margin-top: var(--space-2); text-wrap: pretty; }

/* the permission-model hint — grouped INSIDE the footer, not floating alone
   in dead space (which read as a rendering artifact at low contrast) */
.sf-perm {
  font-size: 10.5px; line-height: 1.45; color: rgba(241,238,251,.55);
  padding-bottom: var(--space-4); margin-bottom: var(--space-4);
  border-bottom: 1px solid rgba(255,255,255,.08);
}

/* sidebar footer — identity block, no awkward wrapping */
.sf-who { display: flex; align-items: center; gap: var(--space-3); }
.sf-av {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  background: var(--chrome-accent-soft); border: 1px solid rgba(167,139,250,.4);
  color: #fff; display: grid; place-items: center;
  font-family: var(--f-display); font-weight: 700; font-size: 12.5px;
}
.sf-txt { display: flex; flex-direction: column; min-width: 0; }
.sf-name { font-size: 12.5px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sf-role { font-size: 10.5px; color: rgba(241,238,251,.55); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* sub-feature tab bar sits *inside* a section — visually secondary to the sidebar */
/* `background: var(--bg)`, not `transparent`: this variant is sticky too (it
   inherits position from .tabbar), so a transparent fill let content scroll
   through the tab labels — same defect as the parent, same reason. */
.tabbar.sub { background: var(--bg); border: none; border-bottom: 1px solid var(--line); border-radius: 0; padding: 0 0 var(--space-1); gap: var(--space-2); }
.tabbar.sub .tab { border-radius: var(--r-sm) var(--r-sm) 0 0; font-size: 12.5px; }
.tabbar.sub .tab.active { background: var(--panel); box-shadow: none; border-bottom: 2px solid var(--teal); color: var(--teal-strong); }

/* ---------------- micro-interactions ----------------
   The design was built static-first; these transitions/hover states make the
   live demo feel responsive to touch. All auto-disabled under the reduced-motion
   block below (which sets `* { transition: none !important }`). */
/* One transition definition for every interactive surface. `.card` used to
   declare its own `transition: box-shadow` further down the file, which — being
   later — WON, so cards animated their shadow but snapped their transform and
   border. Folded in here so the whole system eases identically.
   Timings sit in the 120–200ms band: fast enough to feel like a direct
   response, slow enough to be seen. */
.btn, .nav a, .tab, .chip, .sw, .ic,
.kpi, .token, .bed, .feed-item, .wa-btn, .hos-x,
.card, .ai-block,
:where([data-toast], [data-goto], [data-reveal], [data-toggle], [data-modal-title], [data-select-group]) {
  transition: transform .14s ease, background-color .16s ease,
              box-shadow .2s ease, border-color .16s ease, color .16s ease;
}
.btn:hover { box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(1px); box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--teal-strong); }
.btn-ai:hover { background: var(--ai-deep); }

/* clickable domain tiles get a gentle lift on hover — the raised step, so the
   difference between resting and lifted is actually perceptible */
.kpi:hover, .token:not(.now):hover, .bed:hover {
  transform: translateY(-2px); box-shadow: var(--shadow-lg);
}
.bed { cursor: pointer; }
.token:not(.now) { cursor: pointer; }

/* nav & tabs feel snappier */
.nav a { will-change: background-color; }
.wa-btn { cursor: pointer; }
.wa-btn:hover { background: #F0F7FF; }

/* any element wired as a demo action gets a pointer cursor + subtle press */
[data-toast], [data-goto], [data-busy], [data-reveal], [data-toggle],
[data-append], [data-bump], [data-modal-title], [data-modal-body],
[data-ai-target], [data-select-group], [data-close-modal] { cursor: pointer; }
[data-select-group]:active { transform: translateY(1px); }

/* selected state for single-select groups (beds, queue items, hospital cards) */
.is-active { box-shadow: inset 0 0 0 2px var(--teal); border-color: var(--teal) !important; }

/* (`.card`'s transition now lives in the shared micro-interaction rule above —
   declaring it again here overrode that rule's transform and border-color.) */

/* ---------------- responsive ---------------- */
@media (max-width: 1080px) {
  :root { --sidebar-w: 64px; }
  .sidebar .brand-name, .sidebar .brand-sub, .sidebar .nav a span,
  .sidebar .nav-label, .sidebar-foot { display: none; }
  .sidebar { padding: var(--space-6) var(--space-4); align-items: center; }
  .nav a { justify-content: center; padding: var(--space-3); }
  /* icon-only rail: switcher + sections collapse to their monograms */
  .sb-aside, .sf-txt, .sf-perm, .hq-scope { display: none; }
  .sf-who { justify-content: center; }
  .ws-switch .ws-cur, .ws-switch .ws-chev { display: none; }
  .ws-switch { justify-content: center; padding: var(--space-3); }
  .ws-switch-wrap { padding: 0 0 var(--space-3); width: 100%; }
  .ws-menu { left: calc(100% + 8px); right: auto; top: 0; width: 250px; }
  .sec-nav { padding: 0; width: 100%; }
  .sec-item { justify-content: center; padding: var(--space-3); }
  .sec-l, .sec-badge { display: none; }
}
@media (max-width: 720px) {
  /* horizontal rail on phones — sections scroll sideways */
  .ws-switch-wrap { padding: 0; width: auto; }
  .sec-nav { flex-direction: row; }
}

/* ---------------- narrow-viewport safety net ----------------
   Two failure modes kept surfacing as horizontal page overflow on phones:
   multi-column form rows, and grid children that can't shrink below the
   min-content of a wide table/chart they contain. Fix both generically so
   individual pages don't each have to remember. */
@media (max-width: 640px) {
  .crumb-bar { padding: var(--space-3) var(--space-6); font-size: 11.5px; }
  .f-row { grid-template-columns: 1fr !important; }
  /* Pages set their column ratio inline (style="grid-template-columns:1.2fr 1fr"),
     which outranks any stylesheet rule — so a two-column split stayed two columns
     on a phone and pushed the page sideways (Training Sandbox: 607px in a 390px
     viewport). !important is the only thing that beats an inline style, and it is
     the same escape hatch .f-row above already uses. */
  .split, .split-2 { grid-template-columns: 1fr !important; }
  /* long informational chips in card headers (e.g. "▲ 6 vs last month · est.
     ₹40,800") are nowrap by design; let the header wrap so they drop to their
     own line instead of pushing the page sideways. */
  .card-h { flex-wrap: wrap; }
}
.split > *, .split-2 > *, .ns-grid > *, .kpi-row > * { min-width: 0; }

/* THE big one: .tabpanel is a flex column, and flex children default to
   `min-width: auto` — so a card containing a wide table refuses to shrink and
   pushes the whole page sideways instead of letting the table scroll inside its
   own .scroll-x wrapper. This one rule fixes that class of bug everywhere. */
.tabpanel > * { min-width: 0; }
.card, .ai-block { min-width: 0; }

/* Horizontal scroll wrapper for wide tables. These two names were each defined
   PAGE-LOCALLY — .scroll-x only in billing and pharmacy, .table-scroll only in
   administration and superadmin — so reusing a table wrapper on a page that
   didn't happen to define that name silently produced no scrolling and pushed
   the whole page sideways instead. Both live here now, so either name works
   everywhere and the trap is gone. */
.scroll-x, .table-scroll { overflow-x: auto; }
@media (max-width: 720px) {
  .app { grid-template-columns: 1fr; }
  .sidebar { position: static; height: auto; flex-direction: row; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--chrome-line); }
  .page { padding: var(--space-6) var(--space-6) var(--space-10); }
  .topbar { padding: var(--space-4) var(--space-6); gap: var(--space-4); }
  .topbar-search { display: none; }
  /* the "· Kukatpally, Hyderabad · 60 beds" suffix gets ellipsis-clipped
     mid-word on a phone; drop it and show the hospital name cleanly */
  .topbar .hospital .tiny { display: none; }
  /* the pill keeps its ✦ and shortcut but loses the words — the sheen and the
     saffron still read as "AI" at a glance */
  .tb-ask { padding: var(--space-3); font-size: 0; gap: 0; }
  .tb-ask .tb-star { font-size: 16px; }
  .tb-ask .tb-kbd { display: none; }
  /* the active rail marks a row in a VERTICAL list; once the sidebar becomes a
     horizontal strip it reads as a stray divider between two icons */
  .sec-item::before { display: none; }
  .tabbar { position: static; }
}
