/* A metric-matched stand-in for Inter while Inter is still downloading. Without
   it the swap reflows every line of the page (measured +19px on the workflows
   grid); with it the fallback occupies exactly the same space, so the real face
   arrives invisibly. */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.12%;
  ascent-override: 90.20%;
  descent-override: 22.48%;
  line-gap-override: 0%;
}

@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
/* ===========================================================================
   OMNI ECHO — MASTER DESIGN SYSTEM  (single, portable, shareable stylesheet)
   ---------------------------------------------------------------------------
   One file = the whole look: design tokens, light/dark themes, page layout, the
   card/box system, the elevation (drop-shadow) system, every component, and an
   optional animated background. Drop it into any project for the same UI.

   ── QUICK START ────────────────────────────────────────────────────────────
     <link rel="stylesheet" href="omni-echo.css">
     <html data-theme="dark">      <!-- or "light" -->
   Fonts load themselves (Google Fonts @import on line 1). No build step.

   ── THEMING ────────────────────────────────────────────────────────────────
   Everything is driven by CSS custom properties. Switch the entire palette with
   <html data-theme="light|dark">. To re-brand, override only the TOKENS you
   need (never the component rules), e.g.:  :root { --accent: #2563eb; }

   ── LAYOUT & BOXES ──────────────────────────────────────────────────────────
     .container ......... 1120px max-width page gutter
     .tool-list ......... the INDEX pattern: .tool-group (uppercase .tool-group-label)
                          over a .tool-rows.panel of .tool-row links — icon, name +
                          badge, one-line summary, chevron. Use this for "here are all
                          the things you can open"; a wall of equal boxes is not an index
     .product-wordmark .. the product lockup ("OMNI AURA") — uppercase, product half
                          shiny in --product-metal. ONE per app, on the home page;
                          white-label rewrites it from /api/branding
     .pillar ............ lighter feature box (used in the .pillars grid)
     .step / .glass-card / .panel ... generic frosted content panels
     .section-band ...... full-width lower-section wrapper
     .app-header / .app-footer ...... top bar / bottom bar

   ── ELEVATION (DROP-SHADOW) SYSTEM ──────────────────────────────────────────
   Two layered shadow tokens carry the frosted-glass depth (defined per theme):
     --shadow-glass ... resting: inset top highlight + soft ambient + big soft cast
     --shadow-hover ... lifted  (apply on :hover, usually with translateY(-4px))
   Use them as  box-shadow: var(--shadow-glass);  Cards also get a 1px metallic top
   "sheen" (::before, var(--sheen)); buttons carry their own inset highlight + glow.

   ── LOGO ────────────────────────────────────────────────────────────────────
   .logo-mark paints a brand image. Point it at your own asset by overriding:
     :root { --logo-dark:  url('/path/light-on-dark.png');    (shown on dark bg)
             --logo-light: url('/path/dark-on-light.png'); }   (shown on light bg)

   ── OPTIONAL ANIMATED BACKGROUND ────────────────────────────────────────────
   The .bg-fx / .fx-* rules style a comet-rides-a-trend-line background that the
   companion omni-echo.js injects. Without that JS they're inert (nothing renders);
   the static depth glow + blueprint grid still show via body::before / ::after.
=========================================================================== */

/* ---------------------------------------------------- tokens · light (clean monochrome) */
:root {
  --bg-base: #f5f5f6;
  --surface: rgba(255, 255, 255, 0.85);
  --surface-strong: rgba(255, 255, 255, 0.96);
  --surface-input: #ffffff;
  /* OPAQUE surface for the few panels that must not let the background through.
     Every other panel is deliberately translucent — that is the glass look — but a
     surface you READ a list off needs its own contrast, not the watermark's. */
  --surface-solid: #ffffff;
  --tile: #f0f0f2;
  --border: rgba(18, 19, 23, 0.10);
  --border-strong: rgba(18, 19, 23, 0.20);
  --shadow-glass: 0 1px 0 rgba(255, 255, 255, 0.8) inset, 0 2px 8px rgba(18, 19, 23, 0.05), 0 16px 40px rgba(18, 19, 23, 0.10);
  --shadow-hover: 0 1px 0 rgba(255, 255, 255, 0.9) inset, 0 10px 22px rgba(18, 19, 23, 0.10), 0 28px 64px rgba(18, 19, 23, 0.18);

  /* secondary · silver (the brand mark is metallic) — for depth, sheen + hover highlight */
  --silver: #868c96;
  --silver-strong: #51565f;
  --silver-soft: rgba(120, 128, 140, 0.12);
  --sheen: linear-gradient(90deg, transparent, rgba(18, 19, 23, 0.18) 40%, rgba(18, 19, 23, 0.06) 60%, transparent);
  --metal: linear-gradient(180deg, #5b606a 0%, #9aa0ab 100%);

  --glow-1: rgba(234, 98, 16, 0.10);
  --glow-2: rgba(120, 90, 250, 0.07);
  --grid-line: rgba(51, 65, 85, 0.10);   /* cool slate blueprint grid, visible on the light bg */

  --text: #14151a;
  --text-dim: #3d3f47;
  --text-muted: #74767f;

  --accent: #e0620c;
  --accent-strong: #c2410c;
  --accent-soft: rgba(234, 98, 16, 0.13);
  --accent-text: #ffffff;

  --action: var(--accent);      /* primary button fill — tied to the orange accent */
  --action-text: #ffffff;

  --ok: #15803d;   --ok-bg: rgba(22, 163, 74, 0.12);
  --warn: #b45309; --warn-bg: rgba(234, 179, 8, 0.16);
  --err: #b91c1c;  --err-bg: rgba(220, 38, 38, 0.10);
  --info-bg: rgba(234, 98, 16, 0.06);

  --radius-sm: 9px;
  --radius: 16px;
  --radius-lg: 22px;
  --radius-pill: 999px;
  --blur-strong: 26px;

  --space-1: 4px;  --space-2: 8px;  --space-3: 12px; --space-4: 16px;
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; --space-12: 48px;
  /* 7 and 9 were MISSING while .hero referenced both (2026-08-05). An undefined var
     with no fallback invalidates the whole declaration at computed-value time, so the
     hero's `padding: var(--space-9) var(--space-6) var(--space-7)` resolved to NO
     padding at all — which is why "Workflows" sat flush against the top bar on every
     hero page. The gap in the scale, not the rule, was the bug. */
  --space-7: 28px; --space-9: 36px;

  --font-sans: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-display: 'Space Grotesk', 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, Consolas, 'SF Mono', monospace;

  /* brand logo images for .logo-mark — override with your own lockup (theme-aware) */
  --logo-dark: url('/assets/omni-echo-dark.png');         /* shown on dark backgrounds */
  --logo-light: url('/assets/omni-echo-light-gold.png');  /* shown on light backgrounds */
}

/* ---------------------------------------------------- tokens · dark (brand-true) */
[data-theme="dark"] {
  --bg-base: #0b0b0e;
  /* Visibility bump (user 2026-07-29: "make the dark mode better so everything is
     actually visible") — surfaces/borders/inputs lifted so cards and fields read
     clearly against the near-black base. */
  --surface: rgba(255, 255, 255, 0.08);
  --surface-strong: rgba(255, 255, 255, 0.13);
  --surface-input: rgba(255, 255, 255, 0.08);
  /* Solid, and a touch LIGHTER than --bg-base so the card still reads as a card —
     "darker" here means denser/opaque, which is what stops the mark bleeding through. */
  --surface-solid: #15141a;
  --tile: rgba(255, 255, 255, 0.065);
  --border: rgba(255, 255, 255, 0.14);
  --border-strong: rgba(255, 255, 255, 0.28);
  --shadow-glass: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 2px 8px rgba(0, 0, 0, 0.40), 0 22px 50px rgba(0, 0, 0, 0.50);
  --shadow-hover: 0 1px 0 rgba(255, 255, 255, 0.12) inset, 0 12px 26px rgba(0, 0, 0, 0.45), 0 32px 72px rgba(0, 0, 0, 0.62);

  /* secondary · silver (matches the metallic brand mark) */
  --silver: #c4c8d2;
  --silver-strong: #eef0f4;
  --silver-soft: rgba(196, 200, 210, 0.16);
  --sheen: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.22) 40%, rgba(255, 255, 255, 0.06) 60%, transparent);
  --metal: linear-gradient(180deg, #f3f4f7 0%, #c2c6d0 52%, #a2a8b4 100%);

  --glow-1: rgba(255, 150, 70, 0.16);
  --glow-2: rgba(140, 110, 255, 0.10);
  --grid-line: rgba(255, 255, 255, 0.038);

  --text: #f3f3f6;
  --text-dim: #cfd0d8;
  --text-muted: #9a9ca6;

  --accent: #ff9d57;
  --accent-strong: #ffb780;
  --accent-soft: rgba(255, 157, 87, 0.18);
  --accent-text: #1a0d02;

  --action: var(--accent);      /* primary button fill — tied to the orange accent */
  --action-text: var(--accent-text);

  --ok: #4ade80;   --ok-bg: rgba(22, 163, 74, 0.18);
  --warn: #fbbf24; --warn-bg: rgba(234, 179, 8, 0.16);
  --err: #fca5a5;  --err-bg: rgba(220, 38, 38, 0.16);
  --info-bg: rgba(255, 157, 87, 0.09);
}

/* ---------------------------------------------------- base */
* { box-sizing: border-box; }
*::selection { background: var(--accent-soft); color: var(--text); }

/* html gets a fixed height; body must NOT — height:100% pinned body's box to one
   viewport, so every position:sticky child (top bar, rails) stopped sticking the
   moment you scrolled past the first screen (2026-07-29). min-height keeps full-
   bleed backgrounds without capping the box. */
html { height: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--text);
  background: var(--bg-base);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100vh;
  /* Sticky footer: a flex column lets .app-footer take margin-top:auto and sink to the
     bottom on a SHORT page instead of floating right under the content with blank space
     below it (user 2026-08-05: "the footer floats in the middle of the screen"). The
     fixed layers (bg-fx, watermark, sidebar, env banner) are position:fixed, so they sit
     outside this flow and are unaffected. */
  display: flex;
  flex-direction: column;
  position: relative;
  /* clip, not hidden: overflow-x hidden makes body a scroll container, which
     silently KILLS every position:sticky descendant (the vanishing top bar /
     runaway SONAR sidebar bugs, 2026-07-29). clip clips without scroll-container
     semantics, so sticky headers actually stick. hidden stays as the fallback. */
  overflow-x: hidden;
  overflow-x: clip;
  letter-spacing: -0.005em;
}

/* Cool depth glows — low opacity, slow ambient drift + gentle scroll parallax.
   echoDrift owns `transform`; scroll owns `background-position` (independent
   properties, so they compose without fighting). The deepest layer → moves least. */
body::before {
  content: '';
  position: fixed; inset: -15%;
  z-index: -2; pointer-events: none;
  /* NO runtime filter here — EVER (2026-08-04 lag autopsy; CI-pinned). This layer is
     130% of the viewport and it DRIFTS: a 30px runtime blur plus the animation meant
     the GPU re-blurred the entire screen every frame on every page of every build
     (fx-off never touched it — that only hides .bg-fx). The softness now lives in
     the gradients themselves (wider falloff stops), so the drift is pure compositor
     work. Same look, ~zero standing cost. */
  background:
    radial-gradient(52% 46% at 12% -5%, var(--glow-1) 0%, transparent 74%),
    radial-gradient(50% 42% at 92% 4%, var(--glow-2) 0%, transparent 72%),
    radial-gradient(44% 38% at 72% 112%, var(--glow-1) 0%, transparent 72%);
  animation: echoDrift 34s ease-in-out infinite alternate;
  will-change: transform;
}
/* Reduced effects = a genuinely still page: the glow stops drifting too. */
html.fx-off body::before { animation: none; }
/* Faint blueprint grid (the brand's circuit-line vibe), fading toward the bottom.
   Static (fixed) — no scroll-linked repaint, so scrolling stays smooth. */
