/* ===========================================================================
   flo-nav.css — THE Flourisha 5-pillar navigation stylesheet.
   W048, 2026-07-29.

   One file, paired with flo-nav.js, serving every surface from /nav/.
   It replaces three copies that had already drifted apart:
     - prototype/_nav-shared.css       (tier-later .5,  tier-paid .72, icon 18px)
     - the SIDEBAR block in src/app/globals.css  (.28, .48, icon 20px)
     - the inline <style> nav block in the productivity report

   Where they disagreed, the values below are globals.css's, because those are
   the ones Greg ruled on (live 1 / paid .48 / later .28, badges suppressed on
   every non-live tier). The one exception is the item glyph box: 18px, from
   gtm-v1-nav, which is the rail's design reference. Recorded here rather than
   changed quietly — it is a 2px shrink on the app.


   ── EVERY var() FALLBACK BELOW IS A CANONICAL VALUE, NOT A GUESS ──────────
   Greg, 2026-07-30, looking at the productivity report beside the app: "This
   is the NAV in the productivity report and you're telling me it's shared."

   He was right and the file was not the problem — the TOKENS were. This
   stylesheet consumes tokens it does not define, and the report is a
   standalone page that defines only the five pillar hexes. So every
   var(--fg2, <fallback>) in here resolved to the FALLBACK on the report and
   to the real token in the app. Same bytes, different pixels, which is
   exactly the failure this whole exercise was meant to end — item colour
   #5c514c vs #4a3f3a, group label 12px vs 11px.

   The fallbacks were values I invented. They are now copied verbatim from
   colors_and_type.css / globals.css, which already agree with each other, and
   scripts/check-nav-token-fallbacks.py fails the build if they ever drift
   apart again. A page with no token sheet now renders identically to one
   with it.

   TOKENS: this file consumes tokens, it does not define them. Two surfaces
   name the pillar hues differently — the app's globals.css says
   --pillar-capture, the brand bundle's colors_and_type.css says --capture —
   so every pillar rule reads var(--pillar-x, var(--x)) and works under either.
   Load the surface's token sheet BEFORE this one.

   DO NOT re-declare any rule below in a page-local <style> or in globals.css.
   That is precisely the move that produced the three-way drift this file ends.
   =========================================================================== */

/* The custom element itself must not create a box — host layouts position
   .sidebar directly (flex child, grid cell, sibling of a collapse toggle). */
flo-nav { display: contents; }


/* ═══════════════════════════════════════════════════════════════════════════
   TOP BAR — the same chrome on every app and every page.

   W048 2026-07-31. Greg: "add the top bar that is the consistent top bar
   across all apps and pages." These rules were lifted VERBATIM out of
   src/app/globals.css, where they were the app's private `.top-header` block,
   so the app can mount <flo-top> and lose its own JSX without moving a pixel.
   Every static surface now gets the same bar from the same file.

   Do not re-declare any of these in globals.css or a page <style>. Same rule
   as the rail, same reason.
   ═══════════════════════════════════════════════════════════════════════════ */
flo-top { display: block; flex-shrink: 0; }

