/* styles/shell.css */
/* NEXORA frame: plain-CSS translation of src/routes/__root.tsx + Sidebar.tsx + Topbar.tsx + styles.css.
   Layer order mirrors Tailwind 4 (base < utilities); the page stylesheets and nexora-cluster.css are
   un-layered, exactly as the React build cascades them.
   Radii follow the the reference design theme (--radius 1rem): rounded-md 12, -lg 16, -xl 22, -2xl 28, -3xl 36. */

@layer base, utilities;

/* ---- tokens (styles.css :root, verbatim) ---------------------------------------------------
   This block is the dashboard's ONE source of colour. Nothing below it and nothing in any page
   stylesheet, script or template holds a colour of its own: every one of them resolves back to a
   token declared here, so changing a palette here changes the whole dashboard.
   The block has three parts:
     1. the palette itself (the reference design's styles.css :root), plus the few names the ports needed;
     2. a derived layer, which builds the shadows, the glow, the canvas wash, the instrument
        cluster's skin and Agent City's campus out of that palette;
     3. an Ember block that pins the derived families to the exact values the pages were ported
        against, so the default palette is unchanged to the pixel.
   A theme (styles/theme.css) redeclares part 1 and inherits part 2. */
:root{
  --radius:1rem;
  --radius-sm:calc(var(--radius) - 8px);
  --radius-md:calc(var(--radius) - 4px);
  --radius-lg:var(--radius);
  --radius-xl:calc(var(--radius) + 6px);
  --radius-2xl:calc(var(--radius) + 12px);
  --radius-3xl:calc(var(--radius) + 20px);
  --radius-4xl:calc(var(--radius) + 28px);

  /* the reference design's @theme inline: the pairing chosen in Settings wins, the house faces are the fallback,
     so with no choice made these are exactly the three families the pages were ported against. */
  --font-display:var(--app-font-display,"Instrument Serif",ui-serif,Georgia,serif);
  --font-sans:var(--app-font-sans,"Work Sans",ui-sans-serif,system-ui,sans-serif);
  --font-mono:var(--app-font-mono,"IBM Plex Mono",ui-monospace,SFMono-Regular,monospace);

  --background:oklch(0.951 0.014 84);
  --foreground:oklch(0.19 0.008 60);
  --card:oklch(0.985 0.007 88);
  --card-foreground:oklch(0.19 0.008 60);
  --popover:oklch(0.985 0.007 88);
  --popover-foreground:oklch(0.19 0.008 60);
  --primary:oklch(0.155 0.006 60);
  --primary-foreground:oklch(0.97 0.01 85);
  --secondary:oklch(0.925 0.016 84);
  --secondary-foreground:oklch(0.24 0.008 60);
  --muted:oklch(0.928 0.014 84);
  --muted-foreground:oklch(0.52 0.018 68);
  --accent:oklch(0.665 0.238 36);
  /* The accent does two jobs: it fills a button, with --accent-foreground written on top, and it
     colours text and icons directly on pale cards. Those want different colours. As a fill it is
     the brand; as text on white it measured 3.35 to 1 in Ember and 1.65 in Solar Graphite, which
     is a pale smudge rather than a word. This is the same accent darkened just far enough to be
     read, and every palette sets its own: two of the five already passed and are left alone. */
  --accent-text:color-mix(in oklab,var(--accent) 82%,#000);
  /* The third job. --accent-text is the accent darkened for a pale card, and on an ink panel that
     is the wrong direction: the word goes dark on dark. On ink the accent needs no darkening at
     all, so this is the accent itself, and the three palettes whose raw accent still does not read
     on their own ink override it in their own blocks. This is the same defect that made Solar
     Graphite's ink panels mustard, fixed once for every panel instead of one page at a time. */
  --accent-on-ink:var(--accent);
  --accent-foreground:oklch(0.985 0.01 85);
  --destructive:oklch(0.58 0.2 22);
  --destructive-foreground:oklch(0.985 0.01 85);
  --border:oklch(0.882 0.016 84);
  --input:oklch(0.882 0.016 84);
  --ring:oklch(0.665 0.238 36);
  --ink:oklch(0.155 0.006 60);
  --ink-elevated:oklch(0.225 0.008 62);
  --ink-foreground:oklch(0.965 0.012 86);
  --ink-muted:oklch(0.68 0.014 76);
  --void:oklch(0.215 0.022 245);
  --void-foreground:oklch(0.965 0.012 86);
  --void-muted:oklch(0.62 0.018 76);
  --bone:oklch(0.985 0.007 88);
  --bone-deep:oklch(0.915 0.018 84);
  --chart-1:oklch(0.665 0.238 36);
  --chart-2:oklch(0.155 0.006 60);
  --chart-3:oklch(0.82 0.13 58);
  --chart-4:oklch(0.9 0.045 70);
  --chart-5:oklch(0.86 0.03 84);
  --sidebar:oklch(0.966 0.011 86);
  --sidebar-foreground:oklch(0.19 0.008 60);
  --sidebar-border:oklch(0.882 0.016 84);

  /* names the ports needed that the the reference design root did not have:
       --accent-2      the palette's second accent (the reference design added it; Ember reads it as chart-3)
       --ok            a command that succeeded, in the Terminal
       --theme-shadow  the colour every shadow is cast in
       --canvas-*      the two washes of the page's fixed background gradient
       --feature-*     the reference design's feature-panel family; carried so a palette is complete, and not
                       yet read by any page here */
  --accent-2:oklch(0.82 0.13 58);
  --ok:oklch(0.79 0.1 148);
  --theme-shadow:oklch(0.19 0.008 60);
  --canvas-highlight:oklch(0.995 0.006 88);
  --canvas-tint:oklch(0.9 0.06 55);
  --feature:oklch(0.155 0.006 60);
  --feature-foreground:oklch(0.965 0.012 86);
  --feature-muted:oklch(0.68 0.014 76);
  --feature-elevated:oklch(0.225 0.008 62);

  /* Tailwind's @theme aliases (the React markup reads these inside SVG) */
  --color-background:var(--background);
  --color-foreground:var(--foreground);
  --color-card:var(--card);
  --color-secondary:var(--secondary);
  --color-muted-foreground:var(--muted-foreground);
  --color-accent:var(--accent);
  --color-accent-foreground:var(--accent-foreground);
  --color-border:var(--border);
  --color-ink:var(--ink);
  --color-ink-foreground:var(--ink-foreground);
  --color-ink-muted:var(--ink-muted);
}

/* ---- derived: everything a palette does not state outright -----------------------------------
   A theme sets the palette above and inherits this whole block, so a new palette never has to
   restate a shadow, a glow or a campus. Where a value used to be a colour at an alpha, it is now
   oklch(from var(--token) l c h / a): the same channels, only the alpha replaced, so the value
   follows its token and still renders to the same pixel. color-mix() is kept for the places the
   Tailwind build already used it, and for genuine blends of two colours. */
:root{
  --shadow-soft:0 1px 2px oklch(from var(--theme-shadow) l c h / .04),0 18px 44px oklch(from var(--theme-shadow) l c h / .06);
  --shadow-lift:0 2px 4px oklch(from var(--theme-shadow) l c h / .05),0 28px 60px oklch(from var(--theme-shadow) l c h / .1);
  --glow-accent:radial-gradient(circle at 50% 40%,oklch(from var(--accent) l c h / .4) 0%,oklch(from var(--accent) l c h / .08) 42%,transparent 70%);
  --gradient-canvas:radial-gradient(120% 70% at 50% -20%,var(--canvas-highlight) 0%,transparent 55%),radial-gradient(90% 60% at 95% 0%,oklch(from var(--canvas-tint) l c h / .35) 0%,transparent 60%);

  /* The ink family where it lands on a CARD rather than on an ink panel. In a light palette the
     two are the same thing: ink is near-black and reads on near-white. In a dark palette they are
     opposites, and the panel colours sink into the card and disappear, which is how Midnight lost
     five readings on the cockpit. Ember pins both back to the ink family exactly. */
  --ink-on-card:var(--card-foreground);
  --ink-muted-on-card:var(--muted-foreground);

  /* The instrument cluster (styles/nexora-cluster.css) came from the previous NEXORA dashboard,
     which had its own near-neighbours of this palette: a slightly different ink, a slightly
     different orange. They keep their own names so the port stays exact, and a palette moves them
     with the family they sit beside. */
  --nx-ink:var(--ink);
  --nx-ink-elevated:var(--ink-elevated);
  --nx-ink-foreground:var(--ink-foreground);
  --nx-accent:var(--accent);
  --nx-accent-2:var(--accent-2);
  --nx-orange:var(--accent);
  --nx-shell:var(--ink);                       /* the cockpit's outer edge */
  --nx-highlight:var(--ink-foreground);        /* the thin light along its top lip */
  --nx-well:var(--theme-shadow);               /* the inner shadow that sinks it */
  --nx-pill:var(--ink-foreground);             /* the status pill's wash and rule */
  --nx-pill-border:var(--ink-muted);

  /* Agent City's campus: a ground, the chrome around the stage, and the dark glass cards that sit
     on top of it. scripts/city.js reads the --city3d-* half of this for the WebGL scene, so the
     3D campus is themed from here too and not from colours buried in the script. */
  --city-campus:var(--secondary);
  --city-chrome:var(--card);
  --city-chrome-border:var(--border);
  --city-chrome-ink:var(--muted-foreground);
  --city-btn:var(--secondary);
  --city-btn-border:var(--border);
  --city-btn-ink:var(--foreground);
  --city-pin:oklch(from var(--card) l c h / .94);
  --city-pin-border:var(--border);
  --city-pin-ink:var(--foreground);
  --city-divider:var(--border);
  --city-footer-eyebrow:var(--accent);
  --city-footer-body:var(--muted-foreground);
  --city-footer-head:var(--foreground);
  --city-footer-value:var(--accent);
  --city-glass:var(--ink);                     /* the dossier and the live feed */
  --city-shadow:var(--theme-shadow);           /* what those two cards cast on the campus */
  --city-glass-ink:var(--ink-foreground);
  --city-glass-body:var(--ink-muted);
  /* the dossier's initials tile: always a pale tile with dark lettering, whatever the palette,
     because the two sit on each other and nothing else can guarantee they stay apart */
  --city-badge:var(--ink-foreground);
  --city-badge-ink:var(--ink);
  --city-badge-border:var(--ink-muted);
  /* these three sit on the dark glass cards, so they are mixed toward the light end: a palette
     whose accent is itself dark (Light Blue's cobalt) would otherwise put dark on dark */
  --city-dossier-eyebrow:color-mix(in oklab,var(--accent) 45%,var(--ink-foreground));
  --city-feed-eyebrow:color-mix(in oklab,var(--accent) 55%,var(--ink-foreground));
  --city-feed-sub:var(--ink-muted);
  --city-feed-em:color-mix(in oklab,var(--accent) 40%,var(--ink-foreground));
  --city-work:var(--accent-2);                 /* the pulse on an agent that is working */
  --city-accent:var(--accent);                 /* the selected pin, the handoff badge, the chat bubble */
  --city-accent-hover:color-mix(in oklab,var(--accent) 86%,var(--ink));
  --city-accent-ink:var(--accent-foreground);
  --city-pop-shadow:color-mix(in oklab,var(--accent) 60%,var(--theme-shadow));

  /* The WebGL campus. The three lights stay light in every palette (a palette tints them, it does
     not dim them), and every material is placed a measured distance from the campus ground along
     the ground-to-foreground axis. That way a dark palette lifts the towers off a dark campus by
     exactly as much as a light one settles them onto a light campus, instead of losing them. */
  --city3d-sky:var(--ink-foreground);
  --city3d-sun:var(--ink-foreground);
  --city3d-fill:var(--ink-foreground);
  --city3d-bounce:var(--muted-foreground);
  --city3d-ground:var(--city-campus);
  --city3d-building:color-mix(in oklab,var(--city-campus) 78%,var(--ink-foreground));
  --city3d-roof:color-mix(in oklab,var(--city-campus) 82%,var(--foreground));
  --city3d-glass:color-mix(in oklab,var(--city-campus) 45%,var(--foreground));
  --city3d-road:color-mix(in oklab,var(--city-campus) 25%,var(--foreground));
  --city3d-land:color-mix(in oklab,var(--city-campus) 88%,var(--foreground));
  --city3d-water:color-mix(in oklab,var(--city-campus) 70%,var(--foreground));
  --city3d-tree:color-mix(in oklab,var(--city-campus) 55%,var(--foreground));
  --city3d-trim:color-mix(in oklab,var(--city-campus) 66%,var(--foreground));
  --city3d-park:color-mix(in oklab,var(--city-campus) 76%,var(--foreground));
  --city3d-path:color-mix(in oklab,var(--city-campus) 60%,var(--ink-foreground));
  --city3d-pebble:color-mix(in oklab,var(--city-campus) 60%,var(--foreground));
  --city3d-lit:var(--accent);                  /* a tower with an agent awake in it */
  --city3d-lit-glow:var(--accent);
  --city3d-window:color-mix(in oklab,var(--accent) 82%,var(--ink));
  --city3d-window-glow:var(--accent);
  --city3d-work:var(--city-work);

  --nx-glow:rgb(from var(--nx-orange) r g b / .28);        /* the light bar under the cluster */
  --nx-dot-halo:rgb(from var(--nx-orange) r g b / .14);
  --nx-ring-wash:rgb(from var(--nx-orange) r g b / .055);
  --nx-lip:rgb(from var(--nx-highlight) r g b / .035);     /* the thin light along the top edge */
  --nx-sink:rgb(from var(--nx-well) r g b / .26);          /* the shadow that hollows the cockpit */
  --nx-pill-wash:rgb(from var(--nx-pill) r g b / .075);
  --nx-pill-rule:rgb(from var(--nx-pill-border) r g b / .34);
  --city-card:rgb(from var(--city-glass) r g b / .9);      /* the dossier */
  --city-card-rule:rgb(from var(--city-glass-ink) r g b / .12);
  --city-tile:rgb(from var(--city-glass-ink) r g b / .045);
  --city-card-shadow:rgb(from var(--city-shadow) r g b / .4);
  --city-feed:rgb(from var(--city-glass) r g b / .84);     /* the live feed */
  --city-feed-rule:rgb(from var(--city-glass-ink) r g b / .1);
  --city-pop-glow:rgb(from var(--city-pop-shadow) r g b / .28);
  --city-work-halo:rgb(from var(--city-work) r g b / .45);
  --city-work-halo-out:rgb(from var(--city-work) r g b / 0);

  /* the two scrollbar thumbs Missions asks for: one on an ink panel, one on a light one */
  --scroll-thumb-dark:oklch(from var(--ink-foreground) l c h / .45);
  --scroll-thumb-light:oklch(from var(--theme-shadow) l c h / .25);
}

/* Ember, the palette every page was ported against, pinned to the exact values it was measured at.
   The instrument cluster and Agent City carry their own greys and oranges here rather than the
   derived ones above, because the two ports are a pixel-for-pixel copy of the old dashboard and a
   derived near-miss would move them. Every other palette takes the derived layer as it stands.
   The bare :root:not([data-theme]) arm covers the moment before the theme script has run. */
:root[data-theme="ember"],
:root:not([data-theme]){
  --ink-on-card:var(--ink);
  --ink-muted-on-card:var(--ink-muted);

  --nx-ink:oklch(0.16 0.012 50);
  --nx-ink-elevated:oklch(0.22 0.014 50);
  --nx-ink-foreground:oklch(0.97 0.012 75);
  --nx-accent:oklch(0.66 0.22 38);
  --nx-accent-2:oklch(0.78 0.16 50);
  --nx-orange:#e25822;
  --nx-shell:#0b0908;
  --nx-highlight:#fff2de;
  --nx-well:#000;
  --nx-pill:#e8d8b4;
  --nx-pill-border:#cfb89e;

  --city-campus:#eee8df;
  --city-chrome:#fcfaf5;
  --city-chrome-border:#d6cbbb;
  --city-chrome-ink:#685c52;
  --city-btn:#f3eee5;
  --city-btn-border:#d7c9bc;
  --city-btn-ink:#48382c;
  --city-pin:rgba(252,250,245,.94);
  --city-pin-border:#cdbbad;
  --city-pin-ink:#382a20;
  --city-divider:#ddd0c4;
  --city-footer-eyebrow:#9c4d2b;
  --city-footer-body:#64584e;
  --city-footer-head:#241b15;
  --city-footer-value:#a8441b;
  --city-glass:#110b08;
  --city-shadow:#000;
  --city-glass-ink:#fff;
  --city-glass-body:#a9958a;
  --city-badge:oklch(0.93 0.012 75);
  --city-badge-ink:oklch(0.16 0.012 50);
  --city-badge-border:oklch(0.85 0.012 70);
  --city-dossier-eyebrow:#cf8c69;
  --city-feed-eyebrow:#f28c5e;
  --city-feed-sub:#8f7d72;
  --city-feed-em:#f4a47c;
  --city-work:#2b5fc4;
  --city-accent:#e25822;
  --city-accent-hover:#c94a17;
  --city-accent-ink:#fff;
  --city-pop-shadow:#5a230a;

  --city3d-ground:#eee8df;
  --city3d-sky:#fffaf6;
  --city3d-bounce:#827970;
  --city3d-sun:#fff7ed;
  --city3d-fill:#ffffff;
  --city3d-building:#eeeae5;
  --city3d-roof:#c6bdb3;
  --city3d-glass:#746a60;
  --city3d-road:#494744;
  --city3d-land:#c8beb3;
  --city3d-water:#bcb6ae;
  --city3d-tree:#958a7e;
  --city3d-trim:#b8aa9b;
  --city3d-park:#bfb4a8;
  --city3d-path:#e5dfd7;
  --city3d-pebble:#a89b8e;
  --city3d-lit:#e96c08;
  --city3d-lit-glow:#ff5a00;
  --city3d-window:#bd5006;
  --city3d-window-glow:#ff5c00;
  --city3d-work:#2b5fc4;

  --nx-glow:rgba(226,88,34,.28);
  --nx-dot-halo:rgba(226,88,34,.14);
  --nx-ring-wash:rgba(226,88,34,.055);
  --nx-lip:rgba(255,242,222,.035);
  --nx-sink:rgba(0,0,0,.26);
  --nx-pill-wash:rgba(232,216,180,.075);
  --nx-pill-rule:rgba(207,184,158,.34);
  --city-card:rgba(17,11,8,.9);
  --city-card-rule:rgba(255,255,255,.12);
  --city-tile:rgba(255,255,255,.045);
  --city-card-shadow:rgba(0,0,0,.4);
  --city-feed:rgba(17,11,8,.84);
  --city-feed-rule:rgba(255,255,255,.1);
  --city-pop-glow:rgba(90,35,10,.28);
  --city-work-halo:rgba(43,95,196,.45);
  --city-work-halo-out:rgba(43,95,196,0);

  --scroll-thumb-dark:rgba(255,255,255,.45);
  --scroll-thumb-light:rgba(0,0,0,.25);
}

/* ---- Tailwind 4 preflight (the parts that shape layout and type) -------------------------- */
@layer base{
  *,::after,::before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}
  html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:var(--font-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}
  hr{height:0;color:inherit;border-top-width:1px}
  abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}
  h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}
  a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}
  b,strong{font-weight:bolder}
  code,kbd,samp,pre{font-family:var(--font-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}
  small{font-size:80%}
  sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
  sub{bottom:-.25em}
  sup{top:-.25em}
  table{text-indent:0;border-color:inherit;border-collapse:collapse}
  :-moz-focusring{outline:auto}
  progress{vertical-align:baseline}
  summary{display:list-item}
  ol,ul,menu{list-style:none}
  img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}
  img,video{max-width:100%;height:auto}
  button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:transparent;opacity:1}
  :where(select:is([multiple],[size])) optgroup{font-weight:bolder}
  :where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}
  ::file-selector-button{margin-inline-end:4px}
  ::placeholder{opacity:1}
  @supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}
  textarea{resize:vertical}
  ::-webkit-search-decoration{-webkit-appearance:none}
  ::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}
  ::-webkit-datetime-edit{display:inline-flex}
  ::-webkit-datetime-edit-fields-wrapper{padding:0}
  ::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-block:0}
  :-moz-ui-invalid{box-shadow:none}
  button,input:where([type=button],[type=reset],[type=submit]),::file-selector-button{appearance:button}
  ::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}
  [hidden]:where(:not([hidden=until-found])){display:none!important}

  /* styles.css @layer base */
  *{border-color:var(--color-border)}
  body{background-color:var(--background);background-image:var(--gradient-canvas);background-attachment:fixed;color:var(--foreground);font-family:var(--font-sans);-webkit-font-smoothing:antialiased}
}