body::after {
  content: '';
  position: fixed; inset: 0;
  z-index: -1; pointer-events: none;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(120% 80% at 50% -10%, #000 0%, transparent 72%);
  mask-image: radial-gradient(120% 80% at 50% -10%, #000 0%, transparent 72%);
}
/* Translate ONLY (no scale) — a scale on a blur(30px) layer forces the GPU to
   re-rasterize the whole blurred surface every frame; a pure translate just moves
   the already-rasterized layer (compositor-only, near-free). The inset:-15% gives
   slack so the drift never exposes an edge. */
@keyframes echoDrift {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(-2%, 1.4%, 0); }
}
@media (prefers-reduced-motion: reduce) { body::before { animation: none; } }

/* Faded brand mark in the deep background (user 2026-08-05: "the dark omni logo faded
   into the background, behind all the animation and glow"). z-index -3 puts it BELOW the
   glow (body::before, -2) and the grid/comet (-1), so it sits still while everything
   drifts above it. Injected as .bg-watermark by omni-echo.js, so every dashboard page
   gets it with no per-page markup ("give it to everyone"). STATIC — no animation, no
   runtime filter — so it costs nothing per frame (the 2026-08-04 PERF LAW). Branding
   swaps the mark via --bg-watermark; a white-label face sets its own logo there. */
.bg-watermark {
  position: fixed; inset: 0; z-index: -3; pointer-events: none;
  background-image: var(--bg-watermark, url("/assets/omni-watermark.svg"));
  background-repeat: no-repeat;
  background-position: center 45%;
  background-size: min(58vmin, 500px);
  opacity: .05;                 /* barely-there on the light theme */
}
/* The layer is position:fixed and therefore centred on the WINDOW, but the window is not
   what anyone is looking at — the 228px rail covers its left edge, so a mathematically
   centred mark reads as sitting too far left ("even though its centered it doesnt look
   like its centered because of the menues", user 2026-08-05). Offset by half the rail so
   it is centred in the CONTENT, which is the thing the eye measures against. Scoped to
   the width where the rail actually exists; on a phone the drawer is off-canvas and true
   centre is correct. */
@media (min-width: 1101px) {
  .bg-watermark { background-position: calc(50% + 114px) 45%; }
  body.no-echo-rail .bg-watermark, body.login-page .bg-watermark { background-position: center 45%; }
}
[data-theme="dark"] .bg-watermark { opacity: .07; }   /* a touch more to read on near-black */
html.fx-off .bg-watermark { opacity: .04; }           /* calmer in reduced-effects mode, but kept — it never animates */

/* Background FX (DOM injected by omni-echo.js): gossamer stock trend lines with
   a comet of light gliding ALONG each one. The light is a glowing *segment of the
   path itself* (layered for a fading tail + white-hot tip), so it hugs every bend
   fluidly and never drifts off the line. Smooth, steep, rising paths; faint
   sparkles twinkle along them. Staggered + wall-clock-synced so they keep flowing
   across page navigations. Behind the grid + all content, never interactive. */
.bg-fx {
  position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden;
  /* Own compositor layer: the comet animates here without repainting page content,
     so scrolling stays smooth even while the tail keeps moving. */
  transform: translateZ(0);
}
.bg-fx-svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* Confine the comet's per-frame repaint to this layer so it never invalidates page
     content, and hint a dedicated compositor layer. */
  contain: layout paint style; will-change: transform;
  /* Fade BOTH ends (bottom-left AND top-right). The comet's per-loop opacity
     fade-out used to happen in the unmasked top-right and read as a blink; fading
     the top-right too means the fade-out (and the loop teleport) now happen inside
     a hidden region, so the comet smoothly recedes into the corner instead of
     flashing. The bottom-left fade (already present) hides the fade-in / re-entry. */
  -webkit-mask-image: linear-gradient(60deg, transparent 0%, #000 30%, #000 72%, transparent 100%);
  mask-image: linear-gradient(60deg, transparent 0%, #000 30%, #000 72%, transparent 100%);
}

/* the original angular stock-ticker trend lines (accent + fainter silver) */
.fx-line {
  fill: none; stroke: var(--accent); stroke-width: 1.6; opacity: .22;
  vector-effect: non-scaling-stroke;
}
.fx-line-2 { stroke: var(--silver); opacity: .12; filter: none; }
[data-theme="light"] .fx-line { opacity: .15; }
[data-theme="light"] .fx-line-2 { opacity: .10; }

/* sparkles that twinkle along the line */
.fx-sparkle {
  /* No drop-shadow: on an element inside the per-frame-repainting FX layer, the
     filter re-rasters every frame. The bright fill + scale-in twinkle reads as a
     sparkle without it. */
  fill: #fff; opacity: 0; transform-box: fill-box; transform-origin: center;
  animation: fxTwinkle var(--tdur, 3s) ease-in-out infinite;
}
.fx-sparkle.alt { fill: var(--silver); }
[data-theme="light"] .fx-sparkle { fill: var(--accent-strong); }
[data-theme="light"] .fx-sparkle.alt { fill: var(--silver-strong); }
@keyframes fxTwinkle {
  0%, 100% { opacity: 0; transform: scale(.35); }
  50%      { opacity: var(--tw, .7); transform: scale(1); }
}

/* the comet — many overlapping path segments stacked into a real comet tail:
   the longer/fainter layers carry a per-layer blur (var --blur) so their edges
   dissolve into the dark (misty, semi-transparent, soft-edged), while the short
   bright head is sharp with a soft coma halo. */
.fx-comet {
  fill: none; stroke: var(--accent); stroke-linecap: round; stroke-linejoin: round;
  /* CONSTANT opacity (no temporal fade). The old per-loop fade-to-zero WAS the
     flash; now the only brightness change comes from the mask (spatial, tied to
     position) as the comet enters bottom-left and recedes off the top-right, and
     the loop wrap happens off-screen (line ends run past x=1200). Nothing pulses. */
  opacity: var(--lo, .6);
  animation: fxComet var(--dur, 8s) linear infinite;
}
/* No blur on the moving tail layers — blurring a *moving* stroke forces a GPU
   raster pass every frame (the cause of the lag AND the old flashing). The soft
   gradient falloff instead comes from many overlapping low-opacity strokes built
   in omni-echo.js, which composite almost for free. */
.fx-comet.alt { stroke: var(--silver); }
/* White-hot coma: the front group of layers (.tip) render white, so the head
   reads as a larger bright front that melts smoothly into the colored tail.
   Stroke colour only — no filter — so it stays cheap. */
.fx-comet.tip { stroke: #fff; }
.fx-comet.alt.tip { stroke: #fff; }
[data-theme="light"] .fx-comet.tip { stroke: var(--accent-strong); }
[data-theme="light"] .fx-comet.alt.tip { stroke: var(--silver-strong); }
/* NO filter on the head. It looks like a "tiny element" (only the dashed head shows),
   but its geometry `d` is the ENTIRE path, so a drop-shadow's raster region is the
   whole path bounding box (~viewport) — re-blurred every frame, ×2 comets. That was
   the measured 33ms frame-spike / dropped-frame jank. The white-hot look now comes
   entirely from the stacked `.tip` layers (free to composite). To bring a glow back,
   use a small static <circle> tracking the head via offset-path, not a filter here.
   (The head's extra width — the coma — is set inline in omni-echo.js.) */
@keyframes fxComet {
  from { stroke-dashoffset: var(--s); }   /* head at the line start (off-screen) */
  to   { stroke-dashoffset: var(--e); }   /* head at the line end (off-screen) — wrap is unseen */
}

@media (prefers-reduced-motion: reduce) {
  .fx-comet, .fx-sparkle { animation: none; opacity: 0; }
  .fx-line { opacity: .14; }
}
/* (2026-08-03) The measured-degrade guard and the Effects switch both drive the ONE
   existing mechanism — html.fx-off (below) — so there is no second motion system. */

/* Suspend ALL background motion when this tab is hidden or its window is in the
   background (toggled by omni-echo.js via the Page Visibility + focus APIs).
   Paused animations stop compositing, so a backgrounded localhost tab no longer
   competes for the GPU with whatever you've switched to. Zero visual change while
   you're actually looking at the tab. */
html.fx-suspend .fx-comet,
html.fx-suspend .fx-sparkle { animation-play-state: paused; }
html.fx-suspend body::before { animation-play-state: paused; }

/* Background animation turned OFF from the header toggle — drop the comet layer
   entirely and freeze the depth glow (it still shows, just static). The grid and
   static glass styling are unaffected, so the page keeps its look at ~zero
   continuous GPU cost. */
html.fx-off .bg-fx { display: none; }
html.fx-off body::before { animation: none; }

/* ---------------------------------------------------- environment banner
   Slim striped ribbon pinned to the very top on staging / development deploys
   (injected by omni-echo.js from /api/env). Amber hazard stripes so it reads as
   "not live" at a glance; the page is nudged down so it never covers content. */
.env-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center; gap: 12px;
  height: 30px; padding: 0 16px; overflow: hidden; white-space: nowrap;
  font-size: 12px; font-weight: 600; color: #2a1a02;
  background: repeating-linear-gradient(45deg, #f7c948 0, #f7c948 16px, #e3a92e 16px, #e3a92e 32px);
  border-bottom: 1px solid rgba(0, 0, 0, 0.28);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
}
.env-banner strong { letter-spacing: 0.14em; text-transform: uppercase; font-weight: 800; }
.env-banner span { font-weight: 600; opacity: 0.78; overflow: hidden; text-overflow: ellipsis; }
/* Tier tones (2026-07-20 banner law): beta/staging keep the amber hazard stripes
   above; PRODUCTION is a calm solid green (matches the env-badge color law) so the
   two banners can't be confused; LIVE renders no banner at all. */
.env-banner.production {
  background: linear-gradient(90deg, #1a5c44, #23815e);
  color: #eafff5;
}
html.has-env-banner body { padding-top: 30px; }
/* Keep the fixed sidebar clear of the env banner (BETA stripe overlaps top:0). */
html.has-env-banner .echo-sidebar { top: 30px; }
/* The ribbon is position:fixed, so the sticky bar must stop BELOW it — pinned at 0 it
   slid under the ribbon and lost its top 30px the moment you scrolled (same family as
   the "clipping into the top menu" report, 2026-08-05). */
html.has-env-banner body.has-echo-sidebar .app-header { top: 30px; }

/* ------------------------------------------------- runner session alert
   The unattended-run warning, injected at the top of .container on every page by
   omni-echo.js. IN FLOW, not fixed: the env ribbon above already owns top:0 and the
   offsets tuned around it. Amber = a scheduled run will fail soon; red = it is
   already blocked. Colours are self-contained (not --accent) so a white-label build
   cannot skin an alarm into its brand colour and lose the signal. */
.session-alert {
  display: flex; align-items: flex-start; gap: 12px;
  margin: 0 0 var(--space-6); padding: 14px 16px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(227, 169, 46, 0.55);
  background: rgba(227, 169, 46, 0.12);
  color: var(--text);
}
.session-alert.is-error {
  border-color: rgba(214, 69, 69, 0.6);
  background: rgba(214, 69, 69, 0.13);
}
/* On .hero pages (Home, Workflows) there is no .container to sit inside — match the
   hero's measure and inline padding so the alert lines up with the title beneath it. */
.session-alert.is-loose {
  width: calc(100% - 2 * var(--space-6));
  max-width: 1552px;                    /* the hero's 1600 measure minus its 24px inline padding */
  margin: var(--space-6) auto 0;
}
.session-alert-icon { font-size: 16px; line-height: 1.5; flex: none; }
.session-alert-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.session-alert-body strong { font-size: 13px; letter-spacing: 0.02em; }
.session-alert-body span { font-size: 13px; opacity: 0.88; }
.session-alert-due { font-family: var(--font-mono); font-size: 12px !important; opacity: 0.7 !important; }
.session-alert-x {
  flex: none; background: none; border: 0; cursor: pointer;
  color: inherit; opacity: 0.5; font-size: 20px; line-height: 1;
  padding: 0 2px; border-radius: var(--radius-sm);
}
.session-alert-x:hover { opacity: 1; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--surface-strong);
  border: 1px solid var(--border);
  padding: 1px 6px;
  border-radius: 6px;
}

/* ---------------------------------------------------- layout shell */
/* ONE content measure, LEFT-ALIGNED after the rail (user 2026-08-05: "workflows should
   be left aligned so its near the left menu with the description", and "the analytics
   and description should be left aligned to the mark as reviewed box ... so the titles
   are in the same spot").

   Before this there were three different measures — .container 1120, .hero 1600,
   .tool-list 1600 — each independently centred in the space beside the sidebar. Centring
   different widths in the same column means none of their left edges line up, so a page
   title started somewhere inside the boxes underneath it and the whole page read as
   drifting. Sharing one measure and anchoring it left makes every title, every
   description and every panel share a single left edge, on every page.

   Still capped: on an ultrawide, a full-bleed row puts a workflow's name and its chevron
   a foot apart. `margin: 0 auto 0 0` is the left-anchor — auto on the RIGHT only.

   WIDTH: 100% IS LOAD-BEARING. body is a flex column (the sticky footer), and an `auto`
   margin on a flex item's CROSS axis suppresses stretching — the item falls back to
   shrink-to-fit and the auto margin eats the rest. So the left-anchor alone left
   .tool-list at 687px of text width, while .container measured 1600px purely because its
   content happens to be wide. That is how this shipped looking half-right, and why it was
   MEASURED in a browser rather than reasoned about. `width: 100%` restores the fill;
   max-width still caps it and the auto right margin still anchors it left. */
:root { --content-max: 1600px; }
.container { width: 100%; max-width: var(--content-max); margin: 0 auto 0 0; padding: 0 var(--space-6) var(--space-12); }

/* ---------------------------------------------------- header bar */
/* Spans the full content width beside the sidebar. It used to carry the page
   container's 1120px cap, which on any wide screen left it floating as a band
   that stopped short of the right edge and read as cut off.
   2026-08-04: the bar names the PAGE (left) and carries the actions (right) — the
   product wordmark moved out entirely, because the rail already says it. The brand
   lockup is hidden HERE, at stylesheet time, not by the shell script: a 50px logo
   that paints and is then replaced by 25px of text shrinks the header after first
   paint, and every pixel of the page under it jumps. Shell-less pages (sign-in,
   onboarding) opt back into the centred lockup below. */
.app-header {
  /* 1fr auto 1fr — equal space either side of the middle column, which is the only way
     the centre column is actually CENTRED. With `1fr auto auto` the first column ate all
     the free space and shoved the lockup against the controls: "the 'omni aura' at the
     top is all the way on the top right instead of being in the middle" (user
     2026-08-05). minmax(0,1fr) on both sides so a long page name truncates instead of
     widening its column and pushing the centre off. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center;
  /* grid-auto-flow: column keeps the bar ONE ROW TALL. Every child that is explicitly
     placed sits in its column; anything NOT placed — .app-nav, which pages still carry
     and the shell empties — would otherwise be auto-placed into an implicit second ROW
     and make the bar visibly taller (measured 2026-08-05: 92px against the intended 70).
     It went unnoticed until the product lockup took column 2, because until then the
     unplaced nav happened to land in that free column on row 1. Flowing into implicit
     COLUMNS instead means an unplaced child can never add a row again, whatever gets
     added to this bar next. */
  grid-auto-flow: column;
  padding: var(--space-4) var(--space-6);
  max-width: none; margin: 0;
}
.app-header > .app-logo { display: none; }
/* Same reasoning, one element over (user 2026-08-05: "theres an analytics button that
   shows up for a second then it goes back to showing mayfair aura"). Pages still carry
   Home/Workflows/Analytics links in .app-nav, and the shell REMOVES them after load —
   so they painted, took a grid column, and vanished, shoving the lockup sideways on
   every refresh. Hidden at stylesheet time instead: nothing is drawn and then taken
   away. Matched by href so a page-specific action in the same nav is untouched, which
   is the whole reason the JS removal was selective rather than emptying the element.
   Being CSS, it applies to every build — Omni and every white label alike. */
.app-header .app-nav a[href="/"],
.app-header .app-nav a[href="/index.html"],
.app-header .app-nav a[href="/workflows.html"],
.app-header .app-nav a[href="/analytics.html"] { display: none; }
.app-header > .app-page-name { grid-column: 1; justify-self: start; }
/* The product lockup owns the MIDDLE of the bar (user 2026-08-05). It sits in the free
   centre column the grid already had, so page name left / product centre / controls
   right — and it carries no hero margins here, those belonged to the old masthead. */
.app-header > .product-wordmark { grid-column: 2; justify-self: center; margin: 0; }
.app-header > .app-header-actions { grid-column: 3; justify-self: end; }
/* Below 1100px the drawer handle takes the first column and the page name moves to the
   second — which is the lockup's column. Rather than juggle three things into a phone-
   width bar, the lockup yields: the page name labels the view and the drawer's own rail
   carries the mark, so nothing is lost. (Hiding it only under 760px left the two
   colliding in the 760-1100 band.) */
@media (max-width: 1100px) { .app-header > .product-wordmark { display: none; } }
body.login-page .app-header, body.no-echo-rail .app-header { grid-template-columns: 1fr auto 1fr; }
body.login-page .app-header > .app-logo,
body.no-echo-rail .app-header > .app-logo { display: inline-flex; grid-column: 2; justify-self: center; }
/* The page name — SONAR's top bar reads "Reports"; this reads whatever you opened.
   Kept shorter than the 38px action buttons so it can never set the bar's height,
   and clipped rather than wrapped for the same reason. */
.app-page-name {
  font-family: var(--font-display);
  font-size: 19px; font-weight: 700; letter-spacing: -0.015em; line-height: 1.3;
  color: var(--text); min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-logo { display: inline-flex; align-items: center; color: var(--text); text-decoration: none; }
.app-logo:hover { text-decoration: none; }
/* Real brand lockup (swaps per theme). The image already carries the wordmark,
   so the adjacent text span is hidden. Bare .logo-mark is styled so .brand (login)
   gets it too. */
.logo-mark {
  display: inline-block;
  height: 50px; width: auto; aspect-ratio: 822 / 239;
  border: none; border-radius: 0; box-shadow: none;
  background: var(--logo-dark) no-repeat left center / contain;
  transition: filter .12s ease, transform .12s ease;
}
[data-theme="light"] .logo-mark { background-image: var(--logo-light); }
.app-logo > span:not(.logo-mark), .brand > span:not(.logo-mark) { display: none; }
.app-logo:hover .logo-mark { filter: drop-shadow(0 0 12px var(--silver-soft)); transform: translateY(-0.5px); }

/* min-height reserves the bar's row from FIRST PAINT: the cog and the page name are
   both injected by the shell script, and without this the header would be padding-tall
   until they land, then grow — a shift on every page. 38px = .theme-toggle. */
.app-header-actions { display: flex; align-items: center; gap: var(--space-3); min-height: 38px; }

/* Theme toggle — sun / moon swap */
.theme-toggle {
  width: 38px; height: 38px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 11px; color: var(--text-dim); cursor: pointer;

  transition: background .1s ease, border-color .1s ease, color .1s ease, transform .12s ease;
}
.theme-toggle:hover { background: var(--surface-strong); color: var(--text); border-color: var(--border-strong); }
.theme-toggle:active { transform: scale(0.95); }
.theme-toggle svg { width: 16px; height: 16px; display: block; }
[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }

/* App sidebar — the read.ai shell for every Aura page (user 2026-07-29: "aura also
   has no top or side panels"). Injected by omni-echo.js; dark navy in BOTH themes,
   Aura-orange active pill. Pages need no edits: body.has-echo-sidebar shifts the
   whole page right; the sticky .app-header becomes the top panel. */
:root { --esb-bg: #16141a; --esb-item: #b9bac4; --esb-text: #f0f0f4; --esb-line: rgba(255,255,255,.08); }
[data-theme="dark"] { --esb-bg: #121014; }
.echo-sidebar { position: fixed; left: 0; top: 0; bottom: 0; width: 228px; z-index: 55;
  /* Warm charcoal + a faint orange brand glow — matches Aura's silver/orange, not
     the purple navy of the first cut (user 2026-07-29: "doesn't fit"). */
  background:
    radial-gradient(130% 60% at 0% 0%, rgba(255, 157, 87, 0.10) 0%, transparent 55%),
    linear-gradient(180deg, #18161c 0%, #100f13 100%);
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  color: var(--esb-text); display: flex; flex-direction: column;
  padding: 14px 11px; overflow-y: auto; }
/* reserve the rail (2026-08-04): stylesheet-time, so there is no reflow when
   the shell script lands. The sign-in screen opts out — it has no rail. */
body { padding-left: 228px; }
body.login-page, body.no-echo-rail { padding-left: 0; }
body.has-echo-sidebar { padding-left: 228px; }
/* ---------------------------------------------------- mobile navigation
   Below 1100px the rail used to be `display: none` with NOTHING replacing it. The rail
   is the only route between workflows, so on a phone every workflow was unreachable
   from every other one — the top bar carries a link or two and the back-link points at
   the index, and that was the whole of it. It becomes a DRAWER instead: same markup,
   same injected nav, slid off-canvas and opened by a button in the header. */
.echo-railtoggle {
  display: none; align-items: center; justify-content: center;
  width: 38px; height: 38px; margin-right: var(--space-3);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text); cursor: pointer; padding: 0; flex: none;
}
.echo-railtoggle:hover { background: var(--surface-strong); border-color: var(--border-strong); }
.echo-railtoggle svg { width: 18px; height: 18px; display: block; }
.echo-scrim {
  position: fixed; inset: 0; z-index: 54; background: rgba(0, 0, 0, 0.55);
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
}

@media (max-width: 1100px) {
  body, body.has-echo-sidebar { padding-left: 0; }
  .echo-sidebar {
    transform: translateX(-100%);
    transition: transform .18s ease;
    z-index: 1200;
    width: min(84vw, 300px);          /* a phone-width drawer, not a 228px sliver */
  }
  body.rail-open .echo-sidebar { transform: none; box-shadow: 0 0 50px rgba(0, 0, 0, 0.55); }
  body.rail-open .echo-scrim { opacity: 1; pointer-events: auto; }
  /* The toggle sits in the header's first column, ahead of the page name. */
  .echo-railtoggle { display: inline-flex; }
  .app-header { grid-template-columns: auto minmax(0, 1fr) auto; }
  .app-header > .echo-railtoggle { grid-column: 1; justify-self: start; }
  .app-header > .app-page-name { grid-column: 2; }
  .app-header > .app-header-actions { grid-column: 3; }
}
/* A drawer that slides is nice; a drawer that slides when someone asked it not to is
   not. The open/closed STATE is unaffected — only the animation is dropped. */
@media (prefers-reduced-motion: reduce) {
  .echo-sidebar, .echo-scrim { transition: none; }
}
/* The brand block is the MARK ALONE (user 2026-08-05: "leave the logo and remove the
   OMNI AURA text from the top left"). The caption line under it is gone — the product
   names itself once, on the home page (.product-wordmark), and the top bar names the
   page. Sized to match the client mark on the sign-in screen. */
.echo-sidebar .esb-logo { display: flex; flex-direction: column; padding: 6px 9px 14px; text-decoration: none; cursor: default; }
.echo-sidebar .esb-logo .logo-mark { width: 180px; height: 52px; background-size: contain; background-repeat: no-repeat; background-position: left center; }
/* Dark-surface panels (sidebars, head strips) stay dark in BOTH themes, so they
   ALWAYS wear the dark-background wordmark + a legibility lift (user 2026-07-29). */
.echo-sidebar .logo-mark, .sidebar .logo-mark,
[data-theme="light"] .echo-sidebar .logo-mark, [data-theme="light"] .sidebar .logo-mark {
  background-image: var(--logo-dark);
  filter: brightness(1.45) drop-shadow(0 0 6px rgba(255, 216, 160, 0.20));
}
.echo-sidebar .esb-sec { font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--esb-item); opacity: .75; padding: 13px 10px 5px; }
.echo-sidebar .esb-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px;
  color: var(--esb-item); font-size: 13px; text-decoration: none; }
.echo-sidebar .esb-item:hover { background: rgba(255,255,255,.07); color: var(--esb-text); }
.echo-sidebar .esb-item.active { background: var(--accent); color: var(--accent-text, #1a0d02); font-weight: 700; }
/* HELP section: its single item ("How it works") wears the PRODUCT ACCENT, so an
   offer of an explanation reads differently from the destinations above it. The
   accent is the token, never a hex — a white-label instance re-skins it for free.
   The active pill still wins: accent text on an accent fill would be unreadable. */
.echo-sidebar .esb-item.help { color: var(--accent); }
.echo-sidebar .esb-item.help:hover { background: rgba(255,255,255,.07); color: var(--accent); }
.echo-sidebar .esb-item.help.active { color: var(--accent-text, #1a0d02); }
.echo-sidebar .esb-item .ei { width: 17px; text-align: center; flex: none; display: inline-flex; }
.echo-sidebar .esb-item .ei svg { width: 15px; height: 15px; display: block; }
/* Account block at the foot of the rail — SONAR and SOAR both carry one, and Aura
   made you hunt through the settings cog to sign out.
   It is pinned to the bottom by the SPACER above it, not by a margin on itself: the
   block only appears once /api/auth/status answers, and an auto margin here meant
   its arrival shoved the copyright line 181px down the rail — the last layout shift
   left on the page (0.0023). The spacer gives up exactly the height the block takes,
   so nothing below it moves and the block still sits where it should. */
.echo-sidebar .esb-spacer { flex: 1 1 auto; min-height: 10px; }
.echo-sidebar .esb-acct {
  margin: 0; padding: 9px 10px; display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--esb-line, rgba(255,255,255,.09)); border-radius: 12px;
  background: rgba(255, 255, 255, .035);
}
.echo-sidebar .esb-acct[hidden] { display: none; }
/* Identity row: who you are, and the way out pinned right (unchanged). */
.echo-sidebar .esb-acct-id { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* The staff door gets its OWN full-width row under a hairline — at 228px the label
   and "Sign out" side by side wrapped onto two lines and the block lost its shape. */
.echo-sidebar .esb-acct-link {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 12px; font-weight: 600; color: var(--esb-item);
  padding: 8px 0 1px; border-top: 1px solid var(--esb-line, rgba(255,255,255,.09));
  text-decoration: none; cursor: pointer; white-space: nowrap;
}
.echo-sidebar .esb-acct-link[hidden] { display: none; }
.echo-sidebar .esb-acct-link:hover { color: var(--accent); }
.echo-sidebar .esb-acct-link .ei { width: 14px; flex: none; display: inline-flex; }
.echo-sidebar .esb-acct-link .ei svg { width: 14px; height: 14px; display: block; }
.echo-sidebar .esb-acct-av {
  flex: 0 0 auto; width: 27px; height: 27px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11.5px; font-weight: 700; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
}
/* min-width:0 or a long email pushes the button out of the rail. */
.echo-sidebar .esb-acct-who { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.echo-sidebar .esb-acct-who b { font-size: 12.5px; font-weight: 600; color: var(--esb-item-active, #fff); text-transform: capitalize; }
.echo-sidebar .esb-acct-who i {
  font-style: normal; font-size: 10.5px; color: var(--esb-item);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.echo-sidebar .esb-acct-out {
  margin-left: auto; flex: 0 0 auto; border: 0; background: none; cursor: pointer;
  font: 600 10.5px/1 inherit; color: var(--esb-item); padding: 5px 2px;
  text-decoration: underline; text-underline-offset: 3px;
}
.echo-sidebar .esb-acct-out:hover { color: var(--accent); }
/* Padding, never a state-dependent margin: the gap above the footer must be the
   same whether or not the account block rendered, or the footer moves when it does. */
.echo-sidebar .esb-foot { padding: 14px 9px 2px; font-size: 11px; color: var(--esb-item); line-height: 1.5; }
/* The existing header becomes the sticky top panel.
   OPAQUE, not 86% (user 2026-08-05: "that light section is clipping into the top
   menu"). A translucent sticky bar lets whatever scrolls beneath it show through —
   on the home page the hero buttons read straight through the page name. The usual
   remedy is a blur behind the bar, which this stylesheet bans outright (it re-rasters
   the panel on every scroll frame — the perf law, CI-pinned). So: simply solid. */
body.has-echo-sidebar .app-header { position: sticky; top: 0; z-index: 45;
  background: var(--bg-base);
  border-bottom: 1px solid var(--border); }

/* Settings cog menu (user 2026-07-29: the light/dark switcher lives BEHIND the cog,
   never as its own header button). omni-echo.js builds this on every page. */
.echo-menu-wrap { position: relative; }
.echo-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
  min-width: 218px; padding: 8px;
  background: var(--bg-base); border: 1px solid var(--border-strong);
  border-radius: 14px; box-shadow: var(--shadow-hover);
}
.echo-menu[hidden] { display: none; }
.echo-menu .em-label {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-muted); padding: 6px 10px 4px;
}
.echo-menu .em-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 9px; background: none;
  color: var(--text-dim); font: inherit; font-size: 13.5px; text-align: left;
  cursor: pointer; text-decoration: none;
}
.echo-menu .em-item:hover { background: var(--surface-strong); color: var(--text); }
.echo-menu .em-item[aria-checked="true"] { color: var(--text); font-weight: 600; }
.echo-menu .em-item .em-check { width: 14px; text-align: center; color: var(--accent); visibility: hidden; }
.echo-menu .em-item[aria-checked="true"] .em-check { visibility: visible; }
.echo-menu .em-sep { height: 1px; margin: 6px 4px; background: var(--border); }

/* Product lockup LAW (user 2026-07-29): in every product title, "Omni" wears the
   shiny SILVER metal and the product name wears a SHINY gradient of its own color
   (Aura orange here; SONAR overrides --product-metal cyan). Each span clips its
   own gradient, so the pair works inside or outside metal-clipped headings. */
:root { --product-metal: linear-gradient(180deg, #ffcf9e 0%, #ea6210 55%, #9a3c05 100%); }
[data-theme="dark"] { --product-metal: linear-gradient(180deg, #ffe3c2 0%, #ff9d57 52%, #c65a12 100%); }
.brand-omni { background: var(--metal); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.brand-product { background: var(--product-metal); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }

/* Deliverable picker cards (vendor-payments et al): one card per report the page
   can produce — always obvious WHAT you're building. */
.deliv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; margin: 10px 0 4px; }
.deliv-card { text-align: left; border: 1px solid var(--border); border-radius: 13px; padding: 13px 15px;
  background: var(--surface-input); cursor: pointer; font: inherit; color: var(--text);
  display: flex; flex-direction: column; gap: 4px; transition: border-color .1s, background .1s;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.10) inset; }
.deliv-card:hover { border-color: var(--border-strong); }
.deliv-card.active { border-color: var(--accent); background: var(--accent-soft); }
.deliv-card .dc-ico { font-size: 17px; }
.deliv-card .dc-name { font-weight: 700; font-size: 14px; }
.deliv-card .dc-sub { font-size: 12px; color: var(--text-dim); line-height: 1.55; }
.deliv-body { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 4px; }

/* Badge policy (user 2026-07-29): engineering badges are beta-only. On production/
   live builds omni-echo.js adds hide-eng-badges + the friendly access chip. */
html.hide-eng-badges .ro-badge, html.hide-eng-badges .env-badge { display: none; }
/* .eng-only extends the same policy to whole ELEMENTS, not just badges (user
   2026-08-05: "on the analytics page why does it show the page performance"). An FPS
   regression line is instrumentation for whoever ships the CSS — on a client's build
   it is noise at best and reads as a defect notice at worst. Beta keeps it. */
html.hide-eng-badges .eng-only { display: none !important; }
.access-chip { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }

/* QoL (2026-07-29): keyboard focus is always visible, and dark scrollbars stop
   glaring white against the near-black base. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px;
}
[data-theme="dark"] { color-scheme: dark; }
:root { color-scheme: light; }
html { scrollbar-gutter: stable; }              /* growth must not shift the layout */
html, body, .echo-sidebar, .panel, .glass-card, pre, textarea {
  scrollbar-width: thin; scrollbar-color: rgba(120,124,134,.45) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(120, 124, 134, .38); border-radius: 8px;
  border: 3px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(120, 124, 134, .62); background-clip: content-box; }
[data-theme="dark"] ::-webkit-scrollbar { width: 12px; height: 12px; }
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); border-radius: 8px; border: 3px solid #0b0b0e; }
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.30); }
[data-theme="dark"] ::-webkit-scrollbar-track { background: transparent; }

/* Back link / ghost chip on subpages.
   CONVENTION: every workflow subpage gets ONE of these as the FIRST child of its
   main/content wrapper (a pill button at the top), e.g.
     <main class="xx-wrap">
       <a class="back-link" href="/">← Back to workflows</a>
       ...
   Do NOT use a plain text link at the bottom of the page. */
.back-link {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; font-weight: 600; color: var(--text-dim);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: 6px 14px; text-decoration: none;

  margin-bottom: var(--space-3);
  transition: background .1s ease, border-color .1s ease, color .1s ease, transform .12s ease;
}
.back-link:hover { color: var(--text); background: var(--surface-strong); border-color: var(--border-strong); text-decoration: none; transform: translateY(-1px); }
.back-link:active { transform: translateY(0); }
.back { color: var(--accent); text-decoration: none; font-size: 13px; }
.back:hover { text-decoration: underline; }

/* ---------------------------------------------------- hero (homepage) */
/* Same measure and same left anchor as .container / .tool-list, so the hero's title and
   description sit flush with the left edge of everything beneath them. */
.hero { width: 100%; padding: var(--space-9) var(--space-6) var(--space-7); max-width: var(--content-max); margin: 0 auto 0 0; text-align: left; }
/* (.hero-in-container existed to reconcile a 1360px hero with an 1120px container. Both
   now draw on --content-max and both anchor left, so there is one measure and nothing to
   reconcile. The class is kept as a no-op so the markup that carries it is harmless.) */

/* (A vertical-centring rule for the home masthead was tried here and REVERTED the same
   day: centring the block in the leftover height read as "centered and taking up less
   of the screen", which is worse than the top-aligned original. The home page's real
   problem was the missing --space-7/--space-9 tokens above, which had zeroed the
   hero's padding entirely; with the scale fixed, top alignment is right.) */
/* (.hero-eyebrow — the "Omni Echo Solutions" pill with the glowing dot — was
   removed 2026-08-05 with its only user: the home hero now opens on the product
   wordmark below.) */
.hero-title {
  font-family: var(--font-display);
  /* 62px beside 13.5px body copy is the loudest "assembled, not designed" tell.
     Same title, at a size a working tool ships. */
  font-size: clamp(30px, 3.2vw, 40px);
  line-height: 1.08; letter-spacing: -0.025em; font-weight: 700;
  margin: 0 0 var(--space-3);
  background: var(--metal);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
/* Product wordmark — the ONE place the product names itself, and it is the home
   page's masthead (user 2026-08-05: the "Omni Echo Solutions" line with the light on
   the home page becomes "OMNI AURA", in the lockup the shell used to wear). The rail
   carries the client's MARK and the top bar carries the page name, so this is the
   single string a white-label build rewrites — applyBranding fills it from
   /api/branding, and the shine is derived from --accent, so it gleams in the
   client's own colour. Uppercase in CSS, so branding text of any case renders right. */
.product-wordmark {
  /* Air ABOVE it: this is the first thing under the sticky header, and with no
     top margin the two read as touching (user 2026-08-05). */
  display: inline-block; margin: var(--space-5) 0 var(--space-4);
  font-size: 15px; font-weight: 800; letter-spacing: .18em; line-height: 1.4;
  text-transform: uppercase; color: var(--text-dim);
}
.product-wordmark em {
  background: var(--product-metal); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; font-style: normal; font-weight: 800;
}
.hero-sub { font-size: clamp(14px, 1.1vw, 15.5px); color: var(--text-dim); max-width: 580px; margin: 0 0 var(--space-6); line-height: 1.55; }
.hero-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; }
/* Arrow nudge on the hero CTA — same micro-interaction as the tool-row chevron. */
.hero-actions .btn span[aria-hidden] { display: inline-block; transition: transform .12s ease; }
.hero-actions .btn:hover span[aria-hidden] { transform: translateX(3px); }

/* ---------------------------------------------------- tool LIST (workflows page)
   Eleven equal boxes is a wall, not an index (user 2026-08-04: "is there another way
   we can organize all the tools rather than all the boxes so it looks neater"). The
   references (Mayfair SOAR, Omni SONAR) both answer the same way: uppercase section
   labels over grouped rows, one line per thing, very little chrome each. So the grid
   became a grouped LIST — the surface is the shared .panel, the rows are hairlines
   inside it, and the whole page fits on one screen instead of scrolling past ten
   identical rectangles. (.card-grid and .tool-card went with it — the home page's two
   destination cards, their last user, were removed as duplicates of the hero buttons.) */
/* Wider than the old 1360 (user 2026-08-05: "the actual workflow buttons on the workflow
   tab should be wider like how they were before ... so the workflows take up a bit more
   of the screen"). Still centred and still capped — on an ultrawide, a full-bleed row
   puts the workflow name and its chevron a foot apart. The hero above uses the SAME
   measure so the title and description line up flush with the rows beneath them, which
   is the other half of that report. */
/* padding-BOTTOM matches .container's, so the last workflow does not sit flush against
   the footer (user 2026-08-05: "make sure there is a space between the footer and the
   last thing on the page"). .container already had this; the tool list never did, which
   is why only the workflows page showed the problem. The TOP is deliberately untouched —
   the hero owns that gap and the user confirmed it reads correctly. */
.tool-list { width: 100%; max-width: var(--content-max); margin: 0 auto 0 0; padding: 0 var(--space-6) var(--space-12); }
.tool-group + .tool-group { margin-top: var(--space-6); }
.tool-group-label {
  font-family: var(--font-display); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .11em; color: var(--text-muted);
  margin: 0 0 var(--space-3); padding-left: 2px;
  display: flex; align-items: baseline; gap: var(--space-2);
}
.tool-group-label i { font-style: normal; opacity: .65; letter-spacing: .04em; font-weight: 600; }
/* .panel supplies the surface/border/radius/shadow — this only strips its padding so
   the rows can run edge to edge and the hairlines reach both sides. */
/* The workflow index sits directly over the faded brand mark, and at --surface's 8%
   (dark) the watermark read straight THROUGH the rows — the curve of the logo crossing
   a workflow name is exactly the contrast the list cannot afford (user 2026-08-05:
   "make the actual workflow buttons ... a little darker so the mayfair logo in the
   background doesnt make the workflows harder to see"). This is the surface you read
   to get anywhere, so it opts out of the translucency the decorative panels keep. */
.tool-rows.panel { padding: 0; margin-bottom: 0; overflow: hidden; background: var(--surface-solid); }
.tool-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: var(--space-4); padding: 12px var(--space-5);
  color: var(--text); text-decoration: none; cursor: pointer;
  transition: background-color .12s ease;
}
.tool-row + .tool-row { border-top: 1px solid var(--border); }
.tool-row:hover, .tool-row:focus-visible { background: var(--surface-strong); text-decoration: none; }
.tool-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.tool-row-icon {
  width: 34px; height: 34px; border-radius: 10px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tile); border: 1px solid var(--border); color: var(--text-dim);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  transition: color .12s ease, border-color .12s ease;
}
.tool-row-icon svg { width: 17px; height: 17px; display: block; }
.tool-row:hover .tool-row-icon { color: var(--accent); border-color: var(--accent-soft); }
.tool-row-main { min-width: 0; display: block; }
.tool-row-name {
  font-size: 14.5px; font-weight: 600; letter-spacing: -0.012em;
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
.tool-row-desc {
  display: block; font-size: 12.5px; color: var(--text-dim); line-height: 1.45; margin: 2px 0 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tool-row-go { color: var(--text-muted); font-size: 15px; transition: transform .12s ease, color .12s ease; }
.tool-row:hover .tool-row-go { color: var(--accent); transform: translateX(3px); }
@media (max-width: 640px) {
  .tool-list { padding: 0 var(--space-4); }
  .tool-row { padding: 11px var(--space-4); gap: var(--space-3); }
  /* Narrow: the one-liner wraps to two rather than truncating to nothing, and the
     chevron goes — the whole row is the target anyway. */
  .tool-row-desc { white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
  .tool-row-go { display: none; }
}

/* (.tool-card and its parts were removed 2026-08-05 with their last user: the
   home page's two big destination cards, which duplicated the hero buttons above
   them. The tool INDEX pattern is .tool-list / .tool-row above.) */

.badge {
  display: inline-flex; align-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  padding: 3px 8px; border-radius: var(--radius-pill);
  background: var(--surface-strong); color: var(--text-dim); border: 1px solid var(--border);
}

/* ---------------------------------------------------- page header (subpages) */
.page-header { padding: var(--space-2) 0 var(--space-6); }
h1.page-title { margin: var(--space-2) 0 var(--space-1); font-family: var(--font-display); font-size: clamp(26px, 2.8vw, 34px); font-weight: 700; letter-spacing: -0.03em; }
p.sub { color: var(--text-dim); margin: 0 0 var(--space-6); font-size: 14.5px; line-height: 1.55; max-width: 720px; }

/* Title + status badges share one row (env, read-only, and info chips all sit
   here beside the <h1> — never inside a panel/section header). */
.page-header .title-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: var(--space-2) 0 var(--space-1); }
.page-header .title-row h1.page-title { margin: 0; }

/* Status badge chips. .ro-badge = read-only / posting note. .env-badge = which
   Yardi database: PRODUCTION / 1-LIVE is GREEN (the real books, safe because
   read-only), BETA / 2-TEST is AMBER (a sandbox). The colour keys off the
   ENVIRONMENT, never the connection state. .info-badge = a neutral fact chip. */
.ro-badge, .env-badge, .info-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: var(--radius-pill);
  font-size: 12px; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
  border: 1px solid transparent;
}
.ro-badge::before, .env-badge::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: 0 0 7px; }
.ro-badge { background: var(--ok-bg); color: var(--ok); }
.env-badge { background: var(--surface-strong); color: var(--text-muted); }             /* unknown / checking */
.env-badge.prod, .env-badge.ok   { background: var(--ok-bg); color: var(--ok); }        /* PRODUCTION / 1-LIVE = green */
.env-badge.beta, .env-badge.warn { background: var(--warn-bg); color: var(--warn); }    /* BETA / 2-TEST = amber */
.env-badge.err { background: var(--err-bg); color: var(--err); }
.info-badge { background: var(--surface-strong); color: var(--text-dim); border-color: var(--border); font-family: var(--font-mono, monospace); }
.info-badge::before { content: none; }

/* ---------------------------------------------------- glass card / step / panel */
.step, .glass-card, .panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-6); margin-bottom: var(--space-4);

  box-shadow: var(--shadow-glass);
}
.step-header { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); }
.step-num {
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--tile); border: 1px solid var(--border-strong); color: var(--text);
  display: inline-flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;
}
.step-num.inactive { background: var(--surface-strong); color: var(--text-muted); border-color: var(--border); }
.step-title {
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  text-transform: uppercase; letter-spacing: .11em; color: var(--text-dim);
}
/* Bare <h2> section headings inside a panel/card match the .step-title look, so
   non-wizard panels read the same as the numbered-step cards. */
.panel > h2, .glass-card > h2 {
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  text-transform: uppercase; letter-spacing: .11em; color: var(--text-dim);
  margin: 0 0 var(--space-3);
}

/* ---------------------------------------------------- form bits */
.row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim); }
input[type=text], input[type=number], input[type=email], input[type=password], input[type=search], input[type=date], input[type=month], input[type=tel], input:not([type]), select, textarea {
  font: inherit; font-size: 14px; color: var(--text);
  background: var(--surface-input); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 10px 12px; width: 100%; max-width: 320px;
  transition: border-color .1s ease, background .1s ease, box-shadow .1s ease;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
select option, select optgroup { background-color: #ffffff; color: #14151a; }
[data-theme="dark"] select option, [data-theme="dark"] select optgroup { background-color: #16161b; color: #f3f3f6; }

/* ---------------------------------------------------- buttons (pill, brand) */
.btn, .btn-primary {
  background: var(--action); color: var(--action-text);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  padding: 11px 20px; font-size: 14px; font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px; text-decoration: none; letter-spacing: -0.01em;
  /* Soft bevel (user 2026-07-29): light catches the top edge, a whisper of shade
     on the bottom — depth without clunk. */
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.22) inset, 0 -1.5px 0 rgba(0, 0, 0, 0.16) inset, 0 4px 14px var(--accent-soft);
  transition: transform .12s ease, box-shadow .12s ease, background .1s ease, border-color .1s ease, filter .1s ease, opacity .1s ease;
}
.btn:hover, .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.28) inset, 0 10px 26px var(--accent-soft); filter: brightness(1.05); text-decoration: none; }
.btn:active, .btn-primary:active { transform: translateY(0); }
.btn:focus-visible, .btn-primary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled, .btn-primary:disabled { background: var(--surface-strong); color: var(--text-muted); cursor: not-allowed; box-shadow: none; transform: none; border-color: var(--border); }

/* secondary / ghost = outlined pill */
.btn.secondary, .btn.ghost {
  background: transparent; color: var(--text); border-color: var(--border-strong); box-shadow: none;

}
.btn.secondary:hover, .btn.ghost:hover { background: var(--surface); border-color: var(--text-muted); box-shadow: none; }
.btn.ghost { color: var(--accent); border-color: var(--border); }
.btn.ghost:hover { border-color: var(--accent); background: var(--accent-soft); }
/* Compact modifier for dense contexts (e.g. a download button inside a table row). */
.btn.sm { padding: 4px 12px; font-size: 13px; box-shadow: none; }
/* Destructive action (delete/wipe). Was page-local on the Configuration page; shared
   since 2026-07-20 (profiles.html needs it too — same anti-drift law as the rest). */
.btn.danger { background: var(--err-bg); color: var(--err); border-color: color-mix(in srgb, var(--err) 35%, transparent); }
.btn.danger:hover { background: color-mix(in srgb, var(--err) 18%, transparent); box-shadow: none; }

/* ---------------------------------------------------- status pills */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: var(--radius-pill);
  border: 1px solid transparent; letter-spacing: 0.01em;
}
.pill::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 6px; }
.pill.ok { background: var(--ok-bg); color: var(--ok); }
.pill.warn { background: var(--warn-bg); color: var(--warn); }
.pill.err { background: var(--err-bg); color: var(--err); }
.pill.idle { background: var(--surface); color: var(--text-muted); border-color: var(--border); }

