/* ==========================================================================
   Platform theme -- the shared design system for the selector and all apps.
   Light by default, dark via [data-theme="dark"] or system preference.
   Everything is built on CSS custom properties so apps inherit one look.
   ========================================================================== */

@import url('/assets/tokens.css');


* { box-sizing: border-box; }

/* Reserve the vertical scrollbar's space permanently so layout doesn't
   shift when a page grows tall enough to scroll. Fallback keeps the
   scrollbar track always visible; modern browsers reserve the gutter
   and show the bar only when needed. */
html {
    overflow-y: scroll;
}
@supports (scrollbar-gutter: stable) {
    html {
        overflow-y: auto;
        scrollbar-gutter: stable;
    }
}

/* The platform is a desktop layout on purpose: it does not reflow to phone
   widths. Holding the page at --minw keeps multi-column data tables readable --
   a narrow window pans sideways instead of crushing every column. Apps inherit
   this, so no app has to defend its own tables against small viewports. */
body {
    min-width: var(--minw);
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
/* :where() contributes zero specificity, so this lands at (0,1,0) -- the same as a
   bare component class. Any component defined below that says text-decoration:none
   now wins on source order, instead of losing to a:hover and sprouting an underline
   on rollover. Prose links still underline; chrome and controls don't. */
:where(a):hover { text-decoration: underline; }

h1 { font-size: 1.5rem; margin: 0 0 .25rem; letter-spacing: -.01em; }
h2 { font-size: 1.1rem; margin: 0 0 .5rem; }
.muted { color: var(--fg-muted); }
.small { font-size: .85rem; }
.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }

/* ------------------------------ shell chrome ---------------------------- */

.shell-bar {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}
.shell-bar-inner {
    max-width: var(--maxw);
    margin: 0 auto;
    display: flex; align-items: center; gap: 1rem;
    padding: .6rem 1.25rem;
    flex-wrap: wrap;
}
.shell-title { font-weight: 600; }

/* app switcher (native <details> disclosure, no JS) */
.switcher { position: relative; }
.switcher > summary {
    list-style: none; cursor: pointer;
    display: inline-flex; align-items: center; gap: .4rem;
    height: var(--control-h-sm); padding: 0 .7rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm); background: var(--surface);
    font-size: .9rem; user-select: none;
}
.switcher > summary::-webkit-details-marker { display: none; }
.switcher > summary:hover { background: var(--surface-2); }
.switcher-icon { color: var(--accent); }

/* Right cluster of the bar: shortcuts + Resources as one right-aligned group, so a single
   margin-left:auto governs them (two competing autos would center the first). */
.bar-right { margin-left: auto; display: inline-flex; align-items: center; gap: .5rem; }

/* Bar shortcuts: admin-pinned, icon-only app jumps, sitting just left of Resources. */
.bar-shortcuts { display: inline-flex; align-items: center; gap: .35rem; }
.bar-shortcut {
    display: inline-flex; align-items: center; justify-content: center;
    width: var(--control-h-sm); height: var(--control-h-sm); border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm); background: var(--surface); text-decoration: none;
    user-select: none;
}
.bar-shortcut:hover { background: var(--surface-2); }
/* Flex-centred, matching .app-card-icon. It was inline with only a line-height, so an
   inline SVG aligned to the text baseline and sat high. A text glyph tolerated that;
   an SVG does not. */
.bar-shortcut-icon {
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--accent); font-size: 1.05rem; line-height: 1;
}

/* Inline SVG icons. Defaults live on the icon so no wrapper has to know whether it
   holds a glyph or an SVG -- the alternative is fixing this once per wrapper, which is
   how .bar-shortcut-icon came to differ from .app-card-icon in the first place.

   vertical-align stops baseline alignment lifting the icon in an inline context, and is
   simply ignored where the wrapper is flex, so it is safe everywhere. flex:none stops
   it being squeezed when it IS a flex item and the row is tight -- the usual way icons
   end up subtly oval. */
/* Inline SVG icons. Defaults live on the icon so no wrapper has to know whether it
   holds a glyph or an SVG -- the alternative is fixing this once per wrapper, which is
   how .bar-shortcut-icon came to differ from .app-card-icon in the first place.

   Size comes from --icon-size, set by the wrapper, falling back to 1em. That keeps icon
   size DECOUPLED from font-size, which matters because .app-card-icon also renders the
   letter fallback for apps with no icon: a thin line icon needs more size than a bold
   letter to carry the same weight, so the two should not scale together. A wrapper that
   sets nothing still inherits sensibly at 1em.

   vertical-align stops baseline alignment lifting the icon in an inline context, and is
   simply ignored where the wrapper is flex, so it is safe everywhere. flex:none stops
   it being squeezed when it IS a flex item and the row is tight -- the usual way icons
   end up subtly oval. */
.licon {
    width: var(--icon-size, 1em);
    height: var(--icon-size, 1em);
    vertical-align: middle;
    flex: none;
}

/* Grows to its widest item rather than wrapping a long app name inside a fixed box --
   the same grow-to-content idea as the layout floor in 1.1, applied to a menu. 220px
   stays as the floor; the ceiling keeps an absurd name on screen, and is the only
   place the text degrades (to an ellipsis). */
.switcher-menu {
    position: absolute; top: calc(100% + 6px); left: 0;
    min-width: 220px;
    width: max-content;
    max-width: min(90vw, 420px);
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-lift);
    padding: .35rem; z-index: 40;
}
/* Reaches the nested sublist's links too -- they are descendants of .switcher-menu. */
.switcher-menu a {
    display: block; padding: .5rem .6rem; border-radius: var(--radius-sm);
    color: var(--fg); font-size: .92rem;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.switcher-menu a:hover { background: var(--surface-2); text-decoration: none; }
.switcher-menu a.on { color: var(--accent); font-weight: 600; }
.switcher-sep { height: 1px; background: var(--border); margin: .35rem .3rem; }
.switcher-signout { color: var(--danger); }

/* theme toggle */
/* The bar control cluster: theme toggle, density toggle, Backup Login key. One box,
   three selectors -- the same patch appearing in three places is the doctrine's signal
   that it was a shared component. Grouped rather than a new .bar-control class because
   a class would have to be added to three elements' markup for no visual difference;
   if a fourth control appears, that trade flips. Specifics (glyphs, link states) stay
   with each component below. */
.theme-toggle,
.density-toggle,
.backup-login-link {
    display: inline-flex; align-items: center; justify-content: center;
    width: var(--control-h-sm); height: var(--control-h-sm);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--fg);
    font-size: 1rem; line-height: 1; cursor: pointer;
}
.theme-toggle:hover,
.density-toggle:hover,
.backup-login-link:hover { background: var(--surface-2); }
.theme-toggle:focus-visible,
.density-toggle:focus-visible,
.backup-login-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Compact is a mode you are IN, so the control reports it -- the same job the theme
   toggle's glyph swap does, and the same accent-when-active treatment as a.on. */
:root[data-density="compact"] .density-toggle { color: var(--accent); border-color: var(--accent); }
.theme-toggle:hover { background: var(--surface-2); }
/* Lucide sun / moon as CSS masks, matching the icon set used everywhere else. Masks
   rather than text glyphs because a glyph renders differently on every OS -- and rather
   than inline SVG because the shape has to change with the theme, which is a stylesheet
   concern, not a markup one. The SVG supplies the shape; background-color supplies the
   colour, so it follows currentColor like every other icon.

   The button reports the state you are IN, not the state you would get -- same as the
   density toggle taking the accent while compact. */
.theme-toggle::after {
    content: "";
    width: var(--icon-size, 1.05rem);
    height: var(--icon-size, 1.05rem);
    background-color: currentColor;
    -webkit-mask: var(--theme-icon) center / contain no-repeat;
    mask: var(--theme-icon) center / contain no-repeat;
}
.theme-toggle { --theme-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2'/%3E%3Cpath d='M12 20v2'/%3E%3Cpath d='m4.93 4.93 1.41 1.41'/%3E%3Cpath d='m17.66 17.66 1.41 1.41'/%3E%3Cpath d='M2 12h2'/%3E%3Cpath d='M20 12h2'/%3E%3Cpath d='m6.34 17.66-1.41 1.41'/%3E%3Cpath d='m19.07 4.93-1.41 1.41'/%3E%3C/svg%3E"); }
:root[data-theme="dark"] .theme-toggle { --theme-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E"); }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) .theme-toggle { --theme-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E"); }
}
/* Backup Login key: the box comes from the cluster rule above; these are the parts
   only it needs. It is an <a>, so it must opt out of the global hover underline --
   :where(a):hover is (0,1,0) and these are (0,1,0) declared later. */
.backup-login-link { color: var(--fg-muted); text-decoration: none; }
.backup-login-link:hover { text-decoration: none; }
/* Not set up yet: a standing nudge, and the one state that earns colour here. */
.backup-login-link.not-enrolled { color: var(--danger); border-color: var(--danger); }
.backup-login-link.not-enrolled:hover { background: var(--danger-weak); }

/* --------------------------- content containers ------------------------- */

.shell-main {
    max-width: var(--maxw);
    margin: 1.75rem auto;
    padding: 0 1.25rem;
}
/* min-width: min-content lets the content area GROW past the window rather than
   clamping to it. Without it every block is width:auto -- it fills its parent, so a
   table too wide to compress overflows the card and paints out over the page. With
   it, the card grows, the page grows, and the existing page scrollbar does the work.
   One scroll, not a scroller inside a scroller: 1.1 says the PAGE pans.

   min-content, not max-content: prose contributes only its longest word, so a card of
   text is unaffected. A table contributes the width its columns refuse to go below. */
.shell-main.full { max-width: none; min-width: min-content; }
.shell-main.narrow { max-width: 460px; }
/* Sign-in / error pages have no tables to protect, so they opt out of the width
   floor rather than centring a 460px card in a 960px canvas. */
body:has(.shell-main.narrow) { min-width: 0; }

.page-head { margin-bottom: 1.25rem; }
.home-head { margin-bottom: 1.25rem; }
.home-head-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

/* ------------------------------ components ------------------------------ */

.card, .panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.1rem 1.25rem;
    box-shadow: var(--shadow);
    margin: 0 0 1rem;
}

/* Class only -- NOT `.btn, button.btn, a.btn`. A type+class selector scores (0,1,1),
   which outranks every single-class modifier at (0,1,0): on a <button>, .btn-primary,
   .btn-danger and .btn-sm all silently lost to it, leaving coloured buttons rendering
   as plain surface at rest and only taking their colour on :hover (0,2,0). A bare
   `.btn` already beats any browser default, so the type selectors bought nothing. */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
    height: var(--control-h); padding: 0 1rem; border-radius: var(--radius-sm);
    border: 1px solid var(--border-strong); background: var(--surface); color: var(--fg);
    font: inherit; font-weight: 500; cursor: pointer; text-decoration: none;
    user-select: none;
}
.btn:hover  { background: var(--btn-bg); text-decoration: none; }
.btn:active { background: var(--btn-bg-hover); }

/* Primary is deliberately NOT the accent. Colour is reserved for meaning (links,
   current state, danger); a primary action earns its emphasis from a filled
   surface plus heavier text against the default button's outlined white. Each
   step of the ramp is a token, so hover/active never depend on filter() -- which
   multiplies channels and pushes a saturated hue further rather than darkening
   it evenly. */
.btn-primary        { background: var(--btn-bg); border-color: var(--btn-border);
                      color: var(--fg); font-weight: 600; }