/* ---- styles.css utilities + keyframes ------------------------------------------------------ */
@layer utilities{
  .eyebrow{font-family:var(--font-mono);font-size:.625rem;letter-spacing:.18em;text-transform:uppercase;color:var(--muted-foreground)}
  .display{font-family:var(--font-display);font-weight:400;letter-spacing:-.01em;line-height:.95}
  .card-soft{background-color:var(--card);border:1px solid var(--border);border-radius:var(--radius-3xl);box-shadow:var(--shadow-soft)}
  .marquee-track{animation:nexora-marquee 38s linear infinite}
  .live-dot{animation:nexora-pulse 1.8s ease-in-out infinite}
}
.vault-orbit{animation:nexora-orbit 46s linear infinite}
@keyframes nexora-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@keyframes nexora-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.82)}}
@keyframes nexora-orbit{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes mission-grow{from{transform:scaleY(.05);opacity:0}to{transform:scaleY(1);opacity:1}}

/* Settings > Appearance > Density. the reference design's own version only multiplied the root font size by
   0.94, while the control's note promises "tighter panels, more on one screen". The frame's own
   rhythm and the column gap every page root uses come in with it, so the control does what it says.
   Comfortable is the default and is untouched. */
:root[data-density="compact"] .page{gap:14px;padding:18px 16px}
@media(min-width:768px){:root[data-density="compact"] .page{padding:22px 24px}}
:root[data-density="compact"] .page > *{gap:14px}
:root[data-density="compact"] .topbar{gap:12px;padding:14px}
@media(min-width:768px){:root[data-density="compact"] .topbar{padding:14px 24px}}

/* Settings > Appearance > Motion. The switch has to reach the pages themselves, not only the frame,
   so it is written the way the platform's own reduced-motion preference is: everything stops. */
:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after{
  animation-duration:.001ms!important;animation-iteration-count:1!important;
  transition-duration:.001ms!important;scroll-behavior:auto!important}

/* ---- frame: <div class="flex min-h-screen"> ----------------------------------------------- */
.frame{display:flex;min-height:100vh}
.content{min-width:0;flex:1 1 0%}

/* ---- Sidebar.tsx --------------------------------------------------------------------------- */
/* aside: sticky top-0 hidden h-screen shrink-0 flex-col border-r border-border bg-sidebar/70 px-4 py-6 backdrop-blur lg:flex lg:w-[228px] */
.sidebar{position:sticky;top:0;display:none;height:100vh;flex-shrink:0;flex-direction:column;border-right:1px solid var(--border);background-color:color-mix(in oklab,var(--sidebar) 70%,transparent);padding:24px 16px;backdrop-filter:blur(8px)}
@media(min-width:1024px){.sidebar{display:flex;width:228px}}

/* ---- the menu, below 1024px ----
   The sidebar is the only way around this dashboard and it is hidden on anything narrower than a
   laptop, which left a tablet or a phone with no navigation at all: whatever page you landed on
   was the only page you could reach. It slides in as a drawer instead. */
.topbar-menu{display:none;flex:none;width:40px;height:40px;margin-right:2px;border-radius:var(--radius-xl);
  border:1px solid var(--border);background:var(--card);cursor:pointer;
  flex-direction:column;align-items:center;justify-content:center;gap:4px}
.topbar-menu span{display:block;width:16px;height:1.5px;border-radius:2px;background:var(--foreground);transition:transform .22s,opacity .18s}
.topbar-menu[aria-expanded="true"] span:nth-child(1){transform:translateY(5.5px) rotate(45deg)}
.topbar-menu[aria-expanded="true"] span:nth-child(2){opacity:0}
.topbar-menu[aria-expanded="true"] span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg)}
.nx-scrim{position:fixed;inset:0;z-index:120;background:color-mix(in oklab,var(--ink) 55%,transparent);
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:nx-scrim-in .2s ease-out}
.nx-scrim[hidden]{display:none}
@keyframes nx-scrim-in{from{opacity:0}to{opacity:1}}
@media(max-width:1023px){
  .topbar-menu{display:flex}
  html.nx-menu .sidebar{display:flex;position:fixed;left:0;top:0;bottom:0;z-index:130;width:min(284px,82vw);
    height:100dvh;overflow-y:auto;box-shadow:0 30px 80px rgba(0,0,0,.45);animation:nx-drawer-in .24s cubic-bezier(.4,0,.2,1)}
  html.nx-menu{overflow:hidden}
}
@keyframes nx-drawer-in{from{transform:translateX(-100%)}to{transform:none}}

/* The brand is the first item in the rail, so it is padded like one: the same 10px by 12px, the
   same gap and the same corner as every .nav-item below it. At 0 by 8px its hover ground closed on
   the wordmark, and the N sat four pixels left of every nav mark under it. */
.brand{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius-xl);
  transition:background-color .15s cubic-bezier(.4,0,.2,1)}
/* it is a link to the Cockpit on every page and it answered to nothing: pixel for pixel identical
   before and after the pointer arrived */
.brand:hover{background:var(--secondary)}
.brand-mark{display:grid;width:36px;height:36px;place-items:center;border-radius:var(--radius-xl);background:var(--ink);color:var(--ink-foreground)}
.brand-mark-n{font-size:1.125rem;line-height:1}
.brand-name{display:block;font-size:0.875rem;line-height:calc(1.25/.875);font-weight:600;letter-spacing:.22em}

.nav{margin-top:20px;display:flex;flex:1 1 0%;flex-direction:column;gap:4px}
/* idleCls: flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground
   activeCls adds bg-ink text-ink-foreground. Tailwind emits text-ink-foreground before text-muted-foreground,
   so the active item keeps the muted text colour on the ink pill; the hover utilities stay live on it too. */
.nav-item{display:flex;align-items:center;gap:12px;border-radius:var(--radius-xl);padding:10px 12px;font-size:0.875rem;line-height:calc(1.25/.875);color:var(--muted-foreground);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}
.nav-item:hover{background-color:var(--secondary);color:var(--foreground)}
/* The pill goes dark, so its text has to go light with it. It kept the muted grey meant for
   the pale sidebar, which left the page you are actually on the hardest one to read at 3.5:1.
   The hover state is named too, because hovering the current item would otherwise put the
   dark foreground colour straight back on the dark pill. */
.nav-item.is-active,
.nav-item.is-active:hover{background-color:var(--ink);color:var(--ink-foreground)}
span.nav-item{cursor:default}
.nav-glyph{width:16px;text-align:center;font-size:0.8125rem}
.nav-dot{margin-left:auto;width:6px;height:6px;border-radius:999px;background:var(--accent)}
.nav-eyebrow{margin-top:24px;padding:0 12px}

.ask-btn{margin-top:24px;width:100%;border-radius:var(--radius-2xl);background:var(--accent);padding:12px 16px;font-size:0.875rem;line-height:calc(1.25/.875);font-weight:500;color:var(--accent-foreground);transition-property:transform,translate,scale,rotate;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;cursor:default}
.ask-btn:hover{transform:translateY(-2px)}

/* ---- Topbar.tsx ---------------------------------------------------------------------------- */
/* header: flex flex-wrap items-center gap-4 border-b border-border px-5 py-5 md:px-8 */
.topbar{display:flex;flex-wrap:wrap;align-items:center;gap:16px;border-bottom:1px solid var(--border);padding:20px}
@media(min-width:768px){.topbar{padding:20px 32px}}
/* The greeting is the first thing anyone reads, and flex:1 1 0% with no floor let the controls
   beside it squeeze it to 157px on a laptop and 48px on a phone, where the subtitle wrapped to
   seven lines and the header ate a third of the screen. It keeps a sensible width and the row
   wraps instead, which is what flex-wrap on the bar was already there for. */