/* ---------------------------------------------------- dropzone */
.dropzone {
  display: block; border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
  padding: var(--space-10); text-align: center; cursor: pointer; background: var(--surface);

  transition: border-color .12s ease, background .12s ease, transform .12s ease;
}
.dropzone:hover { border-color: var(--accent); background: var(--surface-strong); }
.dropzone.drag { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.005); }
.dropzone p { margin: 4px 0; color: var(--text-dim); font-size: 14px; }
.dropzone p strong { color: var(--text); }
.dropzone .small { color: var(--text-muted); font-size: 12px; }
.file-name { color: var(--accent); font-weight: 600; margin-top: var(--space-2); }
input[type=file] { display: none; }
/* Opt back in to a visible, styled file input (for manual-upload fallbacks that
   aren't full .dropzone areas): put .file-field on the wrapping <label>. */
.file-field input[type=file] {
  display: block; width: 100%; max-width: 440px; margin-top: 6px; cursor: pointer;
  font: 500 .82rem var(--font-sans); padding: 12px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  background: var(--surface-input); color: var(--text-dim);
}
.file-field input[type=file]:hover { border-color: var(--accent); background: var(--accent-soft); }
.file-field input[type=file]::file-selector-button {
  font: 600 .82rem var(--font-sans); padding: 8px 16px; margin-right: 14px;
  border: none; border-radius: var(--radius-sm); background: var(--action); color: var(--action-text); cursor: pointer;
}