.btn-primary:hover  { background: var(--btn-bg-hover); border-color: var(--btn-border); }
.btn-primary:active { background: var(--btn-bg-active); }

.btn-danger        { background: var(--danger); border-color: var(--danger); color: var(--danger-fg); }
.btn-danger:hover  { background: var(--danger-hover); border-color: var(--danger-hover); }
.btn-danger:active { background: var(--danger-hover); }

/* Soft (tonal) state buttons: a weak background carrying its own state colour -- the
   same strong/weak pairing .notice-* and .badge-* use, applied to a control.

   For REVERSIBLE state toggles, where a full-strength .btn-danger overstates the
   consequence. Hiding a dashboard is a nuisance you can undo in one click; deleting a
   record is not. Two levels of danger, so the loud one still means something.

   The text colour is derived by mixing the state colour toward --fg: that darkens it
   in light mode and lightens it in dark, from one declaration, with no extra tokens
   and no pair to keep in sync. Contrast stays at or above 4.6:1 in every state of
   both themes. */
.btn-soft-ok, .btn-soft-warn, .btn-soft-danger { font-weight: 600; }

.btn-soft-ok {
    background: var(--ok-weak);
    border-color: color-mix(in srgb, var(--ok) 35%, transparent);
    color: color-mix(in srgb, var(--ok) 78%, var(--fg));
}
.btn-soft-ok:hover  { background: color-mix(in srgb, var(--ok-weak) 90%, var(--ok)); }
.btn-soft-ok:active { background: color-mix(in srgb, var(--ok-weak) 86%, var(--ok)); }

.btn-soft-warn {
    background: var(--warn-weak);
    border-color: color-mix(in srgb, var(--warn) 35%, transparent);
    color: color-mix(in srgb, var(--warn) 78%, var(--fg));
}
.btn-soft-warn:hover  { background: color-mix(in srgb, var(--warn-weak) 90%, var(--warn)); }
.btn-soft-warn:active { background: color-mix(in srgb, var(--warn-weak) 86%, var(--warn)); }

.btn-soft-danger {
    background: var(--danger-weak);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
    color: color-mix(in srgb, var(--danger) 78%, var(--fg));
}
.btn-soft-danger:hover  { background: color-mix(in srgb, var(--danger-weak) 90%, var(--danger)); }
.btn-soft-danger:active { background: color-mix(in srgb, var(--danger-weak) 86%, var(--danger)); }

/* Buttons had no focus ring at all: the browser default is often invisible
   against a custom background. :focus-visible so a mouse click doesn't leave one
   behind. */
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Disabled. The hover overrides are needed because .btn:hover (0,2,0) would otherwise
   still repaint a disabled button on rollover; :disabled:hover is (0,3,0) and wins. */
.btn:disabled { opacity: .55; cursor: default; }
.btn:disabled:hover         { background: var(--surface); }
.btn-primary:disabled:hover { background: var(--btn-bg); }
.btn-danger:disabled:hover  { background: var(--danger); }

.btn-block { display: flex; width: 100%; }
.btn-sm { height: var(--control-h-sm); padding: 0 .7rem; font-size: .85rem; }

