/* LAST CHANGED: 04 Sep 2026 — WHAT'S NEW (D-OPSNAVSIDE, GAP_REGISTER.md): one new rule,
   `a.nav-item { text-decoration: none; }` — see the comment right above that rule (next to
   a.btn's own identical fix) for the full reasoning. First real consumer of `.nav-item` as an
   anchor: the Operations Manager App's new grouped left sidebar (apps/web/operations/
   index.html + the new nav-side.js), replacing its old flat 41-link `a.btn` header row. No
   other rule in this file touched — `.side`/`.nav-item`/`.nav-item[aria-current="true"]`
   already existed, unused by any real app page until now, and needed no changes to be reused.
   ---
   PRIOR: 03 Sep 2026 (Fable remediation of `8caa1cb`) — `.pill`/`.pill.ontime`/
   `.pill.risk`/`.pill.charging`/`.pill.warn` MOVED here from operations.css (removed there,
   its own header updated to match): the Founder App's new emergency ack fan-out view
   (founder.js's renderAckFanOut) needed the exact same status-pill vocabulary, and every
   one of the five apps already loads this stylesheet, so a shared component belongs here,
   not duplicated per-app. Purely a relocation — no rule's declaration changed.
   ---
   PRIOR: 29 Aug 2026 — WHAT'S NEW: Phase 1 Design Language Refresh, Deliverables
   2-6 (PHASE1_DESIGN_LANGUAGE_REFRESH_SPEC.md, Founder-confirmed) — built standalone
   here, not yet wired into any app (that happens per-app, each its own Gate-C diff,
   per the spec's rollout sequence). Nine new component groups, all additive:
   .rec-card (Deliverable 2, generalizes Founder's .appr approval-card shape — title,
   up to 3 lines of context, .btn-row actions, optional severity-tint background);
   .chip .reason (3a, ConnectivityBadge — same calm .chip/.chip.offline visual as
   before, but the underlying reason must now always be named in the label text, not
   just implied by a reused warn color — see this file's ConnectivityBadge comment
   below for why that mattered); .stale-marker (3b, extracted from Operations'
   .age/.age.stale into a shared component); .precommit-label (3c, built off the
   Founder App doc's existing NEVER QUEUED/ALWAYS QUEUED/DECLARED PER TYPE
   restatement — CLAUDE.md's own citation for this is a separate, still-pending
   Gate-C diff); .reconnect-digest (3d, a fresh build — nothing like it existed in
   code despite the design doc's mention); .ack-state (Deliverable 4, scoped to
   DriverMessage's real sent/acknowledged/read timestamps only — IncidentThread's
   fuller state machine has no backend yet, so its visuals are deliberately not built
   here); .btn:disabled/:active + .skeleton (Deliverable 5 — .btn had no disabled or
   pressed state at all before this; skeleton is Tier-3-web-only by discipline, not
   enforced by the CSS itself); .search-filter-bar (Deliverable 6, standalone — its
   three target surfaces' asset-register screens don't exist in code yet). Also fixes
   3 of the 9 repo-wide sub-48px touch-target violations found in this same pass
   (.crumb button, .tabs button, .groupby select, .seg .btn) — see tokens.css's own
   LAST CHANGED for the full list. Full findings + every recommendation's Founder
   confirmation: PHASE1_DESIGN_LANGUAGE_REFRESH_SPEC.md. Gap records: GAP_REGISTER.md.
   ---
   PRIOR: 04 Jul 2026 — WHAT'S NEW: a THIRD, deeper phone form-factor bug found
   after the flex-wrap/min-width pass below actually shipped to the emulator: every
   app's outer `display:grid` container (.app here, .ops/.en/.driver/.fr per-app) declared
   no grid-template-columns, or a bare `1fr`/`auto` track — either way, an implicit
   single-column grid track floors at the MAX-CONTENT width of its widest child (a long
   line of text, a data table) instead of the container's width. One unshrinkable child
   silently blew out the whole page on real devices, while Chrome's desktop preview (where
   nothing happens to be that wide at typical test widths) didn't show it — only found by
   measuring document.body.scrollWidth vs window.innerWidth at the real device viewport.
   Fixed everywhere in this file by changing every `1fr`/implicit column track to
   `minmax(0, 1fr)`, which keeps the same flexible sizing but lets the track shrink to 0
   instead of refusing to go below content width. Same pattern fixed in every per-app
   grid (.ops/.board, .en/.board, .driver/.dmain, .fr/.fmain/.grid2/.grid3,
   .dash/.kpis) — see each file. Operations' wide data tables (table.t) additionally got
   `display:block; overflow-x:auto` so they scroll within their card instead of forcing
   the blowout themselves.
   ---
   WHAT'S NEW (04 Jul 2026, earlier pass): phone form-factor fit (found via real-device
   emulator testing of the native-wrapped apps). Two root causes, both fixed here: (1)
   .topbar had no flex-wrap, so on a narrow phone its row of chips/buttons overflowed the
   viewport instead of dropping to a second line; (2) .prompt input had no min-width — a
   flex:1 child defaults to min-width:auto, so it refused to shrink below its own content
   width and forced the WHOLE PAGE wider than the screen. html,body overflow-x:hidden is
   a defensive backstop, not the fix — the two rules above are. Each app's own header/
   prompt classes needed the same pair of fixes; see operations.css/energy.css/
   founder.css/driver.css for the per-app half of this.
   ---
   WHAT'S NEW: .seg segmented-control (mutually-exclusive view
   tabs, e.g. heat-map scope); .seg-on marks the active tab. Reusable across all apps.
   ---
   MOOEV OS — shared components. Framework-agnostic; consumes tokens.css.
   Every app is built from these, so all five feel like one OS. */

* { box-sizing: border-box; }
/* D-OPSNAVSIDE shared extraction (04 Sep 2026) — this codebase has NO author `[hidden]`
   rule anywhere (confirmed by grep across every app's CSS before adding this); only the
   UA stylesheet hides `[hidden]`, and any author `display:` rule on the same element beats
   UA origin regardless of specificity. Proven live, not assumed: `apps/web/founder/
   founder.js`'s `<div class="form-row" id="emgClusterRow" hidden>` renders VISIBLE today
   whenever `founder.css`'s `.form-row { display: flex }` applies, because nothing overrides
   the UA default with `!important`. This one rule closes that gap everywhere at once —
   needed now because `apps/web/screens.js` (D-HOME-IA-SMARTPHONE)'s router toggles the
   `hidden` attribute on `<section data-screen>` elements, several of which are themselves
   grid/flex containers with their own `display` rule; without this, a "hidden" screen
   would still render. Toggle visibility with `el.hidden = true/false`, never
   `style.display`, everywhere in this codebase from here on. */
