/* ============================================================= App Shell Template — pure HTML/CSS, no JavaScript ------------------------------------------------------------- Architecture - Design tokens via light-dark() so theme follows system by default and can be forced Light / Dark via radio + :has(). - Everything is a reusable "block": .nav-item, .filter, .chip, .badge, .btn, .menu, .table — combine freely. ============================================================= */ /* ---------- 1. TOKENS ---------------------------------------- Two raw palettes (--l-* light, --d-* dark) are defined once. A small mapping (--bg: var(--l-bg) ...) picks the active set. Theme resolution (no JS): • default + @media(dark) → follows the OS (Auto) • html:has(#theme-light) → force light • html:has(#theme-dark) → force dark We avoid light-dark()/color-scheme for app colors because their resolution is unreliable across engines; :has() variable swaps are rock-solid. color-scheme is still set as a hint for native widgets (date picker, scrollbars, selects). ------------------------------------------------------------- */ :root { /* ---- raw LIGHT palette ---- */ --l-bg:#f3f3f4; --l-surface:#ffffff; --l-surface-2:#f6f6f7; --l-surface-3:#fbfbfc; --l-border:#e5e5e8; --l-border-2:#d6d6da; --l-text:#18181b; --l-text-muted:#6b6b73; --l-text-faint:#9a9aa2; --l-accent:oklch(0.52 0.12 256); --l-accent-bg:oklch(0.96 0.03 256); --l-accent-bd:oklch(0.82 0.07 256); --l-focus:oklch(0.52 0.14 256); --l-pos:oklch(0.54 0.13 150); --l-pos-bg:oklch(0.95 0.03 150); --l-pos-bd:oklch(0.83 0.07 150); --l-neg:oklch(0.55 0.18 25); --l-neg-bg:oklch(0.96 0.03 25); --l-neg-bd:oklch(0.85 0.08 25); --l-warn:oklch(0.55 0.12 75); --l-warn-bg:oklch(0.96 0.04 85); --l-warn-bd:oklch(0.84 0.08 85); --l-info:oklch(0.54 0.12 245);--l-info-bg:oklch(0.96 0.03 245);--l-info-bd:oklch(0.83 0.07 245); /* ---- raw DARK palette ---- */ --d-bg:#0d0d0f; --d-surface:#161619; --d-surface-2:#1d1d22; --d-surface-3:#1a1a1e; --d-border:#2a2a31; --d-border-2:#3a3a43; --d-text:#ededf0; --d-text-muted:#9b9ba6; --d-text-faint:#6a6a74; --d-accent:oklch(0.70 0.12 256); --d-accent-bg:oklch(0.32 0.06 256); --d-accent-bd:oklch(0.48 0.08 256); --d-focus:oklch(0.74 0.13 256); --d-pos:oklch(0.74 0.14 150); --d-pos-bg:oklch(0.30 0.05 150); --d-pos-bd:oklch(0.45 0.07 150); --d-neg:oklch(0.74 0.16 25); --d-neg-bg:oklch(0.30 0.07 25); --d-neg-bd:oklch(0.47 0.10 25); --d-warn:oklch(0.80 0.13 85); --d-warn-bg:oklch(0.31 0.05 80); --d-warn-bd:oklch(0.48 0.07 80); --d-info:oklch(0.74 0.12 245);--d-info-bg:oklch(0.31 0.06 245);--d-info-bd:oklch(0.47 0.08 245); /* ---- active mapping: default = LIGHT ---- */ --bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3); --border:var(--l-border); --border-2:var(--l-border-2); --text:var(--l-text); --text-muted:var(--l-text-muted); --text-faint:var(--l-text-faint); --accent:var(--l-accent); --accent-bg:var(--l-accent-bg); --accent-bd:var(--l-accent-bd); --focus:var(--l-focus); --pos:var(--l-pos); --pos-bg:var(--l-pos-bg); --pos-bd:var(--l-pos-bd); --neg:var(--l-neg); --neg-bg:var(--l-neg-bg); --neg-bd:var(--l-neg-bd); --warn:var(--l-warn); --warn-bg:var(--l-warn-bg); --warn-bd:var(--l-warn-bd); --info:var(--l-info); --info-bg:var(--l-info-bg); --info-bd:var(--l-info-bd); color-scheme: light; /* layout + density (compact) */ --radius: 5px; --nav-w: 264px; --row-h: 34px; --pad-x: 12px; --fz: 13px; --fz-sm: 12px; --fz-xs: 11px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size: var(--fz); line-height: 1.45; } /* shared DARK mapping, applied to Auto-via-OS and forced-dark */ @media (prefers-color-scheme: dark) { :root:not(:has(#theme-light:checked)):not(:has(#theme-dark:checked)) { --bg:var(--d-bg); --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3); --border:var(--d-border); --border-2:var(--d-border-2); --text:var(--d-text); --text-muted:var(--d-text-muted); --text-faint:var(--d-text-faint); --accent:var(--d-accent); --accent-bg:var(--d-accent-bg); --accent-bd:var(--d-accent-bd); --focus:var(--d-focus); --pos:var(--d-pos); --pos-bg:var(--d-pos-bg); --pos-bd:var(--d-pos-bd); --neg:var(--d-neg); --neg-bg:var(--d-neg-bg); --neg-bd:var(--d-neg-bd); --warn:var(--d-warn); --warn-bg:var(--d-warn-bg); --warn-bd:var(--d-warn-bd); --info:var(--d-info); --info-bg:var(--d-info-bg); --info-bd:var(--d-info-bd); color-scheme: dark; } } /* forced DARK (overrides everything) */ html:has(#theme-dark:checked) { --bg:var(--d-bg); --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3); --border:var(--d-border); --border-2:var(--d-border-2); --text:var(--d-text); --text-muted:var(--d-text-muted); --text-faint:var(--d-text-faint); --accent:var(--d-accent); --accent-bg:var(--d-accent-bg); --accent-bd:var(--d-accent-bd); --focus:var(--d-focus); --pos:var(--d-pos); --pos-bg:var(--d-pos-bg); --pos-bd:var(--d-pos-bd); --neg:var(--d-neg); --neg-bg:var(--d-neg-bg); --neg-bd:var(--d-neg-bd); --warn:var(--d-warn); --warn-bg:var(--d-warn-bg); --warn-bd:var(--d-warn-bd); --info:var(--d-info); --info-bg:var(--d-info-bg); --info-bd:var(--d-info-bd); color-scheme: dark; } /* forced LIGHT (overrides the OS-dark media query) */ html:has(#theme-light:checked) { --bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3); --border:var(--l-border); --border-2:var(--l-border-2); --text:var(--l-text); --text-muted:var(--l-text-muted); --text-faint:var(--l-text-faint); --accent:var(--l-accent); --accent-bg:var(--l-accent-bg); --accent-bd:var(--l-accent-bd); --focus:var(--l-focus); --pos:var(--l-pos); --pos-bg:var(--l-pos-bg); --pos-bd:var(--l-pos-bd); --neg:var(--l-neg); --neg-bg:var(--l-neg-bg); --neg-bd:var(--l-neg-bd); --warn:var(--l-warn); --warn-bg:var(--l-warn-bg); --warn-bd:var(--l-warn-bd); --info:var(--l-info); --info-bg:var(--l-info-bg); --info-bd:var(--l-info-bd); color-scheme: light; } /* ---------- 2. RESET ---------------------------------------- */ *, *::before, *::after { box-sizing: border-box; } html, body { height: 100%; } body { margin: 0; background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; } button { font: inherit; color: inherit; } ul { list-style: none; margin: 0; padding: 0; } a { color: inherit; text-decoration: none; } summary::-webkit-details-marker { display: none; } summary { list-style: none; cursor: pointer; } :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 3px; } @media (prefers-reduced-motion: no-preference) { .sidebar, .scrim, summary, .nav-item, .btn, .chip { transition: .15s ease; } } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* tiny icon helper */ .ico { width: 16px; height: 16px; flex: 0 0 auto; stroke: currentColor; stroke-width: 1.75; fill: none; stroke-linecap: round; stroke-linejoin: round; } .ico-sm { width: 14px; height: 14px; } /* ---------- 3. APP GRID ------------------------------------- */ .app { display: grid; grid-template-columns: var(--nav-w) minmax(0, 1fr); height: 100dvh; overflow: hidden; } /* ---------- 4. SIDEBAR -------------------------------------- */ .sidebar { grid-column: 1; display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-right: 1px solid var(--border); } .brand { display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 14px; flex: 0 0 auto; border-bottom: 1px solid var(--border); } .brand-mark { width: 22px; height: 22px; border-radius: 5px; flex: 0 0 auto; background: var(--accent); display: grid; place-items: center; color: #fff; } .brand-mark .ico { stroke: #fff; } .brand-name { font-weight: 650; letter-spacing: -.01em; } .brand-sub { color: var(--text-faint); font-size: var(--fz-xs); margin-left: auto; } /* nav scroll region */ .nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 8px; } /* ============================================================ UNIFIED NAV NODE — ONE component for every menu item. A node is a row: [ toggle | spacer ] + [ label ] • HEADER = the node HAS children → renders the chevron toggle. Childless nodes render a spacer so labels stay aligned. • CLICKABLE = label is → navigates on click. STATIC = label is → not a navigation target. So "header" and "clickable" are independent: any combination works. Children sit in a sibling