.topbar-title{min-width:min(200px,100%);flex:1 1 200px}
@media(max-width:1023px){
  /* the hamburger sat on a row of its own with the rest of the bar empty beside it, because the
     greeting took a whole row to itself. They share one. */
  .topbar-menu{order:-2}
  .topbar-title{flex:1 1 auto;order:-1;min-width:0}
  .topbar-sub{display:none}          /* the greeting alone on a narrow bar, not a paragraph */
}
.topbar-h1{font-size:1.5rem;line-height:calc(2/1.5)}
@media(min-width:768px){.topbar-h1{font-size:1.75rem}}
.topbar-sub{margin-top:4px;font-size:0.75rem;line-height:calc(1/.75);color:var(--muted-foreground)}

/* label: hidden h-11 w-72 items-center gap-2 rounded-2xl border border-border bg-card px-4 text-sm md:flex */
.topbar-search{display:none;height:44px;width:288px;align-items:center;gap:8px;border-radius:var(--radius-2xl);border:1px solid var(--border);background:var(--card);padding:0 16px;font-size:0.875rem;line-height:calc(1.25/.875)}
@media(min-width:768px){.topbar-search{display:flex}}
/* between a laptop and a wide screen the row is about 100px over its own width, so the search gives
   some back rather than the greeting being crushed to nothing. It was a hard 208px, which left the
   input 135px for a placeholder that needs 214: it rendered as "Search agents, miss" with no
   ellipsis, running into the shortcut chip beside it. It flexes between the two now, and anything
   still too long ends in an ellipsis rather than mid-word. */