[hidden] { display: none !important; }
/* D-HOME-IA-SMARTPHONE (2 Sep 2026): font-size:100% establishes 1rem = the browser's own
   default (usually 16px, but genuinely whatever the OS/browser dynamic-type setting has
   scaled it to) as the root for every rem-based --fs-* token in tokens.css — a raw px value
   never responds to that setting; rem does. Previously unset (browser default, effectively
   the same 100% today), made explicit here so the intent — dynamic type actually working —
   is a stated decision, not an accident of the default. */
html, body { margin: 0; padding: 0; overflow-x: hidden; font-size: 100%; }

/* Accessibility by design — visible keyboard focus on every interactive control.
   All controls are real <button>/<a>/<select>/<input>, so they're keyboard-reachable;
   this makes the focus ring clear and consistent across all apps. */
:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: var(--r-sm); }
:focus:not(:focus-visible) { outline: none; }
body {
  font-family: var(--font); font-size: var(--fs-md); line-height: var(--lh);
  color: var(--c-text); background: var(--c-bg);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;            /* long tokens (RTO numbers) never overflow */
  -webkit-text-size-adjust: 100%;       /* honour OS text size, no surprise reflow */
}

/* App frame: reflows from single-column (phone) to sidebar (tablet/desktop) */
.app {
  display: grid; min-height: 100vh;
  grid-template-columns: minmax(0, 1fr);      /* minmax(0,..), not bare 1fr/auto — an
    auto/1fr track floors at its content's max-content width, so one unshrinkable child
    (a wide table, a long unbroken string) silently blows out the whole page. */
  grid-template-rows: auto 1fr auto;          /* topbar / content / fallback */
}
@media (min-width: 800px) {
  .app {
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    grid-template-areas: "side top" "side main" "side fall";
  }
}

/* Top bar */
.topbar {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4); background: var(--c-surface);
  border-bottom: 1px solid var(--c-border); box-shadow: var(--shadow);
}
/* grid-area only assigned where grid-template-areas actually names it (>=800px) — see
   the LAST CHANGED note above: an unconditional grid-area with no matching named area
   at the current breakpoint gets honoured anyway (a Chrome quirk, not spec fallback-to-
   auto), collapsing .topbar/.side/.main/.fallback into one overlapping cell below 800px. */
@media (min-width: 800px) { .topbar { grid-area: top; } }
.brand { font-weight: var(--fw-medium); font-size: var(--fs-lg); color: var(--c-brand); }
.spacer { flex: 1; }

/* Connection chip — Input Visibility / offline-first made visible at a glance */
.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 6px var(--s-3); border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: var(--fw-medium);
  background: var(--c-surface-2); color: var(--c-text-muted);
  border: 1px solid var(--c-border);
}
.chip .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--c-ok); }
.chip.offline .dot { background: var(--c-warn); }
.chip.offline { color: var(--c-warn); }
/* Real clickable .chip controls (#conn on Energy/Founder headers) need the same 48px
   touch-target floor every other interactive atom has. Scoped to this modifier, not bare
   .chip, because .chip is also used as a passive inline badge elsewhere (Operations'
   ReconnectDigest samples, gallery.html, Driver storage/outbox badges) where a forced 48px
   height would break inline flow. */
.chip.interactive { min-height: var(--touch-min); cursor: pointer; }

/* Sidebar / role nav. On phone it becomes a bottom bar. */
.side {
  background: var(--c-surface); border-right: 1px solid var(--c-border);
  padding: var(--s-4) var(--s-2); display: none;
}
@media (min-width: 800px) { .side { display: block; grid-area: side; } }
.nav-item {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; min-height: var(--touch-min); padding: 0 var(--s-3);
  border: 0; background: transparent; border-radius: var(--r-md);
  color: var(--c-text); font-size: var(--fs-sm); text-align: left; cursor: pointer;
}
.nav-item[aria-current="true"] { background: var(--c-surface-2); color: var(--c-brand); font-weight: var(--fw-medium); }

/* D-NAVRAIL-COLLAPSE (04 Sep 2026) — icon-only collapsed state, shared by `.side`
   (Operations) and `.appnav` (Energy/Founder, its own rule further below). Toggled by the
   new apps/web/nav-collapse.js: a `collapsed` class on the rail element itself drives this
   half (label hiding, icon centering); the column-width half is a plain CSS custom
   property (`--rail-w`, tokens.css) that module sets directly on the app shell element —
   no `:has()` selector needed for that part. `.nav-icon`/`.nav-label` are a plain span
   split every nav item (both `.side`'s `a.nav-item` and `.appnav button`) gains — see each
   app's own index.html for the exact markup. Icon-only buttons keep a real `aria-label`
   (wired by the app, not this rule) so they stay accessible with the text hidden — this
   rule only ever hides `.nav-label` VISUALLY (the visually-hidden pattern above, not
   display:none), so a screen reader still reads it regardless of collapsed state; sighted
   users get the icon alone.

   Every collapsed-state rule below is scoped inside the SAME media query `.side`/`.appnav`
   already use for their own desktop-sidebar layout (800px / 900px, `.appnav`'s own rule
   further below) — deliberately, not left unscoped: `.appnav` is a horizontal chip row
   below 900px (not display:none like `.side` below 800px), so an unscoped `.appnav.collapsed`
   rule would still visually squash that phone-width chip row if the class were ever present
   at a narrow viewport (e.g. after collapsing at desktop, then resizing down without a
   reload) — collapsing is a desktop-only concept, this makes that true by construction, not
   by trusting JS to always remove the class in time. */
.nav-icon { flex: none; font-size: var(--fs-lg); line-height: 1; display: flex; align-items: center; justify-content: center; width: 24px; }
/* `.nav-group-toggle` — a real <button> inside each `.nav-group`'s existing <h2>, always
   present in the markup (so no innerHTML swap is needed to enter/leave collapsed mode) but
   only VISUALLY a button once collapsed; expanded, it inherits the h2's own plain-text
   colour/font/text-transform (both are inherited CSS properties, so a plain reset of the
   non-inherited button chrome below is enough — no `all: unset` here, DELIBERATELY: that
   was tried first and found to reset `outline` to its initial value too, at the same
   (0,1,0) specificity as the shared `:focus-visible` rule above and LATER in source order,
   so it silently won the cascade and produced a real no-visible-focus-indicator bug this
   pass's own tests/layer_web/test_keyboard_focus.py caught — this exact "all:unset beats
   :focus-visible" class of bug already has a documented precedent elsewhere in this file,
   see the .prompt input's own history note further down). Click only does something while
   collapsed (nav-collapse.js's own guard) — clicking a category expands the whole rail;
   there is no single page a category icon could navigate to on its own (Operations only;
   Energy/Founder have no `.nav-group`).
   min-height: var(--touch-min) applies in BOTH states, not just collapsed — this is now a
   real, always-clickable, always-focusable <button>, not a plain heading, so it needs a
   real touch target regardless of which state a sighted user happens to be looking at (a
   sub-48px interactive control is exactly the class of bug this design system's own
   test_touch_targets.py exists to catch, even though that test's static CSS scan happens
   not to catch THIS specific case — verified live in-browser during this pass, not left to
   luck). The category heading row is a few px taller than before as a result — a small,
   deliberate, disclosed change. */
