/* ============================================================
   daviddn.cloud — dashboard suite design system
   Language borrowed from Reve Internal v4 (white rounded panels,
   pill controls, off-white canvas, hairline borders, light-dark
   pairs) set entirely in one neutral modern font (Inter) — no
   mono, no display family. Each module lives on its own
   subdomain; this file is the one shared visual contract.
   ============================================================ */

/* ---------- Inter, self-hosted ----------
   Was two preconnects plus a stylesheet from fonts.googleapis.com and the font
   files themselves from fonts.gstatic.com — four third-party round-trips (DNS,
   TLS, CSS, font) in front of first paint, on EVERY navigation, measured at
   ~127ms for the CSS alone before a single glyph was fetched. Same font, served
   from our own origin on a connection the browser already has open.
   Only latin and latin-ext are shipped: those cover the UI and the accented
   names in the data. Cyrillic, Greek and Vietnamese were 5 of the 7 subsets
   Google offered and none of them is ever rendered here. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(/assets/fonts/inter-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(/assets/fonts/inter-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light dark;

  --dd-font-sans: "Inter", -apple-system, "Segoe UI", sans-serif;

  /* type scale — pick a step, never a literal */
  --dd-fs-3xs: .625rem;   /* 10px dense meta */
  --dd-fs-2xs: .6875rem;  /* 11px micro-labels */
  --dd-fs-xs:  .75rem;    /* 12px */
  --dd-fs-sm-: .8125rem;  /* 13px compact body */
  --dd-fs-sm:  .875rem;   /* 14px body */
  --dd-fs-md:  1rem;      /* 16px */
  --dd-fs-lg:  1.125rem;  /* 18px */

  /* ---- Palette: Claude Code Desktop -------------------------------
     Lifted from the app's own [data-theme=claude] token block (its --bg-*,
     --text-* and --border-* HSL values converted to hex), so both schemes
     share Claude's warm-gray family rather than a cool slate.

     dark   canvas = bg-200 · panel = bg-000 · wash = bg-100
     light  canvas = bg-200 · panel = bg-100  (bg-000 is pure #fff — skipped
            on purpose: no pure white, no pure black anywhere in this system)

     Ink is Claude's text-100/200/400. The one deviation: the light quiet tier
     is darkened from Claude's #73726c to #676660, because Claude uses that
     step for larger secondary text while we use it for 11px micro-labels,
     where #73726c measured 3.97:1 on the hover wash — below AA. Everything
     here was contrast-checked against panel, canvas AND the wash; the quiet
     tier is the tightest at 4.7:1, so re-measure before touching it.
     ------------------------------------------------------------------- */

  /* grays — washes, hovers, dividers (Claude's border hue over the panel) */
  --dd-gray-50:  light-dark(#eae9e3, #262624);
  --dd-gray-100: light-dark(#e4e3df, #41413e);
  --dd-gray-200: light-dark(#d7d6d1, #4b4a47);
  --dd-gray-300: light-dark(#cac9c5, #565652);
  --dd-gray-500: light-dark(#979694, #7e7d77);

  /* ink tiers — Claude text-100 / text-200 / text-400 */
  --dd-ink:       light-dark(#141413, #faf9f5);
  --dd-ink-light: light-dark(#3d3d3a, #c2c0b6);
  --dd-ink-quiet: light-dark(#676660, #9c9a92); /* AA on panel, canvas + wash */

  /* chrome */
  --dd-canvas:  light-dark(#f5f4ed, #1f1e1d);
  --dd-panel:   light-dark(#faf9f5, #30302e);
  --dd-border:  light-dark(#e0dfdb, #474643);
  --dd-hairline:light-dark(#ebeae6, #3e3e3b);
  --dd-inverse: light-dark(#141413, #faf9f5);
  --dd-on-inverse: light-dark(#faf9f5, #1f1e1d);

  /* semantic — status only, everything else monochrome. Red is Claude's
     danger step for the scheme (light danger-100, dark danger-000 — the
     lighter dark step, since danger-100 is only 3.4:1 on the panel). */
  --dd-green:  light-dark(#1a7742, #63d18c);
  --dd-red:    light-dark(#b53333, #fe8181);
  --dd-amber:  light-dark(#8a6410, #f0c057);
  --dd-blue:   light-dark(#2a5fd0, #7fb2ff);

  /* washes and scrims — a tint is still a colour, so it still needs a token */
  --dd-blue-wash: light-dark(#eef2fb, #23293a);
  --dd-backdrop: light-dark(#14141347, #0a0a0999);

  --dd-radius-s: 6px;
  --dd-radius-m: 10px;
  --dd-radius-l: 16px;
  --dd-radius-full: 999px;

  --dd-sidebar-w: 232px;
  --dd-view-max: 1240px;

  /* layout rhythm — these four values are the whole system */
  --dd-header-gap: 24px;
  --dd-stack-gap: 16px;
  --dd-grid-gap: 16px;
  --dd-pad-x: 20px;

  --dd-shadow-pop: 0 4px 16px light-dark(#14141317, #00000066);
  --dd-shadow-dialog: 0 12px 40px light-dark(#14141322, #00000099);

  --dd-ease: cubic-bezier(.3, .7, .3, 1);
  --dd-dur: 160ms;
  --dd-dur-slow: 220ms;  /* chrome that changes size, e.g. a collapsing rail */

  /* identity palette — names things (calendars, mail labels), never state.
     Muted per scheme; chips derive tints via color-mix so one token per hue. */
  --dd-id-blue:   light-dark(#2a5fd0, #7fb2ff);
  --dd-id-green:  light-dark(#1a7742, #63d18c);
  --dd-id-amber:  light-dark(#8a6410, #f0c057);
  --dd-id-violet: light-dark(#6d4fc4, #b3a0f2);
  --dd-id-rose:   light-dark(#b13a72, #f094bd);
  --dd-id-teal:   light-dark(#0f766e, #5eead4);

  --dd-subbar-w: 240px;
  --dd-app-gap: 16px;   /* air around and between an app's panes */

  /* checkbox tick, used as a mask so it inherits the scheme's ink */
  --dd-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.6 6.3 4.8 8.5 9.4 3.7' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"]  { color-scheme: dark; }

* { box-sizing: border-box; }

html {
  /* The root must paint the canvas itself. Left transparent, any exposed edge
     of the document — a macOS rubber-band, an overscroll bounce — is filled by
     the browser's OWN default canvas, which `color-scheme: light dark` allows
     to be near-black. That is the "black strip beside the sidebar". */
  background: var(--dd-canvas);
  scrollbar-width: thin;
  scrollbar-color: var(--dd-gray-300) transparent;
  /* No page in this suite scrolls sideways, so a horizontal trackpad gesture
     must never reach the browser and fire swipe-to-go-back. Panes that DO
     scroll horizontally contain their own overscroll as well. */
  overscroll-behavior-x: none;
}

body {
  margin: 0;
  font-family: var(--dd-font-sans);
  font-size: var(--dd-fs-sm);
  color: var(--dd-ink);
  background: var(--dd-canvas);
  -webkit-font-smoothing: antialiased;
}

[hidden] { display: none !important; }
::selection { background: var(--dd-inverse); color: var(--dd-on-inverse); }

a { color: inherit; }

/* One focus treatment everywhere. Never remove it, never restyle per component. */
:focus-visible {
  outline: 2px solid var(--dd-ink);
  outline-offset: 2px;
}

/* ---------- shell: sidebar + main column ---------- */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--dd-sidebar-w);
  flex: none;
  position: sticky; top: 0; height: 100vh;
  display: flex; flex-direction: column;
  background: var(--dd-panel);
  border-right: 1px solid var(--dd-border);
  padding: 20px 12px 16px;
  /* The rail must scroll when it is taller than a short viewport, or its footer
     (Theme / Settings / account) is unreachable. Note: pairing `clip` with a
     scrollable axis is defined to resolve to `hidden`, so the used value here is
     `hidden auto` — written explicitly rather than implying `clip` does
     something it cannot. Each label clips itself, so the x axis is belt only. */
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: none;
  /* Collapsing is a size change, so it gets the slower duration. Labels give up
     their width instead of being display:none'd — that is what lets the rail
     animate rather than snap. */
  transition: width var(--dd-dur-slow) var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease);
}
.sidebar::-webkit-scrollbar { width: 0; }
.brand-name,
.side-nav a > span:not(.avatar),
.side-nav button > span:not(.avatar),
.side-nav .sub {
  overflow: hidden; white-space: nowrap;
  max-width: 180px;
  transition: max-width var(--dd-dur-slow) var(--dd-ease),
              opacity 110ms var(--dd-ease);
}
/* SCOPED TO .sidebar ON PURPOSE. `.side-label` is a shared class — every app's
   subbar uses it for its section headings ("Areas", "My calendars") — so an
   unscoped rail rule collapses those headings too whenever the rail collapses. */
.sidebar .side-search, .sidebar .side-label {
  overflow: hidden;
  max-height: 60px;
  transition: max-height var(--dd-dur-slow) var(--dd-ease),
              opacity 110ms var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease);
}

/* The control that collapses the rail lives IN the rail, on the brand's row —
   it belongs to the thing it operates, not to the content area's topbar. */
.side-head {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 0 18px;
}
.brand {
  display: flex; align-items: center; gap: 10px;
  padding: 0 8px;
  flex: 1; min-width: 0;
  text-decoration: none;
  /* on the BASE rule so the fade runs in both directions — a transition
     declared only in the collapsed state does not animate the way back */
  transition: opacity var(--dd-dur) var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease),
              gap var(--dd-dur-slow) var(--dd-ease);
}
.rail-btn {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  border: 0; background: transparent;
  border-radius: var(--dd-radius-m);
  color: var(--dd-ink-quiet);
  cursor: pointer;
  transition: background var(--dd-dur) var(--dd-ease), color var(--dd-dur) var(--dd-ease);
}
.rail-btn:hover { background: var(--dd-gray-100); color: var(--dd-ink); }
.rail-btn svg { width: 16px; height: 16px; stroke-width: 1.75; }
.brand-mark {
  width: 28px; height: 28px; flex: none;
  border-radius: 8px;
  background: var(--dd-inverse); color: var(--dd-on-inverse);
  display: grid; place-items: center;
  font-weight: 700; font-size: var(--dd-fs-sm-);
  letter-spacing: -.02em;
}
.brand-name { font-weight: 650; font-size: var(--dd-fs-sm); letter-spacing: -.01em; }
.brand-name span { color: var(--dd-ink-quiet); font-weight: 450; }

.microlabel {
  font-size: var(--dd-fs-2xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--dd-ink-quiet);
}

.side-search { padding: 0 2px 14px; }
.side-search .search-pill { width: 100%; }

.side-label { padding: 8px 8px 6px; }

.side-nav { display: flex; flex-direction: column; gap: 2px; }
.side-nav a, .side-nav button {
  display: flex; align-items: center; gap: 10px;
  border: 0; background: transparent; width: 100%; text-align: left;
  font: inherit; cursor: pointer;
  /* 8px matches .brand and .side-label, so every item shares one left edge */
  padding: 8px;
  border-radius: var(--dd-radius-m);
  text-decoration: none;
  font-size: var(--dd-fs-sm);
  font-weight: 480;
  color: var(--dd-ink-light);
  /* padding and gap are animated because collapsing re-centres the icon by
     changing them (see the collapsed block) — without these the icon jumps */
  transition: background var(--dd-dur) var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease),
              gap var(--dd-dur-slow) var(--dd-ease);
}
.side-nav a:hover, .side-nav button:hover { background: var(--dd-gray-50); color: var(--dd-ink); }
.side-nav a.active { background: var(--dd-gray-100); color: var(--dd-ink); font-weight: 600; }
.side-nav svg { width: 16px; height: 16px; flex: none; stroke-width: 1.75; }
.side-nav .sub {
  margin-left: auto;
  font-size: var(--dd-fs-3xs);
  color: var(--dd-ink-quiet);
}

.side-foot { margin-top: auto; display: flex; flex-direction: column; gap: 2px; padding-top: 12px; border-top: 1px solid var(--dd-hairline); }

/* The rail's collapse row: always visible, in both states, saying which way it
   goes — « to collapse, flipped to » once collapsed. A control that operates
   the panel lives in the panel, at the bottom where every tool keeps it. */
.rail-collapse svg { transition: transform var(--dd-dur-slow) var(--dd-ease); }
:root.rail-collapsed .rail-collapse svg { transform: rotate(180deg); }

.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ---------- account row (the topbar's only survivor) ----------
   There is no global topbar: it cost 64px of vertical space to show a hostname
   the sidebar already implies. Page identity now lives in each app's subbar
   header (.pane-brand) and the account chip sits at the bottom of the rail. */

.side-user .avatar {
  width: 22px; height: 22px;
  font-size: var(--dd-fs-4xs, .625rem);
  border-radius: var(--dd-radius-full);
}
.spacer { margin-left: auto; }

.search-pill {
  display: flex; align-items: center; gap: 8px;
  font-family: inherit;
  height: 34px; width: 100%;
  padding: 0 14px;
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-full);
  background: var(--dd-canvas);
  color: var(--dd-ink-quiet);
  font-size: var(--dd-fs-sm-);
  cursor: pointer;
  transition: border-color var(--dd-dur) var(--dd-ease);
}
.search-pill:hover { border-color: var(--dd-gray-300); }
.search-pill kbd {
  margin-left: auto;
  font-size: var(--dd-fs-3xs);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-s);
  padding: 1px 5px;
  background: var(--dd-panel);
}

.avatar {
  width: 32px; height: 32px; flex: none;
  font-family: inherit; color: inherit;
  border-radius: var(--dd-radius-full);
  background: var(--dd-gray-100);
  border: 1px solid var(--dd-border);
  display: grid; place-items: center;
  font-size: var(--dd-fs-2xs); font-weight: 650;
  cursor: pointer;
  transition: background var(--dd-dur) var(--dd-ease),
              border-color var(--dd-dur) var(--dd-ease);
}
.avatar:hover { background: var(--dd-gray-200); border-color: var(--dd-gray-300); }

/* ---------- main / view ---------- */

main { padding: 32px 40px 40px; }
.view { max-width: var(--dd-view-max); margin: 0 auto; }
.view > * + * { margin-top: var(--dd-stack-gap); }

.view-header {
  min-height: 48px;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-bottom: var(--dd-header-gap);
}
.view-eyebrow { display: block; margin-bottom: 6px; }
.view-title {
  margin: 0;
  font-size: 2.125rem;
  font-weight: 600;
  letter-spacing: -.025em;
  line-height: 1.1;
}
.view-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 16px;
  border-radius: var(--dd-radius-full);
  border: 1px solid var(--dd-border);
  background: var(--dd-panel);
  color: var(--dd-ink);
  font: inherit; font-size: var(--dd-fs-sm-); font-weight: 550;
  cursor: pointer;
  transition: background var(--dd-dur) var(--dd-ease), border-color var(--dd-dur) var(--dd-ease);
}
.btn:hover { background: var(--dd-gray-50); border-color: var(--dd-gray-300); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--dd-inverse); color: var(--dd-on-inverse); border-color: var(--dd-inverse); }
.btn-primary:hover {
  background: color-mix(in srgb, var(--dd-inverse) 82%, var(--dd-canvas));
  border-color: color-mix(in srgb, var(--dd-inverse) 82%, var(--dd-canvas));
}
.btn-icon { width: 34px; padding: 0; justify-content: center; }
.btn-sm { height: 28px; padding: 0 13px; font-size: var(--dd-fs-xs); }
.btn svg { width: 16px; height: 16px; stroke-width: 1.75; }
.btn[disabled] { opacity: .5; cursor: default; pointer-events: none; }

/* ---------- stat row ---------- */

.stats-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--dd-grid-gap);
}
.stat-tile {
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  padding: 16px var(--dd-pad-x) 14px;
  text-decoration: none;
  display: block;
  min-width: 0;
  transition: border-color var(--dd-dur) var(--dd-ease);
}
a.stat-tile:hover { border-color: var(--dd-gray-300); }
.stat-value {
  font-size: 2rem;
  font-weight: 620;
  letter-spacing: -.02em;
  margin-top: 8px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat-value .of { font-size: var(--dd-fs-md); font-weight: 500; color: var(--dd-ink-quiet); }
.stat-sub { margin-top: 8px; font-size: var(--dd-fs-xs); color: var(--dd-ink-quiet); }
.stat-sub b { color: var(--dd-ink); font-weight: 600; }

/* ---------- panels ---------- */

.panel {
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.panel-header {
  min-height: 56px;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px var(--dd-pad-x);
  border-bottom: 1px solid var(--dd-hairline);
}
.panel-title { font-size: var(--dd-fs-md); font-weight: 600; letter-spacing: -.01em; }
.panel-count { font-size: var(--dd-fs-xs); color: var(--dd-ink-quiet); }
.panel-body { padding: var(--dd-pad-x); }
.panel-link {
  font-size: var(--dd-fs-sm-); font-weight: 550;
  color: var(--dd-ink-light); text-decoration: none;
}
.panel-link:hover { color: var(--dd-ink); }

/* Pinned bottom action for a truncated list: full-width, its own hairline,
   always flush with the panel's bottom edge — so the action sits on the same
   line in both panels of a row instead of floating in the header. */
.panel-foot {
  margin-top: auto;
  display: flex; align-items: center; justify-content: center;
  min-height: 44px;
  padding: 0 var(--dd-pad-x);
  border-top: 1px solid var(--dd-hairline);
  font-size: var(--dd-fs-sm-); font-weight: 550;
  color: var(--dd-ink-light);
  text-decoration: none;
  transition: background var(--dd-dur) var(--dd-ease), color var(--dd-dur) var(--dd-ease);
}
.panel-foot:hover { background: var(--dd-gray-50); color: var(--dd-ink); }

.panel-columns {
  display: grid; gap: var(--dd-grid-gap);
  /* Side-by-side panels are the same size — a jagged pair of boxes reads as
     unfinished. The rows inside absorb the extra height (see .row-list below),
     so equal height never costs a dead void either. Pair panels with
     comparable row counts and this stays invisible. */
  align-items: stretch;
}
/* opt out when a panel must hug its content (e.g. beside a much longer list) */
.panel-columns.cols-top { align-items: start; }
.cols-2 { grid-template-columns: 1fr 1fr; }
.cols-2-1 { grid-template-columns: 2fr 1fr; }

/* stack several panels inside ONE column of a .panel-columns row. The panels
   share the column's height so the stack ends flush with its neighbour. */
.col-stack { display: flex; flex-direction: column; gap: var(--dd-grid-gap); }
.col-stack > .panel { margin-top: 0; flex: 1; }

/* ---------- rows / lists ---------- */

.row-list {
  list-style: none; margin: 0; padding: 0;
  flex: 1;
  display: flex; flex-direction: column;
}
/* Rows grow to fill a stretched panel instead of leaving a gap beneath the
   last one. min-height keeps a single-row list from ballooning. */
.row-list > li { flex: 1 0 auto; min-height: 46px; }
.row-list li {
  display: flex; align-items: center; gap: 12px;
  padding: 12px var(--dd-pad-x);
  border-bottom: 1px solid var(--dd-hairline);
  transition: background var(--dd-dur) var(--dd-ease);
}
.row-list li:last-child { border-bottom: 0; }
.row-list li:hover { background: var(--dd-gray-50); }
.row-main { min-width: 0; flex: 1; }
.row-title { font-weight: 520; }
.row-sub { font-size: var(--dd-fs-xs); color: var(--dd-ink-quiet); margin-top: 2px; }
/* Rows are a scannable rhythm: title and sub-line each stay on ONE line and
   ellipsize. A single wrapping row makes every row height in the list differ. */
.row-title, .row-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-meta {
  margin-left: auto; flex: none;
  font-size: var(--dd-fs-xs);
  color: var(--dd-ink-quiet);
  font-variant-numeric: tabular-nums;
}
/* leading meta (a time column on the left) — same look, no auto margin.
   The width is fixed so titles align across rows; keep lead labels short. */
.row-meta.lead { margin-left: 0; width: 96px; flex: none; }

/* trailing numeric VALUE (money, size, count) — data, not meta, so it reads
   in full ink. Right-aligned with a fixed column so values line up. */
.row-value {
  margin-left: auto; flex: none;
  min-width: 88px; text-align: right;
  font-size: var(--dd-fs-sm); font-weight: 550;
  font-variant-numeric: tabular-nums;
}

.check {
  width: 17px; height: 17px; flex: none;
  border: 1.5px solid var(--dd-gray-300);
  border-radius: var(--dd-radius-s);
  appearance: none; margin: 0; cursor: pointer;
  background: var(--dd-panel);
  transition: background var(--dd-dur) var(--dd-ease),
              border-color var(--dd-dur) var(--dd-ease),
              transform var(--dd-dur) var(--dd-ease);
}
.check:hover { border-color: var(--dd-gray-500); }
.check:active { transform: scale(.88); }
.check:checked {
  background: var(--dd-inverse);
  border-color: var(--dd-inverse);
}
/* The tick is a mask, so it takes --dd-on-inverse and inverts with the scheme
   automatically. Masking keeps the stroke crisp and optically centred at any
   box size — the old rotated-border tick sat low and read as a heavy blob. */
.check:checked::after {
  content: "";
  display: block; width: 100%; height: 100%;
  background: var(--dd-on-inverse);
  -webkit-mask: var(--dd-tick) center / 10px 10px no-repeat;
  mask: var(--dd-tick) center / 10px 10px no-repeat;
}
/* The visual box stays tight at 17px; the label wrapper (added by app.js)
   pads the hit area out to ~25px so the touch target is honest. */
.check-hit {
  display: inline-flex; align-items: center;
  padding: 4px; margin: -4px;
  flex: none; cursor: pointer;
}

/* A finished task steps back quietly: quiet ink + a hairline-weight rule
   through the title, never a heavy default strikethrough. Every trailing chip
   dims with it — a struck-through row showing a full-strength red "Overdue"
   contradicts its own state. */
/* Scoped to .row-list: a checkbox does not always mean "completed". A mail list
   uses checkboxes for bulk selection, and an unscoped rule struck through the
   subject of every selected message. */
.row-list li:has(.check:checked) .row-title {
  color: var(--dd-ink-quiet);
  text-decoration: line-through;
  text-decoration-color: var(--dd-gray-300);
  text-decoration-thickness: 1px;
}
.row-list li:has(.check:checked) .tag,
.row-list li:has(.check:checked) .status-pill { opacity: .65; }

/* ---------- status pills / tags ---------- */

.status-pill {
  display: inline-flex; align-items: center; gap: 6px;
  height: 22px; padding: 0 10px;
  border-radius: var(--dd-radius-full);
  background: var(--dd-gray-100);
  font-size: var(--dd-fs-2xs); font-weight: 550;
  white-space: nowrap;
}
.status-pill::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--dd-gray-500);
}
.status-pill.green::before { background: var(--dd-green); }
.status-pill.yellow::before { background: var(--dd-amber); }
.status-pill.red::before { background: var(--dd-red); }
.status-pill.blue::before { background: var(--dd-blue); }
.status-pill.outline { background: transparent; border: 1px solid var(--dd-border); }
.status-pill.outline::before { display: none; }

/* inside rows a pill is a column: uniform width, centered, so titles and
   right edges line up across rows */
.row-list > li > .status-pill { min-width: 92px; justify-content: center; }
/* trailing category chip is a column too, so times/values below it align */
.row-list > li > .tag { min-width: 60px; justify-content: center; }

.tag {
  display: inline-flex; align-items: center;
  height: 20px; padding: 0 8px;
  border-radius: var(--dd-radius-s);
  background: var(--dd-gray-100);
  font-size: var(--dd-fs-3xs);
  color: var(--dd-ink-light);
  white-space: nowrap;
}

/* ---------- tables ---------- */

.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th {
  text-align: left;
  padding: 10px var(--dd-pad-x);
  border-bottom: 1px solid var(--dd-hairline);
  font-size: var(--dd-fs-2xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--dd-ink-quiet);
  white-space: nowrap;
}
td {
  padding: 12px var(--dd-pad-x);
  border-bottom: 1px solid var(--dd-hairline);
  vertical-align: middle;
}
tr:last-child td { border-bottom: 0; }
tbody tr { transition: background var(--dd-dur) var(--dd-ease); }
tbody tr:hover { background: var(--dd-gray-50); }
td.num { font-size: var(--dd-fs-sm-); font-variant-numeric: tabular-nums; }
th.right, td.right { text-align: right; }
td .neg { color: var(--dd-ink); }
td .pos { color: var(--dd-green); }

/* ---------- calendar grid ---------- */

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}
/* grid items default to min-width:auto, so a long event chip would otherwise
   force its 1fr track wider and steal width from the other days */
.cal-grid > * { min-width: 0; }
.cal-dow {
  padding: 10px 0 8px;
  text-align: center;
  border-bottom: 1px solid var(--dd-hairline);
}
.cal-cell {
  min-height: 96px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--dd-hairline);
  border-right: 1px solid var(--dd-hairline);
  font-size: var(--dd-fs-xs);
}
.cal-cell:nth-child(7n) { border-right: 0; }
.cal-cell .d {
  font-size: var(--dd-fs-2xs);
  color: var(--dd-ink-quiet);
}
.cal-cell.out { color: var(--dd-ink-quiet); background: var(--dd-gray-50); }
.cal-cell.today .d {
  display: inline-grid; place-items: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--dd-inverse); color: var(--dd-on-inverse);
}
.cal-event {
  margin-top: 5px;
  padding: 3px 7px;
  border-radius: var(--dd-radius-s);
  background: var(--dd-gray-100);
  font-size: var(--dd-fs-2xs);
  font-weight: 520;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-event.tint { background: var(--dd-blue-wash); color: var(--dd-blue); }

/* No weekend tint and no today wash: Google Calendar marks today with the date
   circle alone and treats weekends like any other day, which keeps the grid
   calm. --dd-weekend-tint was removed with this rule. */

/* ---------- mail (dot column is scoped to .mail-list — never global) ---------- */

.mail-row-unread .row-title { font-weight: 650; }
.mail-list .mail-row-unread::before {
  content: ""; width: 7px; height: 7px; flex: none;
  border-radius: 50%; background: var(--dd-blue);
}
.mail-list li:not(.mail-row-unread)::before { content: ""; width: 7px; flex: none; }

/* ---------- app cards ---------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--dd-grid-gap);
}
.app-card {
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  padding: 18px var(--dd-pad-x) 16px;
  display: flex; flex-direction: column; gap: 10px;
  transition: border-color var(--dd-dur) var(--dd-ease);
}
.app-card:hover { border-color: var(--dd-gray-300); }
.app-card-head { display: flex; align-items: center; gap: 10px; }
.app-name { font-size: var(--dd-fs-md); font-weight: 620; letter-spacing: -.01em; }
.app-host {
  font-size: var(--dd-fs-xs);
  color: var(--dd-ink-quiet);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-meta { display: flex; gap: 6px; flex-wrap: wrap; }
.app-actions { margin-top: auto; padding-top: 6px; display: flex; gap: 8px; }
.app-actions .btn { height: 30px; padding: 0 13px; font-size: var(--dd-fs-xs); }

/* ---------- VPS metric strip (flat band, no boxes-in-boxes) ---------- */

.vps-strip {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
}
.vps-cell {
  padding: 14px var(--dd-pad-x) 16px;
  border-right: 1px solid var(--dd-hairline);
  /* each cell opens its own metric history, so it is a real button */
  background: transparent; border-top: 0; border-bottom: 0; border-left: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: background var(--dd-dur) var(--dd-ease);
}
.vps-cell:hover { background: var(--dd-gray-50); }
.vps-cell:last-child { border-right: 0; }
.vps-value {
  font-size: 1.5rem;
  font-weight: 620;
  letter-spacing: -.02em;
  margin-top: 7px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.vps-value small { font-size: var(--dd-fs-sm-); font-weight: 500; color: var(--dd-ink-quiet); }
.vps-sub { font-size: var(--dd-fs-xs); color: var(--dd-ink-quiet); margin-top: 6px; }
.vps-meter {
  margin-top: 10px;
  height: 4px;
  border-radius: 2px;
  background: var(--dd-gray-100);
  overflow: hidden;
}
.vps-meter i {
  display: block; height: 100%; width: 0;
  border-radius: 2px;
  background: var(--dd-ink);
  transition: width 400ms var(--dd-ease);
}
.vps-meter i.warn { background: var(--dd-amber); }

@media (max-width: 900px) {
  .vps-strip { grid-template-columns: 1fr 1fr; }
  .vps-cell:nth-child(2n) { border-right: 0; }
  .vps-cell { border-bottom: 1px solid var(--dd-hairline); }
  .vps-cell:nth-last-child(-n+2) { border-bottom: 0; }
  /* an odd trailing cell takes the full row instead of dangling half-width */
  .vps-cell:last-child:nth-child(odd) { grid-column: 1 / -1; border-right: 0; }
}

/* ---------- breakdown rows (label + proportional bar + value) ----------
   The standard way to show parts-of-a-whole: categories, storage, budgets.
   Never a pie chart. */

.breakdown { padding: 6px var(--dd-pad-x) 16px; }
.breakdown-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding: 9px 0;
}
.breakdown-row + .breakdown-row { border-top: 1px solid var(--dd-hairline); }
.breakdown-label { font-size: var(--dd-fs-sm); }
.breakdown-value {
  font-size: var(--dd-fs-sm); font-weight: 550;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.breakdown-bar {
  grid-column: 1 / -1;
  height: 4px; border-radius: 2px;
  background: var(--dd-gray-100);
  overflow: hidden;
}
.breakdown-bar i {
  display: block; height: 100%; border-radius: 2px;
  background: var(--dd-ink);
}

/* ---------- empty / placeholder ---------- */

.empty-state {
  padding: 40px var(--dd-pad-x);
  text-align: center;
  color: var(--dd-ink-quiet);
  font-size: var(--dd-fs-sm-);
}

.chart-placeholder { padding: 14px var(--dd-pad-x) 6px; }
.chart-placeholder svg { width: 100%; height: 180px; display: block; }
.chart-axis {
  display: flex; justify-content: space-between;
  padding: 8px var(--dd-pad-x) 0;
}
.chart-footer {
  padding: 10px var(--dd-pad-x) 14px;
  border-top: 1px solid var(--dd-hairline);
  margin-top: 10px;
  display: flex; align-items: center; gap: 10px;
}

/* ---------- toasts ---------- */

.toast-stack {
  position: fixed; bottom: 24px; left: 50%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  z-index: 100;
  pointer-events: none;
}
.toast.toast-action {
  display: flex; align-items: center; gap: 14px;
  pointer-events: auto;
  padding-right: 8px;
}
.toast-action button {
  border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: var(--dd-fs-sm-); font-weight: 650;
  color: var(--dd-on-inverse);
  text-decoration: underline; text-underline-offset: 2px;
  padding: 4px 8px; border-radius: var(--dd-radius-full);
}
.toast-action button:hover { background: color-mix(in srgb, var(--dd-on-inverse) 16%, transparent); }
.toast {
  background: var(--dd-inverse);
  color: var(--dd-on-inverse);
  border-radius: var(--dd-radius-full);
  padding: 9px 18px;
  font-size: var(--dd-fs-sm-);
  font-weight: 550;
  box-shadow: var(--dd-shadow-pop);
  animation: dd-pop-in var(--dd-dur) var(--dd-ease);
}
@keyframes dd-pop-in { from { opacity: 0; transform: translateY(6px); } }

/* ---------- dialogs (native <dialog class="dd-modal">) ---------- */

dialog.dd-modal {
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  background: var(--dd-panel);
  color: var(--dd-ink);
  padding: 0;
  width: min(480px, calc(100vw - 32px));
  box-shadow: var(--dd-shadow-dialog);
  animation: dd-pop-in var(--dd-dur) var(--dd-ease);
}
dialog.dd-modal.wide { width: min(680px, calc(100vw - 32px)); }
dialog.dd-modal::backdrop { background: var(--dd-backdrop); }
.modal-title {
  margin: 0;
  padding: 20px var(--dd-pad-x) 0;
  font-size: 1.375rem;
  font-weight: 620;
  letter-spacing: -.02em;
  line-height: 1.25;
}
.modal-sub {
  padding: 8px var(--dd-pad-x) 0;
  font-size: var(--dd-fs-sm-);
  color: var(--dd-ink-quiet);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.modal-sub .seg { margin-left: auto; }
.modal-body { padding: 12px var(--dd-pad-x) 18px; color: var(--dd-ink-light); }
.modal-body > p { margin: 0; }
.detail-empty { font-size: var(--dd-fs-sm-); color: var(--dd-ink-quiet); }

/* label/value rows inside a detail dialog */
.detail-list { margin: 8px 0 0; }
.detail-list > div {
  display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  padding: 10px 0;
  border-top: 1px solid var(--dd-hairline);
  font-size: var(--dd-fs-sm);
}
.detail-list > div:first-child { border-top: 0; }
.detail-list dt { color: var(--dd-ink-quiet); flex: none; }
.detail-list dd {
  margin: 0; text-align: right; font-weight: 520; color: var(--dd-ink);
  font-variant-numeric: tabular-nums; min-width: 0; overflow-wrap: anywhere;
}

/* rows/cards that open a detail dialog (class added by app.js) */
.clickable { cursor: pointer; }
.modal-footer {
  display: flex; gap: 8px; justify-content: flex-end;
  padding: 14px var(--dd-pad-x);
  border-top: 1px solid var(--dd-hairline);
}
.modal-footer .danger { margin-right: auto; color: var(--dd-red); }

/* ---------- responsive ---------- */

@media (max-width: 1100px) {
  .panel-columns { grid-template-columns: 1fr !important; }
  .card-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 900px) {
  /* The rail's WIDTH is not set here. It used to be pinned to 64px at this
     breakpoint, which silently outranked :root.rail-collapsed and turned the
     collapse control into a dead button — clicking it toggled the class while
     the width never moved. The rail is now driven by that one class in every
     case, and shared/app.js picks the initial state from the viewport, so a
     narrow window still opens collapsed but the user can always override it. */
  /* Same lesson as the rail, one control over: narrow the subbar's DEFAULT, never
     its used width. `body[data-app] .subbar { width: 184px }` used to live here
     and outranked the clamp on `--dd-subbar-w`, so below this width the resize
     handle was a dead control — you could drag it and nothing moved. Lowering the
     token instead keeps the floor (`flex: none` means the subbar never yields, and
     .app-content is overflow:hidden, so an unyielding subbar clips the work
     surface with no scrollbar to recover it) while an explicit drag still wins:
     the drag writes --dd-subbar-w inline on <html>, which beats this. */
  :root { --dd-subbar-w: 184px; }
  main { padding: 24px 20px 32px; }
}
@media (max-width: 640px) {
  .stats-row { grid-template-columns: 1fr 1fr; }
  .card-grid { grid-template-columns: 1fr; }
  .view-title { font-size: 1.55rem; }
  .cal-cell { min-height: 56px; }
  .cal-event { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}


/* ============================================================
   App pages — full-viewport standalone apps (tasks, calendar,
   mail). body[data-app] opts a page into this layout; the page
   manages its own scrolling, the document never scrolls.
   ============================================================ */

body[data-app] main {
  padding: 0;
  height: 100vh;
  display: flex;
  overflow: hidden;
  background: var(--dd-canvas);
}
/* The app's panes are inset cards floating on the canvas — they never touch the
   window edges or each other. Edge-to-edge panes read as enterprise software;
   a little air and a radius make the app feel like an app. */
.app {
  flex: 1; min-width: 0;
  display: flex;
  overflow: hidden;
  gap: var(--dd-app-gap);
  padding: var(--dd-app-gap);
}

/* ---------- secondary sidebar (per-app) ---------- */

.subbar {
  width: clamp(184px, var(--dd-subbar-w), 340px);
  flex: none;
  display: flex; flex-direction: column;
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  overflow-y: auto;
  padding: 12px 10px 14px;
  gap: 2px;
  transition: width var(--dd-dur-slow) var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease),
              opacity var(--dd-dur) var(--dd-ease);
}
/* The app's identity, where the topbar used to be: icon + page name, with the
   rest of the row free for controls a module wants to add later. */
.pane-brand {
  display: flex; align-items: center; gap: 9px;
  padding: 4px 8px 14px;
  font-size: var(--dd-fs-md);
  font-weight: 620;
  letter-spacing: -.01em;
}
.pane-brand svg { width: 18px; height: 18px; flex: none; stroke-width: 1.75; }

/* ---- one create block for every app ----
   Calendar had `.subbar-create`, Mail had `.mail-compose-slot`, and Tasks had
   nothing. Measured, the two buttons came out 202px and 214px wide and sat 6px
   apart horizontally — neither aligned with the nav rows beneath them. The block
   is defined ONCE here, its button fills the subbar's content box exactly like
   every row below it, and an app supplies only the label and the icon. */
.subbar-create { padding: 2px 0 12px; }
.subbar-create .btn { width: 100%; justify-content: center; }

/* ---- one section heading for every app ----
   `.subbar-head` is the heading-with-a-trailing-action variant (Mail's Labels +).
   Both share the label's padding, so a heading sits in the same place whether or
   not it carries a button. 16px of air above, because 10px read as cramped after
   a list — which is why two apps had each hand-tuned their own value (14 and 24)
   and no two headings in the suite lined up. */
.subbar .side-label,
.subbar-head { padding: 16px 8px 6px; }
.subbar .side-label:first-child,
.subbar-head:first-child { padding-top: 0; }
.subbar-head { display: flex; align-items: center; gap: 6px; }
.subbar-head > .microlabel { flex: 1 1 auto; min-width: 0; }

/* subbar nav rows reuse the rail's anatomy: icon + name + trailing count */
.subbar-nav { display: flex; flex-direction: column; gap: 1px; }
/* Direct children only. A nested wrapper row (label + edit affordance) puts two
   buttons side by side; if both claimed width:100% they would fight over the
   row and the flexible one would collapse to nothing. */
.subbar-nav > a, .subbar-nav > button,
.subbar-nav > * > a, .subbar-nav > * > button {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 8px;
  border: 0; background: transparent; text-align: left;
  border-radius: var(--dd-radius-m);
  text-decoration: none; cursor: pointer;
  font: inherit; font-size: var(--dd-fs-sm); font-weight: 480;
  color: var(--dd-ink-light);
  transition: background var(--dd-dur) var(--dd-ease);
}
.subbar-nav > a, .subbar-nav > button { width: 100%; }
/* inside a wrapper row the primary button takes the leftover space */
.subbar-nav > * > a, .subbar-nav > * > button { flex: 1 1 auto; min-width: 0; }
.subbar-nav > * > button + button,
.subbar-nav > * > a + button { flex: 0 0 auto; }
.subbar-nav a:hover, .subbar-nav button:hover { background: var(--dd-gray-50); color: var(--dd-ink); }
.subbar-nav .active { background: var(--dd-gray-100); color: var(--dd-ink); font-weight: 600; }
.subbar-nav svg { width: 16px; height: 16px; flex: none; stroke-width: 1.75; }
.subbar-nav .count {
  margin-left: auto;
  font-size: var(--dd-fs-2xs);
  color: var(--dd-ink-quiet);
  font-variant-numeric: tabular-nums;
}
.subbar-foot { margin-top: auto; padding-top: 10px; }

/* drag handle between subbar and content; double-click resets */
.subbar-resizer {
  width: var(--dd-app-gap); flex: none;
  transition: width var(--dd-dur-slow) var(--dd-ease);
  margin: 0 calc(var(--dd-app-gap) * -1);
  cursor: col-resize;
  position: relative; z-index: 5;
}
.subbar-resizer:hover::after, .subbar-resizer.dragging::after {
  content: ""; position: absolute;
  top: 12px; bottom: 12px; left: 50%;
  width: 3px; margin-left: -1.5px;
  border-radius: 2px;
  background: var(--dd-gray-300);
}
/* The subbar collapses by giving up its width so the content pane grows into
   the space smoothly. Border and padding go with it, or a 1px seam and 10px of
   padding survive at width 0. */
body.subbar-collapsed .subbar {
  width: 0; min-width: 0;
  padding-left: 0; padding-right: 0;
  border-width: 0;
  opacity: 0;
}
body.subbar-collapsed .subbar-resizer { width: 0; margin: 0; pointer-events: none; }

/* ---------- app content column ---------- */

/* Every pane in an app is one of these cards. .app-content is the flexible one
   (there is exactly one per app); .app-pane is an additional fixed-or-resizable
   column, e.g. mail's message list sitting between the subbar and the reader.
   An app's panes are siblings of .subbar inside .app, so they all share the
   same gap and radius automatically. */
.app-content,
.app-pane {
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
}
.app-content { flex: 1; min-width: 0; }
.app-pane { flex: none; min-width: 0; }

/* A card's own header strip. Reuses .app-toolbar's shape but sized for a
   narrower column. */
.pane-head {
  min-height: 48px; flex: none;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 14px;
  border-bottom: 1px solid var(--dd-hairline);
}
.app-toolbar {
  min-height: 56px; flex: none;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 20px;
  border-bottom: 1px solid var(--dd-hairline);
  /* A toolbar sizes its contents from ITS OWN width, not the window's: in a split
     view the window is wide while the pane is half of it, so viewport media
     queries are blind and the right-hand cluster overflowed into the pane's
     clipped edge. Containment is deliberately scoped to the toolbar rather than
     the whole pane — `container-type` implies `contain: layout`, which would make
     the container the containing block for FIXED descendants too, and the apps
     hang their editors, popovers and toasts off the viewport. */
  container-type: inline-size;
  container-name: toolbar;
}
/* Whatever an app puts in its toolbar, the groups wrap before they clip. */
@container toolbar (max-width: 560px) {
  .app-toolbar > * { flex-wrap: wrap; }
  .app-toolbar h1 { font-size: var(--dd-fs-md); }
}
/* A segmented control cannot wrap and stay legible as one control, so instead it
   gives up padding — five views still fit, and none of them disappears. */
@container toolbar (max-width: 460px) {
  .seg button { padding: 0 8px; font-size: var(--dd-fs-2xs); letter-spacing: 0; }
}
@container toolbar (max-width: 360px) {
  .seg button { padding: 0 6px; }
}
/* Narrower than its own min-content and the switcher has to fold. It wraps rather
   than dropping views: a control you cannot reach is worse than an unusual shape,
   so the stadium becomes a rounded block for two rows. */
@container toolbar (max-width: 330px) {
  .seg { flex-wrap: wrap; justify-content: center; border-radius: var(--dd-radius-m); }
}
.app-toolbar h1 {
  margin: 0;
  font-size: var(--dd-fs-lg);
  font-weight: 620;
  letter-spacing: -.015em;
}
.app-body { flex: 1; min-height: 0; display: flex; overflow: hidden; }
.app-scroll { overflow-y: auto; min-height: 0; }

/* toolbar icon button that toggles the subbar */
.subbar-toggle { margin-left: -8px; }

/* ---------- segmented control ---------- */

.seg {
  display: inline-flex; align-items: center;
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-full);
  background: var(--dd-panel);
  padding: 2px;
}
.seg button {
  border: 0; background: transparent; cursor: pointer;
  height: 26px; padding: 0 12px;
  border-radius: var(--dd-radius-full);
  font: inherit; font-size: var(--dd-fs-sm-); font-weight: 550;
  color: var(--dd-ink-light);
  transition: background var(--dd-dur) var(--dd-ease), color var(--dd-dur) var(--dd-ease);
}
.seg button:hover { color: var(--dd-ink); }
.seg button.active { background: var(--dd-inverse); color: var(--dd-on-inverse); }

/* ---------- forms (dialogs and inline) ---------- */

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field > label {
  font-size: var(--dd-fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--dd-ink-quiet);
}
.input {
  font: inherit; font-size: var(--dd-fs-sm);
  color: var(--dd-ink);
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-m);
  padding: 8px 12px;
  width: 100%;
  transition: border-color var(--dd-dur) var(--dd-ease);
}
.input:hover { border-color: var(--dd-gray-300); }
.input:focus { border-color: var(--dd-gray-500); outline: none; }
textarea.input { min-height: 72px; resize: vertical; }
select.input { cursor: pointer; }
.input-row { display: flex; gap: 10px; }
.input-row > * { min-width: 0; }
.input-row .grow { flex: 1; }
.switch-row {
  display: flex; align-items: center; gap: 10px;
  font-size: var(--dd-fs-sm); color: var(--dd-ink-light);
  margin-bottom: 14px; cursor: pointer;
}

/* ---------- identity dots & chips ---------- */

.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--id, var(--dd-gray-500)); }
.id-chip {
  --id: var(--dd-gray-500);
  background: color-mix(in srgb, var(--id) 13%, var(--dd-panel));
  color: var(--id);
}

/* Set an element's identity hue by class, since inline style is banned. Any
   element can carry one: .dot, .id-chip, or a container whose children read
   var(--id). */
.id-blue   { --id: var(--dd-id-blue); }
.id-green  { --id: var(--dd-id-green); }
.id-amber  { --id: var(--dd-id-amber); }
.id-violet { --id: var(--dd-id-violet); }
.id-rose   { --id: var(--dd-id-rose); }
.id-teal   { --id: var(--dd-id-teal); }

/* ---------- mini calendar (subbar) ---------- */

.mini-cal { padding: 4px 6px 8px; user-select: none; }
.mini-cal-head {
  display: flex; align-items: center; gap: 4px;
  padding: 2px 2px 8px;
  font-size: var(--dd-fs-sm-); font-weight: 600;
}
.mini-cal-head .spacer { margin-left: auto; }
.mini-cal-head button {
  border: 0; background: transparent; cursor: pointer;
  width: 22px; height: 22px; border-radius: var(--dd-radius-full);
  color: var(--dd-ink-quiet); font: inherit;
  display: grid; place-items: center;
}
.mini-cal-head button:hover { background: var(--dd-gray-100); color: var(--dd-ink); }
.mini-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.mini-cal-grid .dow {
  font-size: var(--dd-fs-3xs); text-align: center;
  color: var(--dd-ink-quiet); padding: 2px 0 4px;
  text-transform: uppercase; letter-spacing: .04em;
}
.mini-cal-grid button {
  border: 0; background: transparent; cursor: pointer;
  aspect-ratio: 1; border-radius: var(--dd-radius-full);
  font: inherit; font-size: var(--dd-fs-2xs);
  color: var(--dd-ink-light);
  font-variant-numeric: tabular-nums;
}
.mini-cal-grid button:hover { background: var(--dd-gray-100); }
.mini-cal-grid button.out { color: var(--dd-gray-500); }
.mini-cal-grid button.today { font-weight: 700; color: var(--dd-ink); }
.mini-cal-grid button.sel { background: var(--dd-inverse); color: var(--dd-on-inverse); }

/* ---------- chart (shared ddChart renderer) ---------- */

.chart { position: relative; }
.chart svg { display: block; width: 100%; }
.chart-legend {
  display: flex; gap: 16px; flex-wrap: wrap;
  padding: 8px 0 2px;
  font-size: var(--dd-fs-xs);
  color: var(--dd-ink-light);
}
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
.chart-legend i {
  width: 14px; height: 2px; border-radius: 1px; flex: none;
  /* the swatch inherits the series colour via inline `color`, so a dashed
     series can reuse currentColor for its gradient */
  background: currentColor;
}
.chart-legend i.dash {
  background: linear-gradient(90deg, currentColor 0 4px, transparent 4px 7px);
  background-size: 7px 2px; background-repeat: repeat-x;
}
.chart-tip {
  position: absolute; pointer-events: none;
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-m);
  box-shadow: var(--dd-shadow-pop);
  padding: 6px 10px;
  font-size: var(--dd-fs-xs);
  white-space: nowrap;
  z-index: 10;
}
.chart-tip b { font-variant-numeric: tabular-nums; }

/* ---------- global rail collapse ----------
   The state class lives on <html>, NOT on <body>: an inline script in every
   page's <head> sets it before first paint. Setting it from shared/app.js (which
   loads at the end of <body>) meant each navigation painted the rail expanded
   and then animated it shut — a flash on every single page load.

   Collapsed geometry, all measured from the rail's 64px:
     64 − 2×8 rail padding      = 48px row box
     icon   16 → padding-x 16   → 24..40, centre 32 ✓
     avatar 22 → padding-x 13   → 21..43, centre 32 ✓
     mark   28 → padding-x 10   → 18..46, centre 32 ✓
   Centring is done with that arithmetic and NOT with `justify-content: center`,
   which cannot work here: the labels keep their flex gaps when they drop to
   max-width 0, and `.sub`'s `margin-left: auto` swallows every pixel of free
   space, so a "centred" row put its icon hard against the left edge — and one
   pixel further left on rows that have a `.sub` than on rows that don't. That
   ragged left edge is the misalignment. Kill the gap, kill the auto margin,
   then pad to centre. */

:root.rail-collapsed .sidebar { width: 64px; padding: 20px 8px 16px; }
:root.rail-collapsed .sidebar .brand-name,
:root.rail-collapsed .sidebar .side-nav a > span:not(.avatar),
:root.rail-collapsed .sidebar .side-nav button > span:not(.avatar),
:root.rail-collapsed .sidebar .side-nav .sub { max-width: 0; opacity: 0; }
:root.rail-collapsed .sidebar .side-search,
:root.rail-collapsed .sidebar .side-label { max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0; }
:root.rail-collapsed .sidebar .side-nav a,
:root.rail-collapsed .sidebar .side-nav button { gap: 0; padding: 10px 16px; }
:root.rail-collapsed .sidebar .side-nav .sub { margin-left: 0; }
:root.rail-collapsed .sidebar .side-nav .side-user { padding-left: 13px; padding-right: 13px; }

/* Collapsed, the rail is 64px of icons and the brand mark sits alone at the
   top. The collapse/expand control does NOT live here any more: hiding it
   behind a hover over the mark made the expand affordance invisible — the
   classic hover-reveal failure. It is an always-visible footer row now
   (.rail-collapse), which also makes touch work for free. */
:root.rail-collapsed .sidebar .side-head { padding: 4px 0 14px; }
:root.rail-collapsed .sidebar .brand { gap: 0; padding: 0 10px; }

/* ============================================================
   Split screen — two modules side by side in one window.
   Drag a module out of the rail and drop it on either half.

   The pane you are already in stays the LIVE DOCUMENT (its scroll
   position, open editors and unsaved state all survive); the module
   you drop arrives as a same-origin iframe with ?embed=1, which
   hides its own rail. That asymmetry is the whole trick: no app had
   to be rewritten to be embeddable, and each one still runs exactly
   as it does standalone — which is also what keeps them separable
   into products later.
   ============================================================ */

/* In split mode every page behaves like an app page: the document
   itself never scrolls, each pane scrolls on its own. */
/* GRID, not flex, and the columns come from a custom property. Two reasons, both
   learned the hard way: flex bases negotiate with the iframe's intrinsic size, so
   a 50/50 split came out 750/442; and a media query cannot override an inline
   `grid-template-columns`, whereas it can always override the fallback of a
   variable the script sets. Same rule as the rail and the subbar — never let a
   breakpoint fight a used value, give it a token to change. */
body.split main {
  height: 100vh;
  padding: 0;
  display: grid;
  grid-template-columns: var(--dd-split-cols, 1fr auto 1fr);
  overflow: hidden;
}
body.split .split-pane {
  min-width: 0;               /* or a long title in either pane sets the floor */
  display: flex;
  overflow: hidden;
  position: relative;
}
/* A dashboard page (hub, finances, apps) keeps its air and scrolls inside its
   own half instead of scrolling the document. */
body.split:not([data-app]) .split-mine {
  display: block;
  overflow: auto;
  padding: 24px var(--dd-app-gap) 32px;
}
/* ---- the seam between the two apps ----
   Each pane carries its own --dd-app-gap of padding, so left alone the two cards
   sit TWO gaps apart — double the distance between the rail and the first card,
   which is the rhythm everything else in the suite follows. So the two edges that
   FACE each other give up half each, and 8px + 8px reads as one 16px gap.
   The live pane knows which edge faces from body[data-split-side] (the guest's
   side); the guest is told directly, via ?edge= on its URL, because it is a
   separate document and cannot see any of this. */
body.split[data-split-side="right"] .split-mine .app { padding-right: calc(var(--dd-app-gap) / 2); }
body.split[data-split-side="left"]  .split-mine .app { padding-left:  calc(var(--dd-app-gap) / 2); }
body.split[data-split-side="right"]:not([data-app]) .split-mine { padding-right: calc(var(--dd-app-gap) / 2); }
body.split[data-split-side="left"]:not([data-app])  .split-mine { padding-left:  calc(var(--dd-app-gap) / 2); }

:root.embed[data-edge="left"]  body[data-app] .app { padding-left:  calc(var(--dd-app-gap) / 2); }
:root.embed[data-edge="right"] body[data-app] .app { padding-right: calc(var(--dd-app-gap) / 2); }
:root.embed[data-edge="left"]  body:not([data-app]) main { padding-left:  calc(var(--dd-app-gap) / 2); }
:root.embed[data-edge="right"] body:not([data-app]) main { padding-right: calc(var(--dd-app-gap) / 2); }

body.split .split-guest-frame {
  width: 100%; height: 100%;
  border: 0; display: block;
  background: transparent;
  color-scheme: normal;       /* the frame inherits our scheme, not the UA's */
}

/* The divider owns the split: it resizes it, and it carries the two controls
   that end or flip it. Always visible, never hover-revealed — the same rule the
   rail's collapse row learned. It sits in the air between two inset cards, so it
   needs no background of its own. */
/* Zero width: the two panes already hold `--dd-app-gap` of their own padding, so
   the divider lives in that air rather than adding more. Its hit area and its
   controls overhang it, the way .subbar-resizer does. */
.split-div {
  width: 0;
  position: relative;
  z-index: 6;
  display: flex;
  justify-content: center;
}
.split-div::before {                    /* the grab area — the whole gap */
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: calc(var(--dd-app-gap) / -2); right: calc(var(--dd-app-gap) / -2);
  cursor: col-resize;
}
/* The grabber, macOS Split View's: a short rounded bar at the MIDDLE of the
   divider, always visible, so the seam says "drag me" without being hovered
   first. It is the divider's only content — the swap and close buttons used to
   sit here and made every press near them ambiguous with the resize. */
.split-div::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 4px; height: 40px;
  margin: -20px 0 0 -2px;
  border-radius: var(--dd-radius-full);
  background: var(--dd-gray-300);
  transition: background var(--dd-dur) var(--dd-ease),
              height var(--dd-dur-slow) var(--dd-ease);
}
.split-div:hover::after, .split-div.dragging::after {
  background: var(--dd-gray-500);
  height: 56px;
}

/* ---- the drag ----
   A module being dragged out of the rail follows the cursor as a chip, and the
   two halves of the work area light up as targets. The half under the cursor
   shows the module's name where it will actually land, at the size it will
   actually be — the answer to "where would this go" is the shape itself, not a
   line of copy explaining it. */
.split-ghost {
  position: fixed;
  z-index: 90;
  pointer-events: none;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px;
  border-radius: var(--dd-radius-m);
  background: var(--dd-panel);
  border: 1px solid var(--dd-gray-300);
  box-shadow: var(--dd-shadow-pop);
  font-size: var(--dd-fs-sm);
  font-weight: 560;
  color: var(--dd-ink);
  transform: translate(-50%, -50%);
}
.split-ghost svg { width: 15px; height: 15px; stroke-width: 1.75; flex: none; }

.split-zones {
  position: fixed;
  z-index: 80;
  display: flex;
  gap: var(--dd-app-gap);
  padding: var(--dd-app-gap);
  /* the zones cover the work area only — the rail stays live so the cursor can
     travel back to it to cancel */
}
.split-zone {
  flex: 1;
  border-radius: var(--dd-radius-l);
  border: 2px dashed var(--dd-gray-300);
  background: color-mix(in srgb, var(--dd-canvas) 55%, transparent);
  /* the label sits high rather than dead centre: the ghost chip is pinned to the
     cursor, and a centred label ends up underneath it */
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: clamp(56px, 14vh, 140px);
  transition: border-color var(--dd-dur) var(--dd-ease),
              background var(--dd-dur) var(--dd-ease),
              flex-grow var(--dd-dur-slow) var(--dd-ease);
}
/* The hot half does NOT grow. It is exactly the half the app will occupy, and a
   target that swells past that promises a pane bigger than the one you get. */
.split-zone.hot {
  border-style: solid;
  border-color: var(--dd-ink);
  background: color-mix(in srgb, var(--dd-panel) 88%, transparent);
}

/* A live preview of the app being dropped, laid over the hot half at the exact
   size and position it will land in. Fixed rather than parented into the zone:
   re-parenting an iframe reloads it, so moving between sides would blank the app
   on every pass. */
.split-preview {
  position: fixed;
  z-index: 81;
  border: 0;
  border-radius: calc(var(--dd-radius-l) - 2px);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dd-dur) var(--dd-ease);
}
.split-preview.on { opacity: 1; }
/* Named while the preview loads, and legible on top of it once it has: a pill,
   not bare text, because it now sits over live app content. */
.split-zone-label {
  position: relative; z-index: 82;
  display: flex; align-items: center; gap: 7px;
  padding: 7px 13px;
  border-radius: var(--dd-radius-full);
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  box-shadow: var(--dd-shadow-pop);
  color: var(--dd-ink);
  font-size: var(--dd-fs-sm);
  font-weight: 560;
  opacity: 0;
  transition: opacity var(--dd-dur) var(--dd-ease);
}
.split-zone.hot .split-zone-label { opacity: 1; }
.split-zone-label svg { width: 16px; height: 16px; stroke-width: 1.75; }

/* A rail row must never be dragged as a LINK: WebKit needs this property (it has
   no equivalent to draggable=false for its own link-drag heuristics), and the
   labels are navigation, so losing text selection on them costs nothing. */
.side-nav a { -webkit-user-drag: none; user-select: none; }

/* The row being dragged reads as picked-up, and text selection must not start
   anywhere while a drag is live. */
body.split-dragging { user-select: none; }
body.split-dragging .sidebar .side-nav a.split-source { background: var(--dd-gray-100); opacity: .55; }

/* An embedded page has no rail of its own — the host's rail is the only one. */
:root.embed .sidebar { display: none; }
:root.embed .shell { min-height: 0; }

/* ---- the live pane's own width ----
   split.js mirrors the live pane's width onto <html> as data-pane, because that
   pane is narrow inside a WIDE window and every viewport breakpoint is blind to
   it. (The guest needs none of this: an iframe's window is its pane, so its own
   media queries are already right — which is exactly why the guest fitted
   perfectly while the host clipped.) These thresholds match the viewport ones. */
:root[data-pane="narrow"] { --dd-subbar-w: 184px; }

/* Tighter than this, an app's own sidebar cannot coexist with its work surface:
   at a 360px pane a 240px subbar left the calendar a 40px toolbar. So the subbar
   yields entirely — forced by width, not by the user's toggle, and it returns the
   moment the pane grows. Mirrors body.subbar-collapsed so the two look identical
   rather than merely similar. */
:root[data-pane="tight"] .subbar {
  width: 0; min-width: 0;
  padding-left: 0; padding-right: 0;
  border-width: 0;
  opacity: 0;
}
:root[data-pane="tight"] .subbar-resizer { width: 0; margin: 0; pointer-events: none; }

/* A dashboard page in a narrow pane needs the same treatment its own breakpoints
   would give it in a narrow window — two-column panels in a 600px pane truncated
   row titles to a single letter. These mirror the 1100px and 640px viewport rules
   exactly, so a pane and a window of the same width look the same. */
:root[data-pane="narrow"] .panel-columns,
:root[data-pane="tight"] .panel-columns { grid-template-columns: 1fr !important; }
:root[data-pane="narrow"] .card-grid { grid-template-columns: 1fr 1fr; }
:root[data-pane="tight"] .stats-row { grid-template-columns: 1fr 1fr; }
:root[data-pane="tight"] .card-grid { grid-template-columns: 1fr; }
:root[data-pane="tight"] .view-title { font-size: 1.55rem; }

/* Below this width two panes cannot both be useful, so the split collapses back
   to one. The state is kept, not discarded: widen the window and it returns. */
@media (max-width: 720px) {
  body.split main { grid-template-columns: 1fr; }
  body.split .split-pane.is-guest, body.split .split-div { display: none; }
  /* one pane again, so the halved seam edge goes back to a full gap */
  body.split .split-mine .app { padding: var(--dd-app-gap); }
  body.split:not([data-app]) .split-mine { padding: 24px var(--dd-app-gap) 32px; }
}

/* ---------- quick add: one natural-language field, every app ----------
   Calendar had this as a wide grey pill whose placeholder was a fake entry
   ("Lunch with Tom tomorrow 12:30") — it read as content rather than as an
   affordance, and it was too long for the pane, so it arrived pre-truncated.
   The field now NAMES what it does, and the example moves to a hint that appears
   while you are actually typing, where it can afford to be specific.
   The sparkle is the promise that this understands language, not syntax. */

.quick-add { position: relative; padding: 0 0 12px; }
.quick-add > svg {
  position: absolute; left: 10px; top: 9px;
  width: 15px; height: 15px;
  stroke-width: 1.6;
  color: var(--dd-ink-quiet);
  pointer-events: none;
}
.quick-add input {
  width: 100%; height: 33px;
  padding: 0 11px 0 31px;
  font: inherit;
  font-size: var(--dd-fs-sm-);
  color: var(--dd-ink);
  /* canvas on panel: one step down, so it reads as a place to type without the
     heavy grey fill the old pill used */
  background: var(--dd-canvas);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-m);
  transition: border-color var(--dd-dur) var(--dd-ease),
              background var(--dd-dur) var(--dd-ease);
}
.quick-add input::placeholder { color: var(--dd-ink-quiet); }
.quick-add:hover input { border-color: var(--dd-gray-300); }
/* focus keeps the ONE shared outline — this only warms the field underneath it */
.quick-add input:focus { background: var(--dd-panel); }
.quick-add.busy input { color: var(--dd-ink-quiet); }

/* The example, revealed only while the field has focus: at rest it would be one
   more line of grey text in a pane that already carries two headings. */
.quick-hint {
  margin: 0;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  padding: 0 2px;
  font-size: var(--dd-fs-2xs);
  line-height: 1.45;
  color: var(--dd-ink-quiet);
  transition: max-height var(--dd-dur-slow) var(--dd-ease),
              opacity var(--dd-dur) var(--dd-ease),
              padding var(--dd-dur-slow) var(--dd-ease);
}
.quick-add:focus-within .quick-hint {
  max-height: 48px;
  opacity: 1;
  padding: 6px 2px 0;
}

/* ---------- sign in ----------
   The suite's own chrome, minus the chrome: one panel card on the canvas, the
   brand mark it opens with everywhere else, and exactly two fields. Nothing here
   is a new visual idea — .input, .btn-primary, .microlabel and the tokens are the
   same ones the dashboard is built from, which is the whole point. */

.login-body { min-height: 100vh; }
.login-wrap {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: 24px;
}
.login-card {
  width: 100%; max-width: 344px;
  display: flex; flex-direction: column;
  background: var(--dd-panel);
  border: 1px solid var(--dd-border);
  border-radius: var(--dd-radius-l);
  padding: 28px 26px 26px;
  box-shadow: var(--dd-shadow-pop);
  /* lifted slightly above dead centre: optically centred beats measured */
  margin-bottom: 6vh;
}
.login-mark { width: 34px; height: 34px; border-radius: 10px; font-size: var(--dd-fs-md); }
.login-title {
  margin: 16px 0 0;
  font-size: var(--dd-fs-lg);
  font-weight: 650;
  letter-spacing: -.015em;
}
.login-title span { color: var(--dd-ink-quiet); font-weight: 450; }
.login-sub {
  margin: 5px 0 22px;
  font-size: var(--dd-fs-sm);
  color: var(--dd-ink-quiet);
}
.login-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.login-field .microlabel { padding-left: 1px; }
.login-error {
  margin: 0 0 14px;
  font-size: var(--dd-fs-sm-);
  color: var(--dd-red);
}
.login-submit { width: 100%; justify-content: center; height: 38px; margin-top: 2px; }

/* A wrong password should be felt, not just read. Small and once — and skipped
   entirely for anyone who asked for less motion. */
@keyframes dd-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}
.login-card.shake { animation: dd-shake 400ms var(--dd-ease); }
@media (prefers-reduced-motion: reduce) {
  .login-card.shake { animation: none; }
}

/* ============================================================
   Navigation feedback — skeletons + the top progress bar.
   This is a traditional multi-page suite (a click is a real
   navigation, not a client-side route swap), so the gap between
   click and next paint is real network+render time. app.js names
   the philosophy directly: no spinners — "a spinner says wait and
   nothing else, a shape says what for." Two pieces:
     1. .dd-nav-progress — fires the instant a link is clicked, so
        the click itself feels acknowledged.
     2. .dd-skel* — sized to the real component each stands in for,
        so its arrival doesn't reflow whatever loaded around it.
   ============================================================ */

/* ---------- skeletons ---------- */

/* The shimmering surface itself. Kept separate from .dd-skel-line so a later
   placeholder shape (an avatar, a thumbnail) gets the same tint + sweep just
   by adding this class alongside its own sizing — radius-s matches the Shape
   foundation's "small chips/tags" step, which is the closest existing shape
   family to a placeholder bar. */
.dd-skel {
  position: relative;
  overflow: hidden;              /* clips the sweep to the bar's own bounds */
  border-radius: var(--dd-radius-s);
  background: var(--dd-gray-100);  /* the resting tint — also IS the reduced-
                                       motion appearance, see below */
}
/* One sweep, built only from the gray ramp: gray-200 as a faint leading/
   trailing shade either side of a gray-50 peak, transparent at both ends so
   it fades into the gray-100 base rather than showing a hard edge — a hard
   edge is what would make this read as a loading bar instead of a surface
   with light passing over it. Percent stops keep the lit band narrow (a wide
   one would look like the tint just changed, not like something swept
   across it). */
.dd-skel::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(90deg,
    transparent,
    var(--dd-gray-200) 40%,
    var(--dd-gray-50) 50%,
    var(--dd-gray-200) 60%,
    transparent);
  transform: translateX(-100%);
  /* 1.8s: this is an ambient loop, not a state change, so neither --dd-dur
     (160ms) nor --dd-dur-slow (220ms) apply — those time chrome reacting to
     an action. The curve is still the system's one easing, never a second
     hand-rolled one. */
  animation: dd-shimmer 1.8s var(--dd-ease) infinite;
}
@keyframes dd-shimmer { to { transform: translateX(100%); } }

/* One placeholder line of body text. --dd-fs-sm (body) rendered inside a
   single-line .row-list li computes to 22px: the li's own min-height (46px)
   minus its own top+bottom padding (12px + 12px, from ".row-list li{padding:
   12px var(--dd-pad-x)}"). That's the box a real line of body text already
   occupies in this codebase, so it's the box its placeholder needs too. */
.dd-skel-line {
  display: block;
  height: 22px;
}
.dd-skel-line.w30  { width: 30%; }
.dd-skel-line.w40  { width: 40%; }
.dd-skel-line.w60  { width: 60%; }
.dd-skel-line.w80  { width: 80%; }
.dd-skel-line.w100 { width: 100%; }

/* One placeholder list row. Copies ".row-list > li" (min-height: 46px) and
   ".row-list li" (padding: 12px var(--dd-pad-x); border-bottom: 1px solid
   var(--dd-hairline)) directly, so a row of real data dropped in its place
   costs the list no height. Its 1-2 lines stack with the same 2px gap
   ".row-sub" already uses as margin-top under a title line — the one gap
   this codebase uses between two adjacent lines of text. */
.dd-skel-row {
  display: flex; flex-direction: column; justify-content: center;
  gap: 2px;
  min-height: 46px;
  padding: 12px var(--dd-pad-x);
  border-bottom: 1px solid var(--dd-hairline);
}
.dd-skel-row:last-child { border-bottom: 0; }

/* Vertical container for either bare lines or whole rows. flex:1 mirrors
   ".row-list{flex:1}" so a placeholder fills a stretched panel the same way
   the list it precedes will, instead of the panel shrinking now and growing
   once data lands. Zero gap matches ".row-list" too (it sets none — adjacent
   rows touch and rely on their own border-bottom), which is exactly right
   when the stack holds .dd-skel-row (each already carries that border). When
   it instead holds bare lines (kind "lines" has no such border), give those
   the same 2px rhythm as above so they read as distinct lines and not one
   solid tint. */
.dd-skel-stack {
  display: flex; flex-direction: column;
  flex: 1;
  gap: 0;
}
.dd-skel-stack > .dd-skel-line:not(:last-child) { margin-bottom: 2px; }

/* Placeholder for a whole app pane: a header line, then rows. The header
   line stands in for ".pane-head" (min-height: 48px; padding: 6px 14px) —
   the strip a narrow app-pane column opens with. A bare line has no wrapper
   to pad the way pane-head pads a title, so the same box is rebuilt with
   margin instead: 14px keeps pane-head's own horizontal inset; 13px + 12px +
   the line's own 1px border-bottom add up, with the line's 22px (see
   .dd-skel-line above), to pane-head's exact 48px — so the line it replaces
   costs the rows beneath it no shift either. */
.dd-skel-pane {
  display: flex; flex-direction: column;
  height: 100%;
}
.dd-skel-pane > .dd-skel-line:first-child {
  flex: none;
  margin: 13px 14px 12px;
  border-bottom: 1px solid var(--dd-hairline);
}

/* ---------- top progress bar ---------- */

/* Fixed to the very top of the viewport, above every other fixed layer in
   this file — split screen's ghost/zones/preview/label sit at 6, 80-82 and
   90, and the toast stack (this file's previous highest) sits at 100; 200
   clears all of it with room left over, since a navigation can start while
   any of those are open. Driven by transform, not width: a scaleX transform
   never triggers layout on every tick the way animating width would. */
.dd-nav-progress {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 2px;
  z-index: 200;
  background: var(--dd-ink);        /* the one brand accent this system has */
  transform-origin: left;
  transform: scaleX(0);              /* at rest: zero width, invisible */
  transition: transform 900ms var(--dd-ease);
  pointer-events: none;
}
/* .9, never 1: reaching 100% would claim the navigation is complete, which
   would be a lie the bar can't back up — the incoming document replaces it
   before it's ever true. --dd-ease is reused rather than a new curve: it is
   already front-loaded (cubic-bezier(.3,.7,.3,1) is most of the way to its
   end by 30% of the duration), which is what reads as "moves immediately,
   then slows" here without inventing a second easing for one component. */
body.dd-navigating .dd-nav-progress { transform: scaleX(.9); }

/* Redundant with the file's global `* { transition: none; animation: none }`
   kill switch above, same as .login-card.shake's own block — stated here so
   the reduced-motion behaviour for this section reads as deliberate rather
   than incidental. With the transition off, .dd-navigating's scaleX(.9) is
   applied instantly instead of eased into ("the bar simply appears at a
   fixed width"). With the animation off, .dd-skel::after sits at its
   un-animated transform (translateX(-100%): fully outside the clipped
   surface), leaving only .dd-skel's own gray-100 fill visible — a flat tint,
   with no extra rule needed to produce it. */
@media (prefers-reduced-motion: reduce) {
  .dd-nav-progress { transition: none; }
  .dd-skel::after { animation: none; }
}

/* ---------- skeleton variants: one shape per real component ----------
   The base .dd-skel-row is modelled on `.row-list li`, which is right for the hub
   and wrong for all three apps — their panes are not built from that component.
   An audit of the first pass caught it: a placeholder whose proportions differ
   from the thing that replaces it reads as a reflow on arrival, which is the one
   job it had. Each variant below is measured from the rule it stands in for. */

/* Tasks — `.t-line`: min-height 24px, padding 7px 8px, no divider, rounded hover */
.dd-skel-stack.compact .dd-skel-row {
  min-height: 24px;
  padding: 7px 8px;
  border-bottom: 0;
}
.dd-skel-stack.compact .dd-skel-line { height: 15px; }

/* Mail — `.mail-list li.mail-row`: padding 9px 10px, a 1px reserved border and
   radius-m, rows separated by a gap instead of a hairline */
.dd-skel-stack.pills { gap: 2px; padding: 0 8px; }
.dd-skel-stack.pills .dd-skel-row {
  min-height: 0;
  padding: 9px 10px;
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: var(--dd-radius-m);
}

/* Calendar — a time grid has no rows, so columns are the only honest stand-in:
   a header strip over evenly divided day columns. */
/* flex: 1 and width: 100% are both load-bearing: this lands inside `.app-body`,
   which is a ROW flex container, so without them the placeholder shrink-wraps its
   own content — measured 36px wide with 0px columns. */
.dd-skel-grid {
  flex: 1; width: 100%; min-width: 0;
  display: flex; flex-direction: column;
  height: 100%; padding: 10px 12px; gap: 10px;
}
.dd-skel-grid > .dd-skel-line { height: 34px; }
.dd-skel-cols { flex: 1; display: flex; gap: 6px; min-height: 0; }
.dd-skel-col { flex: 1; height: 100%; border-radius: var(--dd-radius-s); }

/* ---------- page transitions: the swap, softened ----------
   Navigation between modules is a real document swap, and once the caching work
   made warm loads land in ~30ms the loading UI became the problem rather than the
   cure: a progress bar and a skeleton each appearing and vanishing inside a tenth
   of a second reads as a flicker. Three fixes, in order of how much they matter:

   1. The browser cross-fades the two documents itself. `navigation: auto` opts
      every same-origin navigation in; the old page stays on screen until the new
      one is ready, then they dissolve. No blank frame, nothing to jitter.
   2. The blur lives ONLY here, on the transition snapshots. Blurring real
      elements would create a containing block mid-animation and make the
      calendar's sticky headers jump — trading one jitter for another. A snapshot
      has no layout to disturb.
   3. Loading UI is DELAYED rather than sped up: see SKEL_DELAY in shared/app.js.
      A placeholder that appears for 40ms taught nobody anything. */

@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: dd-fade-out 140ms var(--dd-ease) both;
}
::view-transition-new(root) {
  animation: dd-fade-in 220ms var(--dd-ease) both;
}
@keyframes dd-fade-out {
  to { opacity: 0; filter: blur(5px); }
}
@keyframes dd-fade-in {
  from { opacity: 0; filter: blur(6px); }
  to { opacity: 1; filter: blur(0); }
}

/* Content arriving inside a page fades on OPACITY ONLY — deliberately no blur.
   `filter` on a live element makes it a containing block for fixed and sticky
   descendants, which is exactly the jitter this section removes. */
@keyframes dd-content-in { from { opacity: 0; } to { opacity: 1; } }
.dd-enter { animation: dd-content-in 200ms var(--dd-ease) both; }

/* The placeholder itself arrives softly too, or the delay just moves the flash
   150ms later. */
.dd-skel-stack[data-skel], .dd-skel-pane[data-skel], .dd-skel-grid[data-skel] {
  animation: dd-content-in 160ms var(--dd-ease) both;
}

/* The bar fades in, and only after the delay in shared/app.js has elapsed, so a
   fast navigation never paints it at all. */
.dd-nav-progress { opacity: 0; transition: opacity 120ms var(--dd-ease), transform .9s var(--dd-ease); }
body.dd-navigating .dd-nav-progress { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  .dd-enter, .dd-skel-stack[data-skel], .dd-skel-pane[data-skel], .dd-skel-grid[data-skel] { animation: none; }
}