@media(max-width:1520px){.topbar-search{width:auto;flex:1 1 208px;min-width:168px;max-width:288px}}
.topbar-search-input{text-overflow:ellipsis}

/* The five controls on the right wrapped one at a time, so between 1280 and 360 the bar ran to
   three, four and five rows, 188 to 212px tall, with the avatar stranded on a row of its own. They
   wrap as one block, against the right edge. */
.topbar-tools{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;
  gap:12px;margin-left:auto}
@media(max-width:767px){
  .topbar{gap:10px;padding:16px 20px}
  .topbar-tools{gap:8px}
}
/* the phone-width trim for these sits in hands.css and theme.css, where their own rules live */
.topbar-search-glyph{color:var(--muted-foreground)}
.topbar-search-input{width:100%;background:transparent;outline:none}
.topbar-search-input::placeholder{color:var(--muted-foreground)}

.topbar-clock{display:none;text-align:right}
@media(min-width:1024px){.topbar-clock{display:block}}
.topbar-clock-eyebrow{display:block}
.topbar-clock-time{font-family:var(--font-mono);font-size:0.875rem;line-height:calc(1.25/.875)}

/* user card: flex items-center gap-3 rounded-2xl border border-border bg-card px-3 py-2 */
.topbar-user{display:flex;align-items:center;gap:12px;border-radius:var(--radius-2xl);border:1px solid var(--border);background:var(--card);padding:8px 12px}
.topbar-avatar{display:grid;width:32px;height:32px;place-items:center;border-radius:var(--radius-xl);background:var(--ink);font-size:0.6875rem;font-weight:500;color:var(--ink-foreground)}
/* The operator's name was shown from 640px up, which cost the control cluster 76px and pushed it
   onto a row of its own: at 1024 and at 1280 the header ran to 172px for the sake of a name the
   greeting two inches away already says. It comes back when there is room for it. */
.topbar-user-copy{display:none}
@media(min-width:1280px){.topbar-user-copy{display:block}}
.topbar-user-name{display:block;font-size:0.75rem;line-height:calc(1/.75);font-weight:500}

/* ---- main: flex flex-col gap-5 px-5 py-6 md:px-8 md:py-8 ---------------------------------- */
.page{display:flex;flex-direction:column;gap:20px;padding:24px 20px}
@media(min-width:768px){.page{padding:32px}}

/* ---- top bar search results ---- */
.topbar-search{position:relative}
.topbar-results{position:absolute;top:calc(100% + 8px);left:0;right:0;z-index:40;display:flex;flex-direction:column;gap:2px;padding:6px;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--popover);box-shadow:var(--shadow-lift)}
.topbar-results[hidden]{display:none}
.topbar-result{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--radius-md);color:var(--foreground);text-decoration:none;font-size:0.8125rem}
.topbar-result:hover{background:var(--secondary)}
.topbar-result .eyebrow{min-width:52px}

/* the operator's uploaded picture replaces the initial */
.topbar-avatar.is-photo{background-size:cover;background-position:center;color:transparent}

/* ---- embedded in the Holo stage (index.html?holo=1): the pages alone, over the video ---- */
html.is-holo body{background:transparent}
html.is-holo .sidebar,html.is-holo .topbar,html.is-holo .hands-hud,html.is-holo .topbar-hands,html.is-holo .topbar-fs{display:none!important}
html.is-holo .page{padding:18px 18px 40px}
html.is-holo .content{background:transparent}

/* ---- "this is not live" ----
   A page that cannot reach the backend keeps its last picture, because a blank dashboard helps
   nobody, but it must never pass that picture off as the machine now. */