.nav-group-toggle {
  background: transparent; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  text-align: inherit; text-transform: inherit; letter-spacing: inherit;
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%; min-height: var(--touch-min); cursor: default; box-sizing: border-box;
}
/* Collapse/expand toggle — one per rail, top of the rail's own scroll area. A plain
   chevron glyph, rotated via the `collapsed` class rather than swapped text, so no i18n
   round-trip is needed for the glyph itself (only its aria-label changes). */
.nav-collapse-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: var(--touch-min); border: 0; background: transparent;
  color: var(--c-text-muted); cursor: pointer; border-radius: var(--r-md);
  margin-bottom: var(--s-2);
}
.nav-collapse-toggle:hover { background: var(--c-surface-2); }
.nav-collapse-toggle .chevron { display: inline-block; transition: transform var(--dur-fast) var(--ease-standard); }
.collapsed .nav-collapse-toggle .chevron { transform: rotate(180deg); }
/* `.side`'s own toggle needs no extra width guard — `.side` itself is display:none below
   800px (its base rule above), which already hides every child including this button.
   `.appnav` stays visible as a chip row below 900px, so its toggle needs an explicit guard
   here — collapsing only ever makes sense once `.appnav` is the sidebar, not the row. */
.appnav .nav-collapse-toggle { display: none; }
@media (min-width: 900px) { .appnav .nav-collapse-toggle { display: flex; } }

@media (min-width: 800px) {
  /* Below 800px .side is display:none anyway (its own base rule above), so the toggle
     button and every collapsed-state rule here are scoped to match — no separate width
     guard needed in nav-collapse.js itself. */
  .side.collapsed { padding-left: 0; padding-right: 0; }
  .side.collapsed .nav-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .side.collapsed .nav-item { justify-content: center; padding: 0; }
  /* Operations only: the 40 individual links hide entirely while collapsed — only the 7
     category icons stay reachable, the Founder's own scoping decision (7 icons, not 40) —
     see nav-collapse.js's own header. */
  .side.collapsed .nav-group a.nav-item { display: none; }
  .side.collapsed .nav-group h2 { margin: 0 0 var(--s-1); padding: 0; }
  .side.collapsed .nav-group-toggle {
    justify-content: center; cursor: pointer; border-radius: var(--r-md);
  }
  .side.collapsed .nav-group-toggle:hover { background: var(--c-surface-2); }
  /* No extra rule needed to hide the category label itself — `.side.collapsed .nav-label`
     above already matches it (a `.nav-label` nested inside `.nav-group-toggle` is still a
     descendant of `.side.collapsed`) via the same visually-hidden clip technique, which
     KEEPS it in the accessibility tree. A `display:none` override here was tried and
     removed — it silently won the cascade (3 classes beat 2) and, unlike the clip
     technique, truly removes the text from the accessibility tree, which
     tests/layer_web/test_nav_collapse.py caught. */
}

/* D-OPSNAVSIDE shared extraction (04 Sep 2026) — atoms promoted here from
   apps/web/operations/operations.css, where D-OPSNAVSIDE (Operations' grouped sidebar +
   per-link toggle switches, 04 Sep 2026) first built them. Promoted, not left per-app,
   because every one of these is a genuine atom (token-only, no domain semantics, no
   feature name in any selector) — the "each app's CSS stays independently readable"
   convention this codebase otherwise follows (energy.css/operations.css's own manuals-
   library blocks) is stated for FEATURE COMPOSITES, not primitives; `.btn`/`.chip`/
   `.card`/`.side`/`.nav-item` above already live here on that same distinction. Promoted
   so the shared apps/web/side-nav.js module (behaviour) and any future app adopting it
   (Energy, Founder — Home-IA programme Steps 2/3) get identical, already-proven styling
   for free, the same way `.side`/`.nav-item` themselves already were unused-but-ready
   before Operations became their first real consumer. */

/* Sidebar group heading — a `.nav-group`'s own `<h2>` (promoted 04 Sep 2026 as `<h2>`, not
   the `<h3>` D-OPSNAVSIDE originally shipped — a skipped heading level found by
   `osverify --tier 3`'s `test_heading_hierarchy.py`; see operations.css's own historical
   header for the full record). Styled after `.home-layout-row`'s own section heading
   below, without literally sharing that class — a sidebar group and a Customize-home
   panel section are visually consistent but structurally different surfaces. */
.side .nav-group h2 { margin: var(--s-4) var(--s-3) var(--s-1); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .02em; color: var(--c-text-muted); }
.side .nav-group:first-child h2 { margin-top: 0; }

/* Narrow-screen nav drawer overlay state — `.side.open` is the shared class half; the
   `#navDrawerBtn`/`#navDrawerScrim` id rules (and their own `.open` pairing) stay
   per-app, in each app's own CSS, since ids are inherently per-page markup. */
@media (max-width: 799px) {
  .side.open {
    display: block; position: fixed; inset: 0 15% 0 0; z-index: 100;
    overflow-y: auto; box-shadow: var(--shadow);
  }
}

/* Toggle switch — track + thumb. The one genuinely new UI atom D-OPSNAVSIDE needed: every
   existing show/hide control in this codebase (checkboxes, incl. `.home-layout-row` below)
   is a plain checkbox. Built from EXISTING tokens only (--c-brand/--c-border/--r-pill/
   --c-surface) — no new colour introduced. A real `<button role="switch"
   aria-checked="...">` (wired by the app's own panel-owning file, e.g. home-layout.js), not
   a styled checkbox, so keyboard/screen-reader semantics are correct without extra ARIA
   wiring beyond aria-checked itself; the shared `:focus-visible` rule above already gives
   it a focus ring for free. `.switch.locked` is a locked-always-on, non-interactive variant
   (`aria-disabled="true"`, `tabindex="-1"` set in markup) — visually dimmed, never
   clickable. The button's own box sizes to `var(--touch-min)` (48x48, the real tap target,
   flex-centered) — NOT the visible 44x26 pill, which lives on `::before` instead — a
   touch-target-floor fix (`test_touch_targets.py`) found on Operations' own first ship;
   see operations.css's own historical header for the full incident record. */
.switch {
  width: var(--touch-min); height: var(--touch-min); padding: 0; border: 0; background: transparent;
  cursor: pointer; position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center;
}
.switch::before { content: ""; display: block; width: 44px; height: 26px; border-radius: var(--r-pill); background: var(--c-border); transition: background var(--dur-fast) var(--ease-standard); }
.switch::after { content: ""; position: absolute; top: 14px; left: 5px; width: 20px; height: 20px; border-radius: 50%; background: var(--c-surface); transition: transform var(--dur-fast) var(--ease-standard); }
.switch[aria-checked="true"]::before { background: var(--c-brand); }
.switch[aria-checked="true"]::after { transform: translateX(18px); }
.switch.locked { cursor: not-allowed; opacity: .7; }

/* Personalization-hidden utility — an item a manager has switched off via a Customize
   panel (side-nav.js) or the pre-existing card-layout feature (home-layout.js). Deliberately
   `!important`: this must always win over a card/link's own layout display value,
   regardless of that rule's specificity. */
.home-hidden { display: none !important; }

/* Customize panel — one row per toggleable item, grouped under a plain-text section
   heading. Shared by every app's own Customize-home-shaped panel (button + collapsed
   panel, the same interaction shape `notify.js`'s bell/panel already established); the
   panel's own button/container markup (ids) stays per-app. */
.home-layout-section h3 { margin: var(--s-4) 0 var(--s-1); font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .02em; color: var(--c-text-muted); }
.home-layout-section:first-child h3 { margin-top: 0; }
.home-layout-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); padding: var(--s-2) 0; border-bottom: 1px solid var(--c-border); flex-wrap: wrap; }
.home-layout-row:last-child { border-bottom: 0; }
.home-layout-row label { display: flex; align-items: center; gap: var(--s-2); }

/* Phone bottom nav */
.bottomnav {
  display: flex; justify-content: space-around; background: var(--c-surface);
  border-top: 1px solid var(--c-border); position: sticky; bottom: 0;
}
@media (min-width: 800px) { .bottomnav { display: none; } }
.bottomnav button {
  flex: 1; min-height: var(--touch-min); border: 0; background: transparent;
  color: var(--c-text-muted); font-size: var(--fs-xs); cursor: pointer; padding: var(--s-2);
}
.bottomnav button[aria-current="true"] { color: var(--c-brand); font-weight: var(--fw-medium); }

/* Main content */
.main { padding: var(--s-4); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-4); align-content: start; }
@media (min-width: 800px) { .main { grid-area: main; padding: var(--s-5); } }

/* Prompt-first bar — level 1 of the three-level fallback, on every app */
.prompt {
  display: flex; gap: var(--s-2); align-items: center;
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-pill); padding: var(--s-2) var(--s-2) var(--s-2) var(--s-4);
  box-shadow: var(--shadow);
}
.prompt input {
  /* No outline:none here (keyboard-focus-order fix, 05 Jul 2026): this rule's own
     specificity + source order was overriding the global :focus-visible accent ring
     defined earlier in this file — a real bug, not a deliberate override. A real Tab-key press
     (not a scripted .focus(), which doesn't register as :focus-visible) confirmed
     the input rendered NO focus indicator at all before this fix. */
  flex: 1; min-width: 0; border: 0; background: transparent; color: var(--c-text);
  font-size: var(--fs-md); min-height: var(--touch-min);
}
.mic {
  min-width: var(--touch-driver); min-height: var(--touch-driver);
  border-radius: 50%; border: 0; background: var(--c-brand); color: var(--c-on-brand);
  font-size: var(--fs-lg); cursor: pointer;
}

/* Accessible-name-only page title (heading-hierarchy fix, 05 Jul 2026): every page's brand
   header already shows "MOOEV" + the app name visually, so a second, VISIBLE <h1> would be
   redundant clutter — but a screen reader needs an <h1> landmark to exist at all. This
   hides it from sight (not from assistive tech) via the standard clip-based pattern, which
   — unlike display:none/visibility:hidden — is NOT display:none, so screen readers still
   announce it. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Cards */
.card {
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-lg); padding: var(--s-4); box-shadow: var(--shadow);
}
.card h2 { margin: 0 0 var(--s-2); font-size: var(--fs-md); font-weight: var(--fw-medium); }
.muted { color: var(--c-text-muted); font-size: var(--fs-sm); }
.grid-cards { display: grid; gap: var(--s-4); grid-template-columns: minmax(0, 1fr); }
@media (min-width: 560px) { .grid-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .grid-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Explainable + overridable recommendation — core interface principle */
.rec { border-left: 4px solid var(--c-accent); border-radius: 0 var(--r-md) var(--r-md) 0; }
.rec .why { color: var(--c-text-muted); font-size: var(--fs-sm); margin: var(--s-2) 0 var(--s-3); }
.btn-row { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* D-ENERGY-HEADER-ACTIONGRID / D-FOUNDER-HEADER-ACTIONGRID: shared header action-button
   grid used by Energy's and Founder's home headers — equal-width 2-col grid so buttons of
   very different label lengths ("Alerts" vs "Explore stations") no longer wrap raggedly at
   their own text width. .btn/a.btn/select.btn children fill their cell via CSS Grid's
   default item blockification+stretch, no extra width rule needed on them. An odd total
   item count spans the last cell full-width, self-adjusting if an item is ever added or
   removed. */
.header-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-3);
  padding: 0 var(--s-4) var(--s-3); background: var(--c-surface); border-bottom: 1px solid var(--c-border); }
.header-actions > :last-child:nth-child(odd) { grid-column: 1 / -1; }

.btn {
  min-height: var(--touch-min); padding: 0 var(--s-4); border-radius: var(--r-md);
  border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text);
  font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer;
}
.btn.primary { background: var(--c-brand); color: var(--c-on-brand); border-color: var(--c-brand); }
/* .btn was written for <button>/<select>/<input> (no underline by default). The Operations
   home-screen nav tiles apply it to <a> instead (~38 links, e.g. operations/index.html) --
   without this, every one renders with the browser's default link underline since nothing
   else in .btn touches text-decoration/display. Scoped to a.btn so it never changes the
   many correct non-anchor .btn usages elsewhere (forms, dialogs, segmented controls). */
a.btn { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }

/* Icon-only .btn (Energy's alerts bell). Keeps --touch-min on BOTH axes — an icon button
   loses the label's intrinsic width, so without this it would fall under the 48px floor
   DESIGN_LANGUAGE.md §7 requires. The glyph inside is aria-hidden, so the button itself
   must carry an aria-label or test_accessibility_basics.py's unnamed-interactive check
   fails it. */
.btn.icon-btn { min-width: var(--touch-min); padding: 0 var(--s-3); display: inline-flex;
  align-items: center; justify-content: center; gap: var(--s-2); position: relative; }
/* Every mockup for this header showed the alert count as a small floating dot on the
   bell's corner, not sitting inline beside it — but bare .notify-badge (used as-is by
   Operations' text+badge bell, "Alerts 3") is a normal inline-flow element there, which
   silently widened #notifyBell to 72px here (icon + gap + badge, all in a row) instead of
   the intended 48px, which was just enough to push the Settings icon onto a second line.
   Scoped to .icon-btn specifically so Operations' bell is untouched. */
