@layer base;

/* Design tokens — one file per token type, in tokens/.
 *
 * Imported here rather than through a tokens/index.css barrel: global.css is a
 * <link> in the document head, so every level of @import nesting is one more
 * serial round trip before first paint. Listed flat, the ten files are
 * discovered in a single parse and fetched in parallel.
 *
 * `node ui/scripts/gen-tokens.mjs` reads the directory, not this list, so it
 * cannot fall out of step with what is actually there. */
@import '/common/tokens/palette.css' layer(base);
@import '/common/tokens/colors.css' layer(base);
@import '/common/tokens/typography.css' layer(base);
@import '/common/tokens/spacing.css' layer(base);
@import '/common/tokens/border-radius.css' layer(base);
@import '/common/tokens/border-width.css' layer(base);
@import '/common/tokens/sizing.css' layer(base);
@import '/common/tokens/elevation.css' layer(base);
@import '/common/tokens/motion.css' layer(base);
@import '/common/tokens/z-index.css' layer(base);

/* Nasiko UI Design System fonts — vendored locally (see nasiko_ui Flutter package) */
@font-face {
  font-family: 'Chivo Mono';
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url('fonts/ChivoMono-Regular.ttf') format('truetype');
}
@font-face {
  font-family: 'Chivo Mono';
  font-weight: 500 700; /* 700 synthesized from Medium — no bold TTF shipped */
  font-style: normal;
  font-display: swap;
  src: url('fonts/ChivoMono-Medium.ttf') format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  src: url('fonts/Inter-VariableFont_opsz.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Inter';
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  src: url('fonts/Inter-Italic-VariableFont_opsz.ttf') format('truetype-variations');
}

@layer base {
/* 1. Design Tokens
 *
 * The tokens themselves live in tokens/ — one file per token type. They are
 * imported at the top of this file, into @layer base, so the cascade is
 * unchanged from when they were declared inline here.
 *
 * tokens/ is the single source of truth and the input to
 * `node ui/scripts/gen-tokens.mjs`, which emits tokens/tokens.json — the
 * machine-readable form a generator reads. Change a value here, regenerate.
 *
 * Theme: light-dark() follows the OS by default; an explicit
 * [data-theme="light"|"dark"] on <html> pins it.
 */
:root {
  color-scheme: light dark;
}

/* Explicit theme pin — wins over the OS preference */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* 2. Base Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;
}

body {
  background-color: var(--shell-bg);
  color: var(--color-text-main);
  font-family: var(--font-sans);
  line-height: 1.5;
  font-size: var(--font-size-base);
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

/* Mobile (<1024px): no rail/card chrome — the content plane is the page
   itself, so page components sit on the white surface directly. Desktop gets
   the floating white card in the @media block below. */
@media (max-width: 1023.98px) {
  /* Allowlisted for the same reason the desktop card rule below is: a denylist
     hands `flex: 1` to anything a browser extension appends to <body>, and on
     this side that is worse — the injected element does not just take a share
     of the height, it is told to grow into it. */
  body:has(> app-header) > [data-page],
  body:has(> app-header) > main {
    background: var(--color-bg-surface);
    flex: 1;
  }
}

main {
  width: min(100% - 2 * var(--s-24), var(--page-max-width));
  margin: 0 auto;
  padding: var(--s-16) 0;
}

/* Full-viewport app-shell layout — no centering, panes scroll independently */
main.app-shell {
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 0;
  height: calc(100dvh - var(--app-header-height));
  overflow: hidden;
}

@media (min-width: 1024px) {
  /* NightOwl shell: fixed 52px ink topbar + fixed left icon rail; the page
     content is the white card floating on the ink backdrop. app-header
     renders both bars; body padding carves out their space. */
  body {
    /* The shell is fixed to the viewport and the card inside it scrolls — the
       app-shell model, not the document model. Nothing here used to have a
       *height*: body, the card and the page were all sized with `min-height`
       ("at least a screenful, taller if needed"), and a box with no definite
       height cannot be a scrollport, so growth propagated to the document and
       the whole page scrolled — chrome and all.

       `height`, not `min-height`, is the whole change. Note what it removes:
       the card below no longer needs
       `calc(100dvh - var(--shell-topbar-height) - var(--s-12))`, because the
       flex chain now produces that height on its own.

       Desktop only. Below 1024px there is no card and no rail, and a fixed
       100dvh body there fights the mobile URL bar and disables pull-to-refresh;
       the document stays the scroller at that width, which is correct for a
       single-column layout. */
    height: 100dvh;
    overflow: hidden;
    padding-top: var(--shell-topbar-height);
    /* Gutter + rail: the rail is fixed at left:var(--shell-gutter), so the
       content plane starts past both it and the gutter it floats in. */
    padding-left: calc(var(--shell-gutter) + var(--app-sidebar-width));
    /* Space the Weave dock takes when its drawer is open — <weave-dock> sets
       the property on <html> and removes it on close, so the content card
       shrinks rather than being covered by the conversation about it. Not
       transitioned: animating body padding reflows the whole document every
       frame, and the drawer has no slide of its own to match. */
    padding-right: var(--app-dock-width, 0px);
  }

  body > main, .app-layout > main {
    flex: 1;
    min-width: 0;
    width: auto;
    max-width: var(--page-max-width);
    margin-inline: auto;
  }

  /* This is the rule that actually did the damage. `flex: 1` on every body
     child that was not the header, the loading bar or main meant an injected
     `<qb-toolbar>` grew to an equal share alongside the content: 376px each
     out of 763. Allowlisted like the two above. */
  body > [data-page] {
    flex: 1;
    min-width: 0;
  }

  body > main.app-shell, .app-layout > main.app-shell {
    max-width: none;
    margin: 0;
    height: calc(100dvh - var(--shell-topbar-height));
  }

  main {
    width: min(100% - 2 * var(--s-24), var(--page-max-width));
    margin: 0 auto;
    padding: var(--s-16) 0;
  }

  main.app-shell {
    height: 100dvh;
  }

  /* Content card — the page component floats as the single white r8 card on
     the ink shell.

     An allowlist, and it has to be. This was a denylist — every body child
     that was not app-header, app-loading-bar, app-nav-search, app-toast,
     main, div or dialog became a white card — which quietly assumed we know
     everything that will ever be a child of <body>. We do not. A QuillBot
     install appends `<qb-toolbar>` on focusing any text box; body is a
     100dvh flex column, so the toolbar became a flex item, took a white card
     of its own and halved the page: measured on /tokenops, the content card
     went 763px -> 376px the moment the composer was clicked. Grammarly,
     password managers and translation extensions all inject the same way.

     The elements that should be cards are ours and there are two shapes of
     them: the router's `main#outlet`, and a standalone document's page
     element. `data-page` marks the second — ui-lint's `page-document-marks-
     its-page` rule keeps every page document carrying it, so a new page
     fails the gate rather than losing its card silently.

     Floating chrome needs no exemption now, which also means weave-dock's
     `display: contents` is no longer load-bearing for this. */
  body:has(> app-header) > [data-page],
  body:has(> app-header) > main {
    background: var(--color-bg-surface);
    border: none;
    border-radius: var(--r-8);
    /* Even frame on left/right/bottom. Left was 0, which put the card's two
       left corners flush against the rail — the 8px radius cut notches of ink
       out of the edge instead of reading as a floating panel. Top stays 0: the
       card butts the topbar by design. */
    margin: 0 var(--shell-gutter) var(--shell-gutter);
    /* The card is the scroller. `min-height: 0` is the load-bearing line: a flex
       item's automatic minimum size is its content, so without it the card
       refuses to shrink below a tall page and its own `overflow` never engages
       — which is exactly how the document ended up scrolling instead.

       This rule matches a router-mounted `main#outlet` AND a standalone
       document's page element, so both shapes get the same scroller. That
       symmetry is deliberate: an app-shell that only existed on the SPA path is
       the same class of split that produced NAS-303 and NAS-368. */
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    box-shadow: 0 8px 16px rgba(100, 116, 139, 0.12);
  }

  /* <main> content pages keep their centered column inside the card */
  body:has(> app-header) > main {
    max-width: none;
    padding: var(--card-content-pad);
  }

  body:has(> app-header) > main > * {
    max-width: var(--page-max-width);
    margin-inline: auto;
  }

  /*
    SPA: the router mounts pages into <main id="outlet">, so the page is a
    grandchild of body, not a child. The rule above centres a <main>'s children
    in a --page-max-width column — correct for an MPA content page, wrong for a
    router-mounted one: it shrinks the whole page to a narrow centred strip
    inside the card and leaves the module-nav gutter empty beside it.

    The outlet is a passthrough. The card geometry above still applies to it,
    but the page inside owns its own padding (styles/page-layout.css) and fills
    the card edge to edge.
  */
  body:has(> app-header) > main#outlet {
    padding: 0;
  }

  body:has(> app-header) > main#outlet > * {
    max-width: none;
    margin-inline: 0;
  }

  body:has(> app-header) > main.app-shell {
    height: calc(100dvh - var(--shell-topbar-height) - var(--s-12));
    padding: 0;
  }

  body:has(> app-header) > main.app-shell > * {
    max-width: none;
  }
}

/*
  The router's outlet is a passthrough, but it is also the box the mounted page
  takes its height from. That used to be `min-height: 100%` on the page — a
  percentage against an auto-height parent, which computes to `auto`, so the page
  box was only ever as tall as its content. Anything the page pinned to its own
  edges shrank with it: the module nav, pinned top and bottom inside the page,
  rendered 212px tall with 228px of rows and grew a scrollbar on a full-height
  card, because a short page (an error state, an empty list) left nothing to
  stretch it. A flex column has no such ambiguity — the page fills the outlet,
  and still grows past it when its content is taller.
*/
main#outlet {
  display: flex;
  flex-direction: column;
}

main#outlet > * {
  flex: 1 0 auto;
}

/* Prevent media from overflowing their containers */
img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

/* Inherit fonts for form controls */
input,
button,
textarea,
select {
  font: inherit;
}

/* Opinionated resets for elements with notable browser defaults */
figure {
  margin: 0;
}

fieldset {
  border: none;
  min-width: 0;
}

legend {
  padding: 0;
}

address {
  font-style: inherit;
}

/* Ensure the hidden attribute works across all browsers */
[hidden] {
  display: none !important;
}

/* Accessibility utility — visually hidden but available to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* h1 inside sectioning elements gets a reduced size in Chrome/Firefox;
   keep it consistent with our type scale */
:is(article, section, aside, nav) h1 {
  font-size: var(--font-size-4xl);
}

/* 3. Typography — DS: titles are Chivo Mono medium; h3 down stays Inter */
h1,
h2,
h3,
h4,
h5,
h6 {
  margin-bottom: var(--s-16);
  font-weight: 600;
  line-height: 1.2;
  color: var(--color-text-main);
}

h1,
h2 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: 0.005em;
}

h3 {
  font-weight: 700;
}

h1 {
  font-size: var(--font-size-4xl);
}

h2 {
  font-size: var(--font-size-3xl);
}

/* NightOwl page title — Chivo Mono 500 20/28 (+0.16px tracking). Lives here
   (not styles/text.css) so it applies at first paint on pages that render
   synchronously — the components.css @import chain arrives after paint. */
.title-page {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 28px;
  font-weight: 500;
  letter-spacing: 0.16px;
  color: var(--color-text-main);
  margin: 0;
}

h3 {
  font-size: var(--font-size-2xl);
}

h4 {
  font-size: var(--font-size-xl);
}

h5 {
  font-size: var(--font-size-lg);
}

h6 {
  font-size: var(--font-size-base);
}

/* Top-level page title inside <main> — use on the <h1> of content pages */
.page-heading {
  font-size: var(--font-size-3xl);
  margin: 0 0 var(--s-24);
}

p {
  margin: 0 0 var(--s-16) 0;
}

/* DS micro-tell: captions are italic with tight tracking */
caption,
figcaption,
.ts-caption {
  font-style: italic;
  font-size: var(--font-size-xs);
  letter-spacing: -0.04em;
  color: var(--color-text-muted);
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  color: var(--color-primary-hover);
}

/* 4. Form Elements */
label {
  display: block;
  font-size: var(--font-size-sm);
  font-weight: 500;
  margin-bottom: var(--s-8);
  color: var(--color-text-main);
}

/* Design-system components render their own <label> and space it themselves —
   app-input/app-select via their .field gap, app-switch/app-radio/app-checkbox
   via the .row that IS the label. The bare-form margin above is not theirs: it
   doubled the label gap on every labelled field, and on the three control rows
   it added 8px of empty box below the control, so anything centred beside one
   (the Agent JSON toggle's <> glyph) sat 4px low. */
:is(app-checkbox, app-input, app-radio, app-select, app-switch) label {
  margin: 0;
}

:is(input[type="text"], input[type="email"], input[type="password"],
  input[type="search"], input[type="url"], input[type="tel"],
  input[type="number"], input[type="date"], input[type="time"],
  input[type="datetime-local"], textarea, select) {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid transparent; /* NightOwl: inputs are flat sand wells */
  border-radius: var(--r-8);
  font-size: var(--font-size-base);
  background: var(--bg-input);
  color: var(--color-text-main);
  line-height: 1.5;

  &:focus {
    outline: none;
    border-color: var(--border-hover);
    box-shadow: 0 0 0 2px var(--color-primary-ring);
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: var(--color-bg-base);
  }
}

:is(input, textarea)::placeholder {
  color: var(--color-text-muted);
}

textarea {
  resize: vertical;
  min-height: 80px;
}

/* Native selects: drop the OS chrome and draw the DS chevron — a stroked
   caret matching icons.js, not a filled triangle. A url() SVG can't read
   custom properties, so the stroke is baked per theme and re-declared for the
   dark side below; `--fg-secondary` is #645C52 light / #9BA0A6 dark.
   `:not([multiple])` also lifts specificity to (0,1,1) so the background-image
   survives the `:is(input…, select)` rule's `background:` shorthand above. */
select:not([multiple]) {
  cursor: pointer;
  appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23645C52' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-position: right 12px center;
  background-size: 12px 12px;
  background-repeat: no-repeat;

  &:hover:not(:disabled) {
    background-color: var(--bg-surface-hover);
  }
}

/* Dark caret: system-dark unless light is pinned, plus explicitly pinned dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select:not([multiple]) {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239BA0A6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  }
}
:root[data-theme="dark"] select:not([multiple]) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239BA0A6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

input[type="file"] {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--r-8);
  font-size: var(--font-size-base);
  background: var(--color-bg-surface);
  color: var(--color-text-main);
  cursor: pointer;

  &:focus {
    outline: none;
    border-color: var(--border-hover);
    box-shadow: 0 0 0 2px var(--color-primary-ring);
  }

  &::file-selector-button {
    margin-right: var(--s-12);
    height: var(--control-h-sm);
    padding: 0 var(--s-12);
    border: 1px solid var(--color-border);
    border-radius: var(--r-4);
    background: var(--color-bg-base);
    color: var(--color-text-main);
    font-size: var(--font-size-sm);
    cursor: pointer;
  }
}

/* Checkbox and Radio */
input[type="checkbox"],
input[type="radio"] {
  width: auto;
  margin-right: var(--s-8);
  cursor: pointer;
}

/* Range Slider */
input[type="range"] {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: var(--color-border);
  outline: none;
  cursor: pointer;
}

/* Button bare reset — variants live in <app-button> or assets/styles/btn.css */
button {
  font: inherit;
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 5. Tables */
table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  margin: var(--s-16) 0;
  background: var(--color-bg-surface);
  border-radius: var(--r-8);
  overflow: hidden;
}

thead {
  background: var(--color-bg-base);
}

th {
  text-align: left;
  padding: 12px 16px;
  font-weight: 600;
  font-size: var(--font-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
}

td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--color-border);
}

tbody tr:last-child td {
  border-bottom: none;
}

/* The empty-state row is not a row you can act on, so it must not light up. */
tbody tr:not(:has(.empty)):hover {
  background: var(--color-bg-base);
}

/* 6. Lists */
ul,
ol {
  margin: 0 0 var(--s-16) 0;
  padding-left: var(--s-24);
}

ul ul,
ol ol,
ul ol,
ol ul {
  margin-bottom: 0;
}

li {
  margin-bottom: var(--s-8);
}

/* 7. Code */
code,
kbd,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--color-bg-base);
  padding: 2px 6px;
  border-radius: var(--r-4);
  color: var(--color-text-main);
}

pre {
  font-family: var(--font-mono);
  background: var(--color-bg-base);
  padding: var(--s-16);
  border-radius: var(--r-8);
  overflow-x: auto;
  margin: var(--s-16) 0;
  border: 1px solid var(--color-border);
}

pre code {
  background: none;
  padding: 0;
}

/* 8. Blockquote */
blockquote {
  margin: var(--s-16) 0;
  padding-left: var(--s-24);
  border-left: 4px solid var(--color-primary);
  color: var(--color-text-muted);
  font-style: italic;
}

/* 9. Horizontal Rule */
hr {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: var(--s-24) 0;
}

/* 10. Details/Summary */
details {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--r-8);
  padding: var(--s-16);
  margin: var(--s-16) 0;
}

summary {
  cursor: pointer;
  font-weight: 600;
  user-select: none;
  list-style: none;
}

summary::-webkit-details-marker {
  display: none;
}