label { display: block; margin: .7rem 0 .25rem; font-weight: 600; font-size: .9rem; }
input, textarea, select {
    width: 100%; padding: .55rem .65rem; font: inherit;
    background: var(--surface); color: var(--fg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}
/* Every single-line control is exactly --control-h, so a field, a select and a
   button sit flush on the same row. Vertical padding goes to zero and the height
   does the work; the text centres itself. textarea keeps its padding (it grows),
   and checkbox/radio are glyphs, not controls, so both are excluded. */
input:not([type=checkbox]):not([type=radio]):not([type=file]), select {
    height: var(--control-h); padding-top: 0; padding-bottom: 0;
}
/* File inputs are excluded above, and this is why. A file input is a native button
   inside shadow DOM plus a text run. Given a fixed height and zero vertical padding it
   lays that button against the TOP of the content box, and it cannot be centred from
   here: the host's flex layout does not reach shadow content in Chromium. So it keeps
   its natural height and symmetric padding does the centring, which works in every
   engine. Lands within a pixel or two of --control-h rather than exactly on it -- an
   acceptable trade for a control that sits on its own row, never in a toolbar. */
input[type="file"] { padding: .4rem .65rem; }
input:focus, textarea:focus, select:focus {
    outline: 2px solid var(--accent-weak); border-color: var(--accent);
}
/* Tick/dot follows the theme accent instead of the browser default blue. */
input[type=checkbox], input[type=radio] { width: auto; accent-color: var(--accent); }

/* Field widths. Fields default to 100%, which makes a two-digit input as wide as
   its container -- fine for a prose textarea, absurd for a port number. Size the
   field to the data it holds. A class beats the bare `input` selector, so these
   just work. */
.field-xs { width: 6rem; }    /*  96px - number, code, percentage */
.field-sm { width: 12rem; }   /* 192px - date, short identifier  */
.field-md { width: 20rem; }   /* 320px - name, email, hostname   */
.field-lg { width: 30rem; }   /* 480px - title, path, URL        */

table.data { border-collapse: collapse; width: 100%; }
table.data th, table.data td {
    text-align: left; padding: .55rem .7rem; border-bottom: 1px solid var(--border);
    vertical-align: top;
}
/* The default is tuned for the KEY/VALUE shape -- two columns, labels down the left.
   `width: 1%` is the shrink-to-content idiom, so each header hugs its own label and
   the value column takes the rest. Kept as the default because that is what every
   already-shipped table expects; changing it would silently reflow all of them. */
table.data th { color: var(--fg-muted); font-weight: 600; white-space: nowrap; width: 1%; }
/* Opt-in for column grids. The default above is the key/value idiom: the header shrinks
   and the value column absorbs the slack -- correct with two columns, meaningless with
   five, where every header is told to shrink and none can absorb.

   Anchored to table.data because a bare `.cols th` is (0,1,1) and loses to
   `table.data th` at (0,1,2). That is not a style choice, it is the only way the rule
   lands, and it is the same trap app authors are warned about in the handoff. */
table.data.cols th { width: auto; white-space: normal; }

/* Opt-in for a real COLUMN GRID: headers over data, many rows. Here shrink-to-content
   is wrong -- it sizes every column to its LABEL, so a "Free" header four characters
   wide sits over "48.2 GB" values while one column absorbs all the slack. Columns
   should be sized by their contents instead.

   `width: auto` is what does the work; the nowrap is dropped too, so a long header can
   wrap over two lines rather than forcing its column wider than the data needs. The
   selector is `table.data.cols th` at (0,2,1) so it out-specifies the default's
   (0,1,1) -- the anchoring the doctrine requires when overriding a themed component. */
table.data.cols th { width: auto; white-space: normal; }

.badge {
    display: inline-block; padding: .12rem .5rem; border-radius: 999px;
    font-size: .78rem; font-weight: 600; background: var(--surface-2); color: var(--fg-muted);
}
.badge-open { background: var(--ok-weak); color: var(--ok); }
/* State badges, matching the .notice-* family so a status reads the same whether it
   is a banner or an inline chip. .badge-open predates these and is kept as-is. */
.badge-ok   { background: var(--ok-weak); color: var(--ok); }
.badge-warn { background: var(--warn-weak); color: var(--warn); }
.badge-err  { background: var(--danger-weak); color: var(--danger); }

.notice { padding: .6rem .8rem; border-radius: var(--radius-sm); margin: 0 0 1rem; }
.notice-err  { background: var(--danger-weak); color: var(--danger); }
.notice-ok   { background: var(--ok-weak); color: var(--ok); }
.notice-warn { background: var(--warn-weak); color: var(--warn); }

/* ------------------------ in-app secondary toolbar ---------------------- */
/* A toolbar an app can render inside its body (below the platform bar): dropdown
   menus, selects, and buttons. Shared here so every themed app looks consistent. */

.app-toolbar {
    display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: .55rem .7rem;
    box-shadow: var(--shadow); margin-bottom: 1.25rem;
}
.app-toolbar-group { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.app-toolbar-group.right { margin-left: auto; }

.app-select {
    padding: 0 .55rem; font: inherit;
    background: var(--surface); color: var(--fg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}

/* --------------------------- select chevron ----------------------------- */
/* The browser's native dropdown arrow can't be positioned by CSS and ignores the
   palette (it stays browser-gray in dark mode). Replace it with our own chevron:
   its position is background-position -- RAISE the .7rem to nudge it further LEFT
   -- and its color follows --fg-muted via the swaps below.

   This rule sits after the two rules above on purpose: both use the `background`
   SHORTHAND, which resets background-image to none, so it has to win on order. */
select, .app-select {
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235c6b7a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .7rem center;
    background-size: 10px 6px;
    padding-right: 1.9rem;
}
:root[data-theme="dark"] select,
:root[data-theme="dark"] .app-select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239aa7b4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) select,
    :root:not([data-theme]) .app-select {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239aa7b4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    }
}
/* A list box (multiple / size>1) has no arrow to replace -- keep it native so a
   stray chevron isn't painted over the list. */
select[multiple], select[size] {
    height: auto; padding-top: .55rem; padding-bottom: .55rem;
    appearance: auto;
    -webkit-appearance: auto;
    background-image: none;
    padding-right: .65rem;
}

.app-menu { position: relative; }
.app-menu > summary {
    list-style: none; cursor: pointer; user-select: none;
    display: inline-flex; align-items: center;
    height: var(--control-h); padding: 0 .7rem; font-size: .92rem;
    background: var(--surface); color: var(--fg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}
.app-menu > summary::-webkit-details-marker { display: none; }
.app-menu > summary:hover { background: var(--surface-2); }
.app-menu-list {
    position: absolute; top: calc(100% + 5px); left: 0; min-width: 180px; z-index: 20;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-lift); padding: .3rem;
}
.app-menu-list a {
    display: block; padding: .45rem .55rem; border-radius: var(--radius-sm);
    color: var(--fg); text-decoration: none; font-size: .9rem;
}
.app-menu-list a:hover { background: var(--surface-2); }

/* --------------------------- app navigation bar ------------------------- */
/* A full-width horizontal bar an app renders directly under the platform bar,
   constant across the app's pages. Define app_navbar() in the app to fill it; the
   platform renders it edge-to-edge with its contents aligned to the content column. */

/* Fixed height, border included (border-box), so --app-nav-h is a number anything
   below a sticky nav can offset by. No flex-wrap: at --minw a nav that would wrap
   has too many links (see the doctrine's 4-7 rule), and we pan rather than reflow. */
.app-nav {
    height: var(--app-nav-h);
    background: var(--surface); border-bottom: 1px solid var(--border);
    position: sticky; top: 0; z-index: 30;
}
.app-nav-inner {
    max-width: var(--maxw); margin: 0 auto; height: 100%;
    display: flex; align-items: center; gap: .5rem;
    padding: 0 1.25rem;
}
.app-nav-links { display: flex; align-items: center; gap: .15rem; }
/* Nav is chrome, so its controls take the small height -- links, buttons and
   dropdowns all match, and the bar is --app-nav-h whatever an app puts in it. */
.app-nav-links a {
    display: inline-flex; align-items: center;
    height: var(--control-h-sm); padding: 0 .7rem;
    border-radius: var(--radius-sm);
    color: var(--fg-muted); text-decoration: none; font-size: .93rem;
}
.app-nav-links a:hover { background: var(--surface-2); color: var(--fg); }
.app-nav-links a.on { color: var(--accent); font-weight: 600; }
.app-nav .btn, .app-nav .app-menu > summary { height: var(--control-h-sm); }
.app-nav .btn { padding: 0 .8rem; font-size: .9rem; }

/* Right-hand group. ONE element carries the auto margin: two competing autos
   distribute the slack between them and strand the first mid-bar. Mirrors
   .bar-right in the platform bar. */
.app-nav-right { display: inline-flex; align-items: center; gap: .5rem; margin-left: auto; }
.app-nav .push-right { margin-left: auto; }

/* --------------------- second-level side navigation --------------------- */
/* A rail of section links beside the content they control -- the Settings
   pattern. Lives INSIDE .shell-main, so it needs no knowledge of whether the app
   is constrained or full width: the rail is fixed at --side-nav-w and the body
   takes whatever is left. */

.side-layout {
    display: grid;
    /* minmax(min-content, 1fr), not minmax(0, 1fr): a zero minimum lets the track
       collapse and the table overflow it, which is exactly the bug. */
    grid-template-columns: var(--side-nav-w) minmax(min-content, 1fr);
    min-width: min-content;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    margin: 0 0 1rem;
}

.side-nav {
    display: flex; flex-direction: column; gap: .1rem;
    padding: .6rem;
    background: var(--surface-2);
    border-right: 1px solid var(--border);
    /* Rounded on its own left edge rather than clipping the parent with
       overflow:hidden -- which would cut off any dropdown opened in the body. */
    border-radius: var(--radius) 0 0 var(--radius);
}
.side-nav a {
    display: flex; align-items: center;
    min-height: var(--control-h-sm);
    padding: .35rem .7rem; border-radius: var(--radius-sm);
    color: var(--fg-muted); text-decoration: none; font-size: .93rem;
}
.side-nav a:hover { background: var(--surface); color: var(--fg); text-decoration: none; }
/* The current item takes the BODY's surface colour, so it reads as continuous
   with the pane it's controlling instead of as a floating highlight. */
.side-nav a.on { background: var(--surface); color: var(--accent); font-weight: 600; }
.side-nav-head {
    font-size: .78rem; text-transform: uppercase; letter-spacing: .05em;
    color: var(--fg-muted); font-weight: 600; padding: .75rem .7rem .25rem;
}
.side-nav-head:first-child { padding-top: .15rem; }

.side-main { padding: 1.1rem 1.25rem; min-width: min-content; }
.side-main > h2:first-child, .side-main > .page-head:first-child { margin-top: 0; }

/* ---- detached variant: the rail as its own floating panel ---------------
   Same grid, different skin. The enclosing card is dropped, the rail becomes a
   panel in its own right, and the area beside it is ordinary page background
   holding its own cards and tables.

   align-items:start is the whole difference. A sticky element cannot be
   stretched, so the enclosed variant (whose rail column MUST run the full height
   of the pane to keep one continuous border) can never stick. Detached can,
   because nothing depends on the rail being as tall as its neighbour. */

.side-layout.detached {
    background: none; border: 0; border-radius: 0; box-shadow: none;
    align-items: start;
    gap: 1.25rem;
}
.side-layout.detached .side-nav {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    position: sticky;
    /* Clears the pinned app nav. When the rail IS the primary navigation there is
       no app nav to clear, so it rides higher -- decided from the document rather
       than from a class the app has to remember to set. */
    top: calc(var(--app-nav-h) + 1rem);
}
body:not(:has(.app-nav)) .side-layout.detached .side-nav { top: 1rem; }

/* Inverted from the enclosed variant: there the rail is --surface-2 and the
   current item takes --surface to merge with the pane. Here the rail is already
   --surface, so the current item steps the other way. */
.side-layout.detached .side-nav a.on { background: var(--surface-2); }
.side-layout.detached .side-main { padding: 0; }

/* --------------------------- rendered Markdown -------------------------- */
/* Everything here is scoped to .md-body so authored content can never leak into
   platform chrome. Three places carry the class -- the read-only document view, the
   editor's live preview, and any app calling platform_markdown_view() -- so one set
   of rules covers every place Markdown appears.

   Not in here: syntax colours. highlight.js ships two vendored themes, swapped
   server-side, and owns the token spans inside a code block. The rule is glyphs vs
   surfaces -- a vendored library may colour glyphs; the platform colours every
   surface. That is why --code-bg exists rather than letting hljs paint the box. */

.md-body { color: var(--fg); line-height: 1.6; }
.md-body > :first-child { margin-top: 0; }
.md-body > :last-child  { margin-bottom: 0; }

.md-body h1, .md-body h2, .md-body h3,
.md-body h4, .md-body h5, .md-body h6 { font-weight: 600; line-height: 1.25; margin: 1.4em 0 .5em; }
.md-body h1 { font-size: 1.5rem; }
.md-body h2 { font-size: 1.25rem; }
.md-body h3 { font-size: 1.1rem; }
.md-body h4 { font-size: 1rem; }
.md-body h5, .md-body h6 { font-size: .93rem; color: var(--fg-muted); }

.md-body p  { margin: .6em 0; }
.md-body ul, .md-body ol { margin: .6em 0; padding-left: 1.5rem; }
.md-body li { margin: .2em 0; }
.md-body a  { color: var(--accent); text-decoration: underline; }
.md-body hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }
.md-body img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }
.md-body sub, .md-body sup { line-height: 0; font-size: .78em; }
.md-body del { color: var(--fg-muted); }

.md-body blockquote {
    margin: .8em 0; padding: .3em 1rem;
    border-left: 3px solid var(--border-strong);
    color: var(--fg-muted);
}

/* Code. The container is ours; what is inside it is highlight.js's.
   NOTE the .hljs on the last selector -- it is load-bearing. hljs ships
   `pre code.hljs { padding: 1em }` at (0,1,2) and its stylesheet loads AFTER this
   one, so a plain `.md-body pre code` ties on specificity and loses on source order,
   leaving the block double-padded. (0,2,2) wins. */
.md-body code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .9em;
    background: var(--code-bg);
    border: 1px solid var(--code-border);
    padding: .08em .35em;
    border-radius: var(--radius-sm);
}
.md-body pre {
    background: var(--code-bg);
    border: 1px solid var(--code-border);
    border-radius: var(--radius);
    padding: .8rem 1rem;
    overflow-x: auto;
    margin: .8em 0;
}
.md-body pre code,
.md-body pre code.hljs {
    background: none; border: 0; padding: 0;
    font-size: .875rem; line-height: 1.5;
}

/* Authored tables are a full grid, and that difference from table.data is DELIBERATE
   -- not an inconsistency to tidy away later. The two do different jobs: a data table
   is scanned (many rows, one value wanted, hairlines are enough), an authored table is
   read (few rows, prose-length cells, sitting between paragraphs, where the eye needs
   help tracking across a wide row).

   What differs is only the border and the header fill -- the "voice". Padding stays
   identical to table.data because density is rhythm, and keeping the split there means
   the two can only drift where it was chosen. */
.md-body table { border-collapse: collapse; width: 100%; margin: .8em 0; }
.md-body th, .md-body td {
    text-align: left; padding: .55rem .7rem;
    border: 1px solid var(--border);
    vertical-align: top;
}
.md-body th { background: var(--surface-2); color: var(--fg-muted); font-weight: 600; }

/* Callouts. Fourth member of the strong/weak state family, after .notice-*,
   .badge-* and .btn-soft-*: weak fill, tinted border, title in the state colour.

   The icon is a CSS MASK, not an image -- the SVG supplies the shape and
   background-color supplies the colour from --cal-color, so each icon is its own
   state colour and flips with the theme for free. No icon font, no CDN, no markup.

   Grid, not a floated glyph: the icon holds column 1 across every row, so a callout
   of any length keeps its text in one column and nothing wraps back underneath it.
   align-self:start pins it to the first line rather than centring it in a tall block. */
.md-callout {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: .7rem;
    align-items: start;
    line-height: 1.6;
    margin: .9em 0;
    padding: .75rem 1rem;
    border: 1px solid color-mix(in srgb, var(--cal-color) 30%, transparent);
    border-radius: var(--radius);
    background: var(--cal-weak);
}
.md-callout > * { grid-column: 2; min-width: 0; }
/* The icon box is ONE LINE TALL (1.6em, matching the line-height set above) with the
   glyph centred inside it, so it lines up with the optical centre of the first line by
   construction. Sizing the box to the glyph and nudging it with a margin instead looks
   right at exactly one type scale and drifts the moment line-height or font-size moves
   -- it was 1.2px high before this. */
.md-callout::before {
    content: "";
    grid-column: 1;
    grid-row: 1 / -1;
    align-self: start;
    width: 1.15rem;
    height: 1.6em;
    background-color: var(--cal-color);
    -webkit-mask: var(--cal-icon) center / 1.15rem 1.15rem no-repeat;
    mask: var(--cal-icon) center / 1.15rem 1.15rem no-repeat;
}
/* Margin reset FIRST, and it is load-bearing for the icon alignment. The title is a
   <p> inside .md-body, so `.md-body p` at (0,1,1) out-specifies a bare
   `.md-callout-title` at (0,1,0) and hands it margin-top: .6em -- pushing the text
   down 9.6px while the ::before stays at the top of the grid row. Child combinators
   here score (0,2,0) and win. */
.md-callout > :first-child { margin-top: 0; }
.md-callout > :last-child  { margin-bottom: 0; }
.md-callout > .md-callout-title {
    font-weight: 600;
    margin: 0 0 .3rem;
    color: color-mix(in srgb, var(--cal-color) 78%, var(--fg));
}
.md-callout-body > :first-child { margin-top: 0; }
.md-callout-body > :last-child  { margin-bottom: 0; }

.md-callout-info    { --cal-color: var(--accent); --cal-weak: var(--accent-weak); }
.md-callout-warning { --cal-color: var(--warn);   --cal-weak: var(--warn-weak); }
.md-callout-danger  { --cal-color: var(--danger); --cal-weak: var(--danger-weak); }
.md-callout-ok      { --cal-color: var(--ok);     --cal-weak: var(--ok-weak); }

.md-callout-info    { --cal-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11.5v5'/%3E%3Cpath d='M12 7.6h.01'/%3E%3C/svg%3E"); }
.md-callout-warning { --cal-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.2 2.2 20.6h19.6z'/%3E%3Cpath d='M12 10v4.4'/%3E%3Cpath d='M12 17.6h.01'/%3E%3C/svg%3E"); }
.md-callout-danger  { --cal-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.3 2.6h7.4l5.7 5.7v7.4l-5.7 5.7H8.3l-5.7-5.7V8.3z'/%3E%3Cpath d='M9.4 9.4l5.2 5.2'/%3E%3Cpath d='M14.6 9.4l-5.2 5.2'/%3E%3C/svg%3E"); }
.md-callout-ok      { --cal-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M7.8 12.4l2.8 2.8 5.6-6'/%3E%3C/svg%3E"); }

/* ---------------------- command palette (Ctrl/Cmd+K) -------------------- */
/* Absorbed from the standalone command-palette.css. It belongs here rather than in
   its own file because it renders on EVERY shell page -- the opposite of
   markdown-editor.css, which is loaded on demand and so rightly stays separate.
   A second stylesheet for two rules is overhead on every request.

   Mechanics preserved verbatim from the functional version: fixed inset:0, the
   flex-start + padding-top anchoring (the box sits near the top, not centred, so a
   long apps list is not cut off), overflow:auto so a long list scrolls, and .open as
   the show/hide toggle the script drives. */

.cmd-palette-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 110;
    align-items: flex-start;
    justify-content: center;
    padding: 6rem 1rem 1rem;
    overflow: auto;
    background: var(--overlay);
}
.cmd-palette-overlay.open { display: flex; }

.cmd-palette-box {
    width: 100%;
    max-width: 22rem;
    padding: 1rem;
    background: var(--surface);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lift);
}

/* The re-rendered apps switcher fills the box. Two deliberate differences from the
   same component in the platform bar:

   - It takes --control-h, not --control-h-sm. The bar is chrome and runs tight (2.7);
     a palette is the thing you are looking at, so it gets the content height.
   - Its menu matches the box width instead of the bar's 220px minimum, so the panel
     reads as one object rather than a dropdown that happens to be inside a box. */
.cmd-palette-box .switcher,
.cmd-palette-box .switcher > summary { width: 100%; }
.cmd-palette-box .switcher > summary { height: var(--control-h); }
.cmd-palette-box .switcher-menu { width: 100%; min-width: 0; }

/* ------------------------------ auth card ------------------------------- */

.auth-card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-lift);
    padding: 1.75rem; margin-top: 2rem; text-align: center;
}
.auth-card h1 { margin-bottom: .5rem; }
.auth-card .btn { margin: 1rem 0 .5rem; }

/* Multi-step variant, for enrollment and any other auth flow that is a FORM rather
   than a single button. .auth-card is centred, which suits "Sign In With Microsoft"
   and actively fights a stack of labels, inputs and explanatory paragraphs -- centred
   labels above left-aligned fields read as a mistake. Wider, too: 34rem, which the
   enrollment page was already reaching for with an inline style. */
.auth-card-wide {
    max-width: 34rem;
    text-align: left;
}
.auth-card-wide .btn { margin: 0; }
.auth-card-wide h1 { text-align: center; }
.auth-card-wide .auth-step { display: block; text-align: center; margin-bottom: 1.25rem;
    font-size: .8rem; letter-spacing: .05em; text-transform: uppercase;
    color: var(--fg-muted); font-weight: 600; }
.auth-card-wide .auth-actions { display: flex; gap: .5rem; margin-top: 1.25rem; }

/* A field's live validation note. .is-ok turns it affirmative once the requirement is
   met -- the JS already appends a check mark, this gives it the colour to match. */
.field-note.is-ok { color: var(--ok); font-weight: 600; }

/* TOTP step: the QR framed as a scan target rather than a centred stray image, with
   the manual-entry secret beneath it. */
.auth-qr {
    display: flex; flex-direction: column; align-items: center; gap: .9rem;
    padding: 1.25rem; margin: 1rem 0;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.auth-qr-code {
    padding: .75rem; background: #fff; border-radius: var(--radius-sm);
    line-height: 0;
}
.auth-qr-code svg { display: block; }
.auth-qr-key {
    width: 100%; text-align: center;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1rem; word-break: break-all; color: var(--fg);
}
.auth-qr-key-label { font-size: .8rem; color: var(--fg-muted); margin-bottom: .2rem; }

/* Recovery codes: a grid, not a run of <br>s. Fixed columns so the codes align into a
   block you can check off while transcribing, and it is obvious at a glance how many
   there are. Needs the markup change noted in the handoff (one element per code). */
.auth-codes {
    display: grid; grid-template-columns: repeat(2, 1fr); gap: .35rem .9rem;
    padding: 1rem 1.25rem; margin: 1rem 0;
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.02rem;
}
.auth-codes span { padding: .15rem .1rem; letter-spacing: .02em; }
.auth-codes-once {
    display: block; margin-top: .6rem;
    color: color-mix(in srgb, var(--warn) 78%, var(--fg)); font-weight: 600;
}

/* ----------------------------- selector grid ---------------------------- */

.app-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem;
}
.app-card-wrap {
    position: relative;
    display: flex;
    --card-accent: var(--accent);
}
.app-card {
    flex: 1 1 auto;
    display: flex; gap: .9rem; align-items: flex-start;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 1.1rem; box-shadow: var(--shadow);
    color: var(--fg); transition: box-shadow .12s ease, transform .12s ease, border-color .12s ease;
}
.app-card:hover {
    text-decoration: none; box-shadow: var(--shadow-lift);
    transform: translateY(-2px); border-color: var(--border-strong);
}
.app-card-icon {
    flex: 0 0 auto; width: 44px; height: 44px; border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    /* font-size/weight are for the LETTER fallback (apps with no icon); --icon-size is
       for the SVG. Deliberately separate -- see .licon. */
    font-size: 1.35rem; font-weight: 700;
    --icon-size: 1.6rem;
    background: color-mix(in srgb, var(--card-accent) 15%, transparent);
    color: var(--card-accent);
}
.app-card-name { font-weight: 600; padding-right: 1.4rem; }
.app-card-desc { color: var(--fg-muted); font-size: .9rem; margin-top: .15rem; }

/* Favorite star: a control layered in the card corner, outside the launch link. */
.fav-toggle { position: absolute; top: .5rem; right: .5rem; margin: 0; }
.fav-star {
    appearance: none; -webkit-appearance: none; border: 0; background: none;
    cursor: pointer; padding: .1rem .25rem; line-height: 1; font-size: 1.2rem;
    color: var(--fg-muted); border-radius: 6px; transition: color .12s ease, background .12s ease;
}
.fav-star:hover { color: #eab308; background: var(--surface-2); }
.fav-star.is-fav { color: #eab308; }

/* Compact density. A global mode, set server-side from the `density` cookie as an
   attribute on the root -- the same shape as data-theme, and written as :root[...] to
   match it.

   The description and the favourite star are hidden; hiding the star is deliberate,
   not a layout workaround (favourites are set in comfortable view).

   The ICON is the point of this block. Once the description is gone the text is only
   ~23px tall, so the 44px icon is what the card's height actually is -- reducing the
   padding alone moves it barely at all. Shrinking the icon to 28px is what takes the
   card from ~64px to ~48px, and once the row is a single line it centres rather than
   aligning to the top.

   The grid keeps its 240px minimum: compact shortens rows, it does not narrow them.
   A narrower grid was compared in the style guide and not chosen. */
:root[data-density="compact"] .app-card-desc { display: none; }
:root[data-density="compact"] .fav-star      { display: none; }
:root[data-density="compact"] .app-card {
    padding: .5rem .75rem;
    gap: .6rem;
    align-items: center;
}
:root[data-density="compact"] .app-card-icon {
    width: 28px; height: 28px;
    font-size: .95rem;
    --icon-size: 1.1rem;
    border-radius: 6px;
}

.app-section-head {
    font-size: .8rem; text-transform: uppercase; letter-spacing: .05em;
    color: var(--fg-muted); font-weight: 600; margin: 1.6rem 0 .7rem;
}
.app-section-head:first-of-type { margin-top: .5rem; }

/* Switcher: pinned-item star, current highlight, and the nested "All apps" list. */
.switcher-star { color: #eab308; margin-right: .45rem; }
.switcher-star.muted { color: var(--fg-muted); }
.switcher-menu a.cur {
    color: var(--accent); font-weight: 600;
    background: var(--surface-2); border-radius: 6px;
}
.switcher-sub { margin: 0; }
.switcher-sub > summary {
    list-style: none; cursor: pointer; user-select: none;
    padding: .5rem .7rem; border-radius: 6px;
    display: flex; align-items: center; justify-content: space-between;
}
.switcher-sub > summary::-webkit-details-marker { display: none; }
.switcher-sub > summary::after { content: "\25B8"; color: var(--fg-muted); font-size: .8em; }
.switcher-sub[open] > summary::after { content: "\25BE"; }
.switcher-sub > summary:hover { background: var(--surface-2); }
/* A <summary>, so .switcher-menu a does not reach it. */
.switcher-sub > summary { white-space: nowrap; }
/* overflow-x: clip, not the default. Setting overflow-y:auto computes overflow-x to
   auto as well -- you cannot scroll one axis and leave the other visible -- so a long
   name in here would produce a SECOND, horizontal scrollbar inside the menu, which
   1.1 rejects. Clip keeps the vertical scroll and drops the horizontal one, and
   because a clip container still contributes its content width, the menu above can
   still size itself to a long name in this list. */
.switcher-sublist { max-height: 260px; overflow-y: auto; overflow-x: clip; }
.switcher-sublist a { padding-left: 1.15rem; }
.switcher-allpage { border-top: 1px solid var(--border); color: var(--fg-muted); }

.empty-state {
    background: var(--surface); border: 1px dashed var(--border-strong);
    border-radius: var(--radius); padding: 2rem; text-align: center;
}

/* Sign-in logo: optional light/dark variants, swapped by theme (explicit or OS preference). */
.auth-logo { text-align: center; margin-bottom: 1.2rem; }
.auth-logo img { max-width: 220px; max-height: 90px; }
.auth-logo .dark { display: none; }
.auth-logo .light { display: block; margin: 0 auto; }
:root[data-theme="dark"] .auth-logo .light { display: none; }
:root[data-theme="dark"] .auth-logo .dark { display: block; margin: 0 auto; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) .auth-logo .light { display: none; }
    :root:not([data-theme]) .auth-logo .dark { display: block; margin: 0 auto; }
}

/* ------------------------------- print ---------------------------------- */
/* Chrome does not print. Bars, rails, toolbars and menus are navigation, and
   navigation is a screen concern -- what prints is the document. The dark
   palette is scoped to @media screen in tokens.css, so paper always gets the
   light values and nothing here has to force text colours. */

@media print {

    /* 1. Navigation, in every form it takes. */
    .shell-bar,
    .app-nav,
    .app-toolbar,
    .side-nav,
    .cmd-palette-overlay,
    .fav-toggle,
    .app-menu { display: none; }

    /* 2. The width floor, the centred column and the grow-to-content rules are
       all screen layout. Paper has its own margins. The rail grid collapses to
       a block, or the hidden rail leaves an empty first column. */
    body { min-width: 0; }
    .shell-main,
    .shell-main.full { max-width: none; min-width: 0; margin: 0; padding: 0; }
    .side-layout,
    .side-layout.detached { display: block; min-width: 0; border: 0; box-shadow: none; }
    .side-main { min-width: 0; padding: 0; }

    /* 3. Paper is already white, so the page surface is dead ink. A token
       override rather than a list of components -- the golden rule still
       applies in print. A literal here is not the usual hard-coded-colour bug:
       print has no second theme to flip to. */
    :root { --bg: #fff; }
    .card, .panel { box-shadow: none; }

    /* 4. Where a fill IS the meaning, it has to survive the browser's
       background-graphics setting. Note the ::before: a callout's icon is a
       mask painted with background-color, so without this the callout loses
       its fill AND its icon and prints as a bare box -- a Warning and a Note
       become indistinguishable. */
    .md-callout,
    .md-callout::before,
    .notice,
    .badge,
    .btn-soft-ok,
    .btn-soft-warn,
    .btn-soft-danger {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    /* Belt and braces: a heavier state-coloured left border reads even if a
       browser ignores print-color-adjust, since borders are not background
       graphics. On a monochrome printer it becomes a weight cue rather than a
       colour one, which is still better than nothing. */
    .md-callout { border-left: 3px solid var(--cal-color); }

    /* 5. Don't split small things across sheets. Cards are deliberately absent:
       one long card would be pushed whole onto a new page and could overflow
       it. */
    .md-callout,
    .md-body pre,
    .md-body tr,
    table.data tr { break-inside: avoid; }
    h1, h2, h3,
    .md-body h1, .md-body h2, .md-body h3 { break-after: avoid; }
}