.icon-btn .notify-badge { position: absolute; top: -4px; right: -4px; }

/* Strips native OS <select> chrome so .station/#themeSelect visually match the button/pill
   family beside them (D-ENERGY-HEADER-ACTIONGRID). Chevron colour is a literal hex baked
   into the data-URI — CSS custom properties cannot be referenced inside url(data:...) — so
   keep these two hexes in sync with --c-text-muted's light/dark values in tokens.css if that
   token is ever retuned. Qualified as `select.select-reset`, not a bare class, so its
   padding-right/background-image reliably beat .station's/.btn's own padding/background
   SHORTHANDS (which implicitly reset those same longhands) regardless of CSS file load order. */
select.select-reset {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  background-repeat: no-repeat; background-position: right var(--s-3) center;
  background-size: 10px 6px; padding-right: var(--s-6);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235b6776' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select.select-reset { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239fb0bf' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
}
:root[data-theme="dark"] select.select-reset { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239fb0bf' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
}

/* D-OPSNAVSIDE (04 Sep 2026): .nav-item has the EXACT same gap a.btn's own comment just above
   already documents — written for <button> only, no underline rule of its own. The Operations
   Manager App's new grouped left sidebar (apps/web/operations/index.html/nav-side.js) is the
   first real consumer of .nav-item as an <a>, replacing the old flat 41-link a.btn header row.
   Scoped to a.nav-item so the existing <button class="nav-item"> app-switcher usage (the
   top-level shell, apps/web/index.html) is unaffected. */
a.nav-item { text-decoration: none; }

/* Segmented control — a small row of mutually-exclusive view tabs (e.g. heat-map scope).
   The active tab (.seg-on) reads as selected; consistent across every app that uses it. */
.seg { display: inline-flex; gap: var(--s-1); flex-wrap: wrap; }
.seg .btn { min-height: var(--touch-min); padding: 0 var(--s-3); }
.seg .btn.seg-on { background: var(--c-brand); color: var(--c-on-brand); border-color: var(--c-brand); }

/* Three-level fallback strip — always present, never strands the user */
.fallback {
  display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center;
  padding: var(--s-3) var(--s-4); background: var(--c-surface-2);
  border-top: 1px solid var(--c-border); font-size: var(--fs-xs); color: var(--c-text-muted);
}
@media (min-width: 800px) { .fallback { grid-area: fall; } }
.fallback b { color: var(--c-text); font-weight: var(--fw-medium); }

/* Big metric */
.metric { font-size: var(--fs-xl); font-weight: var(--fw-medium); color: var(--c-text); }

/* Shared map (Leaflet/OSM) — fixed height so Leaflet sizes correctly; graceful fallback */
.leafmap { border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--c-border); }
/* .leafmap height is set in the Orientation & space adaptivity block below (vh-capped). */
.map-fallback { display: flex; align-items: center; justify-content: center; height: 100%;
  padding: var(--s-4); text-align: center; color: var(--c-text-muted); font-size: var(--fs-sm);
  background: var(--c-surface-2); border-radius: var(--r-md); }
.leaflet-container { font: inherit; }