/* ---------------------------------------------------- tables */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
th {
  background: var(--surface-strong); font-weight: 600; color: var(--text-dim);
  font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
  position: sticky; top: 0; z-index: 1;

}
td.num { text-align: right; font-variant-numeric: tabular-nums; }
tr.offset td { font-weight: 600; background: var(--warn-bg); }
.table-wrap {
  overflow-x: auto; max-height: 420px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);

}

/* ---------------------------------------------------- metrics */
.metrics { display: flex; gap: var(--space-3); margin-bottom: var(--space-4); flex-wrap: wrap; }
.metric {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-3) var(--space-4); min-width: 150px;

}
.metric-label { font-size: 11.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.metric-value { font-family: var(--font-display); font-size: 22px; font-weight: 700; margin-top: 2px; letter-spacing: -0.02em;
  background: var(--metal); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }

/* ---------------------------------------------------- key/value lists */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; font-size: 13px; }
.kv dt { color: var(--text-muted); }
.kv dd { margin: 0; color: var(--text); }

/* ---------------------------------------------------- validation list */
.val-list { margin: var(--space-3) 0 0; padding: 0; list-style: none; }
.val-item { display: flex; align-items: flex-start; gap: var(--space-3); padding: 6px 0; border-bottom: 1px solid var(--border); }
.val-item:last-child { border-bottom: none; }
.val-icon { width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: #fff; margin-top: 2px; }
.val-pass { background: #16a34a; } .val-warn { background: #eab308; } .val-fail { background: #dc2626; }
.val-text { font-size: 13px; color: var(--text); }
.val-detail { font-size: 12px; color: var(--text-muted); margin-top: 2px; white-space: pre-wrap; }

/* ---------------------------------------------------- banners */
.err-banner { background: var(--err-bg); border: 1px solid var(--err); color: var(--err); padding: 10px 14px; border-radius: var(--radius-sm); margin: var(--space-3) 0 0; font-size: 13.5px; white-space: pre-wrap; }
.info-banner { background: var(--info-bg); border: 1px solid var(--border); color: var(--text-dim); padding: 10px 14px; border-radius: var(--radius-sm); margin: var(--space-3) 0 0; font-size: 13px; }

/* ---------------------------------------------------- text utilities */
.small { font-size: 12.5px; color: var(--text-muted); line-height: 1.55; }
.section-divider { margin: var(--space-5) 0 6px; font-size: 11.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.07em; font-weight: 600; }
.hidden { display: none !important; }

/* ---------------------------------------------------- empty / loading */
.empty-state { text-align: center; padding: var(--space-10) var(--space-4); color: var(--text-muted); font-size: 13px; }
.empty-state .empty-icon { width: 48px; height: 48px; margin: 0 auto var(--space-3); border-radius: 13px; background: var(--surface-strong); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; color: var(--text-muted); }

@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }
.skeleton { background: linear-gradient(90deg, var(--surface) 0%, var(--surface-strong) 50%, var(--surface) 100%); background-size: 800px 100%; border-radius: 6px; animation: shimmer 1.4s linear infinite; }

/* ---------------------------------------------------- reveal */
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.fade-in { animation: fadeUp .35s ease both; }
.step:not(.hidden), .glass-card:not(.hidden) { animation: fadeUp .35s ease both; }

/* ---------------------------------------------------- lower section · value band */
.section-band {
  max-width: 1120px; margin: var(--space-12) auto 0; padding: var(--space-12) var(--space-6);
  border-top: 1px solid var(--border);
}
.band-eyebrow {
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--silver); margin: 0 0 var(--space-3);
}
.band-title {
  font-family: var(--font-display); font-weight: 700; letter-spacing: -0.03em;
  font-size: clamp(24px, 3vw, 34px); margin: 0 0 var(--space-3); max-width: 640px;
  background: var(--metal); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.band-sub { color: var(--text-dim); font-size: 15px; line-height: 1.6; max-width: 600px; margin: 0 0 var(--space-8); }
.pillars { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
.pillar {
  position: relative; padding: var(--space-6);
  border: 1px solid var(--border); border-radius: var(--radius);
  background-color: var(--surface);
  background-image: linear-gradient(180deg, var(--silver-soft) 0%, transparent 42%);

  box-shadow: var(--shadow-glass); overflow: hidden;
  transition: transform .12s cubic-bezier(.2,.7,.2,1), border-color .12s ease, box-shadow .12s ease;
}
.pillar::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: var(--sheen); }
.pillar:hover { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: var(--shadow-hover); }
.pillar-icon {
  width: 42px; height: 42px; border-radius: 12px; margin-bottom: var(--space-4);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tile); border: 1px solid var(--border); color: var(--accent);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.pillar-icon svg { width: 20px; height: 20px; }
.pillar-title { font-family: var(--font-display); font-size: 15.5px; font-weight: 600; letter-spacing: -0.015em; margin: 0 0 6px; }
.pillar-desc { color: var(--text-dim); font-size: 13.5px; line-height: 1.55; margin: 0; }

/* ---------------------------------------------------- footer */
.app-footer {
  position: relative; z-index: 1;          /* sit above the fixed bg-fx trend lines */
  background: var(--bg-base);                /* opaque → the footer covers the trendline */
  border-top: 1px solid var(--border);
  /* auto (was space-12): in the flex-column body this absorbs the free space on a short
     page and pins the footer to the bottom; on a long page it resolves to 0 and the
     footer sits right after the content — its own border + space-8 inner padding give
     the separation, so no external gap is needed. */
  margin-top: auto;
}
.footer-inner {
  max-width: 1120px; margin: 0 auto; padding: var(--space-8) var(--space-6);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap;
}
.footer-brand { display: flex; align-items: center; gap: var(--space-3); color: var(--text-muted); font-size: 12.5px; }
.footer-brand .logo-mark { height: 30px; }
.footer-note { color: var(--text-muted); font-size: 12.5px; display: inline-flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.footer-note .dot { width: 4px; height: 4px; border-radius: 50%; background: var(--text-muted); opacity: .6; }
.footer-note .safe { color: var(--ok); }

/* ---------------------------------------------------- responsive */
@media (max-width: 640px) {
  .container { padding: 0 var(--space-4) var(--space-10); }
  .app-header { padding: var(--space-4); }
  .hero { padding: var(--space-8) var(--space-4) var(--space-6); }
  .step, .glass-card, .panel { padding: var(--space-4); }
  .tool-rows.panel { padding: 0; }        /* the rows own their own padding */
  .hero-title { font-size: 28px; }
  .section-band { padding: var(--space-8) var(--space-4); }
  .footer-inner { padding: var(--space-6) var(--space-4); flex-direction: column; align-items: flex-start; }
}

/* ------------------------------------------- property type-ahead combobox
   Shared component (first built on the Cash Analysis page): an input that
   filters a dropdown list as you type. Markup:
     <div class="combo"><input …><ul class="combo-list" hidden></ul></div>
   Behavior comes from echoCombo() in omni-echo.js. */
.combo { position: relative; }
/* The combo input always wears the standard dark field, no matter what `type`
   the page authored it with. The generic input rule above keys off
   input[type=text] — an attribute selector that misses <input> with no type
   (and type=search) — which used to leave the property box a raw white box on
   some pages. Restating the field styling here (full width, no 320px cap) keeps
   every workflow's property picker identical. */
.combo > input {
  width: 100%; max-width: none; box-sizing: border-box;
  font: inherit; font-size: 14px; color: var(--text);
  background: var(--surface-input); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 10px 12px;
  transition: border-color .1s ease, background .1s ease, box-shadow .1s ease;
}
.combo > input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.combo > input::placeholder { color: var(--text-muted); }
.combo-list { position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; right: 0; max-height: 320px; overflow-y: auto;
  margin: 0; padding: 6px; list-style: none; background: var(--surface, #fff); border: 1px solid var(--border, rgba(0,0,0,.14));
  border-radius: var(--radius, 10px); box-shadow: 0 12px 32px rgba(0,0,0,.18); }
[data-theme="dark"] .combo-list { background: #1c1c22; border-color: rgba(255,255,255,.16); }
.combo-list li { padding: 8px 10px; border-radius: var(--radius, 10px); cursor: pointer; display: flex; justify-content: space-between; gap: 10px; }
.combo-list li .cl-name { color: var(--text); font-weight: 600; }
.combo-list li .cl-meta { color: var(--text-muted); font-size: .78rem; font-family: var(--font-mono, monospace); }
.combo-list li:hover, .combo-list li.active { background: var(--silver-soft, rgba(120,120,140,.14)); }
.combo-empty { padding: 8px 10px; color: var(--text-muted); font-size: .85rem; cursor: default; }

/* --- Sessions box: a workflow's data-source logins (Yardi/Voyager, bank,
   SharePoint) shown as side-by-side cards at the TOP of the run flow. EVERY
   workflow that connects to a system presents its logins this way — the
   reference is Replacement Reserve Draws. Wrap the grid in a .step/.panel
   titled "Sessions"; one .session-card per source. Cards wrap to a stack when
   the viewport is narrow. ------------------------------------------------- */
.session-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-3); margin-top: var(--space-3); }
.session-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--space-4); background: var(--surface-strong);
  display: flex; flex-direction: column; gap: 8px;
  transition: opacity .12s ease, border-color .12s ease, box-shadow .12s ease;
}
.session-card .row { gap: 8px; margin: 0; }
.session-card-title { font-size: 12.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
/* After an upload is parsed, highlight the property's bank and dim the other. */
.session-card.bank-needed { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.session-card.bank-muted { opacity: 0.45; }

/* --- The rendered session chips (omni-echo.js): echoSharePointChip fills
   .sp-chip, echoYardiChip fills .yd-chip. Each writes a session-card BODY — a
   title+status row, its message line, and a full-size row of .btn buttons — so
   a rendered card is indistinguishable from a hand-written one beside it. Same
   rules for both; they are one pattern with two data sources. -------------- */
.sp-chip, .yd-chip { display: flex; flex-direction: column; gap: 8px; }
.sp-chip .sp-msg, .yd-chip .yd-msg { font-size: .78rem; color: var(--text-muted); margin: 0; }

/* --- Login columns: the sanctioned alternative to the Sessions box for a
   login-heavy workflow (Bank Recs) — two full login .panels laid side by side,
   stacking when the viewport is narrow. ----------------------------------- */
.login-cols { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-start; margin-bottom: var(--space-4); }
.login-cols > .panel { flex: 1 1 380px; min-width: 300px; margin-bottom: 0; }

/* --- Advanced options (collapsed by default on every workflow page) ------- */
details.advanced { margin-top: 10px; }
details.advanced > summary { cursor: pointer; font-size: .85rem; color: var(--text-muted); user-select: none; }
details.advanced > summary:hover { color: var(--text, #eee); }
details.advanced > .advanced-body { margin-top: 8px; padding: 10px 12px; border: 1px dashed var(--border, #444); border-radius: 8px; display: flex; flex-direction: column; gap: 8px; }

/* Centered header nav (2026-07-17): the Home/Workflows/Analytics links sit in the TOP
   MIDDLE of the header — not crowded between the settings gear and the corner — sized up
   from .sm and kept in the accent (orange) ghost style so they match the header buttons.
   The theme switcher itself moved into Settings → Appearance. */
.app-nav { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; gap: 10px; }
.app-header { position: relative; }
.app-nav .btn { text-decoration: none; font-size: 14px; padding: 9px 20px; }
@media (max-width: 760px) { .app-nav { position: static; transform: none; margin-left: auto; } .app-nav .btn { font-size: 12.5px; padding: 7px 12px; } }

/* hero scale (2026-08-04): the page title was set at a marketing-poster size next
   to 13.5px body copy — the loudest tell that a screen was assembled rather than
   designed. This is the same title at a size a working tool would ship, with the
   sub-line pulled in under it. */
.page-header { margin-bottom: var(--space-6); }
.page-title { font-size: clamp(26px, 2.4vw, 34px); line-height: 1.12; letter-spacing: -0.02em; margin: 0 0 var(--space-2); }
.page-header .sub { font-size: 14px; line-height: 1.55; max-width: 62ch; margin: 0; }
.masthead-title { font-size: clamp(30px, 3vw, 42px); line-height: 1.08; letter-spacing: -0.02em; }

/* The hero and the card grid must share one left edge — the title sitting 24px
   proud of the cards beneath it is the kind of misalignment that reads as sloppy
   before anyone can say why. Stated last so a later shell rule cannot zero it. */
.hero { padding-inline: var(--space-6); }

/* ===========================================================================
   DEMO WELCOME (DEMO_MODE builds only — user 2026-08-05)
   The front door of a sales demo. A visitor who lands straight on a dashboard
   full of numbers has to guess what they are looking at; this says what the
   product is, states plainly that every figure is fictional, and gets out of
   the way on one click. Built by omni-echo.js only when /api/env says
   demo:true, so it cannot appear on a real build.

   Refresh returns here (the kiosk reset, user-directed): a RELOAD anywhere in
   the demo bounces back to this page, so a prospect who gets lost mid-demo has
   one reliable way to the start — press F5.
   =========================================================================== */
.demo-welcome {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-10) var(--space-6);
  background: radial-gradient(1200px 700px at 50% -10%, var(--accent-soft), transparent 60%);
}
.demo-welcome[hidden] { display: none; }
.demo-welcome-card {
  width: min(620px, 100%);
  background: var(--surface-strong);
  border: 1px solid var(--border, rgba(255, 255, 255, 0.12));
  border-radius: var(--radius-lg);
  padding: var(--space-10) var(--space-8);
  text-align: center;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}
.demo-welcome-eyebrow {
  display: inline-block; margin-bottom: var(--space-4);
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700;
  color: var(--accent);
  padding: 5px 12px; border-radius: 999px;
  background: var(--accent-soft); border: 1px solid var(--accent-soft);
}
.demo-welcome-title { margin: 0 0 var(--space-3); font-size: clamp(28px, 5vw, 40px); font-weight: 800; letter-spacing: -0.02em; }
.demo-welcome-title span { color: var(--accent); }
.demo-welcome-sub { margin: 0 auto var(--space-6); max-width: 46ch; color: var(--text-dim); font-size: 16px; line-height: 1.6; }
/* The three steps. Counter-driven so the markup carries no hardcoded numbers —
   adding or reordering a step cannot leave the page numbered 1, 2, 2. */
.demo-welcome-steps { counter-reset: dws; list-style: none; margin: 0 0 var(--space-8); padding: 0; text-align: left; display: grid; gap: var(--space-5); }
.demo-welcome-steps li { counter-increment: dws; display: grid; grid-template-columns: 34px 1fr; gap: var(--space-4); align-items: start; }
.demo-welcome-steps li::before {
  content: counter(dws);
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  font-weight: 800; font-size: 14px;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid var(--accent-soft);
}
.demo-welcome-steps b { display: block; margin-bottom: 3px; color: var(--text); font-size: 15px; }
.demo-welcome-steps span { color: var(--text-dim); line-height: 1.55; font-size: 14px; }
.demo-welcome-actions { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; }
.demo-welcome-note {
  margin: var(--space-6) 0 0; padding-top: var(--space-5);
  border-top: 1px solid var(--border, rgba(255, 255, 255, 0.1));
  color: var(--text-muted); font-size: 13px; line-height: 1.5;
}
@media (max-width: 560px) {
  .demo-welcome-card { padding: var(--space-8) var(--space-5); }
}