summary::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  margin-right: var(--s-8);
  vertical-align: 1px;
  flex-shrink: 0;
}

details[open] summary::before {
  transform: rotate(45deg);
  vertical-align: -1px;
}

details[open] {
  padding-bottom: var(--s-16);
}

/* 12. Dialog */
dialog {
  border: none;
  border-radius: var(--r-12);
  padding: 0;
  box-shadow: var(--shadow-xl);
  background: var(--color-bg-surface);
  color: var(--color-text-main);
  max-width: 90vw;
  max-height: 90vh;
  margin: auto;
}

dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(2px);
}

} /* end @layer base */

/* ─── app-header pre-upgrade placeholder ─────────────────────────────────────
   Shown instantly (pure CSS, no JS) while app-header.js is still loading.
   Mirrors the real header's bar geometry so the page doesn't shift on upgrade.
   Uses ::before (brand pill) + ::after (one nav-link skeleton) as the placeholder.
──────────────────────────────────────────────────────────────────────────── */
@keyframes app-header-skel-pulse {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.35;
  }
}

/* ─── MPA View Transitions ───────────────────────────────────────────────────
   Opt-in to cross-document view transitions, and freeze the shell so a
   navigation never cross-fades the chrome.

   These rules MUST live here, in a render-blocking stylesheet, not in
   app-header.js's adopted sheet: the incoming document is snapshotted when it
   is ready to render, and a deferred module has not run by then. Named there,
   the outgoing page had an `app-header` group and the incoming page had none,
   so the header cross-faded out to nothing and the root group cross-faded the
   whole viewport — the "every click flickers the page" bug. The name is on the
   pre-upgrade placeholder too, for the same reason.
──────────────────────────────────────────────────────────────────────────── */
@view-transition {
  navigation: auto;
}

app-header,
app-header:not(:defined) {
  view-transition-name: app-header;
}

/* Same problem one level down. The module nav is the same nav on both sides of
   a navigation *within* a module (Agents → Builds, Settings → Secrets), but it
   lives inside each page component, so with no name of its own it rode the
   `root` group and dimmed and slid with the page body on every click — the
   "whole page flickers" report. Modules whose rows are in-page sections (MCP,
   most of Settings) never navigate at all, which is why only the link rows did
   it. Named, it gets its own group: the UA cross-fade blends the two halves
   with `plus-lighter`, so identical content holds steady, and it still fades
   properly when a navigation enters or leaves a module and the nav is only on
   one side. No frozen `animation: none` here for that reason — unlike the
   header, this element is not on every page. */
app-module-nav,
app-module-nav:not(:defined) {
  view-transition-name: app-module-nav;
}

/* Frozen regardless of prefers-reduced-motion: the shell is the same shell on
   both sides of the navigation, so there is nothing to animate between. */
::view-transition-old(app-header),
::view-transition-new(app-header) {
  animation: none;
  mix-blend-mode: normal;
}

/* Shared micro-transition for tab/section panel switches — components apply
   `animation: panel-in var(--transition-base)` to their active panel and gate
   it on prefers-reduced-motion themselves. */
@keyframes panel-in {
  from { opacity: 0; translate: 0 4px; }
  to   { opacity: 1; translate: 0 0; }
}

app-header:not(:defined) {
  display: flex;
  align-items: center;
  gap: var(--s-16);
  padding: 0 var(--s-12);
  height: var(--shell-topbar-height);
  background: var(--shell-bg);
  border-bottom: none;
  position: sticky;
  top: 0;
  z-index: 100;
  overflow: hidden;

  &::before {
    content: '';
    display: block;
    width: 36px;
    height: 36px;
    border-radius: var(--r-8);
    background: var(--shell-control);
    flex-shrink: 0;
    animation: app-header-skel-pulse 1.4s ease-in-out infinite;
  }

  &::after {
    content: '';
    display: block;
    margin-left: auto;
    width: 140px;
    height: 28px;
    border-radius: var(--r-8);
    background: var(--shell-control);
    flex-shrink: 0;
    animation: app-header-skel-pulse 1.4s ease-in-out infinite 0.2s;
  }

  @media (prefers-reduced-motion: reduce) {

    &::before,
    &::after {
      animation: none;
      opacity: 0.4;
    }
  }

  @media (min-width: 1024px) {
    /* Reserves the fixed topbar strip; the rail column is carved out by the
       body padding, so no extra placeholder box is needed for it. */
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: var(--shell-topbar-height);
  }
}