/* Drill-down explorer (granularity × dimension) — shared by Operations & Founder */
.crumb { display:flex; flex-wrap:wrap; gap:var(--s-2); align-items:center; font-size:var(--fs-sm); margin-bottom:var(--s-3); }
.crumb button { border:0; background:transparent; color:var(--c-accent); cursor:pointer; font-size:var(--fs-sm); padding:4px 6px; border-radius:var(--r-sm); min-height:var(--touch-min); }
.crumb button:disabled { color:var(--c-text); cursor:default; font-weight:var(--fw-medium); }
.crumb .sep { color:var(--c-text-muted); }
.tabs { display:flex; flex-wrap:wrap; gap:var(--s-2); margin-bottom:var(--s-3); }
.tabs button { border:1px solid var(--c-border); background:var(--c-surface-2); color:var(--c-text); border-radius:var(--r-pill); min-height:var(--touch-min); padding:0 var(--s-3); font-size:var(--fs-sm); cursor:pointer; }
.tabs button[aria-current="true"] { background:var(--c-brand); color:var(--c-on-brand); border-color:var(--c-brand); }
.groupby { display:flex; gap:var(--s-2); align-items:center; flex-wrap:wrap; margin-bottom:var(--s-3); font-size:var(--fs-sm); color:var(--c-text-muted); }
.groupby select { min-height:var(--touch-min); border:1px solid var(--c-border); background:var(--c-surface); color:var(--c-text); border-radius:var(--r-md); padding:0 var(--s-2); }
.drillrow { display:flex; align-items:center; gap:var(--s-3); width:100%; text-align:left; border:0; border-bottom:1px solid var(--c-border); background:transparent; color:var(--c-text); padding:var(--s-3) var(--s-2); cursor:pointer; min-height:var(--touch-min); font-size:var(--fs-md); }
.drillrow:hover { background:var(--c-surface-2); }
.drillrow .nm { font-weight:var(--fw-medium); }
.drillrow .sub { color:var(--c-text-muted); font-size:var(--fs-xs); }
.drillrow .val { margin-left:auto; text-align:right; }
.drillrow .chev { color:var(--c-text-muted); }
.leafdetail { display:grid; gap:var(--s-2); grid-template-columns:minmax(0, 1fr); }
@media (min-width:640px){ .leafdetail { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
.dq { font-size:var(--fs-xs); color:var(--c-text-muted); margin-top:var(--s-2); }

/* Margin waterfall — revenue minus cost steps to contribution */
.wf { display:grid; gap:6px; margin-top:var(--s-3); }
.wf .row { display:grid; grid-template-columns:130px minmax(0, 1fr) auto; align-items:center; gap:var(--s-3); font-size:var(--fs-sm); }
.wf .track { height:14px; background:var(--c-surface-2); border-radius:var(--r-pill); overflow:hidden; }
.wf .fill { height:100%; }
.wf .fill.pos { background:var(--c-brand); }
.wf .fill.neg { background:var(--c-danger); }
.wf .fill.net { background:var(--c-accent); }

/* Orientation & space adaptivity (Founder-set: portrait + landscape; one panel
   never blocks another). Layouts already reflow by viewport WIDTH (the grids above)
   — landscape gets more columns, portrait stacks. These rules add HEIGHT awareness so a
   tall element can't dominate a short (landscape-phone) screen, and respect device safe
   areas.

   UPDATED 2 Sep 2026 (D-HOME-IA-SMARTPHONE) — the line this comment used to have, "No
   element uses fixed/absolute positioning, so nothing overlaps; everything is normal flow
   and scrolls," was disproven by this pass's own live-rendering check: .bottomnav (below)
   claims position:sticky but was verified to never actually reach the viewport on a real
   long scrolling page (it is a direct grid item, so its sticky containing block is its own
   `auto` grid row — sticky has no room to move it). The bounded-frame model below is the
   real fix: an app frame that adopts .promptbar/.appnav caps itself to `height: 100dvh`
   with exactly ONE internal `overflow-y: auto` scroll region, and places .promptbar as a
   SIBLING grid row outside that region — genuinely always on-screen because it is never
   part of the thing that scrolls, not because of position:sticky/fixed doing work on an
   unbounded page. See DESIGN_LANGUAGE.md Sec 2/6 and the plan's own D1 for the full trace.
   .bottomnav itself is left as it was — it belongs only to the root role-selector shell
   (apps/web/index.html), which this pass does not touch; no per-app page has ever used it. */
img, svg, table, .leafmap, .leaflet-container { max-width: 100%; }
.leafmap { height: min(320px, 46vh); }       /* cap by viewport height, not fixed px */
.leafmap.small { height: min(220px, 40vh); }
.bottomnav { padding-bottom: env(safe-area-inset-bottom, 0px); }
@media (orientation: landscape) and (max-height: 560px) {
  .card { padding: var(--s-3); }              /* tighten chrome so content fits */
  .leafmap, .leafmap.small { height: 40vh; }
}

/* Intelligent notifications — collapsed by default (minimise interruption); shared. */
.notify-panel { display: none; }
.notify-panel.open { display: block; }
.notify-item { display: flex; gap: var(--s-3); align-items: flex-start; padding: var(--s-3) 0; border-bottom: 1px solid var(--c-border); }
.notify-item:last-child { border-bottom: 0; }
.nsev { width: 8px; align-self: stretch; min-height: 28px; border-radius: var(--r-pill); background: var(--c-text-muted); }
.nsev.high { background: var(--c-danger); } .nsev.medium { background: var(--c-warn); } .nsev.low { background: var(--c-accent); }
.notify-badge { display: inline-block; min-width: 18px; text-align: center; padding: 0 5px; border-radius: var(--r-pill); background: var(--c-surface-2); color: var(--c-text-muted); font-size: var(--fs-xs); }
.notify-badge.high { background: var(--c-danger); color: var(--c-on-danger); }
.notify-empty { color: var(--c-text-muted); font-size: var(--fs-sm); }

/* ==========================================================================
   Phase 1 Design Language Refresh — Deliverables 2-6, standalone (not yet wired
   into any app). See this file's LAST CHANGED note above for the full picture.
   ========================================================================== */

/* Deliverable 2 — Unified card. Generalizes Founder's .appr approval-card shape:
   title, up to three lines of server-computed context (never raw data dumped for
   the user to interpret), .btn-row actions. Shared shape for Operations' Needs
   Attention Feed, Energy's fault cards, Founder's approval queue, and
   ReconnectDigest entries below. Severity variants use tokens.css's tint
   backgrounds; plain .rec-card (no modifier) has none. */
.rec-card {
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-lg); padding: var(--s-4); margin-bottom: var(--s-3);
}
.rec-card:last-child { margin-bottom: 0; }
.rec-card .head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); margin-bottom: var(--s-2); }
.rec-card .head h3 { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-medium); }
.rec-card .ctx { color: var(--c-text-muted); font-size: var(--fs-sm); margin: 0 0 var(--s-1); }
.rec-card .btn-row { margin-top: var(--s-3); }
.rec-card.sev-danger  { background: var(--c-danger-bg); }
.rec-card.sev-warn    { background: var(--c-warn-bg); }
.rec-card.sev-neutral { background: var(--c-neutral-bg); }

/* Deliverable 3a — ConnectivityBadge. Reuses .chip/.chip.offline as-is (kept
   deliberately one calm, singular visual — not redesigned into several). The real
   fix is a content contract: checked directly, one .chip.offline warn style was
   standing in for at least five different meanings across the app (network down,
   MOOEV.badge()'s 3-state data provenance, per-field staleness, a local geofence
   draft-offline queue, a settings sample note) with no way for a manager to tell
   them apart. .chip .reason lets the specific reason ("— last synced 2m ago",
   "— offline", "not yet available") read as a distinct, de-emphasized clause next
   to the leading state word, instead of relying on a reused color alone. Wiring
   every existing signal source onto this shared text contract is per-app rollout
   work (Operations owns most of them), not done in this standalone pass. */
.chip .reason { color: var(--c-text-muted); font-weight: var(--fw-regular); }
.chip.offline .reason { color: inherit; opacity: .85; }
/* D-HOME-IA-SMARTPHONE (2 Sep 2026): MOOEV.badge()'s new 'cached' state marks a past-
   threshold age with the SAME visual treatment .stale-marker.is-stale (below) already uses
   — same tokens, not a new colour — so "this cached figure is old enough to distrust" reads
   consistently wherever staleness is signalled. .offline .reason's opacity rule above would
   otherwise mute this warning colour along with everything else in the chip, so this
   overrides opacity back to full strength for the stale case specifically. */
.chip .reason.is-stale { color: var(--c-warn); font-weight: var(--fw-medium); }
.chip.offline .reason.is-stale { opacity: 1; }

/* Deliverable 3b — StalenessMarker. Extracted from Operations' own .age/.age.stale
   into a shared component so any app can mark "connected, but this specific figure
   is behind" wherever a live feed (position, SOC, occupancy) sits next to data that
   can go stale independently of overall connectivity. */
.stale-marker { color: var(--c-text-muted); font-size: var(--fs-xs); }
.stale-marker.is-stale { color: var(--c-warn); font-weight: var(--fw-medium); }

/* Deliverable 3c — PreCommitClassLabel. States the offline-write class before the
   person commits, built off the three classes the Founder App design doc already
   restates verbatim (NEVER QUEUED / ALWAYS QUEUED / DECLARED PER TYPE) — CLAUDE.md's
   own citation for this taxonomy ("3A amendment 3") points nowhere today; promoting
   the restatement there is a separate, still-pending Gate-C diff, not blocking this
   label. A NEVER QUEUED control pairs this label with the shared .btn:disabled state
   below (Deliverable 5) — visibly disabled with the reason shown, never failing
   silently after a tap the OS can't honour offline. */
.precommit-label { display: inline-flex; align-items: center; gap: var(--s-1); font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--s-1); }
.precommit-label.never-queued { color: var(--c-warn); }

/* Deliverable 3d — ReconnectDigest. "While you were offline" — built fresh; nothing
   like this existed anywhere in code despite one design doc's mention of it
   (confirmed directly, PHASE1_DESIGN_LANGUAGE_REFRESH_SPEC.md). Resolved/moot items
   render first, muted and dashed, so the eye lands on what changed while the person
   was away before what still needs a decision (the still-actionable group after uses
   plain .rec-card with its normal severity tints, no extra class needed). */