.nx-stale{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:180;
  display:flex;align-items:center;gap:10px;max-width:min(680px,calc(100vw - 32px));
  padding:11px 12px 11px 16px;border-radius:999px;
  border:1px solid color-mix(in oklab,var(--destructive,#c0392b) 45%,transparent);
  background:color-mix(in oklab,var(--ink) 94%,transparent);color:var(--ink-foreground);
  box-shadow:var(--shadow-lift);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  font:400 0.8125rem/1.35 var(--font-sans)}
.nx-stale[hidden]{display:none}
.nx-stale-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--destructive,#c0392b);
  box-shadow:0 0 0 4px color-mix(in oklab,var(--destructive,#c0392b) 22%,transparent)}
.nx-stale b{font-weight:600;white-space:nowrap}
.nx-stale span[data-nx-stale-why]{color:color-mix(in oklab,var(--ink-foreground) 72%,transparent);min-width:0}
.nx-stale-retry{flex:none;margin-left:2px;height:30px;padding:0 14px;border-radius:999px;
  border:1px solid color-mix(in oklab,var(--ink-foreground) 26%,transparent);background:transparent;
  color:var(--ink-foreground);font:500 0.75rem/1 var(--font-sans);cursor:pointer}
.nx-stale-retry:hover{border-color:var(--accent);color:var(--accent)}
@media(max-width:600px){
  .nx-stale{flex-wrap:wrap;border-radius:18px;left:12px;right:12px;transform:none;max-width:none}
}

/* ---- reaching things with a keyboard ----
   Buttons and links carried the browser's own outline, but text inputs took a border colour that
   does not change, so a keyboard user could not see where they were. One ring, on every control,
   only when the keyboard is what put the focus there. */
/* The ring was the plain accent, which is the one colour that cannot be counted on to show up: on
   a pale card in Solar Graphite it measured 1.44 against its own surface, so the ring was in the
   page and invisible. On light it takes the readable accent; on the dark stage, the sidebar's
   current page and a chat window it takes the ink's own foreground, which reads there by
   definition. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible{
  outline:2px solid var(--accent-text);
  outline-offset:2px;
  border-radius:inherit}
input:focus-visible,textarea:focus-visible,select:focus-visible{
  outline:2px solid var(--accent-text);outline-offset:1px;
  border-color:color-mix(in oklab,var(--accent-text) 60%,transparent)}
.olv-stage :where(a,button,input,select,textarea,[tabindex]):focus-visible,
.sidebar .nav-item.is-active:focus-visible,
.agent-chat :where(a,button,input,textarea):focus-visible{outline-color:var(--ink-foreground)}

/* the first thing a keyboard reaches: a way past fourteen navigation items */
.nx-skip{position:fixed;left:12px;top:-60px;z-index:220;
  padding:10px 16px;border-radius:999px;border:1px solid var(--ink);
  background:var(--ink);color:var(--ink-foreground);
  font:600 0.8125rem/1 var(--font-sans);text-decoration:none;
  transition:top .18s ease-out}
.nx-skip:focus,.nx-skip:focus-visible{top:12px !important}

/* ---- said, not shown ----
   The route announcement and the labels a screen reader needs on controls that read fine by eye. */
.nx-visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ---- a sheet is open ----
   The page behind a modal kept scrolling under it: opening a mission and turning the wheel moved
   the log 1489px while the sheet sat still on top. It holds its place now. */
html.has-sheet,html.has-sheet body{overflow:hidden}
/* and the page does not jump sideways when the scrollbar goes with it */
html{scrollbar-gutter:stable}

/* ---- a list that has more in it than it can show ----
   NX.fades() marks which edge has content behind it. #000 here is a mask, not a colour: only its
   alpha is read, so no palette touches it. */
.nx-more:not(.nx-back){-webkit-mask-image:linear-gradient(#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(#000 calc(100% - 34px),transparent)}
.nx-back:not(.nx-more){-webkit-mask-image:linear-gradient(transparent,#000 24px);
  mask-image:linear-gradient(transparent,#000 24px)}
.nx-back.nx-more{-webkit-mask-image:linear-gradient(transparent,#000 24px,#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(transparent,#000 24px,#000 calc(100% - 34px),transparent)}
/* Chromium paints an overlay bar that fades out of sight; these three panels keep theirs. */
.nx-more::-webkit-scrollbar,.nx-back::-webkit-scrollbar{width:6px}
.nx-more::-webkit-scrollbar-thumb,.nx-back::-webkit-scrollbar-thumb{
  border-radius:999px;background:color-mix(in oklab,currentColor 24%,transparent)}

/* the hairlines that break the topbar's right side into find / look / you */
.topbar-rule{flex:none;width:1px;align-self:stretch;margin:4px 2px;background:var(--border)}
@media(max-width:1023px){.topbar-rule{display:none}}

/* ---- one height across the bar ----
   The search was 44px, the two view buttons 34 and the account card 50, side by side in the same
   row: three heights for four controls. They all take the same one, and their contents centre in
   it, so the row reads as a row. */
.topbar{--topbar-control:44px}
.topbar-search,
.topbar-tools .topbar-hands,
.topbar-tools .topbar-fs,
.topbar-tools .topbar-user,
.topbar-tools .topbar-clock{height:var(--topbar-control);box-sizing:border-box}
.topbar-tools .topbar-hands,
.topbar-tools .topbar-fs{padding-top:0;padding-bottom:0;align-items:center}
.topbar-tools .topbar-user{padding-top:0;padding-bottom:0;align-items:center}
.topbar-tools .topbar-clock{display:flex;flex-direction:column;justify-content:center;align-items:flex-end}

/* ---- a pairing changes the voice, not the size of the page ----
   Every rule that sets the display face is listed here so the adjustment reaches all of them at
   once. scripts/settings.js measures the chosen face against Instrument Serif, which the pages
   were drawn around, and publishes --app-display-adjust; Editorial Origin sets it to none and
   nothing happens. Without this the same headline wrapped onto an extra line or two and every
   panel around it grew. */
.display,
.clv-display,.alv-display,.lvc-display,.ilv-display,.dlv-display,.plv-display,
.hlv-page .hlv-display,.xlv-display,.slv-page .slv-display,.tlv-page .tlv-display,
.mlv-page .mlv-display,
.agent-chat-who h3,.clv-page .clv-foot-main strong,.clv-page .clv-welcome h3,
.alv-heat-readouts b,.ilv-page .ilv-readout dd,.ilv-page .ilv-app-name,
.ilv-page .ilv-side-head h2,.ilv-page .ilv-mini dd,.dlv-prose p,
.city-dossier h3,.city-dossier-grid b,.city3d-footer h3,.plv-transcript,
.tlv-page .tlv-readout dd,.tlv-page .tlv-context h3,.tlv-page .tlv-side-head h2,
.tlv-page .tlv-mini dd,.mlv-page .mlv-launch-summary-name,
.nxc .cluster-value,.nxc .hud-content,.nxc .wing-stat b{
  font-size-adjust:var(--app-display-adjust,none)}

/* The body face moves panels too. Fitting only the headline left Signal Grotesk and Press
   Baskerville 55px taller than Editorial Origin, because DM Sans and Archivo are wider than
   Work Sans at the same size. Same measurement, same mechanism, applied at the root so every
   rule that inherits the sans face inherits the fit with it. */
:root{font-size-adjust:var(--app-sans-adjust,none)}

.topbar-search-input{min-height:40px}