.top-header {
  height: var(--header-height);
  padding: 0 var(--top-header-pad);
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--chrome-bar, rgba(255, 255, 255, 0.9));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--glass-border);
  flex-shrink: 0;
  z-index: 10;
  position: relative;
  /* THE RAIL'S SHADOW, ROTATED 90 DEGREES (Greg, 2026-08-05: "add similar drop
     shadow on the top bar as you have on the left panel").

     Same three numbers as .sidebar's `8px 0 20px -6px` — only the offset axis
     changes, y instead of x. Copied rather than re-tuned on purpose: two
     surfaces of the same chrome lifting off the page by different amounts is
     the drift this whole file exists to stop, and "looks about right" is how
     the 8px and the 22px would have quietly diverged.

     The bar sits at z-index 10 and the rail at 4, so this casts ONTO the rail
     — the bar reads as the upper layer, which is the stacking that already
     existed and is now visible. */
  box-shadow: 0 8px 20px -6px rgba(28,8,0,.12);
}
.top-header-left { display: flex; align-items: center; gap: 4px; position: relative; z-index: 2; }
.top-header-spacer { flex: 1; }
.top-header-right { display: flex; align-items: center; gap: 4px; position: relative; z-index: 2; }
.top-header-sep { width: 1px; height: 24px; background: rgba(0, 0, 0, 0.08); margin: 0 8px; }
.top-header-logo { height: 30px; width: auto; margin-right: 4px; }
.top-header-search {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 340px;
  height: 32px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  background: rgba(0, 0, 0, 0.04);
  border: 1px solid rgba(0, 0, 0, 0.07);
  border-radius: 16px;
  cursor: pointer;
  font-family: inherit;
  font-size: 12px;
  color: var(--text-muted);
  transition: all 0.15s;
}
.top-header-search:hover { background: rgba(0, 0, 0, 0.06); border-color: rgba(0, 0, 0, 0.12); }
.top-header-search-shortcut {
  font-size: 10px;
  color: rgba(0, 0, 0, 0.22);
  margin-left: auto;
  font-weight: 500;
  background: rgba(0, 0, 0, 0.04);
  padding: 1px 6px;
  border-radius: 4px;
}
.top-header-user {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all 0.12s;
  position: relative;
}
.top-header-user:hover { background: rgba(0, 0, 0, 0.04); }
.top-header-avatar { width: 30px; height: 30px; border-radius: 50%; overflow: hidden; position: relative; flex-shrink: 0; }
.top-header-avatar img { width: 100%; height: 100%; object-fit: cover; }
.top-header-avatar::after { content: ''; position: absolute; bottom: -1px; right: -1px; width: 8px; height: 8px; background: #22c55e; border: 2px solid rgba(255, 255, 255, 0.9); border-radius: 50%; }
.top-header-name { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.icon-btn { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 6px; border: none; background: transparent; cursor: pointer; transition: all 0.12s; color: var(--text-muted); flex-shrink: 0; }
.icon-btn:hover { background: rgba(0, 0, 0, 0.05); color: var(--text-primary); }
.notif-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  background: var(--molten-lava);
  color: white;
  border-radius: 8px;
  font-size: 9px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  pointer-events: none;
}
.avatar-initial { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: var(--accent, #1a97b8); color: #fff; font-weight: 700; font-size: 13px; border-radius: 50%; }

/* ── New Capture, in the bar ──────────────────────────────────────────────
   MOCKUP, opt-in via <flo-top capture>. Greg, 2026-08-03: "I actually was
   thinking of moving that new capture to the Top NAV. Maybe we can mock that
   up and get it out of there anyways."

   Rendered only where a surface asks for it, so nothing changes until the
   placement is decided — and the rail's own capture button is untouched, so
   the two can be looked at side by side.

   Not a copy of the rail's control, on purpose. The rail's is a dashed DROP
   ZONE that reads as an empty area waiting to be filled, which works in a
   248px column with room to spare. A top bar has 48px of height and no room
   to look empty, so this is a solid pill that reads as an action. It still
   accepts a file drop, and the `drag-over` state is what says so at the moment
   it matters, rather than a dashed border saying it permanently.

   Same event contract as the rail's (capture-click, capture-files), so a host
   can move the control between the two without touching its handlers. */
.top-capture {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 12px;
  border: 1px solid var(--dark-cyan-15, rgba(26,151,184,0.15));
  border-radius: var(--radius-pill, 9999px);
  background: var(--dark-cyan-10, rgba(26,151,184,0.10));
  color: var(--cyan-700, #157a94);
  font-family: var(--font-sans, 'Plus Jakarta Sans', sans-serif);
  font-size: 12px; font-weight: 600; line-height: normal;
  white-space: nowrap; cursor: pointer; flex-shrink: 0;
  transition: background var(--dur-fast, 150ms), border-color var(--dur-fast, 150ms),
    color var(--dur-fast, 150ms);
}
.top-capture:hover {
  background: var(--dark-cyan-15, rgba(26,151,184,0.15));
  border-color: var(--cyan-500, #1a97b8);
  color: var(--cyan-600, #1888a6);
}
.top-capture:focus-visible { outline: 2px solid var(--cyan-500, #1a97b8); outline-offset: 2px; }
/* The drop affordance appears when a drag is actually happening — the moment
   it is useful — instead of a permanent dashed border in the chrome. */
.top-capture.drag-over {
  background: var(--dark-cyan-15, rgba(26,151,184,0.15));
  border-color: var(--cyan-500, #1a97b8);
  border-style: dashed;
  color: var(--cyan-600, #1888a6);
}
.top-capture-glyph { width: 16px; height: 16px; display: block; flex-shrink: 0; }

[data-theme="dark"] .top-capture,
[data-theme="midnight"] .top-capture {
  background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.10);
  color: var(--cyan-400, #4eb5d4);
}

/* Left-panel toggle, beside the logo. Greg, 2026-08-01.
   Replaces the circular button that straddled the rail's right edge: two
   controls doing one job is the duplication this whole exercise removes. */
.panel-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--rail-toggle-size); height: var(--rail-toggle-size); margin-right: 4px;
  border: 0; border-radius: 8px; padding: 0;
  background: transparent; color: var(--fg2, #55504a); cursor: pointer;
  transition: background var(--dur-fast, 150ms), color var(--dur-fast, 150ms);

  /* ON THE RAIL'S AXIS, NOT THE HEADER'S PADDING (Greg, 2026-08-03:
     "the open left panel close left panel icons ... consistently to the
     right of where it should be").

     This rule's own comment has claimed since it was written that the toggle
     "lines up over the rail it controls". It never did. The header insets its
     contents by 16px and the button is 32px wide, so its centre landed on
     32px; the rail's icon column is at 28px. Four pixels, on every surface
     that has a toggle -- and four pixels between two things stacked directly
     on top of each other is exactly the kind of gap that reads as wrong long
     before anyone can name it. A comment asserting an alignment is not an
     alignment.

     `position: relative` rather than a negative margin on purpose: it moves
     the button and its hit area without moving the logo beside it, so there
     is no compensating margin to keep in sync. Derived from
     --rail-icon-axis, so the toggle follows the column if the column moves. */
  position: relative;
  left: calc(var(--rail-icon-axis) - var(--top-header-pad) - var(--rail-toggle-size) / 2);
}
.panel-toggle:hover { background: var(--dark-cyan-10, rgba(26,151,184,0.10)); color: var(--cyan-600, #1888a6); }
.panel-toggle:focus-visible { outline: 2px solid var(--cyan-500, #1a97b8); outline-offset: 2px; }

/* The chevron flips direction when the panel opens or closes, and the swap is
   a NEW <svg> node, so this is a keyframe rather than a transition — there is
   no old value to travel from. A hard glyph swap reads as a flicker; this
   reads as the button answering. Timed off the label clock, not the rail's:
   the button should confirm the click immediately, not half a second later. */
.panel-toggle .top-glyph { animation: panel-glyph-in var(--rail-label-dur) var(--rail-ease); }
@keyframes panel-glyph-in {
  from { opacity: 0; transform: scale(.8); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .panel-toggle .top-glyph { animation: none; }
}

/* Fallbacks for tokens the app defines and a bare page may not — same
   discipline as the rail's, and checked by scripts/check-nav-token-fallbacks.py. */
flo-top .top-header { height: var(--header-height, 48px); }
.top-glyph    { width: 20px; height: 20px; display: block; }
.top-glyph-sm { width: 16px; height: 16px; display: block; }

/* ── App shell: top bar, then rail beside content ──────────────────────────
   Rendered by <flo-frame>. The rail is a flex child here, which is why
   flo-nav.css never positions .sidebar itself. */
flo-frame { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.flo-shell { display: flex; flex: 1; overflow: hidden; min-height: 0; }
.flo-main  { flex: 1; overflow: auto; min-width: 0; }


/* Account dropdown — lifted verbatim from globals.css with the bar it hangs off. */
.account-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 280px;
  background: rgba(255, 255, 255, 0.97);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 32px rgba(28, 8, 0, 0.14);
  overflow: hidden;
  z-index: 200;
}
.account-dropdown.open { display: block; }
.account-dd-profile { padding: 14px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); }
.account-dd-avatar { width: 36px; height: 36px; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
.account-dd-avatar img { width: 100%; height: 100%; object-fit: cover; }
.account-dd-info { flex: 1; }
.account-dd-name { font-size: 14px; font-weight: 700; color: var(--text-primary); }
.account-dd-email { font-size: 11px; color: var(--text-muted); }
.account-dd-section { padding: 6px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.06); }
.account-dd-label { padding: 4px 16px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; color: var(--text-muted); }
.account-dd-item { display: flex; align-items: center; gap: 10px; padding: 8px 16px; cursor: pointer; font-size: 13px; color: var(--text-secondary); transition: all 0.12s; }
.account-dd-item:hover { background: rgba(26, 151, 184, 0.06); color: var(--text-primary); }
.account-dd-item-right { margin-left: auto; font-size: 11px; color: var(--text-muted); }
.account-dd-themes { display: flex; gap: 6px; padding: 6px 16px 8px; }
.account-theme-btn { width: 28px; height: 28px; border-radius: 7px; border: 2px solid rgba(0, 0, 0, 0.08); cursor: pointer; transition: all 0.12s; overflow: hidden; display: flex; }
.account-theme-btn:hover { border-color: var(--dark-cyan); transform: scale(1.1); }
.account-theme-btn.active { border-color: var(--dark-cyan); box-shadow: 0 0 0 2px rgba(26, 151, 184, 0.2); }
.account-theme-half { flex: 1; }
.account-dd-section:last-child { border-bottom: none; }
.account-dd-version { padding: 6px 16px 4px; font-size: 11px; color: var(--fg3, #736c65); text-align: right; user-select: all; }

/* Top-bar theming, moved here from globals.css so the bar has ONE home.
   These beat flo-nav.css's base rules on specificity, which is why leaving
   them behind meant the bar was still styled from two files.

   2026-08-05: the dark bar was a hardcoded rgba(25,25,42,.85) — NAVY — and it
   sat directly above the productivity report's chocolate-brown page. Greg:
   "we've got a mix of blues and browns." It reads --chrome-* from
   flo-theme.css now, so the bar is the same neutral as everything under it and
   a theme moves both together. Midnight keeps its own literal navy because
   midnight IS the blue theme; that one is a choice, not a leak. */
[data-theme="dark"] .top-header {
  background: var(--chrome-bar, rgba(39, 35, 31, 0.88)); border-color: var(--chrome-border, rgba(247, 243, 238, 0.11));
  /* Matches the rail's dark value the same way: 22px and .45, not a new pair. */
  box-shadow: 0 8px 22px -6px var(--chrome-shadow, rgba(0,0,0,.45));
}
[data-theme="midnight"] .top-header {
  background: rgba(25, 25, 42, 0.85); border-color: rgba(255, 255, 255, 0.06);
  box-shadow: 0 8px 22px -6px rgba(0,0,0,.45);
}
[data-theme="slate"] .top-header { background: rgba(244, 245, 247, 0.88); border-color: rgba(0, 0, 0, 0.06); }
[data-theme="dark"] .top-header-sep,
[data-theme="midnight"] .top-header-sep { background: rgba(255, 255, 255, 0.1); }
[data-theme="dark"] .top-header-search {
  background: var(--chrome-field, rgba(247, 243, 238, 0.06)); border-color: var(--chrome-field-bd, rgba(247, 243, 238, 0.09));
}
[data-theme="midnight"] .top-header-search { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.08); }
[data-theme="dark"] .account-dropdown {
  background: var(--chrome-menu, rgba(46, 42, 38, 0.97)); border-color: var(--chrome-border, rgba(247, 243, 238, 0.11));
}
[data-theme="midnight"] .account-dropdown { background: rgba(30, 30, 50, 0.97); border-color: rgba(255, 255, 255, 0.1); }


/* ═══════════════════════════════════════════════════════════════════════════
   THE SHELL — one wireframe everything builds off.

   W048 2026-07-31. Greg: "so we are speaking from one consistent wireframe
   scaffolding structure that everything builds off of."

   Two renderers emit this exact DOM, and that is the whole point:
     <flo-frame>  for static HTML, defined in flo-nav.js
     <AppShell>   for React, src/components/layout/app-shell.tsx
   Both produce:
     .app-shell  > flo-top
                 > .app-layout > [rail] + main.app-main + [right panel]

   It replaces TWO parallel class-sets that did the same job under different
   names, .app-shell/.app-layout/.ai-col and
   .capture-shell-root/.shell/.main-content, each used exactly once.
   ═══════════════════════════════════════════════════════════════════════════ */
flo-frame  { display: contents; }

.app-shell {
  display: flex; flex-direction: column;
  height: 100vh; overflow: hidden;
  position: relative; z-index: 1;
  font-family: var(--font-sans, 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
  color: var(--fg1, #26221f);
  background: transparent;
}
.app-layout { display: flex; flex: 1; min-height: 0; overflow: hidden; }
.app-main {
  flex: 1; display: flex; flex-direction: column;
  min-width: 0; overflow: hidden;
}
/* The one variation a route legitimately needs: whether its content pane
   scrolls itself or hands scrolling to a child. Not a second shell. */
.app-main[data-scroll="auto"] { overflow: auto; }

/* Mesh backdrop. Layout only: the gradient and its motion are written each
   frame by useFlourishaMesh (React) or left to the host page (static). */
.mesh-bg {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-color: var(--bg-base, #fdfbf7);
}

/* Collapse toggle: host chrome, sits OUTSIDE the rail and straddles its edge.
   overflow must stay visible on the wrapper or the circle gets clipped. */
.nav-col-wrap { position: relative; height: 100%; flex-shrink: 0; overflow: visible; display: flex; }

/* ── Rail ────────────────────────────────────────────────────────────────
   Width is the one genuinely per-surface value: the app runs --sidebar-w,
   the brand bundle ships --nav-width, a bare prototype gets 248px. */
.sidebar {
  width: var(--sidebar-w, var(--nav-width, 248px));
  background: var(--chrome-rail, var(--bg-base, #fdfbf7));
  /* TYPOGRAPHY IS OWNED HERE, NOT INHERITED (W048 2026-07-31).
     The rail used to inherit whatever the host page happened to set. That is
     the token bug again in a different coat: same file, different result. It
     bit immediately — the moment the productivity report's own
     `.sidebar { font-family: 'Plus Jakarta Sans' }` was deleted (correctly,
     it was page-local rail styling), that rail started rendering in TIMES NEW
     ROMAN, and line-height went from 19.5px to `normal`, making every nav item
     2px shorter than the app's. Declared here, the rail looks the same on a
     page with no typography of its own as on one with a full theme. */
  font-family: var(--font-sans, 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
  line-height: 1.5;
  border-right: 1px solid var(--chrome-border, rgba(28,8,0,.06));
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  position: relative;
  z-index: 4;
  box-shadow: 8px 0 20px -6px rgba(28,8,0,.12);
  transition: width var(--rail-dur) var(--rail-ease);
}
[data-theme="dark"] .sidebar { box-shadow: 8px 0 22px -6px var(--chrome-shadow, rgba(0,0,0,.45)); }


/* ═══════════════════════════════════════════════════════════════════════════
   RAIL MOTION — how the left panel opens and closes.

   Greg, 2026-08-01: "I'd like it to animate in 1/2 of a second closing and
   opening and I want to have inertia effects where it feels very natural not
   just linear."

   Four tokens and nothing else, per rule 6. A theme changes how every rail in
   every app feels by overriding these; no page owns its own timing.

   THE CURVE IS A GLIDE, NOT A SPRING, AND THAT IS DELIBERATE.
   cubic-bezier(.32,.72,0,1) leaves quickly and takes a long time to settle,
   which is what something with mass does. A springy curve (y > 1) overshoots
   the END value in the direction of travel, so closing would undershoot 56px
   and squeeze the icons out of their own column for a few frames. Width is
   the one property here that cannot afford overshoot.

   SO THE INERTIA LIVES IN THE LABELS. They lag the rail on the way out and
   arrive late on the way in (--rail-label-delay), so the text trails the
   panel edge instead of moving locked to it. That lag is what reads as
   weight, and nothing can be clipped by it.

   Prerequisite, learned the hard way: none of this animates unless the
   .sidebar ELEMENT survives the toggle. See attributeChangedCallback in
   flo-nav.js — the renderer used to rebuild it, and a new element is born at
   its target width with nothing to animate from.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --rail-dur:         500ms;                        /* Greg: half a second */
  --rail-ease:        cubic-bezier(.32, .72, 0, 1); /* heavy glide, no overshoot */
  --rail-label-dur:   260ms;
  --rail-label-delay: 200ms;   /* labels arrive AFTER the rail has opened */

  /* ── THE RAIL'S TWO VERTICAL AXES ──────────────────────────────────────
     Greg, 2026-08-03, looking at the expanded rail: "That's not centered."

     He was looking at the icon COLUMN, and it was not one column. Measured
     from the rail's left edge, three kinds of glyph sat on three axes:

       workspace glyph   23px   (.ws-bar padding 14 + half of an 18px glyph)
       pillar chip       26px   (.nav-scroll padding 12 + half of a 28px chip)
       item icon         29px   (12 + .nav-item padding 8 + half of 18)

     Nobody chose 23, 26 and 29. Each row had picked its own padding for its
     own reasons and the numbers had never been compared, which is why the
     labels DID line up (both landed on 48 by luck of the same arithmetic)
     while the icons above them did not. Six pixels of spread down a single
     column reads as sloppy long before anyone can say why.

     So the axis is a stated number now, and every row derives its padding and
     its gap from it rather than declaring one. Change --rail-icon-axis and
     the whole column moves together; that is the point.

     28px is not arbitrary: it is the collapsed rail's own centre line (56px
     wide, 1px border, content centre 27.5). So a glyph does not travel
     horizontally at all when the rail collapses — it only loses its label. */
  --rail-pad:        12px;   /* .nav-scroll inset */
  --rail-icon-axis:  28px;   /* every glyph's CENTRE, from the rail's left edge */
  --rail-label-x:    48px;   /* every label's LEFT, from the same edge */
  --rail-chip-size:  28px;   /* pillar chip; DS-4 asserts this */
  --rail-glyph-size: 18px;   /* item, workspace and capture glyphs */

  /* The top bar's own inset, and the left-panel toggle that sits in it. Both
     are here rather than next to .top-header because the toggle's POSITION is
     a rail question, not a header question -- see .panel-toggle. */
  --top-header-pad:   16px;
  --rail-toggle-size: 32px;

  /* ── ROW HEIGHTS: THE SAME IN BOTH STATES ──────────────────────────────
     Greg, 2026-08-03: "the whole menu jumps up ... it feels very unstable
     when it opens and closes the side panel."

     Measured, expanded -> collapsed: the capture button 54px -> 40px, each
     group header 38 -> 36, each nav item 33.5 -> 32. None of those is large.
     All of them are in one column, so they ACCUMULATE: the first item moved
     15px, the last moved 31, and the whole rail's content shrank by 32px.
     Every row landing somewhere new is what reads as unstable — the eye is
     tracking the item it was about to click, and that item slides.

     None of it was intentional. Each row simply let its height be decided by
     whatever it happened to contain, and collapsing takes content away: a
     label's line box is 19.5px against an 18px icon, and the pillar chip
     shrinks 28 -> 24. So the heights are stated here and applied in BOTH
     states, and the rule that follows is the one-liner worth remembering:
     COLLAPSING CHANGES WIDTH, NEVER HEIGHT. */
  --rail-row-h:     33.5px;  /* .nav-item — 7px padding x2 + a 19.5px line box */
  --rail-group-h:   38px;    /* .group-header — 28px chip + 4/6 padding */
}

.sidebar.collapsed { width: var(--nav-collapsed-width, 56px); }
/* The workspace dropdown is a popover, not a label: it has no collapsed form
   to travel to, so it leaves outright. */
.sidebar.collapsed .ws-dropdown { display: none; }
.sidebar.collapsed .group-header { justify-content: center; padding: 8px 0 4px; }
/* The collapsed capture button keeps its full height too, so the first pillar
   below it does not slide up 14px. */
/* Height is NOT restated here: it comes from --rail-row-h above and must not
   be overridden, or the column starts creeping again. The margin was `1px 8px`
   and is now `0 8px` for the same reason — 1px top and bottom is 2px per row,
   seventeen times. */
.sidebar.collapsed .nav-item { justify-content: center; padding: 7px 0; margin: 0 8px; }
.sidebar.collapsed .pillar-chip { width: 24px; height: 24px; }
/* The workspace bar never centred, even before the labels started animating —
   it is the one row that was left at flex-start, so its glyph sat 5px left of
   every other one. Measured, not assumed: pre-fix offsets were -0.5 for the
   capture glyph, the pillar chip and the item icon, and -5 for this. */
.sidebar.collapsed .ws-bar { justify-content: center; padding: 0; }

/* ── Centring the collapsed rail ──────────────────────────────────────────
   GAP IS THE WHOLE BUG (Greg, 2026-08-01: "they need to all be centered and
   right now they're not").

   Labels no longer leave the DOM when the rail collapses — they stay at zero
   width so they have something to animate. But a zero-width flex item is
   still a flex item, so its GAP is still applied, and `justify-content:
   center` then centres «icon + gap + nothing». Every glyph in the rail sat
   left of centre by exactly half its container's gap:

     .nav-item        gap 10px  ->  icon      5px  left   (measured -5.5)
     .group-header    gap  8px  ->  chip      4px  left   (measured -4.5)
     .capture-inner   gap  6px  ->  glyph     3px  left   (measured -3.5)

   Arithmetic that exact is a signature, not a coincidence. Zeroing the gap
   fixes all three at once, and gap is animatable, so it closes on the rail's
   own clock instead of snapping.

   (-0.5px remains on everything, equally: .sidebar is 56px INCLUDING its 1px
   right border, so the content box is 55px and its centre is half a pixel
   left of the border box's. The glyphs are centred in the space they actually
   occupy, which is the correct answer.)

   Each of these four elements times its own `gap` on its OWN canonical rule
   further down, never here. A `transition` declared in this block would sit
   EARLIER in the file than .nav-item's and .ws-bar's real rules and lose to
   them silently — the same shadowing that already cost one round on this
   change. One rule per selector. */
:is(.sidebar.collapsed, flo-nav[collapsed]) :is(
  .nav-item, .group-header, .ws-bar
) {
  gap: 0;
}

/* Everything the rail carries that is TEXT. Grouped because they all do the
   same thing: fade, slide a little, and give up their width. */
.item-label, .item-badge, .group-label,
.ws-label, .ws-badge, .ws-chevron {
  max-width: 100%;
  max-height: 4em;
  transition:
    opacity    var(--rail-label-dur) var(--rail-ease) var(--rail-label-delay),
    transform  var(--rail-label-dur) var(--rail-ease) var(--rail-label-delay),
    max-width  var(--rail-label-dur) var(--rail-ease) var(--rail-label-delay),
    max-height var(--rail-label-dur) var(--rail-ease) var(--rail-label-delay);
}

/* `flo-nav[collapsed]` is here as well as `.sidebar.collapsed` because
   <flo-nav groups-only> renders no .sidebar wrapper at all — it used to hide
   its labels by not emitting them, which is exactly the rebuild we just
   stopped doing. The host element always carries the attribute. */
:is(.sidebar.collapsed, flo-nav[collapsed]) :is(
  .item-label, .item-badge, .group-label,
  .ws-label, .ws-badge, .ws-chevron
) {
  opacity: 0;
  transform: translateX(-8px);
  /* Out of the layout, not just invisible — in BOTH axes.
     max-width alone handles the labels that sit in a row, and that is what the
     first cut did. It was wrong for the two that stack: `New Capture` and its
     `Drop files or click to open` hint are block-level, so a zero-width span
     still reserved its LINE, and the collapsed capture button went from a
     compact 40px box to a 110px one that shoved every nav item down the rail.
     Caught in a screenshot, not by any of the geometry numbers, which is rule
     5 doing its job. */
  max-width: 0;
  max-height: 0;
  overflow: hidden;
  margin: 0; padding: 0; border-width: 0;
  pointer-events: none;
  /* Leaving is immediate; only ARRIVING lags. A label that also waited 200ms
     to start disappearing would still be legible while the rail closed on
     top of it. */
  transition-delay: 0ms;
}

/* The rest of the collapsed geometry rides the same clock, so the icons drift
   to centre with the panel rather than jumping there ahead of it. The
   .nav-item half of this lives on the canonical .nav-item rule further down,
   not here — one rule per selector, same reason as everything else in W048. */
.group-header { transition: padding var(--rail-dur) var(--rail-ease), gap var(--rail-dur) var(--rail-ease); }


/* ── Brand lockup ────────────────────────────────────────────────────────
   Rendered only when <flo-nav brand>, for surfaces with no top header of
   their own. W048 2026-07-31: this replaces the productivity report's
   `.rail-brand` + `.rail-dot` — a conic-gradient CSS square next to the word
   "Flourisha" in a bold sans. A hand-drawn fake logo at the top of a rail
   that was meant to be the shared one. The real mark lives here now, so no
   page can draw its own again. */
.nav-brand {
  display: flex; align-items: center;
  padding: 14px 8px 16px;
  flex-shrink: 0;
}
.nav-brand-lockup { height: 26px; width: auto; display: block; }
.nav-brand-mark   { height: 26px; width: 26px; display: block; margin: 0 auto; }
.sidebar.collapsed .nav-brand { justify-content: center; padding: 14px 0 16px; }

/* ── Workspace bar ───────────────────────────────────────────────────────
   Rendered only when <flo-nav workspace>. */
.ws-bar-wrap { position: relative; padding: 0; }
.ws-bar {
  width: 100%; height: 52px;
  /* No --rail-pad term: .ws-bar is a sibling of .nav-scroll, not inside it,
     which is exactly why its glyph was the furthest off the column. */
  padding: 0 14px 0 calc(var(--rail-icon-axis) - var(--rail-glyph-size) / 2);
  display: flex; align-items: center;
  gap: calc(var(--rail-label-x) - var(--rail-icon-axis) - var(--rail-glyph-size) / 2);
  background: transparent; border: 0; border-bottom: 1px solid rgba(0,0,0,.12);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: background var(--dur-fast, 150ms), color var(--dur-fast, 150ms),
    padding var(--rail-dur) var(--rail-ease), gap var(--rail-dur) var(--rail-ease);
  flex-shrink: 0; position: relative; white-space: nowrap;
}
.ws-bar:hover { background: rgba(0,0,0,.02); }
.ws-icon-svg { color: var(--cyan-600, #1888a6); display: flex; flex-shrink: 0; }
.ws-label {
  font-size: 13px; font-weight: 600; color: var(--fg1, #26221f);
  flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-chevron { color: var(--fg2, #55504a); display: flex; }
.ws-glyph { width: 18px; height: 18px; display: block; }
.ws-badge {
  font-size: 10px; font-weight: 700;
  background: var(--dark-cyan-15, rgba(26,151,184,0.15)); color: var(--cyan-600, #1888a6);
  padding: 1px 6px; border-radius: var(--radius-pill, 9999px);
}

.ws-dropdown {
  display: none; position: absolute;
  top: calc(100% + 4px); left: 8px; right: 8px;
  background: rgba(255,255,255,.96); backdrop-filter: blur(20px);
  border: 1px solid rgba(0,0,0,.08);
  border-radius: var(--radius-sm, 8px);
  box-shadow: 0 8px 24px rgba(28,8,0,.12);
  overflow: hidden; z-index: 120; padding: 12px;
}
.ws-dropdown.open { display: block; }
[data-theme="dark"] .ws-dropdown { background: var(--chrome-menu, rgba(46,42,38,.97)); border-color: var(--chrome-border, rgba(247,243,238,.11)); }
[data-theme="midnight"] .ws-dropdown { background: rgba(30,30,50,.96); border-color: rgba(255,255,255,.1); }
.ws-dropdown-title {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--fg3, #736c65);
  padding: 6px 2px 8px 8px;
}
.ws-settings-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 6px;
  color: var(--fg3, #736c65); text-decoration: none;
  transition: background var(--dur-fast, 150ms), color var(--dur-fast, 150ms);
}
.ws-settings-link:hover { background: var(--dark-cyan-10, rgba(26,151,184,0.10)); color: var(--cyan-600, #1888a6); }
.ws-opt {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 6px 7px 8px; border-radius: 8px;
  cursor: pointer; transition: background var(--dur-fast, 150ms);
}
.ws-opt:hover { background: var(--dark-cyan-10, rgba(26,151,184,0.10)); }
.ws-opt-avatar {
  width: 28px; height: 28px; border-radius: 7px;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 12px; color: #fff; flex-shrink: 0;
}
.ws-opt-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.ws-opt-name { font-size: 13px; font-weight: 600; color: var(--fg1, #26221f); }
.ws-opt-meta { font-size: 11px; color: var(--fg3, #736c65); }
.ws-opt-check {
  width: 20px; height: 20px; border-radius: 5px;
  border: 2px solid rgba(28,8,0,.22); background: #fff;
  display: flex; align-items: center; justify-content: center;
  color: #fff; flex-shrink: 0; transition: all var(--dur-fast, 150ms);
}
.ws-check-glyph { width: 14px; height: 14px; display: block; stroke-width: 3; }
.ws-opt.checked .ws-opt-check { background: var(--cyan-500, #1a97b8); border-color: var(--cyan-500, #1a97b8); }
.ws-empty { padding: 8px; font-size: 12px; color: var(--fg3, #736c65); text-align: center; }
.ws-done {
  width: 100%; margin-top: 6px; height: 32px; border: 0;
  border-radius: 8px; background: var(--cyan-500, #1a97b8);
  color: var(--fg-on-cyan, #fff); font-weight: 600; font-size: 12px;
  font-family: var(--font-sans, 'Plus Jakarta Sans', sans-serif); cursor: pointer;
}
.ws-done:hover { background: var(--cyan-600, #1888a6); }

/* ── Scroll area ─────────────────────────────────────────────────────────── */
.nav-scroll {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  padding: 12px; scrollbar-width: thin;
  scrollbar-color: rgba(28,8,0,.1) transparent;
}

/* ── New Capture ─────────────────────────────────────────────────────────
   Rendered only when <flo-nav capture>, always as the first child of the
   first group. */
/* ── New Capture ──
   Not here. It is `.top-capture` in the bar, on every surface, unconditionally
   (Greg, 2026-08-05: "I don't want variations"). The rail's dashed drop zone
   and its .capture-* rules are deleted rather than left dormant: a rule that
   can still style a button is a rule that will, the day someone re-adds one. */

/* ── Group + the divider between pillars ─────────────────────────────────
   Greg 2026-07-29: very light gray line above each pillar, with padding.
   :first-child stays clean so the rail never opens with a rule. */
.nav-group {
  margin-top: 14px;
  border-top: 1px solid var(--border-subtle, rgba(28,8,0,0.10));
  padding-top: 14px;
}
.nav-group:first-child { margin-top: 0; border-top: none; padding-top: 0; }

/* Pillar hue, resolved once per group and inherited by the chip and the
   label. Both token spellings, so one rule serves app and prototypes. */
.nav-group[data-pillar="capture"] { --pillar-color: var(--pillar-capture, var(--capture)); }
.nav-group[data-pillar="store"]   { --pillar-color: var(--pillar-store,   var(--store)); }
.nav-group[data-pillar="think"]   { --pillar-color: var(--pillar-think,   var(--think)); }
.nav-group[data-pillar="achieve"] { --pillar-color: var(--pillar-achieve, var(--achieve)); }
.nav-group[data-pillar="grow"]    { --pillar-color: var(--pillar-grow,    var(--grow)); }

.group-header {
  display: flex; align-items: center;
  box-sizing: border-box; height: var(--rail-group-h);
  /* Derived, never declared. See --rail-icon-axis. */
  padding: 4px 0 6px calc(var(--rail-icon-axis) - var(--rail-pad) - var(--rail-chip-size) / 2);
  gap: calc(var(--rail-label-x) - var(--rail-icon-axis) - var(--rail-chip-size) / 2);
  background: none; border: none; margin: 0;
}
.group-label {
  font-size: var(--label-uppercase-size, 11px);
  font-weight: var(--fw-bold, 700);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--pillar-color, var(--cyan-700, #157a94));
  flex: 1; white-space: nowrap; overflow: hidden;
}

/* ── Pillar chip: 28px circle around a 14px masked glyph ─────────────────
   The 7px ring this produces is the exact measurement that drifted between
   the reference (28/14) and the FloSync panel (22/13) and is what Greg spotted
   by eye. Change these two numbers here or nowhere.

   The mask URLs are relative to THIS stylesheet, which is what makes them
   resolve identically from /nav/flo-nav.css on every surface — the app used
   to point at /brand/icons/ and the prototypes at ../_design-system/assets/,
   two paths to byte-identical files. */
.pillar-chip {
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: color-mix(in srgb, var(--pillar-color) 20%, rgba(255,255,255,.55));
  border: 1px solid color-mix(in srgb, var(--pillar-color) 30%, rgba(255,255,255,.4));
  /* 28px -> 24px on collapse, on the rail's clock. DS-4 asserts the 28px
     resting size; only the journey between the two sizes is timed here. */
  transition:
    background var(--dur-med, 250ms),
    width var(--rail-dur) var(--rail-ease),
    height var(--rail-dur) var(--rail-ease);
}
.pillar-chip-icon {
  width: 14px; height: 14px;
  display: inline-flex; flex-shrink: 0;
  background-color: var(--pillar-color);
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: contain;     mask-size: contain;
}
.nav-group[data-pillar="capture"] .pillar-chip-icon {
  -webkit-mask-image: url('./icons/pillar-capture-icon.svg'); mask-image: url('./icons/pillar-capture-icon.svg');
}
.nav-group[data-pillar="store"] .pillar-chip-icon {
  -webkit-mask-image: url('./icons/pillar-store-icon.svg');   mask-image: url('./icons/pillar-store-icon.svg');
}
.nav-group[data-pillar="think"] .pillar-chip-icon {
  -webkit-mask-image: url('./icons/pillar-think-icon.svg');   mask-image: url('./icons/pillar-think-icon.svg');
}
.nav-group[data-pillar="achieve"] .pillar-chip-icon {
  -webkit-mask-image: url('./icons/pillar-achieve-icon.svg'); mask-image: url('./icons/pillar-achieve-icon.svg');
}
.nav-group[data-pillar="grow"] .pillar-chip-icon {
  -webkit-mask-image: url('./icons/pillar-grow-icon.svg');    mask-image: url('./icons/pillar-grow-icon.svg');
}

/* ── Nav item ───────────────────────────────────────────────────────────── */
.item-link, .item-static { display: block; text-decoration: none; }
.item-static { cursor: default; }

.nav-item {
  display: flex; align-items: center;
  /* box-sizing declared, not inherited: the prototypes set `* { border-box }`
     and the app does too, so this read correctly everywhere and would have
     broken on the first surface that did not. Rule 1 — own your inputs. */
  box-sizing: border-box; height: var(--rail-row-h);
  padding: 7px 8px 7px calc(var(--rail-icon-axis) - var(--rail-pad) - var(--rail-glyph-size) / 2);
  gap: calc(var(--rail-label-x) - var(--rail-icon-axis) - var(--rail-glyph-size) / 2);
  margin: 0; border-radius: 8px;
  font-size: 13px; font-weight: 500; color: var(--fg2, #55504a);
  text-decoration: none; position: relative; cursor: pointer;
  /* Hover colour is instant-feeling; the collapse geometry rides the rail's
     own clock so the icon drifts to centre WITH the panel edge rather than
     jumping there ahead of it. */
  transition:
    background var(--dur-fast, 150ms), color var(--dur-fast, 150ms),
    padding var(--rail-dur) var(--rail-ease),
    margin var(--rail-dur) var(--rail-ease),
    gap var(--rail-dur) var(--rail-ease);
}
.nav-item:hover { background: var(--dark-cyan-10, rgba(26,151,184,0.10)); color: var(--fg1, #26221f); }
.nav-item:focus-visible { outline: 2px solid var(--cyan-500, #1a97b8); outline-offset: -2px; border-radius: 8px; }

.item-icon {
  width: 18px; height: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; color: var(--fg3, #736c65);
}
.item-glyph { width: 18px; height: 18px; display: block; }

/* ── Active = FILLED, not just tinted ─────────────────────────────────────
   Greg, 2026-08-01: "when something is the active state on the navigation you
   were supposed to have a fill symbol not the outline symbol."

   The convention predates this — gtm-v1-nav records "FILL 0 at rest, FILL 1
   on .active" — but it was written against Material Symbols as a VARIABLE
   FONT, so it was expressed as `font-variation-settings: 'FILL' 1` on a
   .material-symbols-outlined span. W048 replaced those spans with inline
   Lucide SVG and the rule kept sitting there, matching nothing. It is dead
   code in that file to this day.

   Every nav glyph now ships BOTH outlines (see NAV_ICONS in flo-nav.js), so
   the state change is two opacities. Consequences worth the doubled path
   data: no re-render when the route changes, no second fetch, and the fill
   EASES the way animating a font axis used to — which is what made the
   original feel like the icon thickening rather than swapping.

   Not `display`, deliberately: display cannot transition, and a hard swap
   between two shapes of the same icon reads as a flicker. */
.item-glyph .glyph-fill { opacity: 0; }
.item-glyph .glyph-rest { opacity: 1; }
.item-glyph .glyph-rest,
.item-glyph .glyph-fill {
  transition: opacity var(--dur-fast, 150ms) var(--ease-out-expo, cubic-bezier(.16,1,.3,1));
}
.nav-item.active .item-glyph .glyph-rest { opacity: 0; }
.nav-item.active .item-glyph .glyph-fill { opacity: 1; }
.item-label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-badge {
  font-size: 10px; font-weight: 600; padding: 1px 7px;
  border-radius: var(--radius-pill, 9999px); min-width: 18px; text-align: center;
  background: var(--dark-cyan-15, rgba(26,151,184,0.15)); color: var(--cyan-700, #157a94);
}

/* ── Render tiers (nav-groups.json `tier`: live | paid | later) ───────────
   Three shades, not two, per Greg 2026-07-29. `paid` is the W038
   grayed-until-activated buy-this tile; `later` is fainter still and carries
   NO purchase affordance, because grey already means "unlock" everywhere else
   in the product and an unbuilt feature drawn at the paid weight reads as a
   promise.

   Keep the gap between .48 and .28 wide. At ~.40 the two stop reading as
   different states against the warm surface and the distinction is gone.
   `.aspirational` is the legacy alias, held equal to `later`. */
.nav-item.aspirational,
.nav-item.tier-later { opacity: .28; pointer-events: none; cursor: default; }
.nav-item.tier-paid {
  opacity: .48; pointer-events: none; cursor: default;
  border-right: 2px solid color-mix(in srgb, var(--pillar-think, var(--think)) 55%, transparent);
}
.nav-item.tier-later:hover,
.nav-item.tier-paid:hover { background: transparent; }

/* ── Active item ───────────────────────────────────────────────────────── */
.nav-item.active {
  background: rgba(28,8,0,.05);
  color: var(--fg1, #26221f);
  font-weight: 600;
}
.nav-item.active .item-icon { color: var(--cyan-600, #1888a6); }
[data-theme="dark"] .nav-item.active { background: rgba(255,255,255,.07); }
[data-theme="dark"] .nav-item.active .item-icon { color: var(--cyan-400, #4eb5d4); }

/* ── Data-load failure ────────────────────────────────────────────────────
   Visible, not silent. A rail that renders nothing looks like a layout choice. */
.flo-nav-error {
  padding: 12px; font-size: 12px; line-height: 1.4;
  color: var(--pillar-achieve, var(--achieve, #b2362b));
  font-family: var(--font-sans, 'Plus Jakarta Sans', sans-serif);
}

/* Vestibular safety valve. Half a second of gliding panel is exactly the kind
   of motion this media query exists for, so the whole rail transition — not
   just the old short one — collapses to an instant state change here. */
@media (prefers-reduced-motion: reduce) {
  .sidebar, .nav-item, .ws-bar, .ws-opt, .pillar-chip,
  .group-header, .item-label, .item-badge, .group-label,
  .ws-label, .ws-badge, .ws-chevron,
  .item-glyph .glyph-rest, .item-glyph .glyph-fill {
    transition: none;
  }
}

/* ==================================================================
   Product lockup — the Flourisha icon plus a product name
   ==================================================================
   FloSYNC, FloBooks, FloDOC. Greg, 2026-08-01: on a product surface the
   Flourisha WORDMARK is replaced by the Flourisha ICON and the product's
   own name, in the top bar.

   Why it lives here and not in the product's page: FloSYNC had been drawing
   its own masthead below the bar with its own gradient square, which is the
   drift this whole file exists to end. The lockup is identical on every
   product; only the word changes.
   ------------------------------------------------------------------ */
.top-header-brand { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
.top-header-icon  { height: 26px; width: auto; display: block; }
.top-header-product {
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--fg1, #26221f);
  line-height: 1;
  white-space: nowrap;
}
/* Two-tone, matching the masthead lockup this replaced: "Flo" in Flourisha
   blue, the product half in its own pillar colour. FloSYNC reads as part of a
   family rather than as a separate app, which is the thing that kept going
   wrong. The element's text is still the single string "FloSYNC". */
.top-header-product .pm-flo  { color: var(--cyan-600, #1888a6); font-weight: 700; }
.top-header-product .pm-rest { color: var(--olive-500, #888d31); font-weight: 800; }

/* ==================================================================
   <flo-env> — the environment bar
   ==================================================================
   Sits above everything. Says which environment this is, and on a
   prototype lists the pages that exist so you can move between them
   without knowing the URLs.

   Production renders NOTHING, so this is safe to leave mounted in the
   shared shell forever.
   ------------------------------------------------------------------ */
.env-bar {
  /* Sticky + a high z-index because several prototypes pin their rail at
     `top: 0; z-index: 20` and painted straight over this bar, hiding the
     PROTOTYPE badge and the first three page links. The one strip that says
     "not production" must not be something a page can cover by accident. */
  position: sticky;
  top: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  gap: 12px;
  height: 32px;
  flex-shrink: 0;
  padding: 0 12px;
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 12px;
  line-height: 1.5;
  color: var(--fg1, #26221f);
  background: var(--orange-500, #f09029);
  border-bottom: 1px solid rgba(28, 8, 0, 0.14);
  overflow-x: auto;
  scrollbar-width: none;
}
.env-bar::-webkit-scrollbar { display: none; }
/* Staging is the one people confuse with production, so it gets the loudest
   treatment rather than the friendliest. */
.env-bar[data-env="staging"] { background: var(--achieve, #b2362b); color: #fdfbf7; }
.env-bar[data-env="staging"] .env-badge { background: rgba(253, 251, 247, 0.22); color: #fdfbf7; }
.env-bar[data-env="staging"] .env-note  { color: rgba(253, 251, 247, 0.85); }

.env-badge {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: 4px;
  background: rgba(28, 8, 0, 0.16);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.env-links { display: flex; align-items: center; gap: 2px; min-width: 0; }
.env-link {
  padding: 3px 8px;
  border-radius: 5px;
  color: inherit;
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0.82;
}
.env-link:hover  { background: rgba(28, 8, 0, 0.10); opacity: 1; }
.env-link.active { background: rgba(253, 251, 247, 0.55); opacity: 1; }
/* A page still being worked on is marked, not hidden. Hiding it is how you
   end up with a prototype nobody can find. */
.env-link[data-status="wip"]::after { content: ' •'; opacity: 0.7; }
.env-link[data-status="ref"] { font-style: italic; }
.env-spacer { flex: 1 1 auto; min-width: 8px; }
.env-note {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 600;
  opacity: 0.78;
  white-space: nowrap;
}

/* ==================================================================
   MEASUREMENT BARS — .flo-meter
   ==================================================================
   Greg, 2026-08-01: "I like the multi coloured measurement bars, and that
   should be a global component that's referenced and styled globally so that
   if I say actually I was wrong I don't like the multi coloured measurement
   bar, I can switch it in one location and the entire set of applications
   gets an upgrade."

   THE ONE LINE THAT CHANGES EVERY BAR EVERYWHERE is --meter-fill, below.
   Set it to a flat colour and every meter in every app goes flat:

     :root { --meter-fill: var(--cyan-500); }

   ── WHY THE INDIRECTION LAYER ─────────────────────────────────────
   The productivity report drew these with
   `linear-gradient(90deg, var(--accent), var(--flagship))`, and NEITHER of
   those tokens is canonical -- they are defined in that page's own :root and
   nowhere else. Lifting that gradient as-is would have resolved to the real
   colours in the report and to hardcoded fallbacks everywhere else: rule 1,
   "a file must own its own inputs", the same bug that once drew the rail in
   Times New Roman. So the component reads canonical tokens (--cyan-500,
   --orange-500) and republishes them as --meter-*, which is the surface a
   theme actually overrides.

   ── THEMING, INCLUDING PER WORKSPACE ──────────────────────────────
   Greg: "people are gonna have their own personal preferences ... I better be
   able to change it to my own personalised warm theme or cool theme or grey
   theme, and even have that different by the workspace I'm in."

   Every value here is a custom property, so it re-resolves at any depth. That
   gives three scopes for free, narrowest wins, with no JS and no rebuild:

     :root                        the product default
     [data-theme="warm"]          the person's chosen theme (html attribute)
     [data-workspace-theme="hgi"] one workspace, overriding inside its subtree

   Only the light theme is tuned today, on purpose. The point is that the next
   five are a block of variables each, not an edit to every page that draws a
   bar.
   ------------------------------------------------------------------ */
:root {
  --meter-track:  var(--surface-3, #f1e9d6);
  --meter-from:   var(--cyan-500, #1a97b8);
  --meter-to:     var(--orange-500, #f09029);
  --meter-angle:  90deg;
  --meter-fill:   linear-gradient(var(--meter-angle), var(--meter-from), var(--meter-to));
  --meter-size:   5px;    /* thickness across the bar */
  --meter-radius: 999px;
  --meter-len:    92px;   /* length along the bar, horizontal only */
  /* What a meter becomes when it sits ON an accent-filled surface (a selected
     day cell), where the gradient loses all contrast. A token, not a hardcoded
     #fff in the host page: a theme that inverts has to be able to move this
     too, or one bar in the set stops answering to the theme. Found by
     asserting "every bar recolours from one declaration" and getting 98/99. */
  --meter-fill-on-accent: #fff;
}

.flo-meter {
  display: inline-block;
  width: var(--meter-len);
  height: var(--meter-size);
  border-radius: var(--meter-radius);
  background: var(--meter-track);
  overflow: hidden;
  flex-shrink: 0;
  vertical-align: middle;
}
.flo-meter-fill {
  display: block;
  height: 100%;
  /* The value is data, so it arrives as a property on the element
     (style="--meter-value: 62%") rather than as one of twelve hand-written
     width classes, which is what the report had: .p1 through .p12 plus
     .pw-2 through .pw-100. Those are values pretending to be styles. */
  width: var(--meter-value, 0%);
  border-radius: inherit;
  background: var(--meter-fill);
}

/* Vertical, for a sparkline column. Same tokens, same fill, so a change to
   --meter-fill moves both without anyone remembering the second one exists. */
.flo-meter[data-orient="v"] {
  --meter-angle: 180deg;
  width: var(--meter-size);
  height: var(--meter-len);
  display: flex;
  align-items: flex-end;
}
.flo-meter[data-orient="v"] .flo-meter-fill {
  width: 100%;
  height: var(--meter-value, 0%);
  min-height: 3px;
}

/* On a selected/inverted surface the gradient loses contrast, so the meter
   flips to a single readable colour. One rule, not one per host page. */
.flo-meter[data-on-accent="true"] { --meter-track: rgba(255, 255, 255, 0.28); }
.flo-meter[data-on-accent="true"] .flo-meter-fill { background: var(--meter-fill-on-accent); }

/* ==================================================================
   --font-voice — Flourisha explaining herself
   ==================================================================
   Greg, 2026-08-01, confirming what this face MEANS rather than just where it
   came from: "Explaining herself in her own voice."

   So the rule is semantic, not decorative. Satisfy is for text where FLOURISHA
   is speaking about what she did or why: a per-record explanation, a rationale,
   an "I matched these because...". It is never for data the user typed or the
   system recorded -- file paths, filenames, counts, timestamps, headers,
   buttons, the product wordmark. If a human wrote it or a machine measured it,
   it is not her voice.

   Applied through --font-voice rather than by naming Satisfy at each call site,
   so the day the face changes it changes once. Same reasoning as --meter-fill:
   Greg has to be able to re-decide this in one place.

   The WOFF2 lives with the other brand assets and is reached through the same
   /nav/ symlink every surface already uses, so a prototype and the app load one
   file rather than each finding their own copy.
   ------------------------------------------------------------------ */
@font-face {
  font-family: 'Satisfy';
  src: url('fonts/Satisfy-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-voice: 'Satisfy', cursive;
  --voice-color: var(--cyan-600, #1888a6);
  --voice-size: 14px;
}

/* Use this class, do not name the font. */
.flo-voice {
  font-family: var(--font-voice);
  font-size: var(--voice-size);
  line-height: 1.3;
  letter-spacing: normal;
  color: var(--voice-color);
}

/* ==================================================================
   .flo-group — the grouping panel
   ==================================================================
   Greg: "we really refined a lot of the cards and grouping panel for new file
   uploads versus new file downloads. A nice use of the colour palette."

   The refinement was real; the SHAPE was the problem. It was five hand-written
   pairs of rules -- .uploads-wrap, .downloads-wrap, .deletions-wrap,
   .conflicts-wrap, .moves-wrap -- each repeating the same border, background,
   radius and padding with a different colour. Five today, and every new kind of
   change is a sixth pair someone has to remember to write the same way.

   One component, keyed by what the group IS. A new kind is three tokens, not a
   new rule pair, and restyling every panel everywhere is one edit to .flo-group.

     <section class="flo-group" data-kind="upload">

   KIND, NOT COLOUR, is the attribute on purpose. `data-kind="upload"` survives
   Greg deciding uploads should be olive; `class="cyan-panel"` would not. This
   is rule 4 again: a surface says what a thing IS, never how to draw it.
   ------------------------------------------------------------------ */
:root {
  /* Each kind is a bg / border / text triple. A theme overrides these; the
     component below never names a colour. */
  --group-upload-bg:     rgba(26, 151, 184, 0.08);
  --group-upload-border: rgba(26, 151, 184, 0.30);
  --group-upload-text:   var(--cyan-700, #157a94);
  --group-download-bg:     rgba(136, 141, 49, 0.08);
  --group-download-border: rgba(136, 141, 49, 0.30);
  --group-download-text:   var(--olive-500, #888d31);
  --group-danger-bg:     rgba(195, 45, 23, 0.07);
  --group-danger-border: rgba(195, 45, 23, 0.28);
  --group-danger-text:   var(--lava-500, #c32d17);
  --group-caution-bg:     rgba(232, 185, 20, 0.10);
  --group-caution-border: rgba(232, 185, 20, 0.32);
  --group-caution-text:   #8a6a06;
  --group-neutral-bg:     rgba(28, 8, 0, 0.04);
  --group-neutral-border: rgba(28, 8, 0, 0.14);
  --group-neutral-text:   var(--fg2, #55504a);

  --group-radius:  14px;
  --group-pad:     4px;
  --group-gap:     18px;
}

.flo-group {
  /* Defaults, so an unknown kind renders as a plain panel rather than
     collapsing to an unstyled div. */
  --group-bg:     var(--group-neutral-bg);
  --group-border: var(--group-neutral-border);
  --group-text:   var(--group-neutral-text);

  margin-top: var(--group-gap);
  border: 1px solid var(--group-border);
  background: var(--group-bg);
  border-radius: var(--group-radius);
  padding: var(--group-pad);
}
.flo-group[data-kind="upload"]   { --group-bg: var(--group-upload-bg);   --group-border: var(--group-upload-border);   --group-text: var(--group-upload-text); }
.flo-group[data-kind="download"] { --group-bg: var(--group-download-bg); --group-border: var(--group-download-border); --group-text: var(--group-download-text); }
.flo-group[data-kind="delete"]   { --group-bg: var(--group-danger-bg);   --group-border: var(--group-danger-border);   --group-text: var(--group-danger-text); }
.flo-group[data-kind="conflict"] { --group-bg: var(--group-caution-bg);  --group-border: var(--group-caution-border);  --group-text: var(--group-caution-text); }
.flo-group[data-kind="move"]     { --group-bg: var(--group-neutral-bg);  --group-border: var(--group-neutral-border);  --group-text: var(--group-neutral-text); }

/* The group's own heading takes its kind's colour without the host page
   restating it per kind. */
.flo-group-head { color: var(--group-text); font-weight: 700; }

/* A surface's own prototype controls, adopted into the bar. Sized down to the
   bar's scale so a page's button does not stretch the strip. */
/* An adopted control arrives with the padding, border, radius and shadow it
   needed as a floating panel. In the bar it is not a panel any more, so those
   are stripped: the FloSYNC switcher measured 37px tall inside a 32px strip
   and hung 3px out of both edges. The bar owns the chrome; the page keeps
   only its buttons and their behaviour. */
.env-extra { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* .env-bar .env-extra > * (0,2,0), not .env-extra > * (0,1,0). A host page's
   own `.scenario-bar` rule is also (0,1,0) and its inline <style> comes AFTER
   this linked sheet, so on a tie the page wins and the reset silently does
   nothing. The control stayed 37px in a 32px bar until this was specific
   enough to beat it. */
.env-bar .env-extra > * {
  display: flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 0; margin: 0;
  box-shadow: none; backdrop-filter: none; border-radius: 0;
  position: static; transform: none;
}
.env-bar .env-extra button,
.env-bar .env-extra .scenario-pill {
  font: inherit; font-size: 11px; font-weight: 700;
  padding: 3px 9px; border-radius: 5px;
  border: 1px solid rgba(28, 8, 0, 0.18);
  background: rgba(253, 251, 247, 0.55);
  color: inherit; cursor: pointer; white-space: nowrap;
}
.env-bar .env-extra button:hover, .env-bar .env-extra .scenario-pill:hover { background: rgba(253, 251, 247, 0.85); }
.env-bar .env-extra [aria-pressed="true"] { background: var(--fg1, #26221f); color: var(--bg-base, #fdfbf7); border-color: transparent; }
.env-bar .env-extra .lbl { font-size: 10px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }
