/* ===========================================================================
   Sanctum — in Decipher's design language (decipher/frontend/docs/
   design-guidelines.md): a light-first app with a dark twin, one navy brand
   hue (222), Inter for text and Montserrat for headings, generous rounding
   (12px controls, 24px page surface and dialogs) and depth from soft shadows
   plus the "blick" inset rim rather than from borders.
   App chrome: sidebar (brand + nav) · header (search, theme, account) · the
   page drawn on one large card (Decipher's PageSurface). Detail / create /
   edit open in a centered modal dialog.
   =========================================================================== */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 300 700; font-display: swap;
  src: url("../fonts/inter-latin.65850a373e25.woff2") format("woff2"); }
@font-face { font-family: "Montserrat"; font-style: normal; font-weight: 500 700; font-display: swap;
  src: url("../fonts/montserrat-latin.c154477b9aff.woff2") format("woff2"); }

/* THEME — every colour token states its light and its dark value side by side
   (`light-dark()`), so the two themes cannot drift apart the way two copies of
   a token block do. Which one applies is `color-scheme`: the OS preference by
   default, or the viewer's own choice, which theme.js writes to
   <html data-theme> before the first paint. The values are Decipher's own
   (app/globals.css): --bg/--surface are its background/card, --surface-2 its
   translucent --input fill, --accent its --primary. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }
:root {
  --bg:        light-dark(hsl(210 15% 97%), hsl(220 6% 0%));   /* page floor, sidebar, header */
  --surface:   light-dark(#fff, hsl(220 0% 9%));                /* the page card, cards, modal */
  /* Translucent on purpose (Decipher's --input): it darkens whatever it sits
     on by a fixed step, so one value separates from the floor, a card and a
     modal alike. The navy overlay keeps the brand's tint in the greys. */
  --surface-2: light-dark(hsl(220 40% 25% / 6%), hsl(220 40% 84% / 12%));  /* fields, wells, chips */
  --surface-hi:light-dark(hsl(220 40% 25% / 5%), hsl(220 40% 84% / 7%));   /* row / control hover */
  --border:    light-dark(hsl(220 13% 90%), hsl(220 5% 18%));
  --border-hi: light-dark(hsl(220 13% 84%), hsl(220 5% 26%));
  --text:      light-dark(hsl(222 28% 11%), hsl(0 0% 95%));
  --muted:     light-dark(hsl(220 10% 44%), hsl(220 5% 58%));
  --faint:     light-dark(hsl(220 8% 62%), hsl(220 5% 42%));
  --accent:    light-dark(hsl(222 28% 28%), hsl(222 38% 57%));  /* Decipher navy (--primary) */
  --accent-2:  light-dark(hsl(222 30% 22%), hsl(222 45% 64%));
  --accent-sf: light-dark(hsl(222 40% 28% / 9%), hsl(222 50% 60% / 16%));
  --on-accent: light-dark(hsl(210 20% 98%), #fff);
  --ring:      light-dark(hsl(222 47% 28%), hsl(222 57% 61%));
  --info:      light-dark(hsl(222 55% 86%), hsl(222 25% 18%));
  --info-fg:   light-dark(hsl(222 45% 26%), hsl(222 40% 88%));
  --success:   light-dark(hsl(142 72% 29%), hsl(142 69% 58%));
  --danger:    light-dark(hsl(0 70% 45%), hsl(0 70% 62%));
  --danger-sf: light-dark(hsl(0 70% 45% / 8%), hsl(0 70% 55% / 14%));
  /* One categorical scale, each hue a light/dark pair from Decipher's status
     palette (the --family-status strokes). Item types, project colours and file
     categories all draw from it, so a tint is always a colour the brand owns. */
  --hue-green:  light-dark(hsl(166 76% 25%), hsl(161 47% 58%));
  --hue-blue:   light-dark(hsl(210 74% 37%), hsl(208 71% 72%));
  --hue-amber:  light-dark(hsl(34 84% 33%),  hsl(35 86% 54%));
  --hue-orange: light-dark(hsl(22 78% 42%),  hsl(22 75% 62%));
  --hue-gold:   light-dark(hsl(38 85% 36%),  hsl(43 90% 60%));
  --hue-purple: light-dark(hsl(270 55% 42%), hsl(270 55% 72%));
  --hue-pink:   light-dark(hsl(330 65% 45%), hsl(330 70% 74%));
  --hue-rose:   light-dark(hsl(350 70% 42%), hsl(350 70% 72%));
  --pw:   var(--hue-green);    /* passwords */
  --note: var(--hue-blue);     /* notes */
  --file: var(--hue-amber);    /* files */
  --proj: var(--hue-purple);   /* projects */
  --kb:   var(--hue-rose);     /* knowledge */
  --star: light-dark(hsl(38 92% 48%), hsl(43 96% 58%));   /* pinned */
  /* The toast is inverted chrome (Decipher's pill): navy-black in light, near
     white in dark. */
  --inverse:    light-dark(hsl(222 28% 11% / 92%), hsl(0 0% 95% / 94%));
  --on-inverse: light-dark(#fff, hsl(222 28% 11%));
  --overlay:    light-dark(hsl(222 28% 11% / 32%), hsl(0 0% 0% / 62%));
  /* The blick: a 1px specular rim along a lifted edge (Decipher's `blick` /
     `blick-sm` utilities). It is what marks the active nav item and raised
     controls, instead of a border. */
  /* The outline button (Decipher's `outline`): its fill, inner ring and hover.
     Every control drawn in that recipe reads these three, not its own copy. */
  --btn-bg:     light-dark(#fff, #000);
  --btn-ring:   inset 0 0 0 1px light-dark(rgb(229 229 229 / .8), var(--border));
  --btn-hover:  light-dark(hsl(219 12% 95%), hsl(220 6% 12%));
  --blick:      light-dark(hsl(0 0% 100%), hsl(0 0% 16%));
  --blick-shadow: light-dark(rgb(0 0 0 / .1), rgb(0 0 0 / .5));

  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --heading: "Montserrat", var(--sans);
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;

  --r-sm: 8px;    /* menu items, chips, small controls (rounded-lg) */
  --r:    12px;   /* every field, button and card (rounded-xl) */
  --r-lg: 24px;   /* the page surface and dialogs (rounded-3xl) */

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);
  --shadow:    0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);
  --shadow-xl: 0 20px 25px -5px light-dark(hsl(222 28% 11% / 6%), rgb(0 0 0 / .5)),
               0 8px 10px -6px light-dark(hsl(222 28% 11% / 5%), rgb(0 0 0 / .4));
  --shadow-lg: 0 25px 50px -12px light-dark(hsl(222 28% 11% / 25%), rgb(0 0 0 / .7));
  --inset-field: inset 0 1px 1px 0 rgb(0 0 0 / .04);
  --blick-sm: 0 1px 3px 0 var(--blick-shadow), 0 1px 2px -1px var(--blick-shadow),
              inset 0 1px 0 0 color-mix(in srgb, var(--blick) 50%, transparent),
              inset 0 0 1px 0 color-mix(in srgb, var(--blick) 50%, transparent),
              inset 1px 0 0 0 color-mix(in srgb, var(--blick) 50%, transparent),
              inset -1px 0 0 0 color-mix(in srgb, var(--blick) 50%, transparent),
              inset 0 -1px 0 0 color-mix(in srgb, var(--blick) 20%, transparent);

  --sidebar-w: 240px;
  --sidebar-w-c: 60px;   /* collapsed to its icons */
  --topbar-h:  64px;
  --ctrl-h: 36px;

  /* The content rail: wide enough that a normal laptop screen is filled edge to
     edge, capped only so ultrawide monitors don't stretch list rows past
     reading length. --surface-gap is the margin the page card keeps from the
     window on the right and at the bottom (Decipher's `sm:pr-8 sm:pb-6`). */
  --content-max: 1720px;
  --content-pad: 1.5rem;
  --surface-gap: 2rem;
}
* { box-sizing: border-box; }
/* SCROLLBARS — everything scrolls, nothing draws a bar for it.
   A browser-painted scrollbar is the same foreign artefact a native checkbox is
   (see the design notes): it ignores the palette, it is a different width on
   every platform, and on a card it reads as a seam down the edge. Wheel,
   trackpad, keyboard, drag and scroll-anchoring all keep working — what is given
   up is the bar as a hint that a region scrolls, which is why the regions that
   do (the modal's two columns) are cut off at a visible edge rather than fading
   out. This also retires `scrollbar-gutter: stable`: with no bar there is no
   width for a modal's `overflow: hidden` to reclaim, so nothing can jolt. */
* { scrollbar-width: none; }
*::-webkit-scrollbar { display: none; }
html { -webkit-text-size-adjust: 100%; font-size: 14.5px; }   /* Decipher's base size */
body {
  margin: 0; color: var(--text); font-family: var(--sans);
  font-size: 13.5px; line-height: 1.5; -webkit-font-smoothing: antialiased;
  background: var(--bg); min-height: 100vh;
}
/* Montserrat on every heading, as Decipher sets it on h1–h6. */
h1, h2, h3, h4, h5, h6 { font-family: var(--heading); }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-2); text-decoration: underline; text-underline-offset: 3px; }

/* ICONS — Fluent System Icons, each drawn twice (vault/icons.py): the regular
   outline at rest, the filled one while a control is hovered, focused or
   active, the swap Decipher makes on every clickable thing. An icon that is
   not inside a control never swaps. */
.ico { display: block; flex: none; fill: currentColor; }
/* The page's one copy of every icon (icon_sprite in base.html), referenced by
   <use>. Out of the layout rather than display:none, which some engines treat
   as unrenderable for what references it. */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.ico-f { display: none; }
/* A control that is itself the active one (a nav link, a tab, a scope) — not
   anything inside an active container: a previewed row is .is-active too, and
   its pin must not read as pinned. */
:is(a, button):is(:hover, :focus-visible) .ico-r, :is(a, button).is-active .ico-r, .on .ico-r { display: none; }
:is(a, button):is(:hover, :focus-visible) .ico-f, :is(a, button).is-active .ico-f, .on .ico-f { display: inline; }

/* ===========================================================================
   APP CHROME — grid: [sidebar | top bar] / [sidebar | main], one window tall.
   The sidebar and the top bar sit on the page floor; the page itself is one
   large card (.surface) floating on it, as Decipher draws a PageSurface — and
   like Decipher's, the card stays put while what is on it scrolls (.content),
   so the top bar and the card's edges are always where the eye left them.
   =========================================================================== */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: var(--topbar-h) minmax(0, 1fr);
  grid-template-areas: "sidebar topbar" "sidebar main";
  height: 100vh; height: 100dvh;
  transition: grid-template-columns .15s ease-out;
}
/* Decipher's icon rail (⌘B): the width the nav gives up goes to the page. */
.app.is-collapsed { --sidebar-w: var(--sidebar-w-c); }

/* ------------------------------------------------------------- sidebar --- */
.sidebar { grid-area: sidebar; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: .4rem; padding: 0 .6rem 1rem;
  background: var(--bg); }
.side-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  flex: none; height: var(--topbar-h); padding: 0 .1rem 0 .65rem; }
.brand { display: flex; align-items: center; min-width: 0; color: var(--text); }
.brand:hover { text-decoration: none; color: var(--text); }
/* The round control that folds the sidebar to its icons — Decipher's, with the
   blick rim every raised control wears. */
.side-toggle { display: grid; place-items: center; flex: none; width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 50%; cursor: pointer; color: var(--muted); background: transparent;
  transition: color .12s, background .12s, box-shadow .12s; }
.side-toggle .ico { width: 16px; height: 16px; }
.side-toggle:hover { color: var(--text); background: var(--surface); box-shadow: var(--blick-sm); }

/* The lock-up: the mark in the brand navy, the wordmark in ink, the product
   name where Decipher's own app writes "IP". */
.logo { display: inline-flex; align-items: center; gap: .5rem; line-height: 1; }
.logo-mark { width: 20px; height: 20px; flex: none; color: var(--accent); }
.logo-word { height: 13px; width: auto; flex: none; color: var(--text); }
.logo-product { font-family: var(--heading); font-weight: 700; font-size: 14px;
  letter-spacing: -.01em; color: var(--accent); margin-left: -.15rem; }

.nav { display: flex; flex-direction: column; gap: 2px; }
/* A group: its micro-label is the control that folds it (Decipher's "MY WORK"
   with the chevron at the far edge), remembered per group in a cookie. */
.nav-sec { display: flex; flex-direction: column; gap: 2px; }
.nav-group { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  width: 100%; margin: .75rem 0 .1rem; padding: .3rem .65rem; border: 0; background: none; cursor: pointer;
  border-radius: var(--r-sm); font-family: var(--sans); font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em; color: var(--text); text-align: left; }
.nav-group:hover { background: var(--surface-hi); }
.nav-chev { width: 14px; height: 14px; color: var(--muted); transform: rotate(90deg);
  transition: transform .15s ease-out; }
.nav-sec.is-closed .nav-chev { transform: none; }
.nav-sec-body { display: flex; flex-direction: column; gap: 2px; }
.nav-sec.is-closed .nav-sec-body { display: none; }
.nav-empty { margin: 0; padding: .35rem .65rem; font-size: 12.5px; color: var(--faint); }
/* A nav item is a 36px row, rounded like every control; the active one is
   lifted onto the card colour with the blick rim (Decipher's
   `bg-card/90 blick-sm`), its icon filled. */
.nav-link { position: relative; display: flex; align-items: center; gap: .65rem;
  height: var(--ctrl-h); padding: 0 .65rem; border-radius: var(--r); color: var(--text);
  font-size: 13.5px; transition: background .15s ease-out, box-shadow .15s ease-out; }
.nav-link:is(:hover, .is-active) { background: color-mix(in srgb, var(--surface) 90%, transparent);
  box-shadow: var(--blick-sm); color: var(--text); text-decoration: none; }
.nav-link.is-active { font-weight: 500; }
.nav-ico { width: 20px; height: 20px; color: var(--text); opacity: .6; transition: opacity .15s; }
.nav-link:hover .nav-ico, .nav-link.is-active .nav-ico { opacity: 1; }
.nav-link.is-active .nav-ico { color: var(--kc, var(--accent)); }
/* A recent item names a thing, not a place: its icon keeps the type colour. */
.nav-recent .nav-ico { color: var(--kc); opacity: .85; }
.nl-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.side-foot { margin-top: auto; display: flex; flex-direction: column; gap: .7rem; padding: .5rem .65rem 0; }
.chip { display: inline-flex; align-items: center; gap: .4rem; font-size: 11.5px;
  color: var(--muted); align-self: flex-start; }
.chip .ico { width: 16px; height: 16px; color: var(--success); }

/* Collapsed: icons only, every group open (a rail has no room to fold
   anything), each item's name a tooltip beside it. The rail is short enough
   not to scroll, which is what lets a tooltip leave it. A phone has its own
   tab bar, so the rail is a desktop state only. */
@media (min-width: 768px) {
  .is-collapsed .sidebar { overflow: visible; padding: 0 .45rem 1rem; }
  .is-collapsed .side-head { justify-content: center; padding: 0; }
  .is-collapsed .brand, .is-collapsed .nav-group, .is-collapsed .nav-recents,
  .is-collapsed .chip > span { display: none; }
  .is-collapsed .nav-sec-body { display: flex; }
  .is-collapsed .nav-sec { margin-top: .45rem; padding-top: .45rem; border-top: 1px solid var(--border); }
  .is-collapsed .nav-link { justify-content: center; padding: 0; }
  .is-collapsed .side-foot { align-items: center; padding: .5rem 0 0; }
  .is-collapsed .nav-link .nl-label { position: absolute; left: calc(100% + 10px); top: 50%; z-index: 50;
    flex: none; transform: translateY(-50%); padding: .3rem .6rem; border-radius: var(--r-sm);
    background: var(--inverse); color: var(--on-inverse); font-size: 12px; font-weight: 500;
    box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: opacity .12s; }
  .is-collapsed .nav-link:is(:hover, :focus-visible) .nl-label { opacity: 1; }
}

/* ---------------------------------------------------------------- top bar - */
/* The page's own head at the left (Decipher's PageHeaderSlot), search in the
   middle, the viewer's own controls at the right. Three columns, so the
   search stays centred over the page card whatever width either end takes. */
.topbar { grid-area: topbar; position: relative; z-index: 30;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 360px) minmax(0, 1fr);
  align-items: center; gap: 1rem;
  padding: 0 var(--surface-gap) 0 var(--content-pad); background: var(--bg); }
.top-brand { display: none; }
.top-head { grid-column: 1; display: flex; align-items: center; gap: .4rem; min-width: 0; }
.top-title { margin: 0; min-width: 0; font-size: 16.5px; font-weight: 600; letter-spacing: -.01em;
  line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Back is a fixed parent page, drawn as Decipher's ghost chevron. */
.top-back { display: grid; place-items: center; flex: none; width: 28px; height: 28px;
  margin-left: -.35rem; border-radius: var(--r-sm); color: var(--muted); transition: color .12s, background .12s; }
.top-back .ico { width: 18px; height: 18px; }
.top-back:hover { color: var(--text); background: var(--surface-hi); text-decoration: none; }
.top-search { grid-column: 2; position: relative; }
.top-search-ico { position: absolute; left: .75rem; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--faint); pointer-events: none; }
.top-search input { height: 34px; min-height: 0; padding: 0 3rem 0 2.2rem; font-size: 13px; cursor: pointer; }
.top-search input:focus ~ .top-search-ico { color: var(--accent); }
.top-search-kbd, .search-kbd { font-family: var(--sans); font-size: 11px; font-weight: 600; color: var(--text);
  background: var(--surface); border-radius: 6px; padding: .05rem .4rem; line-height: 1.5;
  box-shadow: var(--shadow); pointer-events: none; white-space: nowrap; }
.top-search-kbd { position: absolute; right: .45rem; top: 50%; transform: translateY(-50%); }
.top-actions { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: .35rem; }
/* One icon control for the chrome's buttons (the bell, a dialog's close). */
.icon-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--ctrl-h); height: var(--ctrl-h); padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--muted); border-radius: var(--r); transition: color .12s, background .12s; }
.icon-btn .ico { width: 18px; height: 18px; }
.icon-btn:hover, .icon-btn[aria-expanded="true"] { color: var(--text); background: var(--surface-hi); text-decoration: none; }
.bell-dot { position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger); box-shadow: 0 0 0 2px var(--bg); }
/* The viewer: their name and their initial, the trigger of the account menu. */
.top-user { display: inline-flex; align-items: center; gap: .6rem; height: var(--ctrl-h);
  padding: 0 .3rem 0 .7rem; margin-left: .2rem; border: 0; background: none; cursor: pointer;
  border-radius: var(--r); font-family: var(--sans); font-size: 13.5px; font-weight: 500; color: var(--text);
  white-space: nowrap; transition: background .12s; }
.top-user:hover, .top-user[aria-expanded="true"] { background: var(--surface-hi); }
.avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; display: grid; place-items: center;
  font-size: 12px; font-weight: 600; color: var(--muted);
  background: var(--surface); box-shadow: var(--blick-sm), inset 0 0 0 1px var(--border); }

/* ------------------------------------------------------------------ menus - */
/* A dropdown the chrome opens (the account menu, notifications): a card under
   its trigger, right-aligned to it. app.js opens one at a time and closes it
   on Escape or a click elsewhere. */
.menu { position: relative; }
.menu-pop { position: absolute; right: 0; top: calc(100% + 6px); z-index: 60; min-width: 250px;
  display: flex; flex-direction: column; padding: .35rem; background: var(--surface);
  border-radius: var(--r); box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--border);
  animation: menu-in .14s ease-out both; }
.menu-pop[hidden] { display: none; }
@keyframes menu-in { from { opacity: 0; transform: translateY(-4px); } }
.menu-item { display: flex; align-items: center; gap: .6rem; min-height: 34px; padding: .35rem .6rem;
  border-radius: var(--r-sm); color: var(--text); font-size: 13.5px; }
.menu-item > span { flex: 1; min-width: 0; }
.menu-item .ico { width: 16px; height: 16px; flex: none; opacity: .7; }
.menu-item:hover, .menu-item:focus-visible, .menu-item.is-active { background: var(--surface-hi); color: var(--text);
  text-decoration: none; }
.menu-item:hover .ico { opacity: 1; }
.menu-badge { flex: none; min-width: 20px; padding: 0 .4rem; text-align: center; border-radius: 20px;
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--on-accent); background: var(--danger); }
.menu-badge:empty { display: none; }
.menu-sep { height: 1px; margin: .3rem -.35rem; background: var(--border); }
.menu-who { display: flex; flex-direction: column; padding: .45rem .6rem .35rem; }
.menu-who-name { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; }
.menu-who-role { font-size: 12px; color: var(--muted); }
.menu-label { margin: 0; padding: .45rem .6rem .3rem; font-family: var(--heading); font-size: 11px;
  font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.menu-empty { margin: 0; padding: .4rem .6rem .6rem; font-size: 13px; color: var(--muted); }
.menu-theme { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .2rem .25rem .2rem .6rem; font-size: 13.5px; }
.menu-theme .seg-group { height: 30px; padding: 2px; }
.menu-theme .seg { padding: 0 .5rem; }
.menu-theme .seg .ico { width: 15px; height: 15px; }
:root:not([data-theme]) [data-theme-set="system"],
:root[data-theme="light"] [data-theme-set="light"],
:root[data-theme="dark"] [data-theme-set="dark"] { color: var(--text); background: var(--surface); box-shadow: var(--blick-sm); }

/* ------------------------------------------------------------- the page - */
.main { grid-area: main; display: flex; min-width: 0; min-height: 0;
  padding: 0 var(--surface-gap) 1.5rem 0; }
/* The page card (Decipher's PageSurface): white, 24px corners, a long soft
   shadow, the window's full height whatever is on it. */
.surface { flex: 1; display: flex; min-width: 0; min-height: 0; width: 100%;
  max-width: var(--content-max); margin: 0 auto; overflow: hidden;
  background: var(--surface); border-radius: var(--r-lg); box-shadow: var(--shadow-xl); }
/* What scrolls on it. With no scrollbar anywhere (see SCROLLBARS), the edge
   that has more beyond it fades out instead — Decipher's ScrollArea fade, set
   per edge by app.js so a page that fits shows a hard, finished edge. */
@property --fade-t { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --fade-b { syntax: "<length>"; inherits: false; initial-value: 0px; }
.content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 1.6rem var(--content-pad) 2rem;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-t), #000 calc(100% - var(--fade-b)), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-t), #000 calc(100% - var(--fade-b)), transparent);
  transition: --fade-t .2s, --fade-b .2s; }
.content[data-fade~="top"] { --fade-t: 2rem; }
.content[data-fade~="bottom"] { --fade-b: 2rem; }

/* ===========================================================================
   HEADS
   =========================================================================== */
.page-title { font-size: 20px; font-weight: 600; margin: 0 0 .25rem; letter-spacing: -.01em; line-height: 1.3; }
.page-sub { color: var(--muted); font-size: 13.5px; margin: 0 0 1.6rem; }
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.4rem; }
.page-head .page-sub { margin: 0; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 2rem 0 .8rem; }
/* Decipher's card section title ("MY PORTFOLIO"): small, bold, uppercase — one
   micro-label for section heads and card heads. */
.section-title, .ecard-t { font-family: var(--heading); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text); }
.section-title { margin: 0; }

/* A project workspace's type groups: every block is already a card, so the
   space between two of them is the whole separator, with no rule in between. */
.db-block { margin: 0; }
[data-workspace] .db-block + .db-block { margin-top: .9rem; }

/* ===========================================================================
   DASHBOARD — where to pick up: a quiet line of counts, a line of security
   status, then one Continue card (tabs over a list) beside the projects.
   Colour is spent on one thing at a time: the rows here are drawn neutral and
   their actions wait for the pointer, so the only loud thing on the page is a
   security problem when there is one.
   =========================================================================== */
.dash { container-type: inline-size; }
.dash-new { margin-left: .4rem; }
.dash-new .menu-pop { min-width: 200px; left: 0; right: auto; }
.dash-new .menu-item .kico { width: 16px; height: 16px; flex: none; color: var(--kc); }
.dash-new-chev { width: 14px; height: 14px; margin: 0 -.15rem 0 .1rem; opacity: .8; }

.dash-sum { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .55rem;
  margin: 0 0 .9rem; font-size: 13.5px; color: var(--muted); }
.dash-sum a { color: var(--muted); }
.dash-sum a:hover { color: var(--text); text-decoration: none; }
.dash-sum b { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.dash-dot { color: var(--faint); }
.dash-when { margin-left: auto; font-size: 12px; color: var(--faint); }

.dash-health { display: flex; align-items: center; gap: .65rem; margin-bottom: 1.1rem;
  padding: .6rem .9rem; border-radius: var(--r); font-size: 13px; color: var(--text);
  background: var(--surface-2); transition: background .12s; }
.dash-health:hover { text-decoration: none; }
.dash-health-ico { flex: none; width: 18px; height: 18px; }
.dash-health-t { font-weight: 600; }
.dash-health-s { color: var(--muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-health.ok .dash-health-ico { color: var(--success); }
.dash-health.ok .dash-health-s { margin-left: auto; color: var(--faint); font-size: 12px; }
.dash-health.ok:hover { background: var(--surface-hi); }
.dash-health.bad { background: var(--danger-sf); }
.dash-health.bad .dash-health-ico { color: var(--danger); }
.dash-health.bad:hover { background: color-mix(in srgb, var(--danger) 18%, transparent); }
.dash-health-go { display: inline-flex; align-items: center; gap: .2rem; margin-left: auto; flex: none;
  font-weight: 600; color: var(--danger); }
.dash-health-go .ico { width: 15px; height: 15px; }

.dash-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 340px); gap: 1rem; align-items: start; }
@container (max-width: 760px) { .dash-grid { grid-template-columns: minmax(0, 1fr); } }

/* The tab strip is the card's head: the hairline under it is the card's.
   (Scoped by the card, so it outranks the record page's .utabs further down.) */
.dash-main .dash-tabs { margin: 0; padding: 0 1rem; gap: 1.4rem; }
.dash-main [data-tab-panel][hidden] { display: none; }
.ecard-more.dash-add { justify-content: flex-start; gap: .4rem; }
/* The side card's head stands as tall as the tab strip beside it. */
.dash-side .ecard-head { box-sizing: border-box; height: 43px; padding-block: 0; }
@container (max-width: 560px) { .dash-health.bad .dash-health-s { display: none; } }

/* When it last changed sits at the row's far edge; the row's actions take
   that spot on hover instead of costing every row their width. */
.dash-main .ecard-rows .item .when { display: block; margin-left: auto; }

/* Rows drawn neutral: the type speaks through its glyph only, and the tile
   behind it is the same grey for every type. */
.dash .ecard-rows .item .ic { background: var(--surface-2); }
.dash .ptile-n { color: var(--muted); background: var(--surface-2); }
@media (hover: hover) {
  .dash .ecard-rows .item-actions { position: absolute; right: .5rem; top: 50%; translate: 0 -50%;
    opacity: 0; pointer-events: none; transition: opacity .12s; }
  .dash .ecard-rows .item:hover .item-actions,
  .dash .ecard-rows .item:focus-within .item-actions { opacity: 1; pointer-events: auto; }
  .dash .ecard-rows .item:is(:hover, :focus-within) :is(.when, .ptile-n) { visibility: hidden; }
}

.actions { display: flex; gap: .55rem; flex-wrap: wrap; }

/* ===========================================================================
   CARDS — Decipher's card, the furniture of the dashboard's widgets and the
   project workspace. A collection a board wide (a workspace's Pinned and
   Recently opened) runs its rows across columns, stated per breakpoint
   rather than auto-fitted: four on a desktop, two on a tablet, one on a phone.
   =========================================================================== */
.ecard-rows.cols { display: grid; grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1499px) { .ecard-rows.cols { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px)  { .ecard-rows.cols { grid-template-columns: 1fr; } }

/* A Decipher card: white on the white page card, set apart by a hairline and
   its 12px corners rather than by a shadow. */
.ecard { display: flex; flex-direction: column; min-width: 0; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); }
/* Head: the badge and name of what the card holds, its total, and whatever
   acts on it — the dashboard's quiet create, a workspace group's pair of
   buttons. On the dashboard the name is the link to the full list, so the card
   needs no second "View all" until there is more than it shows (.ecard-more);
   in a workspace the same title is a plain label, hence the hover on `a` only. */
.ecard-head { display: flex; align-items: center; gap: .6rem; padding: .7rem .7rem .7rem .9rem;
  border-bottom: 1px solid var(--border); }
/* Title and count are one label and stay together; whatever acts on the card
   goes to the far edge. The count carries the push (margin-right: auto), not
   the title, or a wide card would strand its own total next to the buttons. */
.ecard-t { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ecard-head .col-count { margin-right: auto; }
.ecard-go { flex: none; width: 28px; height: 28px; border-radius: var(--r-sm); }
.ecard-go .ico { width: 16px; height: 16px; }
.ecard-go:hover { color: var(--kc, var(--accent)); }
a.ecard-t:hover { color: var(--kc, var(--accent)); text-decoration: none; }

/* Rows: the flat list row, one notch denser — the card is the panel, so the
   rows inside it carry no border or background of their own. --ctrl-h scopes
   the shrink to every button the row draws (copy, pin) in one place. */
.ecard-rows { --ctrl-h: 30px; }
.ecard-rows .item { padding: .55rem .65rem; gap: .6rem; }
.ecard-rows .item .ic { width: 30px; height: 30px; }
.ecard-rows .item .ic .kico { width: 16px; height: 16px; }
.ecard-rows .item .body .t { font-size: 13.5px; }
.ecard-rows .item .body .s { font-size: 12.5px; }
.ecard-rows .item .when { display: none; }   /* a column this narrow spends its width on the name */

/* A collection a board wide (a project's Pinned and Recently opened, `cols`)
   runs its rows *across* the columns instead of down one; the grid itself is
   cut above. The hairline between rows is
   the row's own border-top — and the top row needs no exception for it, at any
   column count: every cell in the first grid row shares its top edge with the
   card head, so pulling the grid up by exactly that border lands them on each
   other and reads as the one line the head already draws. */
.ecard-rows.cols { margin-top: -1px; }

/* The line in place of rows. On the dashboard the whole line is the create
   link; in a workspace group it is a sentence with two links in it, because
   there filing something already here and making something new are different
   answers. Only the link form lights up on hover.

   A line of text, not a flex row: a sentence with links in it has to keep its
   own word spacing, and a flex gap would fall between "create new" and the
   full stop that ends it. The lone icon aligns itself instead. */
.ecard-empty { display: block; margin: 0; padding: 1.3rem .8rem;
  font-size: 13px; color: var(--muted); text-align: center;
  transition: color .12s, background .12s; }
.ecard-empty .ico { display: inline-block; width: 15px; height: 15px; vertical-align: -3px; margin-right: .3rem; }
.ecard-empty a { color: var(--muted); }
.ecard-empty a:hover { color: var(--kc, var(--accent)); }

.ecard-more { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .6rem .9rem; border-top: 1px solid var(--border);
  font-size: 12.5px; font-weight: 500; color: var(--muted); transition: color .12s, background .12s; }
.ecard-more .ico { width: 15px; height: 15px; }
.ecard-more:hover { color: var(--text); background: var(--surface-hi); text-decoration: none; }

/* A project row is an item row plus the two things only a project has: its own
   colour (set inline, per project) and the number of items filed under it. It
   is also a drop target — see .ptile.drop-hover further down. */
.ptile-n { flex: none; min-width: 24px; height: 20px; padding: 0 .45rem; display: inline-flex; align-items: center;
  justify-content: center; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--kc, var(--proj)); border-radius: 20px;
  background: color-mix(in srgb, var(--kc, var(--proj)) 14%, transparent); }

/* ===========================================================================
   TYPE HEADINGS — the icon badge + count that label each dashboard/section
   =========================================================================== */
.col-ico { width: 28px; height: 28px; }
/* Any glyph, not just the registry's .kico: a heading badge also wears the
   pinned star or the shared-in arrow, and no icon carries a size of its own
   (see the `icon` tag). */
.col-ico svg { width: 16px; height: 16px; }
.col-count { font-size: 11.5px; font-weight: 500; color: var(--muted); background: var(--surface-2);
  border-radius: 20px; min-width: 22px; text-align: center; padding: 0 .45rem; font-variant-numeric: tabular-nums; }

/* ===========================================================================
   CARDS + LIST
   =========================================================================== */
.grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(220px,1fr)); gap: .8rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  padding: 1rem 1.1rem; transition: border-color .12s; }
.card:hover { border-color: var(--border-hi); }
.card-top { display: flex; align-items: center; justify-content: space-between; }
.card h3 { margin: .6rem 0 .15rem; font-size: 15px; font-weight: 600; }
.card h3 a { color: var(--text); }
.card h3 a:hover { color: var(--accent); text-decoration: none; }
.card .meta { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.badge { display: inline-block; font-size: 11px; font-weight: 600; padding: .15rem .55rem; border-radius: var(--r-sm);
  color: var(--kc, var(--accent));
  background: color-mix(in srgb, var(--kc, var(--accent)) 12%, transparent); }

.filter { height: var(--ctrl-h); padding: 0 .8rem; margin-bottom: .9rem; }

/* list toolbar: filter + sort side by side, chips on their own row below */
.list-tools { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; margin-bottom: .9rem; }
.panel + .list-tools { margin-top: 1rem; }
.list-tools .filter { flex: 1; min-width: 200px; margin-bottom: 0; }

/* Decipher's tab list: a recessed track, the selected segment lifted onto the
   card colour with the blick rim. */
.seg-group { display: inline-flex; flex: none; gap: 2px; padding: 3px; height: var(--ctrl-h);
  background: var(--surface-2); border-radius: var(--r); }
.seg { display: inline-flex; align-items: center; border: 0; background: none; cursor: pointer;
  color: var(--muted); font-family: var(--sans); font-size: 13px; font-weight: 500; padding: 0 .8rem; line-height: 1;
  border-radius: var(--r-sm); transition: color .12s, background .12s, box-shadow .12s; }
.seg:hover { color: var(--text); }
.seg.is-active { color: var(--text); background: var(--surface); box-shadow: var(--blick-sm); }

.list { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
/* One height for every row in a flat list, whether or not the type has a
   subtitle — a mixed list (search, shared with me) otherwise steps up and down
   row by row. Card grids set their own height from their content. */
.list .item { min-height: 58px; }
/* …and one grid of action columns. The actions are right-aligned, so a row
   with no pin pulls every button one slot over and the copy icons of a shared
   row stop lining up with the ones above it. Holding the column open costs a
   blank 36px and keeps the list rectangular. Asked of the row itself rather
   than of a class the template would have to remember to set — the star is the
   pin. A pseudo-element, so it does not count as content: an actions strip
   with nothing in it still matches :empty and stays hidden in the card grids. */
.list .item:not(:has(.star)) .item-actions::after { content: ""; flex: none; width: var(--ctrl-h); }
.item { position: relative; display: flex; align-items: center; gap: .85rem; padding: .75rem 1rem;
  border-top: 1px solid var(--border); cursor: pointer; transition: background .1s; }
.item[hidden] { display: none; }  /* [hidden] alone loses to display:flex above */
.item:first-child { border-top: 0; }
.item:hover { background: var(--surface-hi); }
/* icon badge shared by list rows (.ic), section and card headings (.col-ico)
   and the confirm dialog (.confirm-ico): a rounded, tinted square holding the
   kind glyph. Each site sets only its size above; the tint recipe lives here
   once. */
.item .ic, .pal-row .ic, .dt-name .ic, .col-ico, .confirm-ico, .rec-ic { flex: none; display: grid; place-items: center;
  border-radius: var(--r-sm); color: var(--kc, var(--accent));
  background: color-mix(in srgb, var(--kc, var(--accent)) 11%, transparent); }
.item .ic { width: 34px; height: 34px; }
.item .ic .kico { width: 18px; height: 18px; }
.item .body { min-width: 0; flex: 1; }
/* Title line: the name, plus whatever is flagged about it (a sharing tag). The
   clipping is on the name alone — the same split as .share-name/.share-who in
   the roster — so a long title cannot truncate its own tag. */
.item .body .t { display: flex; align-items: center; gap: .45rem;
  font-weight: 600; font-size: 13.5px; min-width: 0; }
.item .body .t a { min-width: 0; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item .body .t a:hover { color: var(--accent); text-decoration: none; }
.item .body .s { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item .when { color: var(--faint); font-size: 12px; flex: none; font-variant-numeric: tabular-nums; }
.row-copy { display: inline-flex; align-items: center; justify-content: center; flex: none;
  height: var(--ctrl-h); width: var(--ctrl-h); padding: 0; background: none; border: 0; cursor: pointer;
  color: var(--faint); border-radius: var(--r-sm); transition: color .12s, background .12s; }
.row-copy svg { width: 16px; height: 16px; }
.row-copy:hover { background: var(--surface-hi); color: var(--text); }
.row-copy[data-flash] { color: var(--success); }
/* An item row has two zones: the data (icon + title/subtitle) and the
   functional actions — data left, actions right, at every width the app draws
   a row at. Inside a card the row is denser (.ecard-rows) and its buttons
   shrink with --ctrl-h, but it stays one line: two zones, never a stack. */
.item-main { display: flex; align-items: center; gap: .85rem; flex: 1; min-width: 0; }
.item-actions { display: inline-flex; align-items: center; gap: 2px; flex: none; }
.item-actions:empty { display: none; }
/* A file row (in a card): its category's badge, and its size beside the name. */
.file-ic svg { width: 19px; height: 19px; }
.file-size { flex: none; color: var(--faint); font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; }

.pin-form { margin: 0; display: flex; }
.star { display: inline-flex; align-items: center; justify-content: center; height: var(--ctrl-h); width: var(--ctrl-h);
  background: none; border: 0; cursor: pointer; padding: 0; color: var(--faint);
  border-radius: var(--r-sm); transition: color .12s, transform .1s, background .12s; }
.star .ico { width: 17px; height: 17px; }
.star:hover { background: var(--surface-hi); color: var(--star); }
.star.on { color: var(--star); }
.star:active { transform: scale(.88); }

/* ===========================================================================
   DATA VIEW — a list page as Decipher draws one: an overview card, a toolbar,
   the table and its pager, and a preview beside the table once a row is
   opened. vault/list.html; behaviour in app.js (applyList, openPreview).
   =========================================================================== */
.dv { display: flex; flex-direction: column; gap: 1.1rem; }
.dv-note { margin: 0; font-size: 12.5px; color: var(--muted); }

/* --- overview (Decipher's widget row) ---------------------------------- */
/* One card, its figures on one line; a figure that does not fit is dropped
   rather than wrapped (Decipher: "a widget never grows taller because it ran
   out of width"). */
.widget { container: widget / inline-size; padding: 1.25rem 1.6rem 1.4rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); }
.widget-t { margin: 0 0 .9rem; font-family: var(--heading); font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; }
.widget-metrics { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 1.5rem; }
.metric-l { margin: 0; font-size: 12px; color: var(--muted); }
.metric-v { margin: .1rem 0 0; font-family: var(--heading); font-size: 26px; font-weight: 500;
  letter-spacing: -.03em; line-height: 1.15; font-variant-numeric: tabular-nums; }
.metric.tone-warn .metric-v { color: var(--file); }
.metric.tone-bad .metric-v { color: var(--danger); }
@container widget (max-width: 620px) { .metric:nth-child(n+4) { display: none; } }
@container widget (max-width: 420px) { .metric:nth-child(n+3) { display: none; } }
/* How a mixed list divides between types: one bar in the types' colours, and
   its legend kept to one line. */
.mixbar { display: flex; gap: 3px; height: 8px; margin-top: 1.1rem; }
.mixbar span { flex-basis: 0; min-width: 6px; border-radius: 4px; background: var(--kc); }
.mix-legend { display: flex; gap: 1.1rem; margin: .65rem 0 0; padding: 0; list-style: none;
  overflow: hidden; white-space: nowrap; font-size: 12px; color: var(--muted); }
.mix-legend li { display: inline-flex; align-items: center; gap: .4rem; }
.mix-legend li::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--kc); }
.mix-legend b { color: var(--text); font-weight: 600; }

/* --- toolbar ------------------------------------------------------------- */
.dv-tools { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.dv-search { position: relative; flex: 0 1 18rem; min-width: 12rem; }
.dv-search-ico { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--faint); pointer-events: none; }
.dv-search input { height: var(--ctrl-h); min-height: 0; padding: 0 .8rem 0 2.1rem; font-size: 13px; }
.dv-add { margin-left: auto; }
/* A filter is a button naming what it narrows by (Decipher's "⊕ Status"):
   outline at rest, its count once something in it is ticked. */
.fbtn { display: inline-flex; align-items: center; gap: .4rem; height: 32px; padding: 0 .75rem;
  border: 0; border-radius: var(--r); cursor: pointer; background: var(--btn-bg); color: var(--text);
  box-shadow: var(--shadow-sm), var(--btn-ring); font-family: var(--sans); font-size: 12.5px; font-weight: 500;
  white-space: nowrap; transition: background .12s, box-shadow .12s; }
.fbtn .ico { width: 15px; height: 15px; opacity: .7; }
.fbtn:hover, .fbtn[aria-expanded="true"] { background: var(--btn-hover); }
.fbtn.is-on { box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--accent); }
.fbtn.is-on .ico { color: var(--accent); opacity: 1; }
.fbtn-n { min-width: 18px; padding: 0 .35rem; border-radius: 6px; text-align: center; font-size: 11px;
  color: var(--on-accent); background: var(--accent); font-variant-numeric: tabular-nums; }
.fbtn-n[hidden] { display: none; }
.fbtn.ghost { background: none; box-shadow: none; color: var(--muted); }
.fbtn.ghost:hover { color: var(--text); background: var(--surface-hi); }
.fbtn[hidden] { display: none; }
.menu-check { display: flex; align-items: center; gap: .6rem; min-height: 34px; padding: .3rem .6rem;
  border-radius: var(--r-sm); cursor: pointer; font-size: 13.5px; }
.menu-check:hover { background: var(--surface-hi); }
.menu-check > span:first-of-type { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-check-n { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
/* A menu entry may be a button (a form's submit, a copy) as well as a link. */
button.menu-item { width: 100%; border: 0; background: none; cursor: pointer; font-family: var(--sans); text-align: left; }
.menu-item.danger, .menu-item.danger:hover { color: var(--danger); }
.menu-pop form { margin: 0; }
/* A popover in the top layer (app.js → placeFloat): the browser's popover box
   (centred, bordered, Canvas-coloured) undone, placed by left/top alone. Its
   own width wherever it sits: a fixed box shrinks to the room left of its last
   position, so an armed delete (wider) would be placed by its old width. */
.menu-pop.float { position: fixed; inset: auto; margin: 0; border: 0; color: var(--text);
  min-width: 200px; width: max-content; max-width: calc(100vw - 16px);
  max-height: min(60vh, 420px); overflow-y: auto; }

/* --- the table ----------------------------------------------------------- */
.dv-split { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; }
/* The fixed columns are stated here, once; the page's own columns between
   Name and Updated come from the server as --dt-mid (list_context). */
.dt { container: dt / inline-size;
  --c-check: 40px; --c-pin: 40px; --c-name: minmax(14rem, 2.6fr); --c-when: 8.5rem; --c-menu: 48px; }
.dt[hidden] { display: none; }
.dt-head, .dt-row { display: grid; align-items: center; column-gap: .25rem;
  grid-template-columns: var(--c-check) var(--c-pin) var(--c-name) var(--dt-mid,) var(--c-when) var(--c-menu); }
.dt-head { height: 40px; border-bottom: 1px solid var(--border); font-size: 12.5px; font-weight: 500; color: var(--muted); }
.dt-cell { min-width: 0; padding: 0 .5rem; }
.dt-cell.end { text-align: right; justify-self: stretch; }
.dt-sort { display: inline-flex; align-items: center; gap: .3rem; padding: .25rem .35rem; margin-left: -.35rem;
  border: 0; border-radius: var(--r-sm); background: none; cursor: pointer;
  font-family: var(--sans); font-size: 12.5px; font-weight: 500; color: var(--muted); white-space: nowrap; }
.dt-cell.end .dt-sort { margin: 0 -.35rem 0 0; }
.dt-sort .ico { width: 13px; height: 13px; opacity: .45; }
.dt-sort:hover { color: var(--text); background: var(--surface-hi); }
.dt-sort.is-asc, .dt-sort.is-desc { color: var(--text); }
.dt-sort.is-asc .ico, .dt-sort.is-desc .ico { opacity: 1; color: var(--accent); }
.dt-sort.is-desc .ico { transform: scaleY(-1); }
.dt-head .dt-pin .ico { width: 14px; height: 14px; color: var(--faint); margin: 0 auto; }
.dt-check, .dt-pin { display: flex; justify-content: center; padding: 0; }
.dt-menu { display: flex; justify-content: flex-end; padding: 0 .25rem 0 0; }

/* Rows: a hairline under each, rounded ends on hover — Decipher's row
   states, with the previewed row in the accent tint and a checked one grey. */
.dt-row { position: relative; min-height: 54px; border-bottom: 1px solid var(--border); cursor: pointer;
  border-radius: var(--r-sm); transition: background .1s; }
.dt-row[hidden] { display: none; }
.dt-row:hover { background: var(--surface-hi); }
.dt-row.is-selected { background: var(--surface-2); }
.dt-row.is-active { background: var(--accent-sf); }
.dt-name { display: flex; align-items: center; gap: .75rem; padding-top: .45rem; padding-bottom: .45rem; }
.dt-name .ic { width: 32px; height: 32px; }
.dt-name .ic svg { width: 17px; height: 17px; }
.dt-name .body { flex: 1; min-width: 0; }
.dt-name .t { display: flex; align-items: center; gap: .45rem; min-width: 0; font-weight: 600; font-size: 13.5px; }
.dt-name .t a { min-width: 0; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dt-name .t a:hover { color: var(--accent); text-decoration: underline; }
.dt-name .s { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dt-row:hover .tag { border-color: var(--border-hi); color: var(--text); }
.dt-quick { display: inline-flex; gap: 2px; flex: none; opacity: 0; transition: opacity .12s; }
.dt-quick .row-copy { width: 30px; height: 30px; }
.dt-row:hover .dt-quick, .dt-quick:focus-within { opacity: 1; }
.dt-quick:empty { display: none; }
.dt-row .dt-cell:not(.dt-name):not(.dt-check):not(.dt-pin):not(.dt-menu) { font-size: 13px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dt-row .dt-when { color: var(--muted); font-variant-numeric: tabular-nums; }
.dt-none { color: var(--faint); }
.dt-type { display: inline-flex; align-items: center; gap: .4rem; }
.dt-type .kico { width: 15px; height: 15px; color: var(--kc); }
.dt-owner { display: inline-flex; align-items: center; gap: .45rem; }
.dt-owner .avatar { width: 22px; height: 22px; font-size: 10.5px; box-shadow: none; background: var(--surface-2); }
.dt-row .tag { max-width: 100%; height: 24px; }
.badge.flag-weak { --kc: var(--file); margin-right: .25rem; }
.badge.flag-reused { --kc: var(--danger); margin-right: .25rem; }
.dt-row .star { width: 30px; height: 30px; }
.row-more { width: 30px; height: 30px; }
.row-more .ico { width: 17px; height: 17px; }
.dt-row:not(:hover) .row-more:not([aria-expanded="true"]) { color: var(--faint); }

/* Narrower than the columns need (a phone, or the table beside a preview):
   the page's own columns go, name and date stay. Asked of the table's own
   width — a container query, as Decipher's widgets do — because the preview
   narrows it at any window size. */
@container dt (max-width: 720px) {
  /* on the rows, not .dt: a container query cannot style its own container */
  .dt-head, .dt-row { --dt-mid: ; --c-name: minmax(0, 1fr); }
  .dt [data-col] { display: none; }
}
@container dt (max-width: 460px) {
  /* hidden cells leave the grid's flow, so the template counts only those left */
  .dt-head, .dt-row { grid-template-columns: var(--c-check) var(--c-name) var(--c-menu); }
  .dt .dt-pin, .dt .dt-when { display: none; }
}

/* --- pager --------------------------------------------------------------- */
/* Decipher's table footer: a muted band, rows per page on the left, where you
   are and the four steps on the right. */
.dv-pager { display: flex; align-items: center; gap: 1rem; margin-top: 1rem; padding: .55rem .6rem .55rem .9rem;
  background: var(--surface-2); border-radius: var(--r); font-size: 12.5px; }
.dv-pager[hidden] { display: none; }
.dv-size { display: inline-flex; align-items: center; gap: .55rem; color: var(--muted); }
.dv-size select { width: auto; height: 30px; min-height: 0; padding: 0 1.9rem 0 .7rem; font-size: 12.5px;
  background-color: var(--surface); box-shadow: var(--shadow-sm); }
.dv-page { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; }
.dv-page-btns { display: flex; gap: 2px; }
.dv-page-btns .icon-btn { width: 30px; height: 30px; display: inline-flex; gap: 0; }
.dv-page-btns .icon-btn .ico { width: 15px; height: 15px; }
.dv-page-btns .icon-btn .ico + .ico { margin-left: -9px; }
.dv-page-btns .icon-btn:disabled { opacity: .35; pointer-events: none; }

/* --- preview ------------------------------------------------------------- */
/* Beside the table, the height of the card, scrolling on its own; the page's
   conversation stays on the page (Open page). */
.dv.has-preview .dv-split { grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); gap: 1.25rem; }
.dv-preview { position: sticky; top: 0; display: flex; flex-direction: column; min-width: 0;
  max-height: calc(100dvh - var(--topbar-h) - 1.5rem - 3.6rem);
  border-left: 1px solid var(--border); padding-left: 1.25rem; animation: pv-in .18s ease-out both; }
.dv-preview[hidden] { display: none; }
@keyframes pv-in { from { opacity: 0; transform: translateX(8px); } }
.pv-bar { flex: none; display: flex; align-items: center; gap: .4rem; padding-bottom: .7rem;
  border-bottom: 1px solid var(--border); }
.pv-bar .btn { margin-left: auto; }
.pv-bar .btn .ico { order: 1; margin: 0 -.2rem 0 0; }
.pv-hint { font-size: 11.5px; color: var(--faint); }
.pv-hint kbd { font-family: var(--sans); font-size: 10.5px; padding: 0 .3rem; border-radius: 4px;
  background: var(--surface-2); color: var(--muted); margin-right: 2px; }
.pv-body { flex: 1; min-height: 0; overflow-y: auto; padding: 1rem 0 1.25rem; }
/* A glance is the record's header and its Details: the tab strip and the other
   tabs stay on the page (Open page). */
.pv-body .utabs, .pv-body [data-tab-panel]:not([data-tab-panel="details"]) { display: none; }
.pv-body [data-tab-panel="details"] { display: block; }
.pv-body .rec-head { margin-bottom: 1rem; }
.pv-body .rec-ic { display: none; }
.pv-body .page-title { font-size: 18px; }
.pv-body .kv { grid-template-columns: 6.5rem 1fr; }
.pv-body .panel { padding: 0; border: 0; }
@media (max-width: 1023px) {
  /* No room for two columns: the preview is the page, opened on its own. */
  .dv.has-preview .dv-split { grid-template-columns: minmax(0, 1fr); }
  .dv-preview { position: static; max-height: none; border-left: 0; padding-left: 0;
    border-top: 1px solid var(--border); padding-top: 1rem; }
}

/* --- empty state (Decipher's InteractiveEmptyState) ---------------------- */
.empty-state { display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 3rem 1.5rem 3.2rem; border: 1px dashed var(--border-hi); border-radius: var(--r); }
.es-icons { display: flex; align-items: flex-end; margin-bottom: 1.2rem; }
.es-ico { display: grid; place-items: center; width: 44px; height: 44px; border-radius: var(--r);
  background: var(--surface); color: var(--muted); box-shadow: var(--blick-sm), inset 0 0 0 1px var(--border); }
.es-ico svg { width: 22px; height: 22px; }
.es-ico:nth-child(1) { transform: rotate(-8deg) translate(6px, 4px); }
.es-ico:nth-child(2) { z-index: 1; width: 50px; height: 50px; color: var(--kc, var(--accent)); }
.es-ico:nth-child(3) { transform: rotate(8deg) translate(-6px, 4px); }
.es-title { margin: 0; font-family: var(--heading); font-size: 16px; font-weight: 600; }
.es-text { margin: .35rem 0 0; max-width: 42ch; font-size: 13.5px; color: var(--muted); }
.empty-state .btn { margin-top: 1.2rem; }
[data-empty-filter][hidden] { display: none; }

/* --- bulk bar (Decipher's FloatingActionBar) ----------------------------- */
/* Inverted chrome, centred under the page card, while rows are checked. */
.bulk-bar { position: fixed; z-index: 90; bottom: 2.5rem;
  left: calc(var(--sidebar-w) + (100vw - var(--sidebar-w) - var(--surface-gap)) / 2); transform: translateX(-50%);
  display: flex; align-items: center; gap: .35rem; padding: .45rem .5rem .45rem 1.1rem;
  background: var(--inverse); color: var(--on-inverse); border-radius: 1rem;
  box-shadow: var(--shadow-lg); backdrop-filter: blur(8px); animation: bulk-in .18s ease-out both; }
.bulk-bar[hidden] { display: none; }
@keyframes bulk-in { from { opacity: 0; transform: translate(-50%, 10px); } }
.bulk-n { margin-right: .6rem; font-size: 13px; white-space: nowrap; }
.bulk-bar form { display: flex; align-items: center; gap: .2rem; margin: 0; }
.bulk-btn { display: inline-flex; align-items: center; gap: .4rem; height: 34px; padding: 0 .75rem; border: 0;
  border-radius: var(--r); cursor: pointer; background: none; color: inherit;
  font-family: var(--sans); font-size: 13px; font-weight: 500; white-space: nowrap; }
.bulk-btn .ico, .bulk-btn .kico { width: 15px; height: 15px; opacity: .8; }
.bulk-btn:hover, .bulk-btn[aria-expanded="true"] { background: color-mix(in srgb, var(--on-inverse) 14%, transparent); }
.bulk-btn.danger { color: light-dark(hsl(0 91% 75%), hsl(0 72% 45%)); }
.bulk-x { display: grid; place-items: center; width: 30px; height: 30px; margin-left: .2rem; border: 0; border-radius: 50%;
  cursor: pointer; background: color-mix(in srgb, var(--on-inverse) 12%, transparent); color: inherit; }
.bulk-x .ico { width: 14px; height: 14px; }
.bulk-bar .menu-pop .menu-item .kico { width: 16px; height: 16px; color: var(--kc); }
@media (max-width: 767px) {
  /* Wraps rather than scrolls: with no scrollbars anywhere, a bar wider than
     the screen would hide Delete and ✕ behind an unhinted swipe. */
  .bulk-bar { left: 50%; bottom: calc(var(--m-bottom-h, 60px) + 1rem + env(safe-area-inset-bottom));
    width: max-content; max-width: calc(100vw - 1.5rem); flex-wrap: wrap; justify-content: center;
    row-gap: .2rem; padding: .45rem .5rem; }
  .bulk-bar form { flex-wrap: wrap; justify-content: center; }
  .dv-search { flex: 1 1 100%; }
  .widget { padding: 1rem 1.1rem; }
  .metric-v { font-size: 22px; }
}

/* ===========================================================================
   BUTTONS
   =========================================================================== */
/* Decipher's buttons. The plain .btn is its `outline` variant — a white button
   with a hairline ring drawn inside the box (black in dark) — and .primary its
   solid navy `default`. Icons: 16px at 70% ink, a hair outside the label. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  background: var(--btn-bg); border: 0; color: var(--text);
  box-shadow: var(--shadow-sm), var(--btn-ring);
  border-radius: var(--r); height: var(--ctrl-h); padding: 0 1.1rem;
  font-family: var(--sans); font-size: 13.5px; font-weight: 500; cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background .12s, color .12s, box-shadow .12s; }
.btn .ico { width: 16px; height: 16px; opacity: .7; margin-left: -.2rem; }
.btn:hover { background: var(--btn-hover); color: var(--text); text-decoration: none; }
.btn:disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
.btn.primary { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow); }
.btn.primary .ico { opacity: .85; }
.btn.primary:hover { background: color-mix(in srgb, var(--accent) 90%, transparent); color: var(--on-accent); }
.btn.danger { color: var(--danger); }
.btn.danger:hover { background: var(--danger-sf); color: var(--danger); }

/* ===========================================================================
   DETAIL
   =========================================================================== */
/* Decipher's record page: a header — the type's icon tile, the title over a
   line of facts, the actions at the right — then underlined tabs over the
   panels. vault/detail.html is the one place that draws it. */
.rec-head { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: .6rem; }
.rec-ic { width: 44px; height: 44px; border-radius: var(--r); }
.rec-ic .kico { width: 22px; height: 22px; }
.rec-title { flex: 1; min-width: 0; }
.rec-title .page-title { margin: 0 0 .45rem; overflow-wrap: anywhere; }
/* The facts about the record as one row of tags — its type, who it is shared
   with or by, its project — then when it last changed, in the quiet register. */
.rec-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.rec-meta .badge { height: 26px; display: inline-flex; align-items: center; }
.rec-when { font-size: 12px; color: var(--faint); margin-left: .2rem; white-space: nowrap; }
.rec-actions { flex: none; display: flex; align-items: center; gap: .4rem; }
.rec-actions .pin-form { display: flex; }
/* On a phone the title needs the width more than the type needs a tile: the
   badge under it already says what it is. */
@media (max-width: 767px) { .rec-ic { display: none; } .rec-head { gap: .6rem; } }

/* A record's tab strip (the tab itself is the search dialog's .scope recipe):
   a little taller, a link rather than a button, and a count in its label. */
.utabs { display: flex; gap: 1.6rem; margin: 0 0 1.5rem; border-bottom: 1px solid var(--border);
  overflow-x: auto; }
.utab { display: inline-flex; align-items: center; gap: .45rem; height: 44px; font-size: 13.5px; white-space: nowrap; }
.utab.is-active::after { animation: utab-in .2s ease-out both; }
@keyframes utab-in { from { transform: scaleX(.4); opacity: 0; } }
.utab-n { min-width: 20px; padding: 0 .4rem; border-radius: 20px; text-align: center;
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--muted); background: var(--surface-2); }
.utab.is-active .utab-n { color: var(--accent); background: var(--accent-sf); }
.utab:focus-visible { outline: none; box-shadow: 0 2px 0 0 var(--focus, var(--accent)); }
.rec-panel[hidden] { display: none; }
.panel-cta { margin-top: 1.25rem; }

/* Decipher's NativeDelete (vault/_delete.html). Disarmed, a plain delete; armed
   (app.js), the button is the solid destructive one saying "✓ Confirm", a ✕
   stands beside it and what the delete takes is spelled out under both. */
.arm { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: 0; }
.arm-idle, .arm-on { display: contents; }
.arm:not(.is-armed) .arm-on, .arm.is-armed .arm-idle,
.arm:not(.is-armed) .arm-x, .arm:not(.is-armed) .arm-note { display: none; }
.arm.is-armed .arm-go, .arm.is-armed .arm-go:hover { background: var(--danger); color: var(--on-accent); }
.arm.is-armed .arm-go .ico { opacity: 1; }
.arm-x { width: var(--ctrl-h); height: var(--ctrl-h); box-shadow: inset 0 0 0 1px var(--border); }
.arm-note { flex-basis: 100%; margin: .1rem 0 0; font-size: 12px; line-height: 1.45; color: var(--muted); }
/* In a ⋯ menu: the item keeps the menu's width, the ✕ sits at its end, and
   the note reads as the item's own small print. */
.arm-menu .arm-go { flex: 1; width: auto; }
.arm-menu .arm-label { flex: 1; }
.arm-menu .arm-x { width: 34px; height: 34px; }
.arm-menu .arm-note { padding: 0 .6rem .3rem; max-width: 28ch; }
/* In a row's icon strip (a comment): the icon grows into a small labelled pill. */
.arm-sm { flex-wrap: nowrap; gap: .1rem; }
.arm-sm .arm-label { display: none; }
.arm-sm.is-armed .arm-go { width: auto; padding: 0 .5rem; gap: .3rem; font-size: 11.5px; font-weight: 600; }
.arm-sm.is-armed .arm-on .arm-label { display: inline; white-space: nowrap; }
.arm-sm .arm-x { width: 22px; height: 22px; box-shadow: none; }
.arm-sm .arm-x .ico { width: 13px; height: 13px; }

/* The form footer: Decipher's dialog footer, Cancel then the primary action,
   both at the right edge. */
form .actions { display: flex; align-items: center; justify-content: flex-end; gap: .6rem; flex-wrap: wrap;
  margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border); }
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 1.4rem 1.5rem; }
.form-panel { max-width: 560px; }
.empty { color: var(--muted); font-size: 13.5px; background: var(--surface); border: 1px dashed var(--border-hi);
  border-radius: var(--r); padding: 1.6rem; text-align: center; }
/* Baseline-aligned, so a label sits on the line of its value whether the value
   is plain text or a field with buttons in it. */
.kv { display: grid; grid-template-columns: 8rem 1fr; gap: .9rem 1.3rem; font-size: 13.5px; margin: 0;
  align-items: baseline; }
.kv dt { color: var(--muted); font-size: 13px; font-weight: 500; }
.kv dd { margin: 0; word-break: break-word; }
/* A copyable field: value on the left, a copy icon that surfaces on hover. */
.kv-copy { display: flex; align-items: center; gap: .4rem; }
.kv-copy-val { min-width: 0; word-break: break-word; }
/* Kept out of the way until the thing it belongs to is hovered — a detail field,
   a note body, or a comment in a thread. `:focus-visible` on the last line is
   what keeps every one of them reachable by keyboard. */
.kv-copy .row-copy, .note-field .row-copy, .prose-copy,
.cmt-actions .row-copy { opacity: 0; transition: opacity .12s, background .12s, color .12s; }
.kv dd:hover .row-copy, .note-field:hover .row-copy, .prose.has-copy:hover .prose-copy,
.cmt:hover .cmt-actions .row-copy, .row-copy:focus-visible { opacity: 1; }
.note-field { position: relative; }
.note-field .row-copy { position: absolute; top: .3rem; right: .3rem; }
.note { margin: 0; padding: .6rem 2.4rem .6rem .75rem; background: var(--surface-2);
  border: 0; border-radius: var(--r); font-family: var(--mono);
  font-size: 13px; line-height: 1.5; color: var(--text); white-space: pre-wrap; word-break: break-word; }
.prose.has-copy { position: relative; }
.prose-copy { position: absolute; top: .6rem; right: .6rem; }
.secret { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  background: var(--surface-2); border: 0; border-radius: var(--r); box-shadow: var(--inset-field);
  padding: .4rem .4rem .4rem .8rem; }
.secret .secret-val { font-family: var(--mono); word-break: break-all; color: var(--text); letter-spacing: .02em; }
.secret .secret-val.masked { letter-spacing: .2em; color: var(--muted); }
.secret-actions { display: flex; gap: .4rem; flex: none; }
/* A small outline button (Decipher's `outline size=sm`) for the in-field
   controls: Reveal / Copy, Show / Generate. */
.copy { display: inline-flex; align-items: center; justify-content: center; background: var(--btn-bg);
  border: 0; border-radius: var(--r); height: 30px; padding: 0 .8rem;
  box-shadow: var(--shadow-sm), var(--btn-ring);
  font-size: 12.5px; font-weight: 500; cursor: pointer; color: var(--text); font-family: var(--sans); white-space: nowrap;
  transition: background .12s, color .12s; }
.copy:hover { background: var(--btn-hover); }
/* ---------------------------------------------------------------------------
   RICH-TEXT NOTES — one `.prose` class styles both the rendered note and the
   WYSIWYG editor surface (.rte-editor also carries .prose) so they read alike.
   --------------------------------------------------------------------------- */
.prose { color: var(--text); font-size: 14px; line-height: 1.65; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose h1 { font-size: 1.45rem; font-weight: 600; margin: 1.4rem 0 .6rem; letter-spacing: -.01em; }
.prose h2 { font-size: 1.15rem; font-weight: 600; margin: 1.2rem 0 .5rem; }
.prose p { margin: .65rem 0; }
.prose ul, .prose ol { margin: .65rem 0; padding-left: 1.5rem; }
.prose li { margin: .25rem 0; }
.prose a { color: var(--note); text-decoration: underline; }
.prose a:not([href]) { color: inherit; text-decoration: none; }  /* a link the sanitizer refused */
.prose blockquote { margin: .8rem 0; padding: .3rem 0 .3rem 1rem;
  border-left: 3px solid var(--border-hi); color: var(--muted); }
.prose pre { background: var(--surface-2); border: 0;
  border-radius: var(--r); padding: .8rem 1rem; overflow-x: auto; margin: .8rem 0;
  font-family: var(--mono); font-size: 13.5px; white-space: pre-wrap; }
.prose code { font-family: var(--mono); font-size: .92em; }
.prose mark { background: var(--accent-sf); color: var(--text); }

/* checklist — list-marker replaced by a CSS checkbox; state lives in .done */
.prose ul.checklist { list-style: none; padding-left: 0; }
.prose ul.checklist li { position: relative; padding-left: 1.7rem; }
.prose ul.checklist li::before { content: "";
  position: absolute; left: 0; top: .22em; width: 15px; height: 15px; box-sizing: border-box;
  border: 1px solid var(--border-hi); border-radius: 5px; background: var(--surface); }
.rte-editor ul.checklist li::before { cursor: pointer; }
.prose ul.checklist li.done::before { background: var(--accent); border-color: var(--accent); }
.prose ul.checklist li.done::after { content: "✓";
  position: absolute; left: 2.5px; top: .05em; font-size: 12px; font-weight: 700; color: var(--on-accent); }
.prose ul.checklist li.done { color: var(--muted); text-decoration: line-through; }

/* editor chrome */
.rte { border: 0; border-radius: var(--r); background: var(--surface-2); box-shadow: var(--inset-field);
  transition: box-shadow .12s; }
.rte:focus-within { box-shadow: inset 0 0 0 1px var(--ring); }
.rte-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; padding: .3rem;
  border-bottom: 1px solid var(--border); }
.rte-sep { width: 1px; align-self: stretch; margin: .2rem .25rem; background: var(--border); }
.rte-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 30px; height: 30px;
  padding: 0 .4rem; background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  color: var(--muted); font-family: var(--sans); font-size: 13px; line-height: 1; cursor: pointer;
  transition: background .12s, color .12s, border-color .12s; }
.rte-btn:hover { background: var(--surface); color: var(--text); box-shadow: var(--blick-sm); }
.rte-btn:active { transform: translateY(1px); }
.rte-editor { min-height: 10rem; padding: .8rem 1rem; outline: none; overflow-wrap: anywhere; }
.rte-editor:empty::before { content: attr(data-placeholder); color: var(--faint); pointer-events: none; }

/* ===========================================================================
   FORMS
   =========================================================================== */
form p { margin: 0 0 1.15rem; }
/* Decipher's form label: sentence case, medium weight, in ink, a few pixels
   in from the field's rounded edge. */
.form-panel label, .auth-card label, .totp-confirm label { display: block; font-size: 13px; font-weight: 500;
  color: var(--text); margin: 0 0 var(--label-gap, .45rem) var(--label-inset, .7rem); }
.form-panel .helptext { display: block; color: var(--muted); font-size: 12px; margin: .35rem 0 0 .7rem; }
/* Text-like fields. Kept as a bare element selector on purpose: every field in
   the app narrows it with one class (.filter, .code-cell, .top-search input),
   so raising the specificity here — even with a :not() — would quietly outrank
   all of them. Checkboxes and radios opt out below instead, where an attribute
   selector wins on its own. */
/* Decipher's field: no border, the translucent --input fill, a faint inset
   shadow, 12px corners; focus draws the ring *inside* the box. */
input, textarea, select { width: 100%; min-height: var(--ctrl-h); background: var(--surface-2); color: var(--text); border: 0;
  border-radius: var(--r); padding: .55rem .8rem; font-family: var(--sans); font-size: 13.5px;
  box-shadow: var(--inset-field); transition: box-shadow .12s; }
input::placeholder, textarea::placeholder { color: color-mix(in srgb, var(--muted) 55%, transparent); }
/* Every filter in the app is a `type=search`, for the semantics and the Escape
   key it brings. What it also brings is a clear button WebKit paints itself —
   the same foreign artefact as a native checkbox or a scrollbar, in a blue that
   belongs to no palette here. Dropped in one place, like those. Nothing is lost
   with it: the button is WebKit's alone (no other engine draws one), so no
   filter in the app could have depended on it to be clearable. */
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
input:focus, textarea:focus, select:focus { outline: none; box-shadow: inset 0 0 0 1px var(--ring); }
/* A select is a text-like field plus a chevron of our own: the native arrow is
   drawn by the OS and ignores the palette, so it is the one control that would
   still ignore the theme on a form. */
select { appearance: none; -webkit-appearance: none; padding-right: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2373798A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right .55rem center; background-size: 15px; }

/* ---------------------------------------------------------------------------
   CHECKBOX + RADIO — one drawn control for the whole app (the attach picker,
   the share modal's people list and access level, "select all"). `appearance:
   none` is what lets the palette apply at all: the native control paints itself
   from the OS theme and ignores colour, so it reads as a foreign box on the
   card. Selection is always the accent, never the row's --kc, so a checked box
   means the same thing on a purple project row as on a green password one.

   This is also where they shed the text-field rule above: a radio left at
   `width: 100%` eats its row and pushes its own label onto a second line.
   --------------------------------------------------------------------------- */
input[type=checkbox], input[type=radio] {
  appearance: none; -webkit-appearance: none; flex: none; box-shadow: none;
  width: 17px; height: 17px; min-height: 0; margin: 0; padding: 0; cursor: pointer;
  display: inline-grid; place-content: center; vertical-align: -3px;
  background: var(--surface); border: 1px solid var(--border-hi); box-shadow: var(--shadow-sm);
  border-radius: 5px; transition: background .12s, border-color .12s; }
input[type=radio] { border-radius: 50%; }
input[type=checkbox]:hover, input[type=radio]:hover { border-color: var(--accent); }
input[type=checkbox]::after { content: ""; width: 9px; height: 5px; margin-top: -2px;
  border: 2px solid var(--on-accent); border-top: 0; border-right: 0;
  transform: rotate(-45deg) scale(.4); opacity: 0; transition: transform .12s, opacity .12s; }
input[type=radio]::after { content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--on-accent); transform: scale(.4); opacity: 0; transition: transform .12s, opacity .12s; }
input[type=checkbox]:checked, input[type=radio]:checked {
  background: var(--accent); border-color: var(--accent); }
input[type=checkbox]:checked::after { transform: rotate(-45deg) scale(1); opacity: 1; }
/* Some of a page's rows ticked: a bar, not a tick. */
input[type=checkbox]:indeterminate { background: var(--accent); border-color: var(--accent); }
input[type=checkbox]:indeterminate::after { width: 8px; height: 0; margin: 0; border-width: 0 0 2px;
  transform: none; opacity: 1; }
input[type=radio]:checked::after { transform: scale(1); opacity: 1; }
input[type=checkbox]:disabled, input[type=radio]:disabled { opacity: .4; cursor: not-allowed; }
textarea { min-height: 8rem; resize: vertical; }
[data-flash="copied"] { color: var(--success); }
*:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* A field draws its own ring inside the box, so it needs no outline too. */
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: none; }
.pw-tools { display: flex; gap: .45rem; margin-top: .55rem; }
.pw-strength { margin-top: .65rem; }
.pw-bars { display: flex; gap: 4px; }
.pw-bar { height: 5px; flex: 1; background: var(--surface-2); border-radius: 3px; transition: background .15s; }
.pw-bar.on { background: var(--sc, var(--accent)); }
.pw-label { font-size: 12px; color: var(--muted); margin-top: .4rem; min-height: 1em; }

/* ===========================================================================
   AUTH
   =========================================================================== */
/* Decipher's sign-in page (as its gitg tool draws it): a cover — the logo, a
   headline with the brand word in navy, one line under it and an isometric
   drawing — beside the card that holds the form. One column below 900px,
   where the drawing shrinks to a strip above the card. */
.auth-shell { min-height: 100vh; display: grid; place-items: center; padding: 2rem 1rem;
  background: var(--bg); }
.auth-inner { width: 100%; max-width: 68rem; display: grid; align-items: center;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 26rem); gap: clamp(2.5rem, 6vw, 6rem); }
.auth-cover { min-width: 0; }
.auth-top { display: inline-flex; color: var(--text); }
.auth-top:hover { color: var(--text); text-decoration: none; }
.auth-top .logo-word { color: var(--muted); }
.auth-hero { margin: 2.25rem 0 0; font-size: clamp(2rem, 4vw, 3.1rem); line-height: 1.05; font-weight: 600;
  letter-spacing: -.03em; text-wrap: balance; }
.auth-hero span { color: var(--accent); }
.auth-lede { margin: 1rem 0 0; max-width: 40ch; font-size: 1.05rem; line-height: 1.5; color: var(--muted); }

/* The drawing: every block three faces, lit from the top-left; the lit ones
   are the brand navy, a few more are half-lit around them. */