.reconnect-digest { margin-bottom: var(--s-4); }
.reconnect-digest .digest-heading { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--c-text-muted); margin: 0 0 var(--s-2); text-transform: uppercase; letter-spacing: .02em; }
.reconnect-digest .resolved-group { opacity: .75; }
.reconnect-digest .resolved-group .rec-card { border-style: dashed; }

/* Deliverable 4 — Thread/Ack component. Deliberately scoped to what's real today:
   DriverMessage's two actual timestamps (backend/comms/models.py — "sent" is
   implicit in the record existing; acknowledged_at; read_at). IncidentThread's
   fuller sent/seen/being-handled/resolved/timed-out-escalated state machine is
   documented as "canonical" in the OS Design Document but has zero backend
   implementation (confirmed directly) — building that is real functionality, not
   visual polish, so it stays out of this presentation-only pass. .escalated is
   styled here but deliberately unused until IncidentThread's backend actually
   exists — reserved, not wired to anything real yet. Uses --c-safety (not
   --c-danger): an escalated incident thread is genuinely safety-adjacent. */
.ack-state { display: inline-flex; align-items: center; gap: var(--s-1); font-size: var(--fs-xs); color: var(--c-text-muted); }
.ack-state .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--c-text-muted); }
.ack-state.acknowledged .dot { background: var(--c-accent); }
.ack-state.read .dot { background: var(--c-ok); }
.ack-state.escalated { color: var(--c-safety); }
.ack-state.escalated .dot { background: var(--c-safety); }

/* Deliverable 5 — Interaction states. :focus-visible already existed globally
   (top of this file). .btn had no :disabled or :active state at all before this,
   confirmed directly — despite disabled controls already being a real product need
   (NEVER QUEUED, above). Pressed feedback is a light, fast scale (tokens.css's new
   --dur-fast), not a new visual language; the reduced-motion override matches this
   file's existing pattern for .pulse rather than relying on the global !important
   block alone, so pressed state has an explicit, deliberate no-motion fallback. */
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: scale(.98); transition: transform var(--dur-fast) var(--ease-standard); }
@media (prefers-reduced-motion: reduce) { .btn:active:not(:disabled) { transform: none; } }

/* Loading — skeleton, Tier 3 (web/deep-dive) screens ONLY, never a Tier 1 primary
   mobile flow. If a Tier 1 flow seems to need one, that's a sign the offline-cache
   assumption is broken — fix that, don't paper over it with a spinner (this rule
   provides the visual; which tier uses it is an app-wiring discipline, not something
   CSS alone can enforce). Explicit reduced-motion fallback to a static block, rather
   than relying only on the global animation-duration override, for a cleaner frozen
   appearance than a stalled gradient sweep. */
.skeleton {
  background: linear-gradient(90deg, var(--c-surface-2) 25%, var(--c-border) 37%, var(--c-surface-2) 63%);
  background-size: 400% 100%; border-radius: var(--r-sm);
  animation: mooev-skeleton 1.4s ease infinite;
}
@keyframes mooev-skeleton { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; background: var(--c-surface-2); } }

/* Deliverable 6 — SearchFilterBar. Standalone: its three target surfaces (Founder,
   Energy network/station views, Operations/Energy asset registers) include
   asset-register screens that don't exist in code yet on either candidate app
   (confirmed directly) — wiring happens once those screens are built. Shape mirrors
   Energy's existing .msearch/.station pattern and Driver's .gsearch, generalized
   into one component; those per-app rules are candidates to consolidate onto this
   during future app-wiring, not touched in this standalone pass. */
.search-filter-bar { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; margin-bottom: var(--s-3); }
.search-filter-bar .sf-input {
  flex: 1 1 200px; min-width: 160px; min-height: var(--touch-min);
  border: 1px solid var(--c-border); border-radius: var(--r-pill); padding: 0 var(--s-4);
  font-size: var(--fs-md); background: var(--c-surface); color: var(--c-text);
}
.search-filter-bar .sf-chips { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.search-filter-bar .sf-chip {
  border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text);
  border-radius: var(--r-pill); min-height: var(--touch-min); padding: 0 var(--s-3);
  font-size: var(--fs-sm); cursor: pointer;
}
.search-filter-bar .sf-chip[aria-pressed="true"] { background: var(--c-brand); color: var(--c-on-brand); border-color: var(--c-brand); }

/* .pill — MOVED here from apps/web/operations/operations.css (03 Sep 2026, Fable
   remediation of `8caa1cb`, Step 4) — every app's own index.html already loads this
   stylesheet before its own, so this is a safe move, not a duplication: the base rule +
   ontime/risk/charging/warn variants were operations-only in name but were already the
   shared status-pill vocabulary the Founder App's new emergency ack fan-out view
   (founder.js's renderAckFanOut/pushPill/driverPill, `apps/web/founder/founder.js`) needed
   too. operations.css's own `.sup-row .pill` descendant-selector override is untouched and
   still applies — CSS cascade doesn't care which file `.pill` itself is declared in. */
.pill { font-size: var(--fs-xs); padding: 2px 8px; border-radius: var(--r-pill); font-weight: var(--fw-medium); }
.pill.ontime { background: var(--c-surface-2); color: var(--c-ok); }
.pill.risk { background: var(--c-surface-2); color: var(--c-danger); }
.pill.charging { background: var(--c-surface-2); color: var(--c-accent); }
.pill.warn { background: var(--c-surface-2); color: var(--c-warn); }

/* Motion — semantic only (progress / forecast / expected action). The pulse marks
   "what's expected next". prefers-reduced-motion neutralises all motion; the static
   state already conveys the meaning (accessibility by design). */
@keyframes mooev-pulse { 0%,100% { opacity:1; } 50% { opacity:.45; } }
.pulse { animation: mooev-pulse 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
@media (prefers-reduced-motion: reduce) {
  .pulse { animation: none; }
  *, *::before, *::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; }
}

/* ===========================================================================
   D-HOME-IA-SMARTPHONE (2 Sep 2026) — the "hero + <=3 sections + secondary screens"
   home grammar (DESIGN_LANGUAGE.md Sec 2), shared by every app that adopts
   apps/web/screens.js. Three new pieces: .appnav (screen nav), .promptbar (the
   always-on-screen prompt bar), .hero-number (the one-per-role headline figure).
   =========================================================================== */

/* THE BOUNDED-FRAME CONTRACT — read this before wiring .promptbar into an app.
   Each app keeps its OWN top-level frame class in its OWN CSS file (.en energy.css, .ops
   operations.css, .fr founder.css, .driver driver.css) — that class is not defined here.
   For .promptbar to be genuinely always-visible (not just position:sticky on an unbounded
   page — see the corrected comment above this block for why that fails), the app's own
   frame class must itself provide exactly this shape:
     .<frame> { display: grid; height: 100dvh; grid-template-rows: auto 1fr auto; }
   — a fixed top row (the app's own topbar), ONE scrolling middle row (that app's own
   `.board`/`.main`, given `overflow-y: auto; min-height: 0` so the grid track can actually
   shrink below its content's height — grid tracks default to `auto`, which floors at
   content size and defeats the whole point), and a fixed bottom row holding `.promptbar`.
   `min-height: 100vh` (the pre-existing per-app default) must become `height: 100dvh` — a
   min-height frame lets its own content push the page taller than the viewport, which is
   exactly the "document itself scrolls" state that broke .bottomnav. */

.promptbar {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  padding-bottom: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  background: var(--c-surface); border-top: 1px solid var(--c-border);
  /* transform (not position) is what apps/web/screens.js's keyboard-avoidance code
     animates — position:sticky/fixed is deliberately NOT used here; this element's
     always-visible guarantee comes from being a fixed grid row in a height:100dvh frame
     (the contract above), never from its own positioning. */
  transition: transform var(--dur-fast) var(--ease-standard);
}
.promptbar input, .promptbar textarea {
  flex: 1; min-height: var(--touch-min); border: 1px solid var(--c-border);
  border-radius: var(--r-pill); padding: 0 var(--s-4); font-size: var(--fs-md);
  background: var(--c-surface-2); color: var(--c-text);
}
.promptbar button {
  min-height: var(--touch-min); min-width: var(--touch-min); border: 0; border-radius: var(--r-pill);
  background: var(--c-brand); color: var(--c-on-brand); cursor: pointer;
}

/* Screen nav — generalizes Founder App's own .fnav (apps/web/founder/founder.css) into a
   shared component: a horizontal, wrapping chip row on phone; a left sidebar at >=900px.
   Buttons carry data-s="<screen name>" (apps/web/screens.js wires the click + aria-current
   contract); aria-current reads the same "true"/"false" STRING values .nav-item/.bottomnav/
   .tabs above already use. */
.appnav {
  background: var(--c-surface); border-bottom: 1px solid var(--c-border);
  padding: var(--s-2); display: flex; gap: var(--s-1); flex-wrap: wrap;
}
.appnav button {
  display: flex; align-items: center; gap: var(--s-2); min-height: var(--touch-min);
  border: 0; background: transparent; color: var(--c-text); border-radius: var(--r-md);
  padding: 0 var(--s-3); font-size: var(--fs-sm); cursor: pointer; text-align: left;
}
/* D-ENERGY-FLOWLAYOUT: mobile tab bar (4-column icon-top/label-below) — SCOPED to
   [data-app="energy"], not bare .appnav, because .appnav is this shared, cross-app
   component (Founder uses the identical class, still with its original flex-wrap row of
   emoji-then-label buttons). First written unscoped, which silently reskinned Founder's
   mobile nav too — caught by test_visual_regression.py flagging founder/explore.html,
   simulator.html and assets.html (pages whose HTML this pass never touched) as changed,
   and by test_founder_emergency_domain.py's slide-to-confirm gesture test failing because
   the reflow shifted Founder's own #conn. */
[data-app="energy"] .appnav { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-1); }
[data-app="energy"] .appnav button {
  flex-direction: column; justify-content: center; gap: 4px; min-height: 56px;
  padding: 6px 2px; font-size: var(--fs-xs); text-align: center; color: var(--c-text-muted);
}
[data-app="energy"] .appnav .nav-icon { width: 21px; height: 21px; }
@media (min-width: 900px) {
  .appnav {
    flex-direction: column; flex-wrap: nowrap;
    border-bottom: 0; border-right: 1px solid var(--c-border);
  }
  /* Energy's sidebar must look exactly like Founder's (and like Energy's own, before this
     pass) — undo the mobile-only tab-bar shape above rather than let it leak upward.
     `display: flex` has to be restated explicitly, not just flex-direction on the shared
     rule above: [data-app="energy"] .appnav's own specificity (0,2,0) otherwise keeps
     beating the shared .appnav rule's (0,1,0) regardless of this media query, so `display:
     grid` (mobile) silently won even here — caught by a real end-to-end test click failing
     ("#heroCard intercepts pointer events"), not by reading the CSS: with 5 real children
     wrapping into a 4-column, 2-row grid whose rows then stretched to fill the whole
     665px-tall sidebar, one nav button rendered ~327px tall and its center point landed on
     top of the home screen's hero card underneath it. */
  [data-app="energy"] .appnav { display: flex; }
  [data-app="energy"] .appnav button {
    flex-direction: row; justify-content: flex-start; gap: var(--s-2); min-height: var(--touch-min);
    padding: 0 var(--s-3); font-size: var(--fs-sm); text-align: left; color: var(--c-text);
  }
  [data-app="energy"] .appnav .nav-icon { width: 24px; height: 24px; }
  /* D-NAVRAIL-COLLAPSE (04 Sep 2026) — scoped to this same >=900px query, matching `.side`'s
     own >=800px scoping above: below 900px `.appnav` is a horizontal chip row, where
     "collapsed" has no meaning, so these rules must never take visual effect there even if
     a stale `collapsed` class were present after a resize. */
  .appnav.collapsed { padding-left: 0; padding-right: 0; }
  .appnav.collapsed .nav-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .appnav.collapsed button { justify-content: center; padding: 0; }
}
.appnav button[aria-current="true"] { background: var(--c-surface-2); color: var(--c-brand); font-weight: var(--fw-medium); }
/* .appnav button (above) sets display:flex at specificity (0,1,1), which beats a bare
   .tier3-web-only's (0,1,0) — the exact collision founder.css's own D-FOUNDER-TIER-GATING
   comment documents for .fnav button. Mandatory override, not optional; verified against
   the rule above before writing this, per that file's own "don't copy this without
   checking for the same collision first" instruction. */
.appnav button.tier3-web-only { display: none; }
@media (min-width: 1200px) { .appnav button.tier3-web-only { display: flex; } }

/* Hero number — the one-per-role headline figure (DESIGN_LANGUAGE.md Sec 2/3): Energy's
   "chargers available now", Operations' "N items need you", Founder's Total Network
   Revenue. Deliberately its own step above --fs-xl (28px) — the type scale had no hero
   size before this pass. Verify in all 4 theme axes before shipping a new hero (this exact
   brand-on-surface-2 pairing produced 54 contrast failures on 23 Jul 2026 — see
   test_color_contrast.py). */
.hero-number { font-size: var(--fs-hero); font-weight: var(--fw-medium); color: var(--c-brand); line-height: 1.1; }
.hero-number .label { font-size: var(--fs-sm); font-weight: var(--fw-regular); color: var(--c-text-muted); }