.iso { display: block; width: 100%; height: auto; margin-top: 2.5rem; overflow: visible;
  --box-top:   light-dark(#fff, hsl(220 5% 20%));
  --box-left:  light-dark(hsl(215 12% 94%), hsl(220 0% 9%));
  --box-right: light-dark(hsl(219 12% 90%), hsl(220 6% 13%));
  --box-edge:  light-dark(hsl(220 13% 86%), hsl(220 5% 22%)); }
.iso polygon { stroke: var(--box-edge); stroke-width: .6; stroke-linejoin: round; }
.iso .t { fill: var(--box-top); }
.iso .l { fill: var(--box-left); }
.iso .r { fill: var(--box-right); }
.iso .hi polygon { stroke: var(--accent); }
.iso .hi .t { fill: var(--accent); }
.iso .hi .l { fill: color-mix(in srgb, var(--accent) 70%, var(--bg)); }
.iso .hi .r { fill: color-mix(in srgb, var(--accent) 85%, var(--bg)); }
.iso .mid polygon { stroke: color-mix(in srgb, var(--accent) 55%, var(--box-edge)); }
.iso .mid .t { fill: color-mix(in srgb, var(--accent) 18%, var(--box-top)); }
/* The vault's own parts (accounts/_vault_drawing.svg): the door is navy on the
   pale face, its wheel and bolts in the face's own tone, the keypad a recessed
   panel with two keys lit; lines (spokes, the shackle, the lines on a note) are
   strokes in the edge colour. */
.iso .door-rim { fill: color-mix(in srgb, var(--accent) 30%, var(--box-left)); stroke: var(--accent); }
.iso .door { fill: var(--accent); stroke: var(--accent); }
.iso .bolt { fill: var(--box-top); stroke: none; }
.iso .wheel { fill: none; stroke: var(--box-top); stroke-width: 1.6; }
.iso .hub { fill: var(--box-top); stroke: none; }
.iso .spoke { stroke: var(--box-top); stroke-width: 1.6; stroke-linecap: round; }
.iso .pad { fill: color-mix(in srgb, var(--box-edge) 60%, var(--box-right)); }
.iso .key, .iso .screen { stroke: none; }
.iso .key { fill: var(--box-top); }
.iso .key-lit { fill: var(--accent); stroke: none; }
.iso .screen { fill: color-mix(in srgb, var(--accent) 25%, var(--box-top)); }
.iso .key-ring { fill: var(--accent); }
.iso .key-hole { fill: var(--bg); stroke: var(--accent); }
.iso .ink { stroke: var(--accent); stroke-width: 1.2; stroke-linecap: round; opacity: .55; }
.iso .shackle { stroke: var(--accent); stroke-width: 2; stroke-linecap: round; }
.iso .hole-dot { fill: var(--bg); stroke: none; }

/* The card's controls are one size up (48px), set once for every field and
   button inside it. */
.auth-card { --ctrl-h: 3rem; min-width: 0; background: var(--surface); border-radius: 1rem;
  box-shadow: var(--shadow-lg); padding: 1.5rem 2rem 2rem; }
.auth-card .panel { border: 0; background: none; padding: 0; }
.auth-card form.panel { display: flex; flex-direction: column; gap: 1.5rem; }
.auth-card form.panel > p { margin: 0; }
.auth-head { margin-bottom: 1.75rem; }
.auth-title { margin: 0 0 .75rem; font-size: 1.25rem; line-height: 1; font-weight: 600; letter-spacing: -.025em; }
.auth-subtitle { margin: 0; font-size: 13px; }
/* A field: label over a 3rem filled control, the icon at its leading edge. */
.auth-field { display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
.auth-field label { --label-gap: 0; line-height: 1; }
.auth-control { position: relative; display: flex; align-items: center; }
.auth-control .ico { position: absolute; left: .8rem; width: 16px; height: 16px; color: var(--muted);
  opacity: .7; pointer-events: none; }
.auth-control input { padding: 0 1rem 0 2.5rem; }
.auth-control input:-webkit-autofill { -webkit-box-shadow: inset 0 0 0 1000px light-dark(hsl(216 14% 93%), hsl(220 8% 16%));
  -webkit-text-fill-color: var(--text); }
/* The submit is centred and generous, not stretched across the card. */
.auth-submit { display: flex; width: max-content; max-width: 100%; margin: .25rem auto 0;
  padding: 0 2.5rem; border-radius: 1rem; }
.auth-submit .ico { opacity: .6; }
.auth-note { margin: 0; }
.auth-foot { text-align: center; margin: 1.5rem 0 0; font-size: 13px; }
.auth-foot-tight { text-align: center; margin: .5rem 0 0; font-size: 12.5px; }
@media (max-width: 899px) {
  .auth-shell { place-items: start center; }
  .auth-inner { grid-template-columns: minmax(0, 1fr); max-width: 28rem; gap: 2rem; }
  .auth-hero { margin-top: 1.5rem; }
  .iso { margin-top: 1.5rem; max-height: 9rem; }
}
@media (max-width: 639px) {
  .auth-card { padding: 1.25rem 1.25rem 1.5rem; }
  .auth-submit { width: 100%; justify-content: center; }
}
.alert { display: flex; align-items: flex-start; gap: .75rem; background: var(--danger-sf);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent); color: var(--danger);
  border-radius: var(--r); padding: .9rem 1rem; font-size: 13px; margin-bottom: 1.25rem; }
.alert .ico { width: 18px; height: 18px; margin-top: 1px; }

/* ===========================================================================
   TOASTS
   =========================================================================== */
/* Decipher's toast is a pill: inverted chrome (navy-black on light, near white
   on dark), centred at the top of the window, one line that grows to its
   message and wraps rather than truncates. The tone lives on its icon alone,
   drawn as a mask so a toast built by app.js needs no markup for it. */
.toasts { position: fixed; top: var(--topbar-h); left: 50%; transform: translateX(-50%); z-index: 120;
  display: flex; flex-direction: column; align-items: center; gap: .5rem; width: min(92vw, 30rem);
  pointer-events: none; }
.toast { display: flex; align-items: flex-start; gap: .55rem; width: max-content; max-width: 100%;
  background: var(--inverse); color: var(--on-inverse); padding: .6rem 1rem .6rem .8rem;
  border-radius: 1.375rem; font-size: 13.5px; font-weight: 500; line-height: 1.45;
  box-shadow: var(--shadow-lg); backdrop-filter: blur(8px); pointer-events: auto;
  animation: toast-in .2s cubic-bezier(.2,.8,.2,1) both; }
.toast::before { content: ""; flex: none; width: 16px; height: 16px; margin-top: 1px;
  background: var(--toast-tone); -webkit-mask: var(--toast-ico) center / contain no-repeat;
  mask: var(--toast-ico) center / contain no-repeat; }
/* The two tones' glyphs: Fluent's checkmark-circle and warning (filled). */
.toast { --toast-tone: light-dark(hsl(152 76% 60%), hsl(158 64% 32%));
  --toast-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 1 0 16 8 8 0 0 1 0-16m3.36 5.65a.5.5 0 0 0-.64-.06l-.07.06L9 11.3 7.35 9.65l-.07-.06a.5.5 0 0 0-.7.7l.07.07 2 2 .07.06c.17.11.4.11.56 0l.07-.06 4-4 .07-.08a.5.5 0 0 0-.06-.63'/%3E%3C/svg%3E"); }
.toast.error { --toast-tone: light-dark(hsl(0 91% 71%), hsl(0 72% 51%));
  --toast-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.37 3.56a3 3 0 0 1 5.26 0l5.5 10A3 3 0 0 1 15.5 18h-11a3 3 0 0 1-2.63-4.44zM10 12.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5m0-6.25a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 1 0V7a.5.5 0 0 0-.5-.5'/%3E%3C/svg%3E"); }
.toast.hide { opacity: 0; transform: translateY(-6px); transition: opacity .35s, transform .35s; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-8px) scale(.98); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

/* ===========================================================================
   MODAL — centered dialog for detail / create / edit.
   Chrome = a header bar (eyebrow + close) above a scrollable body, so the
   close button never sits on top of the in-body action buttons.
   =========================================================================== */
body.modal-open { overflow: hidden; }
.modal-overlay[hidden] { display: none; }
.modal-overlay { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 1.5rem;
  background: var(--overlay); backdrop-filter: blur(2px); animation: overlay-in .14s ease both; }
/* Decipher's dialog: 24px corners, the deepest shadow in the app, content set
   in 2rem from the sides. */
.modal { position: relative; width: 100%; max-width: 640px; max-height: 90vh; display: flex; flex-direction: column;
  background: var(--surface); border: 0; border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg), inset 0 0 0 1px light-dark(transparent, var(--border));
  overflow: hidden; outline: none; animation: modal-in .2s cubic-bezier(.2,.8,.2,1) both; }
.modal-head { flex: none; display: flex; align-items: center; justify-content: space-between; height: 56px;
  padding: 0 1rem 0 2rem; }
.modal-title { margin: 0; min-width: 0; font-family: var(--heading); font-size: 16px; font-weight: 600;
  letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-x { width: 32px; height: 32px; }   /* an .icon-btn, one size down */
.modal-body { flex: 1; overflow-y: auto; padding: .4rem 2rem 2rem; }
/* The dialog's head states the title, so the heading a fragment carries for a
   page of its own is not drawn twice. */
.modal-body > .page-title:first-child { display: none; }
.modal-body .page-sub { margin-bottom: 1.3rem; }
.modal-body .form-panel, .modal-body .panel { border: 0; background: none; padding: 0; max-width: none; box-shadow: none; }
/* The footer stays at the bottom of a long form while its body scrolls past. */
.modal-body form .actions { position: sticky; bottom: -2rem; z-index: 1; margin-bottom: -2rem;
  padding-bottom: 1.5rem; background: var(--surface); }
/* ---------------------------------------------------------------------------
   CONFIRM / PROMPT — the app's own answer to window.confirm and window.prompt,
   which are drawn by the browser and cannot be themed, sized or worded. It is
   the modal chrome again, one step narrower and one layer up: a question can be
   asked from *inside* an open dialog (a revoke in the share dialog, a link in
   the editor), so this overlay stacks above that one rather than replacing it.
   A delete does not come here — it asks on the spot (vault/_delete.html).
   --------------------------------------------------------------------------- */
.confirm-overlay { z-index: 140; }
.confirm { max-width: 420px; }
.confirm-body { display: flex; align-items: flex-start; gap: .9rem; padding: 1.75rem 1.75rem 1.3rem; }
/* The same icon badge the list rows and section headings wear (its recipe is
   the shared selector further up); here the tint follows the question's tone
   rather than an item type, which is one custom property, not a second recipe. */
.confirm-ico { --kc: var(--tone, var(--accent)); width: 38px; height: 38px; border-radius: var(--r); }
.confirm-ico .ico { width: 20px; height: 20px; }
/* The tone is one attribute on the dialog (app.js `ask`): it picks the colour
   and which of the two icons shows. */
.confirm { --tone: var(--danger); }
.confirm[data-tone="accent"] { --tone: var(--accent); }
.confirm[data-tone="accent"] [data-tone-ico="danger"],
.confirm:not([data-tone="accent"]) [data-tone-ico="accent"] { display: none; }
.confirm-title { margin: 0 0 .3rem; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.confirm-text { margin: 0; font-size: 13.5px; color: var(--muted); line-height: 1.5; }
.confirm-field { margin-top: .9rem; }
.confirm-actions { display: flex; justify-content: flex-end; gap: .6rem;
  padding: 0 1.75rem 1.5rem; }
/* The affirmative button carries the tone, so "Delete" reads as a deletion
   before it is read as a word. */
.btn.tone { background: var(--tone); color: var(--on-accent); box-shadow: var(--shadow); }
.btn.tone:hover { background: color-mix(in srgb, var(--tone) 90%, transparent); color: var(--on-accent); }
/* A question is a question at any width: it stays a centred card on a phone
   rather than becoming the full-screen sheet a detail modal turns into.
   Specificity, not order, is what beats the sheet rule further down. */
@media (max-width: 560px) {
  .modal-overlay.confirm-overlay { padding: 1.2rem; place-items: center; }
  .confirm-overlay .confirm { max-height: 90vh; min-height: 0;
    border-radius: var(--r-lg); }
}

/* ---------------------------------------------------------------------------
   SEARCH (⌘S) — Decipher's global search: the field across the top, the scopes
   as underlined tabs beneath it, then the answer grouped by type. It hangs
   from the upper third rather than centring, so the field does not jump as
   the answer under it grows and shrinks.
   --------------------------------------------------------------------------- */
.search-overlay { z-index: 110; place-items: start center; padding-top: 12vh; }
.modal.search { max-width: 780px; max-height: min(640px, 76vh); }
.search-field { flex: none; display: flex; align-items: center; gap: .6rem; height: 58px;
  padding: 0 .75rem 0 1.25rem; border-bottom: 1px solid var(--border); }
.search-ico { width: 18px; height: 18px; color: var(--muted); }
/* The dialog's field is its whole head, so it drops the field fill. */
.search-field input { flex: 1; min-width: 0; height: 100%; min-height: 0; padding: 0;
  background: none; box-shadow: none; border-radius: 0; font-size: 15px; }
.search-field input:focus { box-shadow: none; }
.search-scopes { flex: none; display: flex; gap: 1.4rem; padding: 0 1.25rem; overflow-x: auto;
  border-bottom: 1px solid var(--border); }
/* Decipher's underlined tab, for the search dialog's scopes and a record's
   tabs alike: the label, and a 3px rounded bar riding the hairline under the
   current one. */
.scope, .utab { position: relative; flex: none; height: 42px; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--sans); font-size: 13px; font-weight: 500; color: var(--muted); transition: color .12s; }
.scope:hover, .utab:hover { color: var(--text); text-decoration: none; }
.scope.is-active, .utab.is-active { color: var(--text); }
.scope.is-active::after, .utab.is-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 3px; border-radius: 3px; background: var(--accent); }
.search-body { flex: 1; min-height: 0; overflow-y: auto; padding: .5rem .6rem 1rem; }
.pal-group + .pal-group { margin-top: .4rem; }
.pal-head { display: flex; align-items: center; gap: .5rem; margin: 0; padding: .65rem .6rem .35rem;
  font-family: var(--heading); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.pal-row { display: flex; align-items: center; gap: .75rem; padding: .45rem .6rem; border-radius: var(--r-sm);
  color: var(--text); }
.pal-row:hover, .pal-row.is-current, .pal-all:hover, .pal-all.is-current { background: var(--surface-hi);
  text-decoration: none; color: var(--text); }
.pal-row .ic { width: 30px; height: 30px; }
.pal-row .ic .kico { width: 16px; height: 16px; }
.pal-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pal-title { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-sub { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-meta { flex: none; font-size: 12px; color: var(--faint); }
/* Enter opens the highlighted row — the arrow says which one that is. */
.pal-go { width: 16px; height: 16px; color: var(--muted); opacity: 0; }
.pal-row.is-current .pal-go { opacity: 1; }
.pal-all { display: flex; align-items: center; justify-content: space-between; margin-top: .5rem;
  padding: .6rem; border-top: 1px solid var(--border); border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-size: 13px; font-weight: 500; color: var(--muted); }
.pal-all .ico { width: 15px; height: 15px; }
.pal-empty { margin: 0; padding: 1.5rem .6rem; text-align: center; font-size: 13px; color: var(--muted); }
@media (max-width: 560px) {
  .search-overlay { padding-top: 0; }
  .search-kbd { display: none; }
}

@keyframes overlay-in { from { opacity: 0; } }
@keyframes modal-in { from { opacity: 0; transform: translateY(10px) scale(.985); } }
@media (prefers-reduced-motion: reduce) { .modal-overlay, .modal { animation: none; } }
@media (max-width: 560px) {
  .modal-overlay { padding: 0; place-items: stretch; }
  .modal { max-width: none; max-height: none; min-height: 100%; border: 0; border-radius: 0; }
}

/* ===========================================================================
   RESPONSIVE
   The desktop grid (sidebar + header) holds all the way down to tablets — an
   iPad in portrait keeps the sidebar, which is the best use of the width. Below
   768px the chrome switches to a mobile app shell: a compact header (the mark,
   search, the viewer's controls) over a fixed bottom tab bar, with the page
   card running edge to edge between them. Safe-area insets keep it clear of
   the notch and the home bar.
   =========================================================================== */

/* Up to a tablet the avatar stands in for the viewer's name in the top bar. */
@media (max-width: 1024px) { .top-user-name { display: none; } .top-user { padding: 0 .3rem; } }

/* ---- tablet: narrow the rail so content keeps its room ------------------ */
@media (min-width: 768px) and (max-width: 1024px) {
  :root { --sidebar-w: 212px; --content-pad: 1.3rem; --surface-gap: 1.2rem; }
  .logo-product { display: none; }
}

/* ---- mobile: top header + fixed bottom tab bar -------------------------- */
@media (max-width: 767px) {
  :root {
    --m-top-h: 56px;
    --m-bottom-h: 60px;
    --m-top-total: calc(var(--m-top-h) + env(safe-area-inset-top));  /* header + notch */
    --content-pad: 1rem;
  }
  body { font-size: 14px; }

  /* Header, then the page; the sidebar is lifted out of the grid entirely and
     fixed to the bottom edge as a tab bar (see .sidebar below). */
  /* On a phone the page scrolls as a page again: the window is the frame,
     and a card scrolling inside it would fight the browser's own bars. */
  .app, .app.is-collapsed { --sidebar-w: 0px; grid-template-columns: minmax(0, 1fr);
    grid-template-rows: var(--m-top-total) minmax(0, 1fr);
    grid-template-areas: "topbar" "main"; height: auto; min-height: 100vh; }

  .topbar { position: sticky; top: 0; display: flex; gap: .5rem; height: 100%;
    padding: env(safe-area-inset-top) .6rem 0 .9rem; }
  .top-brand { display: flex; flex: none; color: var(--text); }
  .top-brand .logo-mark { width: 22px; height: 22px; }
  .top-head { flex: 1; }
  .top-title { font-size: 15px; }
  .top-back { margin-left: 0; }
  /* Search folds to its icon: the dialog it opens is the full-width field. */
  .top-search { flex: none; width: var(--ctrl-h); }
  .top-search input { padding: 0; color: transparent; }
  .top-search input::placeholder { color: transparent; }
  .top-search-ico { left: 50%; transform: translate(-50%, -50%); }
  .top-search-kbd { display: none; }
  .top-actions { gap: 0; }
  .top-user { margin-left: .2rem; }

  /* Bottom tab bar: the brand, the group labels, Recents and the footer chip
     stay behind — the top bar carries the mark, and a tab bar is links only. */
  .nav-sec, .nav-sec-body, .nav-sec.is-closed .nav-sec-body { display: contents; }
  .side-head, .nav-group, .nav-sec.nav-recents, .side-foot { display: none; }
  .sidebar { position: fixed; inset: auto 0 0 0; top: auto; z-index: 40;
    height: auto; flex-direction: row; gap: 0; overflow: visible;
    padding: 4px 4px calc(4px + env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--bg) 92%, transparent); backdrop-filter: blur(10px);
    border-top: 1px solid var(--border); }
  /* Ten destinations do not fit a phone's width at a tappable size, so the bar
     scrolls sideways rather than squeezing every label to an ellipsis. */
  .nav { flex: 1; flex-direction: row; gap: 2px; overflow-x: auto; overscroll-behavior-x: contain; }
  .nav-link { flex: 1 0 64px; min-width: 64px; flex-direction: column; justify-content: center;
    align-items: center; gap: 3px; height: auto; padding: 6px 3px; min-height: 50px;
    font-size: 10px; font-weight: 600; }
  .nav-ico { width: 22px; height: 22px; }
  .nl-label { flex: none; max-width: 100%; }

  /* The page card runs edge to edge: at this width a margin around it is room
     taken from the content. It keeps its top corners, so it still reads as the
     surface the header sits above. */
  .main { display: block; padding: 0; }
  .surface { display: block; overflow: visible; min-height: calc(100vh - var(--m-top-total));
    border-radius: var(--r-lg) var(--r-lg) 0 0; }
  .content { overflow: visible; -webkit-mask-image: none; mask-image: none;
    padding: 1.2rem var(--content-pad) calc(var(--m-bottom-h) + 1.4rem + env(safe-area-inset-bottom)); }

  .page-title { font-size: 18px; }
  .page-sub { font-size: 13px; margin-bottom: 1.2rem; }

  /* The top bar is narrow here: New keeps its icon, gives up its word. */
  .dash-new .btn.sm { width: 32px; padding: 0; font-size: 0; gap: 0; }
  .dash-new .btn.sm .ico { margin: 0; opacity: 1; }
  .dash-new .dash-new-chev { display: none; }
  .dash-when { flex-basis: 100%; margin-left: 0; }

  /* Section-head create ("+ New") collapses to a "+" square; the "View all" /
     "Add existing" text buttons keep their words so the two stay distinct. */
  .group-actions .btn.sm.primary { width: 34px; padding: 0; font-size: 0; gap: 0; }
  .group-actions .btn.sm.primary .ico { width: 16px; height: 16px; margin: 0; }

  /* Tighten the vertical rhythm between dashboard blocks */
  .db-block + .db-block { margin-top: 1.1rem; }
  .section-head { margin: 1.5rem 0 .7rem; }

  /* Denser list rows */
  .item { padding: .65rem .8rem; gap: .7rem; }
  .item .ic { width: 32px; height: 32px; }

  /* Detail: label-over-value reads better than a cramped two-column grid */
  .kv { grid-template-columns: 1fr; gap: .3rem; }
  .kv dt { padding-top: .7rem; }
  .kv dt:first-of-type { padding-top: 0; }
  .secret { flex-wrap: wrap; }
  .panel { padding: 1.1rem 1rem; }
  .modal-head { padding-left: 1.25rem; }
  .modal-body { padding: .4rem 1.25rem 1.6rem; }

  /* File rows: drop the size and date; keep name + download + pin */
  .item .when, .file-size { display: none; }

  /* Keep toasts clear of the header */
  .toasts { top: calc(var(--m-top-total) + .4rem); }

  /* 16px form text stops iOS from zooming the page on focus */
  input, textarea, select, .top-search input, .filter { font-size: 16px; }

}

/* ---- very narrow phones (iPhone SE, 320px): reclaim every pixel --------- */
@media (max-width: 360px) {
  /* labels would ellipsize at this width — go icon-only; the glyphs read fine */
  .nl-label { display: none; }
  .nav-ico { width: 24px; height: 24px; }
  .nav-link { padding: 8px 2px; }
}

/* ---- control center: pinned + recent quick-access ----------------------
   Two cards, not a panel of two lists: each row is an .ecard and brings its
   own chrome, so this is only the stack that spaces them. The classes stay
   because the workspace filter addresses them (app.js). */
.control-center { display: flex; flex-direction: column; gap: .8rem; margin: 1.25rem 0; }
/* [hidden] alone loses to display:flex above (same as .item) */
.control-center[hidden], .cc-row[hidden] { display: none; }

/* ---- project page: grouped contents ----------------------------------
   A group is an .ecard now, so it needs no spacing rules of its own; the class
   stays because the workspace filter hides whole groups by it (app.js). */
/* workspace group actions: add-existing + new, per type */
.group-actions { display: flex; gap: .4rem; flex: none; }
.btn.sm { height: 30px; padding: 0 .8rem; font-size: 12.5px; }
.btn.sm .ico { width: 15px; height: 15px; }
.btn.sm.primary { box-shadow: none; }
/* attach picker: checkbox rows + a bulk "Add selected" bar */
.pick-row { cursor: pointer; gap: .7rem; }
.pick-move { flex: none; font-size: 11px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
/* Footer of a picker: select-all on the left, count and submit on the right,
   set off from the list by the same rule every modal foot uses. */
.pick-bar { display: flex; align-items: center; gap: .9rem; margin-top: 1.25rem;
  padding-top: 1.25rem; border-top: 1px solid var(--border); }
.pick-all { display: inline-flex; align-items: center; gap: .45rem; font-size: 13px;
  color: var(--muted); cursor: pointer; white-space: nowrap; }
.pick-count { margin-left: auto; font-size: 13px; color: var(--faint);
  font-variant-numeric: tabular-nums; white-space: nowrap; }

/* drag a row onto a project row to file it there. The project row sits inside
   a card that owns the borders, so the hover state is drawn *within* the row —
   an inset ring, not a border — and nothing shifts by a pixel as it lands. */
.item.dragging { opacity: .45; }
.ptile.drop-hover { background: color-mix(in srgb, var(--kc, var(--proj)) 14%, transparent);
  box-shadow: inset 0 0 0 1px var(--kc, var(--proj)); }
.ptile.drop-hover .body .t a { color: var(--kc, var(--proj)); }
/* A tag: a bordered pill carrying an icon and a label that clips rather than
   wraps. Two sizes, the same relationship .btn and .btn.sm have — the full one
   states a fact under a detail title (and may be a link), `.sm` rides inside a
   list row's title line. The icon takes the type colour at full size and plain
   currentColor at `.sm`, where the row's own colour is already the icon badge
   beside it. */
.tag { display: inline-flex; align-items: center; gap: .4rem; max-width: 100%;
  height: 26px; padding: 0 .6rem; font-size: 12px; font-weight: 500; color: var(--muted);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm);
  transition: color .12s, border-color .12s; }
.tag.sm { flex: none; max-width: 12rem; height: 19px; gap: .3rem; padding: 0 .4rem;
  font-size: 11px; font-weight: 600; border-radius: 6px; }
.tag svg { flex: none; width: 14px; height: 14px; color: var(--kc, var(--accent)); }
.tag.sm svg { width: 11px; height: 11px; color: inherit; }
.tag span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.tag:hover { text-decoration: none; color: var(--text);
  border-color: color-mix(in srgb, var(--kc, var(--accent)) 55%, var(--border-hi)); }

/* --- two-factor setup ------------------------------------------------- */
/* On the settings page the panel stands alone, so it needs a width of its own;
   mid-login it is simply an auth card and inherits that one — the sign-in flow
   never jumps sideways between steps. */
.totp-panel { max-width: 380px; }
.totp-status { display: flex; align-items: center; gap: .55rem; margin: 0 0 1.2rem; font-weight: 600; }
.totp-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
.totp-actions { display: flex; gap: .6rem; flex-wrap: wrap; }
.totp-actions form { margin: 0; }

/* Everything in the enroll block is one centered column: the QR, the row of
   code cells and the button all share its edges instead of each part finding
   its own width. That width is the row of cells, measured from the same
   custom properties the cells themselves use — a literal would go stale the
   day a cell or a gap changes. */
.totp-enroll { text-align: center;
  --enroll-w: min(calc(6 * var(--code-cell-w) + var(--code-sep-w) + 6 * var(--code-gap)), 100%); }
.totp-lead { margin-bottom: 1.3rem; line-height: 1.55; }
.totp-apps { margin: .8rem 0 0; font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint); }

/* QR: sized by CSS (the SVG keeps only its viewBox). */
.qr { width: var(--enroll-w); margin: 0 auto; background: #fff; padding: 12px; border-radius: var(--r); }
.qr-svg { display: block; width: 100%; height: auto; }

/* Same column as the QR: label, cells and (where it is not full-width) button
   all sit on that one axis. */
.totp-confirm { width: var(--enroll-w); margin: 1.5rem auto 0;
  padding-top: 1.35rem; border-top: 1px solid var(--border); }
.totp-confirm .btn { margin-top: .9rem; }

/* One-time code. Without JS it stays a single field with the digits spread out;
   app.js upgrades it into one cell per digit. The three metrics of that row are
   named because the enroll column above sizes itself from them. */
:root { --code-cell-w: 46px; --code-gap: .4rem; --code-sep-w: 9px; }
/* the cells it labels are centered too, so it gives up the field labels' inset */
.code-label { --label-inset: 0; text-align: center; }
.code-input, .code-cell { text-align: center; font-size: 20px; font-variant-numeric: tabular-nums; }
.code-input { letter-spacing: .34em; text-indent: .34em; }  /* indent re-centers the tracking */
/* Portrait cells, centered as a group rather than stretched across the panel.
   Two declarations look redundant but aren't: as a flex *basis* 46px still
   shrinks on a narrow phone, where a plain `width: 46px` would be floored by
   `min-width: auto` and spill out of the panel; and `padding: 0` overrides the
   global input padding, which at ~320px would clip the digit. `align-items`
   is what keeps the separator on the row's centre line. */
.code-cells { display: flex; align-items: center; justify-content: center; gap: var(--code-gap); }
.code-cell { flex: 0 1 var(--code-cell-w); aspect-ratio: 3 / 4; padding: 0; caret-color: var(--accent); }
.code-cell::selection { background: var(--accent-sf); }
.code-sep { flex: none; width: var(--code-sep-w); height: 1px; background: var(--border-hi); }

/* --- accounts / membership ------------------------------------------------ */
/* One row inside a .panel list — the roster and the share modal. Both are
   tables of one-line facts, so they differ only in spacing and alignment and
   share the separators and the form reset. A comment is *not* one of these: see
   the comments section, where a message is a paragraph and the space around it
   does the separating. */
.team-list, .share-list { padding: 0; }
.team-row, .share-row { display: flex; align-items: center;
            border-bottom: 1px solid var(--border); }
.team-row:last-child, .share-row:last-child { border-bottom: 0; }
.team-row form, .share-row form { margin: 0; }
.team-list { margin: .7rem 0 1.8rem; }
.team-list + .section-title { margin-top: 0; }
.team-row { justify-content: space-between; gap: .7rem; flex-wrap: wrap;
            padding: .85rem 1rem; }
/* Takes the row's slack so the actions stay pinned right and the avatar stays
   next to the name it belongs to. */
.team-who { flex: 1; display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.team-name { font-weight: 600; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  min-width: 0; overflow-wrap: anywhere; }
/* Micro-label: a small uppercase tag beside a name or a title. */
.team-you { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
            border: 1px solid currentColor; border-radius: var(--r-sm);
            color: var(--accent); padding: 0 .35rem; }
.team-meta { font-size: 13px; color: var(--faint); }
.team-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.team-actions form { display: flex; gap: .5rem; margin: 0; }
.team-empty { color: var(--muted); font-size: 13.5px; margin: 0; padding: .85rem 1rem; }

/* --- sharing -------------------------------------------------------------- */
/* Shared state reads as a neutral chip plus the owner's initial, never a new
   accent hue — the per-type colors keep meaning what they mean everywhere else. */
.share-section { display: flex; align-items: baseline; gap: .8rem; margin: 1.6rem 0 0; }
.share-count { margin-left: auto; font-size: 12px; color: var(--faint);
  font-variant-numeric: tabular-nums; }
.share-list { margin: .7rem 0 .8rem; }
.share-row { gap: .7rem; padding: .7rem 1rem; }
/* Name plus whatever is flagged about it, like .team-name in the roster: the
   clipping is on the text alone, so a long username cannot truncate a flag. */
.share-name { flex: 1; min-width: 0; font-weight: 600; display: flex;
  align-items: center; gap: .5rem; }
.share-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Only what differs from the global input/select rule. `background-color`, not
   `background`: the shorthand would drop the chevron the select draws itself. */
.share-perm select { height: 32px; min-height: 0; background-color: var(--surface);
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--border); padding: 0 1.9rem 0 .7rem; font-size: 12.5px; }
/* Under the roster: what a revoke costs on the left, the blanket revoke on the
   right — the dangerous button sits apart from the per-person ones above it.
   The button pushes itself right, so the row lands the same way whether or not
   the hint beside it is there. */
.share-foot { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: .4rem; }
.share-revoke-all { margin: 0 0 0 auto; }
.share-hint { margin: 0; font-size: 12px; color: var(--faint); max-width: 44ch; line-height: 1.45; }
.share-access { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  margin-top: 1rem; }
.share-access-label { font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); }
.share-radio { display: inline-flex; align-items: center; gap: .4rem; font-size: 13px;
  color: var(--muted); cursor: pointer; }
.share-radio input { accent-color: var(--accent); }
/* An account with no keypair yet cannot be sealed to: shown, and shown why. */
.pick-row.is-disabled { opacity: .5; cursor: not-allowed; }

/* The Sharing tab's roster states each grant rather than offering to change
   it — the share dialog is where that happens. */
.share-perm-label { flex: none; font-size: 12.5px; color: var(--muted); }

/* On a list row the sharing mark belongs to the *title*, not to the actions:
   an indicator parked among the buttons adds a slot to some rows and not
   others, so the copy/pin columns stop lining up down the list. It is a plain
   `.tag.sm` (see above); all this section adds is the count's figures and the
   lift it takes from the row's own hover. */
.item .tag { font-variant-numeric: tabular-nums; }
.item:hover .tag { border-color: var(--border-hi); color: var(--text); }

/* --- comments -------------------------------------------------------------
   The conversation is a record's Discussion tab, set to a reading measure
   rather than the page's full width: one grid repeated down the thread — a quiet avatar, then
   a head line over the message — with nothing between two messages but even
   space. No row separators here, unlike the roster and the share modal: those
   are tables of one-line facts, this is prose, and a rule under every paragraph
   would fight the one thing that does carry structure — the branch lines.
   -------------------------------------------------------------------------- */
/* The two numbers the whole block is measured from: the avatar's size and the
   gap beside it. The branch indents are derived from them, so the alignment
   holds if either one is ever retuned. */
.thread { --cmt-av: 26px; --cmt-gap: .6rem; --cmt-step: 1.05rem; max-width: 46rem; }
.thread-empty { margin: 0; font-size: 13px; color: var(--faint); }
/* One rhythm for the whole thread: the gap between two messages, between a
   message and the answers under it, and between two of those answers is the
   same number, at every depth. That is what keeps a deep branch from reading
   as a different kind of list than a flat one. */
.thread-list, .cmt-kids { display: flex; flex-direction: column; gap: var(--cmt-step); }
/* The branch: one hairline on one wrapper, so it runs the whole length of the
   answers instead of breaking between them the way a segment per row does. It
   drops from under the middle of the parent's avatar (--cmt-av / 2) and the
   indent is measured so a reply's own avatar lands exactly under the parent's
   text — the two columns of the grid above, one step in. */
.cmt-kids { margin: var(--cmt-step) 0 0 calc(var(--cmt-av) / 2);
  padding-left: calc(var(--cmt-av) / 2 + var(--cmt-gap) - 1px);
  border-left: 1px solid var(--border); }
/* Past the second answer in a chain the indent all but stops and the stacked
   lines alone carry the nesting: on a phone the thread is ~330px wide, so two
   full steps is what there is room for before the text is squeezed. */
.cmt-kids .cmt-kids .cmt-kids { margin-left: 0; padding-left: .7rem; }
.cmt { position: relative; display: flex; align-items: flex-start; gap: var(--cmt-gap); }
.cmt-main { flex: 1; min-width: 0; }
/* No room reserved on the right: the actions surface *over* the end of this
   line on hover, on the surface colour both the card and the modal are painted
   in — reserving space for them instead would cost every row the width of three
   buttons, and at the bottom of a branch that is the width the name needed. */
.cmt-head { display: flex; align-items: baseline; gap: .5rem; margin: 0;
  min-height: 20px; overflow: hidden; }
.cmt-who { font-size: 13px; font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* The viewer's own message, marked on the name rather than with a bar down the
   row: at four possible indents a left border is one vertical line too many,
   and the accent on the name says the same thing where the eye already is. */
.cmt.is-mine .cmt-who { color: var(--accent); }
.cmt-when { font-size: 12px; color: var(--faint); white-space: nowrap; }
.cmt-body { font-size: 13.5px; line-height: 1.55; color: var(--text);
  margin-top: .1rem; word-break: break-word; }
/* In the row's own top-right corner and only on hover, like the copy icon on a
   detail field: the message is the content, its housekeeping is not — and
   keeping them out of the head line means a row with three actions and a row
   with one are the same shape. */
.cmt-actions { position: absolute; top: -1px; right: 0; display: flex;
  align-items: center; gap: .1rem; border-radius: var(--r-sm); }
/* Painted only while the row is hovered, in the colour both surfaces that draw
   a thread are painted in (a page's card and the modal are both --surface), so
   the strip covers the end of the head line instead of dimming it. */
.cmt:hover .cmt-actions { background: var(--surface); }
.cmt-actions form { margin: 0; }
/* Both icon strips a thread draws — a message's actions and the composer's
   cancel — are the same small control; only where they sit differs. */
.thread .row-copy { width: 22px; height: 22px; }
.thread .row-copy svg { width: 14px; height: 14px; }
.thread .row-copy:hover { color: var(--text); }
.cmt-actions form .row-copy:hover { color: var(--danger); }
/* The avatar is the header's disc, quietened: in a thread every message wears
   one, so a lifted disc on each would be the loudest thing in a column of text. It states who wrote this — and, for the viewer's own message, the
   accent ring that matches the name above it. */
.cmt .avatar { width: var(--cmt-av); height: var(--cmt-av); font-size: 11.5px; font-weight: 600;
  color: var(--muted); background: var(--surface-2); box-shadow: none; }
.cmt.is-mine .avatar { color: var(--accent); background: var(--accent-sf);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent); }
/* The composer closes the column the way the head opens it: a rule, then the
   field and one line of controls. */
.cmt-form { margin-top: 1.2rem; padding-top: 1.2rem;
  border-top: 1px solid var(--border); }
/* Who the one composer is aimed at, shown only while it is aimed at somebody —
   with the ✕ that gives it back to the thread pushed to the same right edge the
   Post button below it sits on. `display` beats the browser's own [hidden]
   rule, so the state has to be spelled out here, the pair .modal-overlay
   carries. */
.cmt-target[hidden] { display: none; }
.cmt-target { display: flex; align-items: center; gap: .35rem; margin: 0 0 .5rem;
  font-size: 12px; color: var(--faint); }
.cmt-target b { color: var(--text); font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.cmt-target .row-copy { margin-left: auto; }
/* Narrows the global input rule with one class, like every other field does. */
.cmt-input { width: 100%; resize: vertical; min-height: 4.4rem; font-size: 13.5px; }
/* Post first, then the note about who will read it — the same order and the
   same left edge the thread's own text starts on, rather than a button pushed to a right edge nothing else
   lines up with. */
.cmt-form-foot { display: flex; align-items: center; gap: .8rem;
  flex-wrap: wrap; margin-top: .7rem; }
.cmt-hint { font-size: 12px; color: var(--faint); line-height: 1.4; }

/* --- knowledge: a list whose rows nest ----------------------------------- */
/* A page is a list row wherever it is drawn, so the tree wears the list's own
   chrome and only the nesting is added here. The branch is drawn the way the
   comment thread draws one: a hairline on one wrapper per level, running the
   whole length of the pages under a section instead of breaking between them. */
.kb-kids { margin-left: calc(1rem + 17px); border-left: 1px solid var(--border); }
/* Past the second step the indent all but stops and the stacked lines alone
   carry the nesting — a row still has to fit its title, its marks and its
   actions, and a full step per level would spend that width on air. */
.kb-kids .kb-kids .kb-kids { margin-left: .8rem; }
/* Where this page sits, above its own body. Ancestors only: the title is the
   heading right above it, and a crumb that ends by repeating it says it twice. */
.kb-path { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
  margin: 0 0 1rem; font-size: 12px; color: var(--faint); }
.kb-path a { color: var(--muted); }
.kb-path a:hover { color: var(--accent); text-decoration: none; }
.kb-sep { color: var(--border); }
/* Handing over a whole branch: one option above the access radios, in the same
   quiet register as the hint text beside them. */
.share-bundle { display: flex; align-items: center; gap: .5rem; margin: .9rem 0 0;
  font-size: 13px; color: var(--muted); }
.share-bundle-n { color: var(--faint); font-size: 12px; }
