/*!
 * open-dashboard v1.2.0
 * Framework-agnostic dashboard layout + component kit. No build step.
 * All classes are prefixed `od-`, all CSS variables `--od-`, all data
 * attributes `data-od-*`, all custom DOM events `od:*`.
 */

/* ============================== 1. Design tokens ============================== */
:root {
    --od-bg: #F6F7F9;
    --od-panel: #FFFFFF;
    --od-border: #E6E8EB;
    --od-border-light: #F1F2F5;
    --od-text: #111723;
    --od-text-secondary: #5B6472;
    --od-text-muted: #8A93A2;
    --od-text-muted-2: #9AA3B1;

    --od-accent: #2A6FDB;
    --od-accent-hover: #205BBD;
    --od-accent-bg: #EAF1FC;
    /* A separate token from --od-accent because --od-accent itself is tuned
       for borders/buttons/backgrounds, not for reading as text: on a white
       panel it only just clears WCAG AA (~4.8:1), and on a dark panel it
       falls under the 4.5:1 minimum entirely (~3.9:1). Use this token (not
       --od-accent) wherever the accent color paints readable text, e.g. an
       active tab or an open accordion summary — --od-accent-hover gives a
       comfortable ~6.4:1 on white, and the dark-theme block below overrides
       this to a lighter blue for the same margin on a dark panel. */
    --od-accent-text: var(--od-accent-hover);

    --od-success: #1F8A5B;
    --od-success-bg: #E8F4EE;
    --od-danger: #D14343;
    --od-danger-bg: #FBEBEB;
    --od-warning: #9A6700;
    --od-warning-bg: #FFF6E5;
    /* Same reasoning as --od-accent-text above: --od-success/-danger/-warning
       are tuned as borders/icons/backgrounds, not as text sitting directly on
       their own translucent -bg tint — on a dark panel that pairing (e.g.
       .od-pill-success, .od-alert-danger) drops under ~3.5:1. Use these
       wherever the color paints readable text on the matching -bg; the
       dark-theme blocks below brighten each to a comfortable margin. */
    --od-success-text: var(--od-success);
    --od-danger-text: var(--od-danger);
    --od-warning-text: var(--od-warning);

    --od-dark: #11161F;
    --od-dark-text: #DFE6F0;
    --od-dark-border: #2B3340;

    --od-row-hover: #F4F8FF;
    --od-nav-hover: #F2F4F7;

    /* Modal/Drawer backdrop. In light mode a dark, fairly translucent
       overlay reads as "dimmed" because it pulls both the light page
       background AND any bright colored content (chart bars, pills) toward
       the same dark shade. In dark mode the page is already close to this
       color, so at the same opacity the plain background barely changes
       while colorful content still shows through at close to full strength
       — a ghostly "echo" of shapes behind the modal. The dark-theme blocks
       below use a near-black, higher-opacity overlay instead, which mutes
       colorful content enough to actually read as dimmed. */
    --od-overlay: rgba(17, 22, 31, .45);

    --od-radius-sm: 6px;
    --od-radius: 8px;
    --od-radius-lg: 12px;

    --od-font-sans: 'Inter', 'Manrope', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --od-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    --od-sidebar-width: 252px;
    --od-topbar-height: 58px;

    /* Tells the browser which native palette to paint chrome it draws itself
       (scrollbars, form controls, etc.) with - without this, that chrome
       stays light-themed even under a dark data-od-theme, since it isn't
       covered by any of the --od-* custom properties above. */
    color-scheme: light;
}

/* Dark theme — three states, resolved in this order:
   1. No preference either way: light tokens above (the :root default).
   2. OS/browser prefers dark AND nothing has opted into light explicitly:
      applied automatically via the media query below.
   3. `data-od-theme` set explicitly (by OpenDashboard.Theme, see od-theme.js,
      or by hand): wins over both — "light" forces light even in a dark OS,
      "dark" forces dark even in a light OS. */
@media (prefers-color-scheme: dark) {
    :root:not([data-od-theme="light"]) {
        --od-bg: #0E1116;
        --od-panel: #161B22;
        --od-border: #262C36;
        --od-border-light: #1D222B;
        --od-text: #E7EBF1;
        --od-text-secondary: #A6AFBC;
        --od-text-muted: #757F8C;
        --od-text-muted-2: #8A93A2;

        --od-accent-bg: rgba(42, 111, 219, .16);
        --od-accent-text: #58A6FF;
        --od-success-bg: rgba(31, 138, 91, .16);
        --od-success-text: #4ADE9A;
        --od-danger-bg: rgba(209, 67, 67, .16);
        --od-danger-text: #FF7B72;
        --od-warning-bg: rgba(154, 103, 0, .18);
        --od-warning-text: #F2C94C;

        --od-row-hover: rgba(42, 111, 219, .08);
        --od-nav-hover: rgba(255, 255, 255, .05);
        --od-overlay: rgba(2, 4, 8, .72);
        color-scheme: dark;
    }
}
[data-od-theme="dark"] {
    --od-bg: #0E1116;
    --od-panel: #161B22;
    --od-border: #262C36;
    --od-border-light: #1D222B;
    --od-text: #E7EBF1;
    --od-text-secondary: #A6AFBC;
    --od-text-muted: #757F8C;
    --od-text-muted-2: #8A93A2;

    --od-accent-bg: rgba(42, 111, 219, .16);
    --od-accent-text: #58A6FF;
    --od-success-bg: rgba(31, 138, 91, .16);
    --od-success-text: #4ADE9A;
    --od-danger-bg: rgba(209, 67, 67, .16);
    --od-danger-text: #FF7B72;
    --od-warning-bg: rgba(154, 103, 0, .18);
    --od-warning-text: #F2C94C;

    --od-row-hover: rgba(42, 111, 219, .08);
    --od-nav-hover: rgba(255, 255, 255, .05);
    --od-overlay: rgba(2, 4, 8, .72);
    color-scheme: dark;
}

/* ============================== 2. Reset & base ============================== */
/* Scoped to our own component roots (never the bare `*` selector) so this
   never changes box-sizing for the host page's own, unrelated elements —
   listed explicitly because .od-modal/.od-dropdown/.od-toast-region are
   commonly mounted outside .od-shell (e.g. appended straight to <body>). */
.od-shell, .od-panel, .od-stat-grid, .od-modal, .od-drawer, .od-dropdown, .od-toast-region, .od-accordion, .od-tabs, [data-od-table], .od-alert, .od-switch, .od-checkbox, .od-radio, .od-segmented, .od-guest-shell, .od-command-palette, .od-context-menu,
.od-shell *, .od-panel *, .od-stat-grid *, .od-modal *, .od-drawer *, .od-dropdown *, .od-toast-region *, .od-accordion *, .od-tabs *, [data-od-table] *, .od-alert *, .od-switch *, .od-checkbox *, .od-radio *, .od-segmented *, .od-guest-shell *, .od-command-palette *, .od-context-menu * {
    box-sizing: border-box;
}
.od-shell { margin: 0; }
/* A <ul>/<ol> a consumer writes into a panel/modal/etc. body (prose, a doc
   list, a numbered how-to) otherwise keeps the browser's default ~40px
   left padding, which then STACKS with any margin/padding the consumer
   adds themselves for their own indent — scoped the same way as the
   box-sizing reset above, so a host page's own unrelated lists elsewhere
   are never touched. */
:where(.od-shell, .od-panel, .od-stat-grid, .od-modal, .od-drawer, .od-dropdown, .od-toast-region, .od-accordion, .od-tabs, [data-od-table], .od-alert, .od-switch, .od-checkbox, .od-radio, .od-segmented, .od-guest-shell, .od-command-palette, .od-context-menu) :is(ul, ol) {
    margin: 0;
    padding: 0;
}
.od-sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; }
.od-mono { font-family: var(--od-font-mono); }
.od-muted { color: var(--od-text-muted); font-size: 13px; }

.od-shell, .od-guest-shell { color: inherit; }
/* :where() keeps the descendant `a` at (0,0,1) specificity - a plain element
   selector, not "class + element". Without it (a bare `.od-shell a`, (0,1,1))
   this reset outranks any single-class color rule further down the cascade
   that happens to land on an `<a>` (.od-btn, .od-nav-link, .od-dropdown-item,
   .od-account-link, ... all (0,1,0)) - e.g. `.od-btn { color: #fff }` lost to
   this, so an `<a class="od-btn">` rendered near-black text on its blue
   background in light mode instead of white. */
:where(.od-shell, .od-guest-shell, .od-modal, .od-drawer, .od-dropdown, .od-command-palette, .od-context-menu) a { color: inherit; text-decoration: none; }
.od-shell h1, .od-guest-shell h1, .od-modal h1, .od-drawer h1 { font-size: 22px; font-weight: 800; letter-spacing: -.4px; margin: 0; }
.od-shell h2, .od-guest-shell h2, .od-modal h2, .od-drawer h2 { font-size: 14.5px; font-weight: 800; margin: 0; }
/* Typography/color are inherited properties, so this also reaches every
   descendant — but .od-modal/.od-drawer/.od-dropdown/.od-toast-region are
   commonly mounted as siblings of .od-shell (not inside it), so each root
   needs its own copy rather than relying on inheritance from .od-shell. */
.od-shell, .od-modal, .od-drawer, .od-dropdown, .od-toast-region, .od-alert, .od-switch, .od-checkbox, .od-radio, .od-segmented, .od-guest-shell, .od-command-palette, .od-context-menu {
    font-family: var(--od-font-sans); -webkit-font-smoothing: antialiased; color: var(--od-text);
}
.od-shell, .od-guest-shell { background: var(--od-bg); }
/* .od-shell/.od-guest-shell fill the viewport exactly (100dvh), but the
   underlying <body>/<html> stay their default (usually white) — visible
   for a moment during elastic overscroll (trackpad/mobile "bounce" past
   the end of a scrollable area). Only applies when one of our shells is
   actually on the page, so this never paints a background for a host page
   that doesn't use open-dashboard's layout at all. */
html:has(.od-shell), html:has(.od-guest-shell),
body:has(.od-shell), body:has(.od-guest-shell) {
    background: var(--od-bg);
}
/* The browser's default <body> margin (usually 8px) otherwise leaves a
   sliver of the host page's own background showing around the shell, and
   on .od-guest-shell it's enough on its own to push the page into
   scrolling even though the visible content fits. Same :has() scoping as
   the background rule above — never touches a host page's <body> unless
   one of our shells is actually on it. */
body:has(.od-shell), body:has(.od-guest-shell) {
    margin: 0;
}
/* .od-shell manages its own internal scrolling — the sidebar/topbar stay
   fixed while .od-main scrolls — so the outer document must never scroll
   on top of that (some content lengths otherwise leave the page itself
   scrollable by a second, redundant amount past .od-shell's own 100dvh
   box, effectively two independent scrollbars for the same content).
   .od-guest-shell only sets a min-height, so its page is meant to scroll
   normally and is deliberately excluded here. */
html:has(.od-shell), body:has(.od-shell) {
    height: 100%;
    overflow: hidden;
}
/* Scrollbar - same scoping as the box-sizing reset above (never a bare `*`).
   Without this, native scrollbars stay light-themed even under a dark
   data-od-theme (see the color-scheme tokens above), and --od-border alone
   is too pale in light mode to read as a scrollbar - it's tuned for 1px
   hairlines, not something you need to see/grab. */
.od-shell, .od-panel, .od-stat-grid, .od-modal, .od-drawer, .od-dropdown, .od-toast-region, .od-accordion, .od-tabs, [data-od-table], .od-alert, .od-switch, .od-checkbox, .od-radio, .od-segmented, .od-guest-shell, .od-command-palette, .od-context-menu,
.od-shell *, .od-panel *, .od-stat-grid *, .od-modal *, .od-drawer *, .od-dropdown *, .od-toast-region *, .od-accordion *, .od-tabs *, [data-od-table] *, .od-alert *, .od-switch *, .od-checkbox *, .od-radio *, .od-segmented *, .od-guest-shell *, .od-command-palette *, .od-context-menu * {
    scrollbar-color: var(--od-text-muted-2) transparent; /* Firefox */
}
.od-shell ::-webkit-scrollbar, .od-panel ::-webkit-scrollbar, .od-stat-grid ::-webkit-scrollbar, .od-modal ::-webkit-scrollbar, .od-drawer ::-webkit-scrollbar, .od-dropdown ::-webkit-scrollbar, .od-toast-region ::-webkit-scrollbar, .od-accordion ::-webkit-scrollbar, .od-tabs ::-webkit-scrollbar, [data-od-table] ::-webkit-scrollbar, .od-alert ::-webkit-scrollbar, .od-switch ::-webkit-scrollbar, .od-checkbox ::-webkit-scrollbar, .od-radio ::-webkit-scrollbar, .od-segmented ::-webkit-scrollbar, .od-guest-shell ::-webkit-scrollbar, .od-command-palette ::-webkit-scrollbar, .od-context-menu ::-webkit-scrollbar {
    width: 10px; height: 10px;
}
.od-shell ::-webkit-scrollbar-track, .od-panel ::-webkit-scrollbar-track, .od-stat-grid ::-webkit-scrollbar-track, .od-modal ::-webkit-scrollbar-track, .od-drawer ::-webkit-scrollbar-track, .od-dropdown ::-webkit-scrollbar-track, .od-toast-region ::-webkit-scrollbar-track, .od-accordion ::-webkit-scrollbar-track, .od-tabs ::-webkit-scrollbar-track, [data-od-table] ::-webkit-scrollbar-track, .od-alert ::-webkit-scrollbar-track, .od-switch ::-webkit-scrollbar-track, .od-checkbox ::-webkit-scrollbar-track, .od-radio ::-webkit-scrollbar-track, .od-segmented ::-webkit-scrollbar-track, .od-guest-shell ::-webkit-scrollbar-track, .od-command-palette ::-webkit-scrollbar-track, .od-context-menu ::-webkit-scrollbar-track {
    background: transparent;
}
.od-shell ::-webkit-scrollbar-thumb, .od-panel ::-webkit-scrollbar-thumb, .od-stat-grid ::-webkit-scrollbar-thumb, .od-modal ::-webkit-scrollbar-thumb, .od-drawer ::-webkit-scrollbar-thumb, .od-dropdown ::-webkit-scrollbar-thumb, .od-toast-region ::-webkit-scrollbar-thumb, .od-accordion ::-webkit-scrollbar-thumb, .od-tabs ::-webkit-scrollbar-thumb, [data-od-table] ::-webkit-scrollbar-thumb, .od-alert ::-webkit-scrollbar-thumb, .od-switch ::-webkit-scrollbar-thumb, .od-checkbox ::-webkit-scrollbar-thumb, .od-radio ::-webkit-scrollbar-thumb, .od-segmented ::-webkit-scrollbar-thumb, .od-guest-shell ::-webkit-scrollbar-thumb, .od-command-palette ::-webkit-scrollbar-thumb, .od-context-menu ::-webkit-scrollbar-thumb {
    background: var(--od-text-muted-2); background-clip: padding-box; border-radius: 999px; border: 2px solid var(--od-panel);
}
.od-shell ::-webkit-scrollbar-thumb:hover, .od-panel ::-webkit-scrollbar-thumb:hover, .od-stat-grid ::-webkit-scrollbar-thumb:hover, .od-modal ::-webkit-scrollbar-thumb:hover, .od-drawer ::-webkit-scrollbar-thumb:hover, .od-dropdown ::-webkit-scrollbar-thumb:hover, .od-toast-region ::-webkit-scrollbar-thumb:hover, .od-accordion ::-webkit-scrollbar-thumb:hover, .od-tabs ::-webkit-scrollbar-thumb:hover, [data-od-table] ::-webkit-scrollbar-thumb:hover, .od-alert ::-webkit-scrollbar-thumb:hover, .od-switch ::-webkit-scrollbar-thumb:hover, .od-checkbox ::-webkit-scrollbar-thumb:hover, .od-radio ::-webkit-scrollbar-thumb:hover, .od-segmented ::-webkit-scrollbar-thumb:hover, .od-guest-shell ::-webkit-scrollbar-thumb:hover, .od-command-palette ::-webkit-scrollbar-thumb:hover, .od-context-menu ::-webkit-scrollbar-thumb:hover {
    background: var(--od-text-secondary); background-clip: padding-box;
}

/* ============================== 3. Layout shell ============================== */
.od-shell { display: flex; height: 100vh; height: 100dvh; width: 100%; overflow: hidden; }

.od-sidebar-toggle { display: none; }

.od-sidebar {
    width: var(--od-sidebar-width); flex-shrink: 0; background: var(--od-panel);
    border-right: 1px solid var(--od-border); display: flex; flex-direction: column;
    min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}
.od-sidebar-burger, .od-sidebar-overlay { display: none; }

.od-brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 16px; }
.od-brand-mark { width: 30px; height: 30px; border-radius: var(--od-radius); background: var(--od-text); color: var(--od-panel); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 16px; flex-shrink: 0; }
.od-brand-name { font-weight: 800; font-size: 15.5px; }

.od-nav-group { padding: 6px 12px; }
.od-section-label { font-size: 10.5px; font-weight: 700; letter-spacing: .8px; color: var(--od-text-muted-2); text-transform: uppercase; padding: 8px 8px 6px; }
.od-nav-list { display: flex; flex-direction: column; gap: 2px; }
.od-nav-link { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: var(--od-radius); color: var(--od-text-secondary); font-weight: 600; font-size: 13.5px; cursor: pointer; }
.od-nav-link:hover { background: var(--od-nav-hover); }
.od-nav-link.is-active { background: var(--od-nav-hover); color: var(--od-text); }
.od-nav-link.is-disabled { color: var(--od-text-muted-2); cursor: default; }
.od-nav-icon { width: 24px; height: 24px; border-radius: var(--od-radius-sm); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; flex-shrink: 0; }
.od-nav-count { font-size: 11.5px; font-weight: 700; color: var(--od-text-secondary); background: var(--od-border-light); padding: 1px 7px; border-radius: 999px; margin-left: auto; }

.od-sidebar-footer { display: flex; align-items: center; gap: 4px; margin-top: auto; padding: 14px 16px; border-top: 1px solid var(--od-border-light); }
.od-avatar { width: 28px; height: 28px; border-radius: 999px; background: var(--od-accent-bg); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; color: var(--od-accent-text); flex-shrink: 0; }
.od-account-link { flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: var(--od-radius); }
.od-account-link:hover { background: var(--od-nav-hover); }
.od-account-link.is-active { background: var(--od-accent-bg); }
.od-account-link > div:last-child { min-width: 0; }
.od-account-name { font-size: 12.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-account-role { font-size: 10.5px; color: var(--od-text-muted-2); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.od-main-col { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.od-topbar { height: var(--od-topbar-height); flex-shrink: 0; background: var(--od-panel); border-bottom: 1px solid var(--od-border); display: flex; align-items: center; padding: 0 24px; gap: 16px; }
.od-topbar-title { font-size: 15px; font-weight: 700; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-main { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 26px 30px 40px; }

/* Off-canvas sidebar on small screens — CSS-only checkbox toggle, no JS. */
@media (max-width: 820px) {
    .od-sidebar-burger { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; margin-right: 6px; border-radius: var(--od-radius); cursor: pointer; color: var(--od-text-secondary); font-size: 18px; flex-shrink: 0; }
    .od-sidebar-burger:hover { background: var(--od-nav-hover); color: var(--od-text); }
    .od-sidebar { position: fixed; top: 0; left: 0; height: 100dvh; z-index: 45; transform: translateX(-100%); transition: transform .22s ease; box-shadow: 6px 0 30px rgba(17, 22, 31, .16); }
    .od-sidebar-toggle:checked ~ .od-sidebar { transform: translateX(0); }
    .od-sidebar-toggle:checked ~ .od-sidebar-overlay { display: block; position: fixed; inset: 0; background: rgba(17, 22, 31, .35); z-index: 44; }
    .od-main { padding: 18px 16px 32px; }
    .od-topbar { padding: 0 14px; }
    .od-panel-header { align-items: flex-start; }
}
/* Opt out of .od-main's own padding/scroll for a page that manages its own
   internal scrolling instead (e.g. a Split view, section 77, filling the
   column edge-to-edge with a Composer, section 79, pinned at the bottom) —
   a separate modifier rather than changing .od-main itself so every other
   page's default padded, single-scrollbar behavior stays untouched. Placed
   after the mobile media query above (same specificity as its `.od-main`
   padding override) so flush wins at every viewport width, not just desktop. */
.od-main--flush { display: flex; flex-direction: column; padding: 0; overflow: hidden; }
/* The flush, edge-to-edge, internally-scrolled layout assumes there's a
   viewport tall enough to give every fixed chrome above it (topbar, a
   toolbar row) its normal height and still leave real room underneath — on
   a narrow phone that chrome commonly wraps onto several lines and eats
   most of the viewport, so instead of the split's own panes getting a fair
   share of what's left, .od-main--flush's overflow:hidden just clips
   whatever didn't fit. Past the same 820px breakpoint as the rest of the
   mobile overrides above, fall back to plain page scrolling instead — see
   the matching Split view mobile override (section 77) that lets its panes
   size to their own content rather than fight for a fixed height. */
@media (max-width: 820px) {
    .od-main--flush { overflow-y: auto; }
}

/* ============================== 4. Panel ============================== */
.od-panel { position: relative; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); margin-bottom: 20px; }
.od-panel-body { padding: 18px; }
.od-panel-header { padding: 13px 18px; border-bottom: 1px solid var(--od-border-light); font-size: 13px; font-weight: 800; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 12px; }
.od-panel-header > div { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ============================== 5. Stat grid ============================== */
.od-stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 20px; }
.od-stat-tile { background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); padding: 15px 16px; }
.od-stat-label { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 700; letter-spacing: .4px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-stat-label .od-i { font-size: 13px; }
.od-stat-value { font-size: 27px; font-weight: 800; letter-spacing: -.5px; margin-top: 6px; }
.od-stat-sub { font-size: 12px; color: var(--od-text-muted); font-weight: 600; margin-top: 2px; }

/* ============================== 6. Pill / badge ============================== */
.od-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 700; padding: 3px 10px; border-radius: 999px; white-space: nowrap; }
.od-pill-dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
.od-pill-success { color: var(--od-success-text); background: var(--od-success-bg); }
.od-pill-neutral { color: var(--od-text-secondary); background: var(--od-border-light); }
.od-pill-danger { color: var(--od-danger-text); background: var(--od-danger-bg); }
.od-pill-warning { color: var(--od-warning-text); background: var(--od-warning-bg); }
.od-pill-accent { color: var(--od-accent-text); background: var(--od-accent-bg); }

/* ============================== 7. Buttons ============================== */
.od-btn, .od-btn-danger, .od-btn-ghost, .od-btn-neutral {
    display: inline-flex; align-items: center; justify-content: center; gap: 6px; margin: 0;
    font-family: inherit; font-weight: 700; font-size: 13px; line-height: 1.2; white-space: nowrap;
    padding: 8px 14px; border: 1px solid transparent; border-radius: var(--od-radius);
    cursor: pointer; text-decoration: none; background: none; color: inherit;
}
.od-btn { background: var(--od-accent); color: #fff; }
.od-btn:hover { background: var(--od-accent-hover); }
.od-btn-danger { background: transparent; color: var(--od-danger); border-color: color-mix(in srgb, var(--od-danger) 30%, transparent); }
.od-btn-danger:hover { background: var(--od-danger-bg); }
.od-btn-ghost { background: var(--od-panel); border-color: var(--od-border); color: var(--od-text-secondary); }
.od-btn-ghost:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-btn-neutral { background: transparent; color: var(--od-text-secondary); border-color: var(--od-border); }
.od-btn-neutral:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-btn-sm { padding: 4px 10px; font-size: 12px; }
.od-btn-icon { width: 32px; height: 32px; padding: 0; flex-shrink: 0; }

/* ============================== 8. Form fields (CSS only, no validation logic) ============================== */
.od-field { margin-bottom: 14px; }
/* :not() exclusions so a Checkbox/Radio/Switch (a <label> in its own right,
   see sections 20/25) dropped directly inside .od-field - a completely
   natural thing to do, e.g. <div class="od-field"><label class="od-checkbox">...
   - keeps its own `display: inline-flex`. Without this, `.od-field > label`'s
   `display: block` wins on specificity ((0,1,1) beats .od-checkbox's (0,1,0)),
   which un-blockifies its inner .od-checkbox-box/.od-radio-box/.od-switch-track
   span back to inline, so `width`/`height` stop applying and the box collapses
   to just its border - the control silently degrades to a bare native
   checkbox/radio with no visible custom box. */
.od-field > label:not(.od-checkbox):not(.od-radio):not(.od-switch) { display: block; font-size: 12.5px; font-weight: 700; color: var(--od-text-secondary); margin-bottom: 5px; }
.od-input, .od-select, .od-textarea {
    width: 100%; font-family: inherit; font-size: 13.5px; padding: 8px 11px;
    border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); color: var(--od-text);
}
.od-input:focus, .od-select:focus, .od-textarea:focus { outline: none; border-color: var(--od-accent); }
.od-checkbox-row { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; color: var(--od-text-secondary); }
.od-field-error { color: var(--od-danger); font-size: 12px; font-weight: 600; margin-top: 5px; }
.od-field.has-error .od-input, .od-field.has-error .od-select, .od-field.has-error .od-textarea { border-color: var(--od-danger); }
.od-banner { padding: 10px 14px; border-radius: var(--od-radius); font-size: 13.5px; font-weight: 600; margin-bottom: 16px; }
.od-banner-error { background: var(--od-danger-bg); color: var(--od-danger-text); }
.od-banner-success { background: var(--od-success-bg); color: var(--od-success-text); }

/* ============================== 9. Table ============================== */
/* [data-od-table] clips its own corners (rather than relying on a parent
   .od-panel's overflow) so that a flush toolbar + table can sit directly
   inside a panel without clipping unrelated content (e.g. a dropdown menu)
   placed elsewhere in that same panel. */
[data-od-table] { border-radius: var(--od-radius-lg); overflow: hidden; }
.od-table-wrap { overflow-x: auto; }
.od-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.od-table th { text-align: left; font-size: 11px; font-weight: 700; letter-spacing: .5px; color: var(--od-text-muted-2); text-transform: uppercase; padding: 11px 16px; background: var(--od-border-light); border-bottom: 1px solid var(--od-border); }
.od-table td { padding: 12px 16px; border-bottom: 1px solid var(--od-border-light); vertical-align: middle; }
.od-table tbody tr:hover { background: var(--od-row-hover); }
.od-table tbody tr:last-child td { border-bottom: none; }
.od-table th[data-od-sort] { cursor: pointer; user-select: none; }
.od-table th[data-od-sort] .od-sort-icon { display: inline-block; margin-left: 4px; opacity: .4; font-size: 10px; }
.od-table th[data-od-sort].is-sorted .od-sort-icon { opacity: 1; color: var(--od-accent); }
.od-table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 16px; border-bottom: 1px solid var(--od-border-light); }
.od-table-search { position: relative; display: flex; align-items: center; min-width: 220px; }
.od-table-search input { width: 100%; font-family: inherit; font-size: 13px; padding: 7px 11px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); color: var(--od-text); }
.od-table-search input:focus { outline: none; border-color: var(--od-accent); }
.od-table-empty { padding: 32px 16px; text-align: center; color: var(--od-text-muted); font-size: 13px; }
.od-table-footer { display: flex; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--od-border-light); }
.od-pagination { display: inline-flex; gap: 4px; }
.od-pagination-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 30px; height: 30px; padding: 0 8px; border: 1px solid var(--od-border); border-radius: var(--od-radius); font: inherit; font-size: 13px; font-weight: 700; color: var(--od-text-secondary); background: var(--od-panel); cursor: pointer; }
.od-pagination-btn:hover:not(:disabled) { background: var(--od-nav-hover); }
.od-pagination-btn.is-active { background: var(--od-accent); border-color: var(--od-accent); color: #fff; }
.od-pagination-btn.is-active:hover { background: var(--od-accent-hover); border-color: var(--od-accent-hover); }
.od-pagination-btn:disabled { opacity: .45; cursor: not-allowed; }

/* ============================== 10. Tabs ============================== */
.od-tabs { min-width: 0; }
.od-tabs-nav { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--od-border); margin-bottom: 20px; }
.od-tab-button { appearance: none; display: inline-flex; align-items: center; gap: 7px; padding: 10px 16px; border: 0; border-bottom: 2px solid transparent; margin: 0 0 -1px; background: transparent; font: inherit; font-size: 13px; font-weight: 700; color: var(--od-text-muted); cursor: pointer; user-select: none; }
.od-tab-button:hover { color: var(--od-text); background: var(--od-nav-hover); }
.od-tab-button:focus-visible { outline: 2px solid var(--od-accent); outline-offset: -2px; border-radius: 5px 5px 0 0; }
.od-tab-button.is-active, .od-tab-button[aria-selected="true"] { color: var(--od-accent-text); border-bottom-color: var(--od-accent); }
.od-tab-button:disabled { opacity: .5; cursor: not-allowed; }
.od-tab-count { font-size: 11px; font-weight: 700; color: var(--od-text-secondary); background: var(--od-border-light); border-radius: 999px; padding: 1px 7px; }
.od-tab-panel { display: none; }
.od-tab-panel.is-active { display: block; }
.od-tabs:not(.od-tabs-initialized) .od-tab-panel:first-child { display: block; }

/* ============================== 11. Accordion / collapse (native <details>) ============================== */
.od-accordion { border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); overflow: hidden; }
.od-accordion-item { border-bottom: 1px solid var(--od-border-light); }
.od-accordion-item:last-child { border-bottom: 0; }
.od-accordion-item > summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 16px; cursor: pointer; list-style: none; font-weight: 800; font-size: 13.5px; color: var(--od-text); outline: none; }
.od-accordion-item > summary::-webkit-details-marker { display: none; }
.od-accordion-item > summary::after { content: "\002B"; font-size: 18px; font-weight: 400; color: var(--od-text-muted); transition: transform .15s ease; }
.od-accordion-item[open] > summary::after { transform: rotate(45deg); }
.od-accordion-item:hover > summary { background: var(--od-nav-hover); }
.od-accordion-item[open] > summary { color: var(--od-accent-text); }
.od-accordion-body { margin-top: 16px; padding: 0 16px 16px; color: var(--od-text-secondary); font-size: 13.5px; line-height: 1.6; }

/* ============================== 12. Dropdown ============================== */
.od-dropdown { position: relative; display: inline-block; }
.od-dropdown-menu { position: absolute; top: calc(100% + 6px); left: 0; min-width: 180px; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius); box-shadow: 0 12px 28px rgba(17, 22, 31, .14); padding: 6px; z-index: 30; }
.od-dropdown-menu[hidden] { display: none; }
.od-dropdown-menu--right { left: auto; right: 0; }
.od-dropdown-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--od-radius-sm); background: transparent; font: inherit; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); text-align: left; cursor: pointer; }
.od-dropdown-item:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-dropdown-item--danger { color: var(--od-danger); }
.od-dropdown-item--danger:hover { background: var(--od-danger-bg); }
.od-dropdown-separator { height: 1px; margin: 6px 4px; background: var(--od-border-light); }

/* ============================== 13. Modal ============================== */
.od-modal { position: fixed; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center; padding: 20px; }
.od-modal.is-open { display: flex; }
.od-modal-overlay { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: var(--od-overlay); cursor: default; }
.od-modal-dialog { position: relative; z-index: 1; width: 100%; max-width: 480px; max-height: 88vh; display: flex; flex-direction: column; background: var(--od-panel); border-radius: var(--od-radius-lg); box-shadow: 0 24px 70px rgba(17, 22, 31, .25); overflow: hidden; }
.od-modal-dialog--wide { max-width: 640px; }
.od-modal-header { padding: 15px 20px; border-bottom: 1px solid var(--od-border-light); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.od-modal-header h2 { font-size: 15px; }
.od-modal-close { background: none; border: none; font-size: 22px; line-height: 1; color: var(--od-text-muted); cursor: pointer; padding: 0 2px; }
.od-modal-close:hover { color: var(--od-text); }
.od-modal-body { padding: 20px; overflow-y: auto; }
.od-modal-footer { padding: 14px 20px; border-top: 1px solid var(--od-border-light); display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0; }
body.od-modal-open { overflow: hidden; }

/* ============================== 14. Toast ============================== */
.od-toast-region { position: fixed; z-index: 80; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 10px; max-width: 340px; }
.od-toast { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border-radius: var(--od-radius); background: var(--od-dark); color: var(--od-dark-text); box-shadow: 0 12px 30px rgba(17, 22, 31, .25); font-size: 13px; line-height: 1.45; opacity: 0; transform: translateY(8px); transition: opacity .18s ease, transform .18s ease; }
.od-toast.is-visible { opacity: 1; transform: translateY(0); }
.od-toast-dot { width: 8px; height: 8px; border-radius: 999px; margin-top: 4px; flex-shrink: 0; background: var(--od-text-muted); }
.od-toast--success .od-toast-dot { background: var(--od-success); }
.od-toast--danger .od-toast-dot { background: var(--od-danger); }
.od-toast--warning .od-toast-dot { background: var(--od-warning); }
.od-toast-message { flex: 1; min-width: 0; }
.od-toast-close { flex-shrink: 0; background: none; border: 0; color: inherit; opacity: .6; cursor: pointer; font-size: 15px; line-height: 1; padding: 0; }
.od-toast-close:hover { opacity: 1; }

/* ============================== 15. Misc utilities ============================== */
.od-code { background: var(--od-dark); color: var(--od-dark-text); border-radius: var(--od-radius-lg); padding: 14px 16px; overflow-x: auto; font-family: var(--od-font-mono); font-size: 12px; line-height: 1.55; white-space: pre; margin: 0; }
.od-divider { height: 1px; background: var(--od-border-light); border: 0; margin: 16px 0; }

/* ============================== 16. Breadcrumbs ============================== */
.od-breadcrumbs { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--od-text-muted); margin-bottom: 16px; }
.od-breadcrumbs a { color: var(--od-text-secondary); }
.od-breadcrumbs a:hover { color: var(--od-text); text-decoration: underline; }
.od-breadcrumbs .is-current { color: var(--od-text); font-weight: 700; }
.od-breadcrumb-sep { color: var(--od-text-muted-2); }

/* ============================== 17. Empty state ============================== */
.od-empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 48px 24px; text-align: center; color: var(--od-text-muted); }
.od-empty-state-icon { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; border-radius: 999px; background: var(--od-nav-hover); color: var(--od-text-muted); font-size: 20px; }
.od-empty-state-title { font-size: 14px; font-weight: 800; color: var(--od-text); }
.od-empty-state-desc { font-size: 13px; max-width: 340px; line-height: 1.5; }
.od-empty-state-actions { margin-top: 6px; display: flex; gap: 8px; }

/* ============================== 18. Theme toggle ============================== */
/* Optional icon-swap helper for a <button data-od-theme-toggle>: give it two
   children (e.g. <span class="od-theme-icon-light">) and od-theme.js keeps
   aria-pressed in sync with the effective theme so exactly one shows. */
[data-od-theme-toggle] .od-theme-icon-dark { display: none; }
[data-od-theme-toggle][aria-pressed="true"] .od-theme-icon-light { display: none; }
[data-od-theme-toggle][aria-pressed="true"] .od-theme-icon-dark { display: inline; }

/* ============================== 19. Alert ============================== */
.od-alert { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border-radius: var(--od-radius); font-size: 13.5px; line-height: 1.5; margin-bottom: 16px; }
.od-alert-icon { flex-shrink: 0; font-size: 15px; line-height: 1.3; }
.od-alert-body { flex: 1; min-width: 0; }
.od-alert-title { font-weight: 800; margin-bottom: 2px; }
.od-alert-close { flex-shrink: 0; background: none; border: 0; opacity: .6; cursor: pointer; font-size: 15px; line-height: 1; color: inherit; padding: 0; }
.od-alert-close:hover { opacity: 1; }
.od-alert-info { background: var(--od-accent-bg); color: var(--od-accent-text); }
.od-alert-success { background: var(--od-success-bg); color: var(--od-success-text); }
.od-alert-warning { background: var(--od-warning-bg); color: var(--od-warning-text); }
.od-alert-danger { background: var(--od-danger-bg); color: var(--od-danger-text); }

/* ============================== 20. Switch ============================== */
.od-switch { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); user-select: none; }
.od-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.od-switch-track { position: relative; width: 36px; height: 20px; flex-shrink: 0; border-radius: 999px; background: var(--od-border); transition: background .15s ease; }
.od-switch-track::before { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s ease; box-shadow: 0 1px 2px rgba(17, 22, 31, .2); }
.od-switch input:checked ~ .od-switch-track { background: var(--od-accent); }
.od-switch input:checked ~ .od-switch-track::before { transform: translateX(16px); }
.od-switch input:focus-visible ~ .od-switch-track { outline: 2px solid var(--od-accent); outline-offset: 2px; }
.od-switch input:disabled ~ .od-switch-track { opacity: .5; cursor: not-allowed; }
.od-switch:has(input:disabled) { cursor: not-allowed; opacity: .7; }

/* ============================== 21. Progress bar ============================== */
.od-progress { height: 6px; border-radius: 999px; background: var(--od-border-light); overflow: hidden; }
.od-progress-bar { height: 100%; border-radius: 999px; background: var(--od-accent); transition: width .2s ease; }
.od-progress-bar--success { background: var(--od-success); }
.od-progress-bar--danger { background: var(--od-danger); }
.od-progress-bar--warning { background: var(--od-warning); }

/* ============================== 22. Skeleton loader ============================== */
.od-skeleton { position: relative; overflow: hidden; border-radius: var(--od-radius-sm); background: var(--od-border-light); }
.od-skeleton::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .6), transparent); animation: od-skeleton-shimmer 1.5s infinite; }
@media (prefers-color-scheme: dark) {
    :root:not([data-od-theme="light"]) .od-skeleton::after { background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .08), transparent); }
}
[data-od-theme="dark"] .od-skeleton::after { background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .08), transparent); }
@keyframes od-skeleton-shimmer { 100% { transform: translateX(100%); } }
.od-skeleton-text { height: 12px; margin-bottom: 8px; border-radius: 4px; }
.od-skeleton-text:last-child { margin-bottom: 0; width: 70%; }
.od-skeleton-title { height: 18px; width: 40%; margin-bottom: 12px; border-radius: 5px; }
.od-skeleton-avatar { width: 40px; height: 40px; border-radius: 999px; }

/* ============================== 23. Tooltip ============================== */
/* Pure CSS — `title`-like, but styled and instant. Put the tooltip text in
   data-od-tooltip on any element; needs no other markup. CSS alone can only
   flip up/down near the top of the topbar (below) — it has no way to know
   how close an element sits to the left/right/bottom edge of the actual
   viewport, so a long tooltip on an edge element can still clip there. Add
   od-tooltip.js (optional) to fix that too: it measures the trigger and
   viewport on hover/focus and writes the --od-tooltip-shift/position
   corrections this CSS reads. --od-tooltip-shift defaults to 0px (no JS =
   plain centered tooltip, same as before). */
[data-od-tooltip] { position: relative; }
[data-od-tooltip]::after {
    content: attr(data-od-tooltip); position: absolute; bottom: calc(100% + 6px); left: 50%;
    width: max-content; max-width: 220px; transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(4px);
    padding: 5px 9px; border-radius: var(--od-radius-sm);
    background: var(--od-dark); color: var(--od-dark-text); font-family: var(--od-font-sans);
    font-size: 11.5px; font-weight: 600; line-height: 1.4; white-space: normal; text-align: center;
    opacity: 0; visibility: hidden;
    transition: opacity .12s ease, transform .12s ease; pointer-events: none; z-index: 50;
}
[data-od-tooltip]:hover::after, [data-od-tooltip]:focus-visible::after { opacity: 1; visibility: visible; transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(0); }
[data-od-tooltip-position="bottom"]::after { bottom: auto; top: calc(100% + 6px); transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(-4px); }
[data-od-tooltip-position="bottom"]:hover::after, [data-od-tooltip-position="bottom"]:focus-visible::after { transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(0); }
/* .od-topbar sits at the very top of the viewport, so a tooltip opening
   upward from it would render off-screen — default to opening downward
   there instead. `data-od-tooltip-position="top"` opts back into "up" for a
   topbar element that has room (e.g. a tall custom topbar). */
.od-topbar [data-od-tooltip]:not([data-od-tooltip-position="top"])::after { bottom: auto; top: calc(100% + 6px); transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(-4px); }
.od-topbar [data-od-tooltip]:not([data-od-tooltip-position="top"]):hover::after, .od-topbar [data-od-tooltip]:not([data-od-tooltip-position="top"]):focus-visible::after { transform: translateX(calc(-50% + var(--od-tooltip-shift, 0px))) translateY(0); }
/* od-tooltip.js (1.1.12+) renders the tooltip as ONE shared position:fixed node
   appended to <body> instead of the ::after above. A fixed node is measured
   from the trigger at hover time, so it escapes any clipping ancestor — a
   scrolling .od-table-wrap (overflow-x:auto also clips vertically), a panel,
   the sidebar — which the ::after never could, no matter how it was shifted.
   The script stamps data-od-tooltip-js on <html>, which switches the CSS
   ::after off; without the script nothing here applies and the pure-CSS
   tooltip above keeps working exactly as before. */
html[data-od-tooltip-js] [data-od-tooltip]::after { display: none; }
.od-tooltip-fixed {
    position: fixed; top: 0; left: 0; width: max-content; max-width: 220px;
    padding: 5px 9px; border-radius: var(--od-radius-sm);
    background: var(--od-dark); color: var(--od-dark-text); font-family: var(--od-font-sans);
    font-size: 11.5px; font-weight: 600; line-height: 1.4; white-space: normal; text-align: center;
    opacity: 0; visibility: hidden; transform: translateY(4px);
    transition: opacity .12s ease, transform .12s ease; pointer-events: none; z-index: 95;
}
.od-tooltip-fixed--bottom { transform: translateY(-4px); }
.od-tooltip-fixed.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }

/* ============================== 24. Avatar group ============================== */
.od-avatar-group { display: flex; align-items: center; }
.od-avatar-group .od-avatar { border: 2px solid var(--od-panel); margin-left: -8px; }
.od-avatar-group .od-avatar:first-child { margin-left: 0; }
.od-avatar-group-more { width: 28px; height: 28px; flex-shrink: 0; border-radius: 999px; border: 2px solid var(--od-panel); margin-left: -8px; background: var(--od-border-light); color: var(--od-text-secondary); display: flex; align-items: center; justify-content: center; font-size: 10.5px; font-weight: 700; }

/* ============================== 25. Kbd ============================== */
.od-kbd { display: inline-flex; align-items: center; justify-content: center; padding: 2px 6px; border: 1px solid var(--od-border); border-bottom-width: 2px; border-radius: 5px; background: var(--od-nav-hover); color: var(--od-text-secondary); font-family: var(--od-font-mono); font-size: 11px; font-weight: 600; line-height: 1.4; }

/* ============================== 26. Segmented control ============================== */
.od-segmented { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--od-radius); background: var(--od-nav-hover); }
.od-segmented-btn { appearance: none; border: 0; padding: 6px 14px; border-radius: var(--od-radius-sm); background: transparent; font: inherit; font-size: 12.5px; font-weight: 700; color: var(--od-text-secondary); cursor: pointer; }
.od-segmented-btn:hover { color: var(--od-text); }
.od-segmented-btn.is-active { background: var(--od-panel); color: var(--od-text); box-shadow: 0 1px 2px rgba(15, 23, 42, .08); }
.od-segmented-btn:disabled { opacity: .5; cursor: not-allowed; }

/* ============================== 27. Bar chart ============================== */
/* A plain CSS bar chart — set each bar's value via inline `style="height:N%"`.
   Pair with `data-od-tooltip` (see Tooltip above) to show the exact value. */
.od-bars { display: flex; align-items: flex-end; gap: 6px; height: 140px; }
.od-bars-col { flex: 1; min-width: 0; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 8px; }
.od-bars-fill { width: 100%; max-width: 28px; min-height: 3px; border-radius: 4px 4px 0 0; background: var(--od-accent); transition: height .2s ease; }
.od-bars-fill--success { background: var(--od-success); }
.od-bars-fill--danger { background: var(--od-danger); }
.od-bars-fill--warning { background: var(--od-warning); }
.od-bars-label { font-size: 10.5px; font-weight: 600; color: var(--od-text-muted); white-space: nowrap; }

/* ============================== 28. Timeline ============================== */
.od-timeline { display: flex; flex-direction: column; }
.od-timeline-item { position: relative; padding: 0 0 20px 22px; }
.od-timeline-item::before { content: ""; position: absolute; top: 3px; left: 0; width: 9px; height: 9px; border-radius: 50%; background: var(--od-accent); }
.od-timeline-item::after { content: ""; position: absolute; top: 14px; left: 4px; bottom: 0; width: 1px; background: var(--od-border); }
.od-timeline-item:last-child { padding-bottom: 0; }
.od-timeline-item:last-child::after { display: none; }
.od-timeline-item--success::before { background: var(--od-success); }
.od-timeline-item--danger::before { background: var(--od-danger); }
.od-timeline-item--warning::before { background: var(--od-warning); }
.od-timeline-time { font-size: 11px; font-weight: 700; color: var(--od-text-muted); }
.od-timeline-title { font-size: 13px; font-weight: 700; color: var(--od-text); margin-top: 2px; }
.od-timeline-desc { font-size: 12.5px; color: var(--od-text-secondary); margin-top: 2px; line-height: 1.5; }

/* ============================== 29. List group ============================== */
.od-list { display: flex; flex-direction: column; }
.od-list-item { display: flex; align-items: center; gap: 12px; padding: 11px 2px; border-bottom: 1px solid var(--od-border-light); }
.od-list-item:last-child { border-bottom: 0; }
.od-list-icon { width: 34px; height: 34px; flex-shrink: 0; border-radius: var(--od-radius); background: var(--od-accent-bg); color: var(--od-accent-text); display: flex; align-items: center; justify-content: center; font-size: 15px; }
.od-list-body { flex: 1; min-width: 0; }
.od-list-title { font-size: 13px; font-weight: 700; color: var(--od-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-list-desc { font-size: 12px; color: var(--od-text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-list-meta { flex-shrink: 0; font-size: 11px; color: var(--od-text-muted); white-space: nowrap; }

/* ============================== 30. Notification badge dot ============================== */
/* Add alongside any icon button classes: <button class="od-btn od-btn-ghost od-btn-icon od-badge-dot">
   Uses ::before (not ::after) so it can be combined with data-od-tooltip on
   the same element — an element can only have one ::after, and the Tooltip
   component (above) already owns that one. */
.od-badge-dot { position: relative; }
.od-badge-dot::before { content: ""; position: absolute; top: 3px; right: 3px; width: 9px; height: 9px; border-radius: 50%; background: var(--od-danger); border: 2px solid var(--od-panel); }

/* ============================== 31. Guest / auth shell ============================== */
/* A centered single-card layout for login/register/forgot-password pages —
   used instead of .od-shell where there's no sidebar/topbar yet. */
.od-guest-shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.od-guest-panel { width: 100%; max-width: 360px; }
.od-guest-brand { display: flex; align-items: center; gap: 10px; justify-content: center; margin-bottom: 22px; }

/* ============================== 32. Stepper ============================== */
/* Presentational only — toggle .is-active/.is-complete yourself as the user
   moves through your form's steps (pair with your own Next/Back buttons). */
.od-stepper { display: flex; align-items: flex-start; }
.od-step { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; text-align: center; }
.od-step:not(:last-child)::after { content: ""; position: absolute; top: 15px; left: 50%; width: 100%; height: 2px; background: var(--od-border); z-index: 0; }
.od-step.is-complete:not(:last-child)::after { background: var(--od-success); }
.od-step-circle { position: relative; z-index: 1; width: 30px; height: 30px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; background: var(--od-panel); border: 2px solid var(--od-border); color: var(--od-text-muted); }
.od-step.is-active .od-step-circle { border-color: var(--od-accent); color: var(--od-accent-text); }
.od-step.is-complete .od-step-circle { background: var(--od-success); border-color: var(--od-success); color: #fff; }
.od-step-label { margin-top: 8px; font-size: 12px; font-weight: 700; color: var(--od-text-muted); }
.od-step.is-active .od-step-label { color: var(--od-text); }
.od-step.is-complete .od-step-label { color: var(--od-text-secondary); }

/* ============================== 33. Sidebar submenu (collapsible nav) ============================== */
/* Native <details>/<summary> — same zero-JS mechanism as the Accordion —
   styled to sit inline in .od-nav-list next to plain .od-nav-link items. */
.od-nav-collapsible > summary.od-nav-toggle { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: var(--od-radius); color: var(--od-text-secondary); font-weight: 600; font-size: 13.5px; cursor: pointer; list-style: none; }
.od-nav-collapsible > summary.od-nav-toggle::-webkit-details-marker { display: none; }
.od-nav-collapsible > summary.od-nav-toggle:hover { background: var(--od-nav-hover); }
.od-nav-collapsible > summary.od-nav-toggle::after { content: "\276F"; margin-left: auto; font-size: 10px; color: var(--od-text-muted-2); transition: transform .15s ease; }
.od-nav-collapsible[open] > summary.od-nav-toggle::after { transform: rotate(90deg); }
.od-nav-sublist { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 4px; padding-left: 34px; }
.od-nav-sublist .od-nav-link { font-size: 12.5px; padding: 6px 8px; }

/* ============================== 34. Confirm dialog ============================== */
/* No CSS of its own — OpenDashboard.Confirm (od-confirm.js) builds a plain
   .od-modal at call time and reuses all of the Modal component's styles. */

/* ============================== 35. Spinner & button loading state ============================== */
.od-spinner { display: inline-block; width: 16px; height: 16px; flex-shrink: 0; border: 2px solid var(--od-border); border-top-color: var(--od-accent); border-radius: 50%; animation: od-spin .6s linear infinite; }
.od-spinner--sm { width: 12px; height: 12px; border-width: 2px; }
.od-spinner--lg { width: 24px; height: 24px; border-width: 3px; }
@keyframes od-spin { to { transform: rotate(360deg); } }
/* Toggle yourself when an action starts/ends — this never guesses when your
   async call is "done", it just renders whichever state you set. Add
   `disabled` alongside the class for real (not just visual) blocking. */
.od-btn.is-loading, .od-btn-ghost.is-loading, .od-btn-neutral.is-loading, .od-btn-danger.is-loading { pointer-events: none; opacity: .75; cursor: wait; }
.od-btn.is-loading::before, .od-btn-ghost.is-loading::before, .od-btn-neutral.is-loading::before, .od-btn-danger.is-loading::before {
    content: ""; width: 13px; height: 13px; flex-shrink: 0; border-radius: 50%;
    border: 2px solid currentColor; border-right-color: transparent; border-top-color: transparent;
    animation: od-spin .6s linear infinite;
}

/* ============================== 36. Copy to clipboard ============================== */
/* No markup of its own — od-copy.js adds this state to whatever trigger
   (button, icon button, link) you already have. */
[data-od-copy].is-copied, [data-od-copy-target].is-copied { color: var(--od-success) !important; border-color: var(--od-success) !important; }

/* ============================== 37. Icon ============================== */
/* Sizing/alignment for your OWN inline SVG icon — stroke="currentColor" (or
   fill="currentColor" for a solid glyph) makes it track the surrounding
   text color automatically, including through dark mode, pills, nav
   active-states, etc. For a ready-made set with zero inline SVG markup at
   all, see open-dashboard-icons.css (the .od-i-* classes) instead. */
.od-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; flex-shrink: 0; }

/* The .od-i-* icon set has moved to its own file, open-dashboard-icons.css —
   the data-URI icons made this file disproportionately large, and most
   pages don't need the whole set. Link that file too (optional) if you use
   the .od-i-* classes; see
   the Icons section of the README and demo/icons.html. */

/* ============================== 38. Period bar (date range filter) ============================== */
.od-period-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
.od-period-link { display: inline-flex; align-items: center; padding: 6px 12px; border-radius: var(--od-radius); border: 1px solid var(--od-border); background: var(--od-panel); font-size: 12.5px; font-weight: 700; color: var(--od-text-secondary); cursor: pointer; }
.od-period-link:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-period-link.is-active { background: var(--od-accent); border-color: var(--od-accent); color: #fff; }
.od-period-custom { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-wrap: wrap; }
.od-period-custom .od-input { width: auto; padding: 5px 9px; font-size: 12.5px; }

/* ============================== 39. Rank list ============================== */
/* A ranked row list with a proportional background bar — "top pages",
   "top referrers", any leaderboard. Set each row's bar width yourself via
   inline `style="width:N%"`, computed as value/max(values)*100. */
.od-rank-list { display: flex; flex-direction: column; }
.od-rank-row { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 16px; overflow: hidden; font-size: 13.5px; border-bottom: 1px solid var(--od-border-light); }
.od-rank-row:last-child { border-bottom: none; }
.od-rank-bar { position: absolute; top: 0; bottom: 0; left: 0; z-index: 0; background: var(--od-accent-bg); }
.od-rank-label, .od-rank-value { position: relative; z-index: 1; }
.od-rank-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
a.od-rank-label:hover { color: var(--od-accent-text); text-decoration: underline; }
.od-rank-value { flex-shrink: 0; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--od-text-secondary); }

/* ============================== 41. Drawer ============================== */
/* A side panel — same contract as Modal (content already in the page, no
   fetching), just sliding in from an edge instead of appearing centered.
   z-index sits just below .od-modal (60) so a confirm/edit modal opened
   from inside a drawer still layers on top of it. */
.od-drawer { position: fixed; inset: 0; z-index: 55; display: none; }
.od-drawer.is-open { display: block; }
.od-drawer-overlay { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: var(--od-overlay); opacity: 0; transition: opacity .2s ease; cursor: default; }
.od-drawer.is-open .od-drawer-overlay { opacity: 1; }
.od-drawer-panel { position: fixed; top: 0; right: 0; z-index: 1; width: 420px; max-width: 92vw; height: 100vh; height: 100dvh; display: flex; flex-direction: column; background: var(--od-panel); border-left: 1px solid var(--od-border); box-shadow: -8px 0 30px rgba(17, 22, 31, .16); transform: translateX(100%); transition: transform .22s ease; }
.od-drawer.is-open .od-drawer-panel { transform: translateX(0); }
.od-drawer-panel--wide { width: 640px; }
.od-drawer-panel--left { top: 0; right: auto; left: 0; border-left: 0; border-right: 1px solid var(--od-border); box-shadow: 8px 0 30px rgba(17, 22, 31, .16); transform: translateX(-100%); }
.od-drawer.is-open .od-drawer-panel--left { transform: translateX(0); }
.od-drawer-header { padding: 15px 20px; border-bottom: 1px solid var(--od-border-light); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.od-drawer-header h2 { font-size: 15px; }
.od-drawer-close { background: none; border: none; font-size: 22px; line-height: 1; color: var(--od-text-muted); cursor: pointer; padding: 0 2px; }
.od-drawer-close:hover { color: var(--od-text); }
.od-drawer-body { padding: 20px; overflow-y: auto; flex: 1; }
.od-drawer-footer { padding: 14px 20px; border-top: 1px solid var(--od-border-light); display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0; }
body.od-drawer-open { overflow: hidden; }

/* ============================== 42. Chip / tag ============================== */
.od-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 999px; background: var(--od-nav-hover); color: var(--od-text-secondary); white-space: nowrap; }
/* The remove button's own hit area already reads as right-side padding, so
   a chip that has one gets a tighter right pad than the default above. */
.od-chip:has(.od-chip-remove) { padding-right: 6px; }
.od-chip-remove { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 999px; border: 0; background: none; color: inherit; opacity: .6; cursor: pointer; font-size: 13px; line-height: 1; padding: 0; }
.od-chip-remove:hover { opacity: 1; background: color-mix(in srgb, currentColor 15%, transparent); }
.od-chip--accent { background: var(--od-accent-bg); color: var(--od-accent-text); }
.od-chip--success { background: var(--od-success-bg); color: var(--od-success-text); }
.od-chip--danger { background: var(--od-danger-bg); color: var(--od-danger-text); }

/* ============================== 43. Description list ============================== */
.od-desc-list { display: flex; flex-direction: column; }
.od-desc-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--od-border-light); font-size: 13.5px; }
.od-desc-row:last-child { border-bottom: none; }
.od-desc-label { flex-shrink: 0; color: var(--od-text-muted); font-weight: 600; }
.od-desc-value { text-align: right; font-weight: 600; min-width: 0; }
.od-desc-list--tight .od-desc-row { padding: 6px 0; }

/* ============================== 44. Top loading bar ============================== */
/* Indeterminate progress hint for full-page navigations — this is a
   classic multi-page app, every real navigation is a browser page load,
   never an AJAX swap — or for a manual long-running action. Add the
   element once, near the top of <body>; od-loading-bar.js toggles
   `.is-active` automatically for same-tab link clicks inside .od-shell/
   .od-guest-shell, or call OpenDashboard.LoadingBar yourself. */
.od-loading-bar { position: fixed; top: 0; left: 0; width: 100%; height: 3px; z-index: 90; background: transparent; pointer-events: none; overflow: hidden; }
.od-loading-bar::before { content: ''; position: absolute; top: 0; left: 0; height: 100%; width: 40%; background: var(--od-accent); opacity: 0; transform: translateX(-100%); }
.od-loading-bar.is-active::before { opacity: 1; animation: od-loading-bar-slide 1.1s ease-in-out infinite; }
@keyframes od-loading-bar-slide {
    0% { transform: translateX(-100%); }
    50% { transform: translateX(60%); }
    100% { transform: translateX(250%); }
}

/* ============================== 45. Mini calendar ============================== */
/* Rendered entirely by od-calendar.js from plain date math (no fetching,
   no server contract) — the element starts empty, this is just the
   styling for the grid the script builds. */
.od-calendar { width: 100%; max-width: 280px; }
.od-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.od-calendar-title { font-size: 13px; font-weight: 800; }
.od-calendar-nav { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: var(--od-radius-sm); border: 1px solid var(--od-border); background: var(--od-panel); color: var(--od-text-secondary); cursor: pointer; font-size: 14px; line-height: 1; padding: 0; }
.od-calendar-nav:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; text-align: center; }
.od-calendar-dow { font-size: 10.5px; font-weight: 700; color: var(--od-text-muted-2); text-transform: uppercase; padding: 4px 0; }
.od-calendar-day { display: flex; align-items: center; justify-content: center; aspect-ratio: 1; border: 0; border-radius: var(--od-radius-sm); background: none; font: inherit; font-size: 12.5px; font-weight: 600; color: var(--od-text); cursor: pointer; padding: 0; }
.od-calendar-day:hover { background: var(--od-nav-hover); }
.od-calendar-day--muted { color: var(--od-text-muted-2); }
.od-calendar-day--today { border: 1px solid var(--od-accent); color: var(--od-accent-text); font-weight: 800; }
.od-calendar-day--active, .od-calendar-day--active:hover { background: var(--od-accent); color: #fff; font-weight: 800; }

/* ============================== 46. Command palette ============================== */
.od-command-palette { position: fixed; inset: 0; z-index: 70; display: none; align-items: flex-start; justify-content: center; padding: 14vh 20px 20px; }
.od-command-palette.is-open { display: flex; }
.od-command-overlay { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: var(--od-overlay); cursor: default; }
.od-command-panel { position: relative; z-index: 1; width: 100%; max-width: 560px; max-height: 60vh; display: flex; flex-direction: column; background: var(--od-panel); border-radius: var(--od-radius-lg); box-shadow: 0 24px 70px rgba(17, 22, 31, .3); overflow: hidden; }
.od-command-search { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--od-border-light); flex-shrink: 0; }
.od-command-search input { flex: 1; min-width: 0; border: 0; background: none; font: inherit; font-size: 14.5px; color: var(--od-text); outline: none; }
.od-command-search input::placeholder { color: var(--od-text-muted); }
.od-command-list { overflow-y: auto; padding: 6px; }
.od-command-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; padding: 10px 12px; border: 0; border-radius: var(--od-radius); background: transparent; font: inherit; font-size: 13.5px; font-weight: 600; color: var(--od-text); text-align: left; text-decoration: none; cursor: pointer; }
.od-command-item[hidden] { display: none; }
.od-command-item:hover, .od-command-item.is-active { background: var(--od-nav-hover); }
.od-command-item-hint { flex-shrink: 0; font-size: 11.5px; font-weight: 700; color: var(--od-text-muted); }
.od-command-empty { padding: 20px 12px; text-align: center; font-size: 13px; color: var(--od-text-muted); }

/* ============================== 47. File dropzone ============================== */
.od-dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 32px 20px; border: 2px dashed var(--od-border); border-radius: var(--od-radius-lg); text-align: center; cursor: pointer; transition: border-color .15s ease, background .15s ease; }
.od-dropzone:hover, .od-dropzone.is-dragover { border-color: var(--od-accent); background: var(--od-accent-bg); }
.od-dropzone-icon { width: 32px; height: 32px; margin-bottom: 6px; color: var(--od-text-muted); }
.od-dropzone-title { font-size: 13.5px; font-weight: 700; color: var(--od-text); }
.od-dropzone-hint { font-size: 12px; color: var(--od-text-muted); }

/* ============================== 48. Context menu ============================== */
.od-context-menu { position: fixed; z-index: 75; min-width: 180px; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius); box-shadow: 0 12px 28px rgba(17, 22, 31, .18); padding: 6px; display: none; }
.od-context-menu.is-open { display: block; }
.od-context-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--od-radius-sm); background: transparent; font: inherit; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); text-align: left; cursor: pointer; }
.od-context-menu-item:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-context-menu-item--danger { color: var(--od-danger); }
.od-context-menu-item--danger:hover { background: var(--od-danger-bg); }
.od-context-menu-divider { height: 1px; margin: 6px 4px; background: var(--od-border-light); }

/* ============================== 49. Sparkline ============================== */
/* An inline SVG line chart — plot the points yourself (e.g. from
   `value / max(values) * 100`), same "no chart library" approach as Bar
   chart, just a continuous line instead of discrete bars. Sizes to
   whatever width/height you give the `<svg>`. */
.od-sparkline { display: block; width: 100%; height: 32px; overflow: visible; }
.od-sparkline polyline { fill: none; stroke: var(--od-accent); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.od-sparkline--success polyline { stroke: var(--od-success); }
.od-sparkline--danger polyline { stroke: var(--od-danger); }
.od-sparkline--warning polyline { stroke: var(--od-warning); }

/* ============================== 50. Donut chart ============================== */
/* Set the fill with the --value custom property (0-100) — same
   inline-style pattern as Progress bar, just a ring instead of a bar.
   CSS conic-gradient, no chart library involved. */
.od-donut { --value: 0; --od-donut-color: var(--od-accent); position: relative; width: 96px; height: 96px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: conic-gradient(var(--od-donut-color) calc(var(--value) * 1%), var(--od-border-light) 0); }
.od-donut::before { content: ''; position: absolute; inset: 12px; border-radius: 50%; background: var(--od-panel); }
.od-donut-label { position: relative; z-index: 1; font-size: 16px; font-weight: 800; }
.od-donut--sm { width: 56px; height: 56px; }
.od-donut--sm::before { inset: 7px; }
.od-donut--sm .od-donut-label { font-size: 12px; }
.od-donut--success { --od-donut-color: var(--od-success); }
.od-donut--danger { --od-donut-color: var(--od-danger); }
.od-donut--warning { --od-donut-color: var(--od-warning); }

/* ============================== 51. Rating ============================== */
/* Read-only display, same "set a fraction inline" pattern as Progress
   bar — set --value to the filled width (e.g. 3.5/5 = 70%). CSS only:
   two stacked identical star strings, the top one clipped by width. */
.od-rating { --value: 0%; position: relative; display: inline-block; font-size: 16px; line-height: 1; letter-spacing: 2px; }
.od-rating-stars { display: block; color: var(--od-border); }
.od-rating-stars--filled { position: absolute; top: 0; left: 0; overflow: hidden; width: var(--value); color: var(--od-warning); white-space: nowrap; }

/* ============================== 52. OTP / PIN input ============================== */
.od-otp { display: flex; gap: 8px; }
.od-otp-input { width: 44px; height: 52px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); color: var(--od-text); font: inherit; font-size: 20px; font-weight: 700; text-align: center; }
.od-otp-input:focus { outline: none; border-color: var(--od-accent); box-shadow: 0 0 0 3px var(--od-accent-bg); }

/* ============================== 53. Split button ============================== */
/* Composes with the existing Dropdown component — the caret is a normal
   dropdown trigger, just visually joined to the button next to it. */
.od-split-btn { display: inline-flex; }
.od-split-btn > .od-btn:first-child, .od-split-btn > .od-btn-ghost:first-child, .od-split-btn > .od-btn-neutral:first-child, .od-split-btn > .od-btn-danger:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.od-btn.od-split-btn-caret, .od-btn-ghost.od-split-btn-caret, .od-btn-neutral.od-split-btn-caret, .od-btn-danger.od-split-btn-caret { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: 1px solid color-mix(in srgb, currentColor 25%, transparent); padding-left: 10px; padding-right: 10px; }

/* ============================== 54. Trend indicator ============================== */
/* Color is sentiment (is this change good or bad), not direction — for
   a metric like "Errors" a downward arrow is the good outcome, so
   direction (the arrow glyph, written by you) and sentiment (the
   modifier class) are deliberately independent. */
.od-trend { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 700; }
.od-trend--positive { color: var(--od-success); }
.od-trend--negative { color: var(--od-danger); }
.od-trend--neutral { color: var(--od-text-muted); }
.od-trend-context { font-weight: 600; color: var(--od-text-muted); }

/* ============================== 55. Range slider ============================== */
.od-range { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; margin: 8px 0; border-radius: 999px; background: var(--od-border-light); outline: none; cursor: pointer; }
.od-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--od-accent); border: 3px solid var(--od-panel); box-shadow: 0 0 0 1px var(--od-border); cursor: pointer; }
.od-range::-moz-range-track { height: 6px; border-radius: 999px; background: var(--od-border-light); }
.od-range::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--od-accent); border: 3px solid var(--od-panel); box-shadow: 0 0 0 1px var(--od-border); cursor: pointer; }
.od-range:focus-visible { box-shadow: 0 0 0 3px var(--od-accent-bg); border-radius: 999px; }
.od-range:disabled { opacity: .5; cursor: not-allowed; }

/* ============================== 56. Vertical stepper ============================== */
/* Add alongside .od-stepper to switch the same markup from a horizontal
   row to a vertical list (checkout/onboarding flows). */
.od-stepper--vertical { flex-direction: column; align-items: stretch; }
.od-stepper--vertical .od-step { flex: none; flex-direction: row; align-items: flex-start; text-align: left; padding-bottom: 28px; }
.od-stepper--vertical .od-step:last-child { padding-bottom: 0; }
.od-stepper--vertical .od-step:not(:last-child)::after { top: 30px; bottom: 0; left: 15px; width: 2px; height: auto; }
.od-stepper--vertical .od-step-label { margin-top: 0; margin-left: 12px; padding-top: 4px; }

/* ============================== 57. Panel loading overlay ============================== */
/* Add/remove this element yourself (or toggle `hidden`) while a panel's
   content is loading — CSS only, no bundled behavior, same as Alert's
   dismiss button. Needs `.od-panel` to be a positioning context, which
   it already is. */
.od-panel-loading { position: absolute; inset: 0; z-index: 5; display: flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--od-panel) 80%, transparent); border-radius: inherit; }
.od-panel-loading[hidden] { display: none; }

/* ============================== 58. Sortable list (drag to reorder) ============================== */
.od-sortable { display: flex; flex-direction: column; gap: 8px; }
.od-sortable-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); font-size: 13.5px; font-weight: 600; }
.od-sortable-item.is-dragging { opacity: .5; }
.od-sortable-item.is-drag-over { border-color: var(--od-accent); }
/* A 2x3 grid of dots drawn with box-shadow rather than a unicode glyph
   (e.g. vertical-ellipsis characters) — glyph metrics vary enough across
   fonts/OSes to leave unpredictable, often oversized gaps; this is a
   fixed, predictable size regardless of font. */
.od-sortable-handle { flex-shrink: 0; position: relative; width: 12px; height: 16px; cursor: grab; touch-action: none; }
.od-sortable-handle::before { content: ''; position: absolute; top: 1px; left: 1px; width: 3px; height: 3px; border-radius: 50%; background: var(--od-text-muted); box-shadow: 7px 0 0 var(--od-text-muted), 0 5px 0 var(--od-text-muted), 7px 5px 0 var(--od-text-muted), 0 10px 0 var(--od-text-muted), 7px 10px 0 var(--od-text-muted); }

/* ============================== 59. Page header ============================== */
/* A page/section title row — an <h1> (+ optional .od-muted description) on
   the left, optional trailing actions on the right. Wraps below on narrow
   screens. Place it as the first thing inside .od-main. */
.od-page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
.od-page-header .od-muted { margin-top: 4px; }
.od-page-header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ============================== 60. Nav status dot ============================== */
/* A small live/inactive indicator inline in a sidebar .od-nav-link — for a
   list of monitored items (sites, servers, environments) rather than plain
   navigation. Distinct from .od-badge-dot (a corner badge on an icon
   button) and .od-pill-dot (a dot inside a pill). Unmodified reads as
   inactive/neutral; add .is-live for the "on" state. */
.od-nav-dot { width: 8px; height: 8px; border-radius: 999px; background: var(--od-border); flex-shrink: 0; }
.od-nav-dot.is-live { background: var(--od-success); }

/* ============================== 61. Button block modifier ============================== */
/* Add alongside any button variant (.od-btn, .od-btn-ghost, .od-btn-neutral,
   .od-btn-danger) to stretch it to the width of its container — e.g. a
   submit button under a narrow guest-shell form. */
.od-btn-block { width: 100%; }

/* ============================== 62. Form field help & actions ============================== */
.od-field-help { font-size: 12px; color: var(--od-text-muted); margin-top: 5px; }
.od-form-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 4px; }

/* ============================== 63. Grid utility ============================== */
/* An equal-width, auto-wrapping grid for placing panels side by side —
   collapses to a single column on narrow screens on its own, no media
   query needed. For an asymmetric split (e.g. a wide chart + a narrow
   sidebar panel), write your own grid-template-columns instead. */
.od-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }

/* ============================== 64. Line / area chart ============================== */
/* A bigger sibling of Sparkline for a full chart rather than an inline
   trend: an SVG line (optionally with a filled area under it) plus
   gridlines, axis labels and a legend. Plot the points yourself (e.g. from
   `value / max(values) * 100`) — same "no chart library" approach as Bar
   chart, just multi-series and full-size instead of inline. */
.od-linechart { display: block; width: 100%; height: 220px; overflow: visible; }
.od-linechart-grid line { stroke: var(--od-border-light); stroke-width: 1; }
.od-linechart-area { stroke: none; fill: var(--od-accent); opacity: .12; }
.od-linechart-area--success { fill: var(--od-success); }
.od-linechart-area--danger { fill: var(--od-danger); }
.od-linechart-area--warning { fill: var(--od-warning); }
.od-linechart-line { fill: none; stroke: var(--od-accent); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.od-linechart-line--success { stroke: var(--od-success); }
.od-linechart-line--danger { stroke: var(--od-danger); }
.od-linechart-line--warning { stroke: var(--od-warning); }
.od-linechart-line--muted { stroke: var(--od-text-muted); stroke-dasharray: 4 3; }
.od-linechart-axis { display: flex; justify-content: space-between; margin-top: 8px; font-size: 10.5px; font-weight: 600; color: var(--od-text-muted); }
.od-linechart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px; font-size: 12px; font-weight: 600; color: var(--od-text-secondary); }
.od-linechart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.od-linechart-legend-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--od-accent); flex-shrink: 0; }
.od-linechart-legend-dot--success { background: var(--od-success); }
.od-linechart-legend-dot--danger { background: var(--od-danger); }
.od-linechart-legend-dot--warning { background: var(--od-warning); }
.od-linechart-legend-dot--muted { background: var(--od-text-muted); }

/* ============================== 65. Combobox (searchable select) ============================== */
/* A text input with a filtered dropdown of options — pairs with a hidden
   native <select> so the picked value(s) still submit with the form. The
   `multiple` attribute on that <select> is what decides multi- vs.
   single-pick behavior (see od-combobox.js). Multi-pick renders chips
   (reusing Chip / tag's `.od-chip-remove`) inside the control. */
.od-combobox { position: relative; }
.od-combobox-control { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; min-height: 38px; padding: 5px 11px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); cursor: text; }
.od-combobox.is-open .od-combobox-control { border-color: var(--od-accent); }
.od-combobox-tag { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 3px 6px 3px 10px; border-radius: 999px; background: var(--od-nav-hover); color: var(--od-text-secondary); white-space: nowrap; }
.od-combobox-input { flex: 1; min-width: 60px; border: 0; background: none; font: inherit; font-size: 13.5px; color: var(--od-text); outline: none; padding: 3px 2px; }
.od-combobox-input::placeholder { color: var(--od-text-muted); }
.od-combobox-menu { position: absolute; top: calc(100% + 6px); left: 0; right: 0; max-height: 240px; overflow-y: auto; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius); box-shadow: 0 12px 28px rgba(17, 22, 31, .14); padding: 6px; z-index: 30; }
.od-combobox-menu[hidden] { display: none; }
.od-combobox-option { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--od-radius-sm); background: transparent; font: inherit; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); text-align: left; cursor: pointer; }
.od-combobox-option[hidden] { display: none; }
.od-combobox-option:hover, .od-combobox-option.is-active { background: var(--od-nav-hover); color: var(--od-text); }
.od-combobox-option.is-selected { color: var(--od-accent); }
.od-combobox-option-check { opacity: 0; width: 14px; height: 14px; flex-shrink: 0; }
.od-combobox-option.is-selected .od-combobox-option-check { opacity: 1; }
.od-combobox-empty { padding: 16px 10px; text-align: center; font-size: 13px; color: var(--od-text-muted); }

/* ============================== 66. Code block: toolbar, line numbers, tabs, syntax tokens ============================== */
/* Everything here is additive to the plain `.od-code` in Misc utilities — a
   bare `<pre class="od-code">` with real text still works with no JS and no
   wrapper. These pieces are opt-in, and combine independently. */

/* Toolbar: a header strip for a language/file label and (composed with Copy
   to clipboard) a copy button. Wrap both in `.od-code-block` so the corners
   join up into one rounded shape. */
.od-code-block { border-radius: var(--od-radius-lg); overflow: hidden; }
.od-code-block .od-code, .od-code-block .od-code-body { border-radius: 0; }
.od-code-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 14px; background: var(--od-dark-border); }
.od-code-lang { font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--od-dark-text); opacity: .6; }
.od-code-toolbar .od-btn-ghost { background: transparent; border-color: var(--od-dark-border); color: var(--od-dark-text); opacity: .8; }
.od-code-toolbar .od-btn-ghost:hover { background: rgba(255, 255, 255, .08); opacity: 1; }

/* Line numbers: a separate gutter column next to the `<pre>`, matched to it
   by font-size/line-height — not a number baked into each code line — so
   selecting/copying the code never picks up the digits, and the `<pre>`'s
   text stays exactly what it was. Author the gutter's spans by hand (e.g.
   from a backend highlighter that already knows the line count), or let
   `od-code.js` build them for you via `data-od-code-numbered`. */
.od-code-body { display: flex; background: var(--od-dark); border-radius: var(--od-radius-lg); }
.od-code-body .od-code { flex: 1; min-width: 0; background: transparent; border-radius: 0; }
/* .od-code itself is dark regardless of page theme (see the syntax-token
   comment below), but the gutter is a separate sibling div next to it, not
   part of it — with no background of its own it fell through to whatever
   sits behind .od-code-body (the page's own panel background), so in light
   mode this rendered as its fixed light-gray digits on a near-white
   background instead of on dark: nearly invisible. .od-code-body now owns
   the shared dark background instead, so both the gutter and .od-code
   (transparent over it) stay on the same dark surface in both themes. */
.od-code-gutter { flex-shrink: 0; padding: 14px 4px 14px 14px; text-align: right; font-family: var(--od-font-mono); font-size: 12px; line-height: 1.55; color: var(--od-dark-text); opacity: .35; user-select: none; }
.od-code-gutter span { display: block; }

/* Multiple files: compose with Tabs (above) instead of new JS — a dark tab
   strip that sits flush on top of the code body, one `.od-tab-panel` per
   file (each wrapping its own `.od-code`). */
.od-code-tabs { border-radius: var(--od-radius-lg); overflow: hidden; }
.od-code-tabs .od-tabs-nav { margin-bottom: 0; padding: 0 6px; background: var(--od-dark-border); border-bottom: 0; }
.od-code-tabs .od-tab-button { color: var(--od-dark-text); opacity: .55; }
.od-code-tabs .od-tab-button:hover { opacity: .85; background: rgba(255, 255, 255, .06); }
.od-code-tabs .od-tab-button.is-active, .od-code-tabs .od-tab-button[aria-selected="true"] { opacity: 1; }
.od-code-tabs .od-code, .od-code-tabs .od-code-body { border-radius: 0; }

/* Syntax tokens — a fixed palette, not theme tokens: `.od-code` always sits
   on the same dark background regardless of light/dark mode, so tokens
   don't need a light-mode variant. Applied by od-code.js; just as valid to
   wrap these classes around your own tokens by hand (e.g. from a
   server-side highlighter) without loading the JS at all. */
.od-tok-comment { color: #7A8699; font-style: italic; }
.od-tok-keyword { color: #FF7AB2; }
.od-tok-string { color: #A5D6A7; }
.od-tok-number { color: #F2C94C; }
.od-tok-tag { color: #FF7AB2; }
.od-tok-attr { color: #82AAFF; }
.od-tok-func { color: #82AAFF; }
.od-tok-punct { color: #9AA5B5; }

/* ============================== 67. Disclosure ============================== */
/* A single collapsible <details> with a rotating caret — lighter than
   .od-accordion, which boxes multiple stacked items into one FAQ-style list.
   Use this for a one-off "show details" affordance inline in other layouts.
   Put .od-caret wherever the arrow belongs — directly in a plain <summary>,
   or nested inside a more complex one (badges, stats, ...) alongside other
   content — it only depends on a [open] ancestor <details>, not on being a
   direct child of <summary>:
     <details class="od-disclosure">
       <summary><span class="od-caret"></span> Show details</summary>
       <div class="od-disclosure-body">...</div>
     </details>
*/
.od-disclosure > summary { cursor: pointer; list-style: none; outline: none; }
.od-disclosure > summary::-webkit-details-marker { display: none; }
.od-caret { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex-shrink: 0; color: var(--od-text-muted-2); transition: transform .15s ease; transform-origin: center; }
.od-caret::before { content: "\25B8"; line-height: 1; }
details[open] .od-caret { transform: rotate(90deg); }
.od-disclosure-body { margin-top: 8px; }

/* ============================== 68. Stat tile: icon variant ============================== */
/* Leading icon badge next to the LABEL only (not the whole value/sub block —
   an icon vertically centered across three lines of text reads as floating,
   unanchored from any single line) for KPI cards where an icon communicates
   the metric type at a glance. The plain .od-stat-tile (label above a big
   value, no icon) stays the default for a stat grid; add this modifier for
   the icon layout. --success/--danger/--warning tint both the icon and (for
   danger) the tile border, matching the .od-alert- and .od-pill- variant
   families (a star immediately followed by a slash anywhere in this comment
   closes it early and silently drops the rules below — that exact typo
   broke this very component once, so never join a trailing "*" and "/"
   back to back in this paragraph).
     <div class="od-stat-tile od-stat-tile--icon od-stat-tile--danger">
       <div class="od-stat-head">
         <span class="od-stat-icon"><i class="od-i od-i-alert-triangle"></i></span>
         <div class="od-stat-label">Errors</div>
       </div>
       <div class="od-stat-value">12 <small>/24h</small></div>
     </div>
*/
.od-stat-tile--icon { padding: 14px 15px; }
.od-stat-tile--icon .od-stat-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.od-stat-icon { width: 30px; height: 30px; flex: 0 0 30px; display: grid; place-items: center; border-radius: var(--od-radius); background: var(--od-accent-bg); color: var(--od-accent); font-size: 15px; }
.od-stat-tile--icon .od-stat-label { min-width: 0; text-transform: none; letter-spacing: 0; font-weight: 600; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-stat-tile--icon .od-stat-value small { margin-left: 6px; font-size: 11px; font-weight: 700; color: var(--od-text-muted); }
.od-stat-tile--success .od-stat-icon { color: var(--od-success-text); background: var(--od-success-bg); }
.od-stat-tile--danger { border-color: color-mix(in srgb, var(--od-danger) 28%, var(--od-border-light)); }
.od-stat-tile--danger .od-stat-icon { color: var(--od-danger-text); background: var(--od-danger-bg); }
.od-stat-tile--warning .od-stat-icon { color: var(--od-warning-text); background: var(--od-warning-bg); }

/* ============================== 69. Status card ============================== */
/* A thin colored left border communicating state at a glance — pairs with an
   existing container rather than dictating a layout of its own, since "status
   card" shows up with very different internal shapes across apps (a wide
   banner, a compact list row, a whole panel section):
     <div class="od-panel od-status-card od-status-card--danger">...</div>
*/
.od-status-card { border-left: 4px solid var(--od-border); }
.od-status-card--success { border-left-color: var(--od-success); }
.od-status-card--danger { border-left-color: var(--od-danger); }
.od-status-card--warning { border-left-color: var(--od-warning); }
.od-status-card--accent { border-left-color: var(--od-accent); }

/* ============================== 70. Snippet ============================== */
/* A single inline command/value in a dark row — "run this" / "here's your
   token" contexts where .od-code's block <pre> is heavier than needed. Pair
   with od-copy.js (data-od-copy-target) for a copy button; without JS it's
   just a read-only display. Add .od-snippet--wrap when the value can run
   long and should wrap instead of truncating.
     <div class="od-snippet"><code>docker pull registry.example.com/app:latest</code></div>
*/
.od-snippet { display: flex; align-items: center; gap: 14px; background: var(--od-dark); color: var(--od-dark-text); border-radius: var(--od-radius-lg); padding: 13px 14px; }
.od-snippet code { flex: 1; min-width: 0; color: var(--od-dark-text); font-family: var(--od-font-mono); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-snippet--wrap { align-items: flex-start; }
.od-snippet--wrap code { white-space: pre-wrap; overflow: visible; text-overflow: clip; word-break: break-all; }

/* ============================== 71. Checkbox / radio (custom) ============================== */
/* CSS-only custom controls over a real <input> — same hidden-input-plus-
   sibling-box technique as .od-switch, so keyboard/screen-reader behavior
   comes from the native element for free.
     <label class="od-checkbox"><input type="checkbox"><span class="od-checkbox-box"></span>Label</label>
     <label class="od-radio"><input type="radio" name="g"><span class="od-radio-box"></span>Label</label>
   Wrap several in .od-check-group (vertical) or .od-check-group--inline
   (wraps horizontally) for consistent spacing. Indeterminate checkboxes
   still need `el.indeterminate = true` in JS — CSS only reads that state. */
.od-checkbox, .od-radio { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--od-text-secondary); user-select: none; }
.od-checkbox input, .od-radio input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.od-checkbox-box, .od-radio-box { position: relative; flex-shrink: 0; width: 18px; height: 18px; border: 1px solid var(--od-border); background: var(--od-panel); transition: background .15s ease, border-color .15s ease; }
.od-checkbox-box { border-radius: 5px; }
.od-radio-box { border-radius: 50%; }
/* top/left 50% + translate(-50%,-50%) self-centers regardless of box size —
   deliberately not a rotated-border-trick tick (top/left offsets tuned by
   eye against one box size, wrong the moment padding/border/size changes).
   The mask reuses .od-i-check's own path, so the tick matches the icon set. */
.od-checkbox-box::before {
    content: ""; position: absolute; top: 50%; left: 50%; width: 12px; height: 12px; background: #fff;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%206L9%2017l-5-5%22/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%206L9%2017l-5-5%22/%3E%3C/svg%3E");
    -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    transform: translate(-50%, -50%) scale(0); transition: transform .12s ease;
}
.od-radio-box::before { content: ""; position: absolute; top: 50%; left: 50%; width: 8px; height: 8px; border-radius: 50%; background: #fff; transform: translate(-50%, -50%) scale(0); transition: transform .12s ease; }
.od-checkbox input:checked ~ .od-checkbox-box, .od-radio input:checked ~ .od-radio-box { background: var(--od-accent); border-color: var(--od-accent); }
.od-checkbox input:checked ~ .od-checkbox-box::before { transform: translate(-50%, -50%) scale(1); }
.od-radio input:checked ~ .od-radio-box::before { transform: translate(-50%, -50%) scale(1); }
.od-checkbox input:indeterminate ~ .od-checkbox-box { background: var(--od-accent); border-color: var(--od-accent); }
.od-checkbox input:indeterminate ~ .od-checkbox-box::before { width: 10px; height: 2px; border-radius: 1px; -webkit-mask-image: none; mask-image: none; transform: translate(-50%, -50%) scale(1); }
.od-checkbox input:focus-visible ~ .od-checkbox-box, .od-radio input:focus-visible ~ .od-radio-box { outline: 2px solid var(--od-accent); outline-offset: 2px; }
.od-checkbox input:disabled ~ .od-checkbox-box, .od-radio input:disabled ~ .od-radio-box { opacity: .5; cursor: not-allowed; }
.od-checkbox:has(input:disabled), .od-radio:has(input:disabled) { cursor: not-allowed; opacity: .7; }
.od-check-group { display: flex; flex-direction: column; gap: 10px; }
.od-check-group--inline { flex-direction: row; flex-wrap: wrap; gap: 8px 18px; }

/* ============================== 72. Calendar: date range mode ============================== */
/* Opt a .od-calendar into range selection by adding data-od-calendar-range
   (see od-calendar.js) — the single-date contract above is untouched.
     <div class="od-calendar" data-od-calendar data-od-calendar-range
          data-od-calendar-start="2026-09-03" data-od-calendar-end="2026-09-08"></div>
   First click sets the start (clears any end); a later click sets the end
   and highlights the days between. Clicking before the current start
   restarts the range from that day. */
.od-calendar-day--in-range { background: var(--od-accent-bg); color: var(--od-accent-text); border-radius: var(--od-radius-sm); }
.od-calendar-day--range-start, .od-calendar-day--range-end,
.od-calendar-day--range-start:hover, .od-calendar-day--range-end:hover { background: var(--od-accent); color: #fff; font-weight: 800; }

/* ============================== 73. Toast: action button ============================== */
/* Pass { action: { label, onClick } } to OpenDashboard.Toast.show(), or use
   the OpenDashboard.Toast.undo(message, onUndo, options) shortcut — see
   od-toast.js. The action button dismisses the toast after onClick runs. */
/* Color is a fixed light-blue, not var(--od-accent-text): the toast surface
   is the always-dark var(--od-dark) regardless of page theme, and the
   light-theme value of --od-accent-text doesn't clear contrast on it. */
.od-toast-action { flex-shrink: 0; background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px; font-weight: 800; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; color: #58A6FF; }
.od-toast-action:hover { opacity: .85; }

/* ============================== 74. Meta bar (compact topbar stat row) ============================== */
/* A row of small label/value pairs for a topbar or page header — queue
   counts, a timer, budget remaining. Compose the value with .od-progress
   (Progress bar, section 21) for a mini bar, or .od-pill for a badge. */
.od-meta-bar { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.od-meta-item { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.od-meta-item--row { flex-direction: row; align-items: center; gap: 8px; }
.od-meta-label { font-size: 10.5px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase; color: var(--od-text-muted-2); white-space: nowrap; }
.od-meta-value { font-size: 12.5px; font-weight: 700; color: var(--od-text); white-space: nowrap; }
.od-meta-bar-track { width: 64px; flex-shrink: 0; }
.od-meta-divider { width: 1px; height: 22px; background: var(--od-border); flex-shrink: 0; }

/* ============================== 75. Queue list (ticket/task sidebar) ============================== */
/* A denser alternative to the sidebar nav list (section 3) for a queue of
   work items — an id, a two-line title, tag text, and an optional thin
   progress bar per item. Mount inside .od-sidebar, or the side pane of a
   Split view (section 77), depending on layout. */
.od-queue-list { display: flex; flex-direction: column; gap: 2px; }
.od-queue-item { display: block; padding: 9px 10px; border-radius: var(--od-radius); border-left: 2px solid transparent; cursor: pointer; }
.od-queue-item:hover { background: var(--od-nav-hover); }
.od-queue-item.is-active { background: var(--od-accent-bg); border-left-color: var(--od-accent); }
.od-queue-id { font-size: 11px; font-weight: 800; color: var(--od-text-muted-2); letter-spacing: .3px; }
.od-queue-item.is-active .od-queue-id { color: var(--od-accent-text); }
.od-queue-title { font-size: 12.5px; font-weight: 600; color: var(--od-text); margin-top: 2px; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.od-queue-tags { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.od-queue-tag { font-size: 10.5px; font-weight: 700; color: var(--od-text-muted); }
.od-queue-progress { height: 3px; border-radius: 999px; background: var(--od-border-light); margin-top: 8px; overflow: hidden; }
.od-queue-progress-fill { height: 100%; border-radius: inherit; background: var(--od-accent); }
.od-queue-progress-fill--success { background: var(--od-success); }
.od-queue-progress-fill--warning { background: var(--od-warning); }
.od-queue-progress-fill--danger { background: var(--od-danger); }
.od-queue-section { font-size: 10.5px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase; color: var(--od-text-muted-2); padding: 12px 10px 6px; }

/* ============================== 76. Flow steps (workflow status bar) ============================== */
/* A horizontal run of named stages (an approval chain, an agent pipeline) —
   like Stepper (section 32), but each stage is a wider box carrying its own
   one-line status text instead of just a numbered circle. Presentational
   only: toggle .is-done/.is-active yourself as the workflow advances. */
.od-flow-steps { display: flex; border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); overflow: hidden; }
.od-flow-step { flex: 1; min-width: 0; position: relative; padding: 10px 14px; background: var(--od-panel); border-right: 1px solid var(--od-border); }
.od-flow-step:last-child { border-right: 0; }
.od-flow-step-title { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 800; color: var(--od-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-flow-step-status { font-size: 11px; font-weight: 600; color: var(--od-text-muted-2); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-flow-step.is-active { background: var(--od-accent-bg); }
.od-flow-step.is-active .od-flow-step-title { color: var(--od-accent-text); }
.od-flow-step.is-active .od-flow-step-status { color: var(--od-accent-text); opacity: .85; }
.od-flow-step.is-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--od-accent); }
.od-flow-step.is-done .od-flow-step-title { color: var(--od-success-text); }
.od-flow-step.is-done .od-flow-step-title .od-i { color: var(--od-success-text); }
.od-flow-step.is-done .od-flow-step-status { color: var(--od-text-muted); }
@media (max-width: 820px) {
    .od-flow-steps { overflow-x: auto; border-radius: var(--od-radius); }
    .od-flow-step { min-width: 150px; }
}

/* ============================== 77. Split view (master-detail layout) ============================== */
/* A two-pane layout — a narrow list/tree pane next to a main content pane.
   Give the wrapper a definite height (e.g. mount it inside .od-main--flush,
   section 3) so the panes' own scrolling has something to clip against.
   .od-split-main deliberately doesn't scroll itself — put a
   .od-split-main-body (or your own flex:1;min-height:0;overflow-y:auto
   wrapper) inside it, followed by a Composer (section 79) pinned below,
   so the composer stays flush at the bottom with no extra positioning.
   Add .od-split--panel to the OUTER .od-split wrapper (never to the side/main
   panes individually — two separately-bordered/rounded boxes never quite
   line up into one shape, and the mobile media query below only knows how
   to resize one shared border on the wrapper) for a bordered, rounded card
   around the whole thing; the shared border-radius + overflow:hidden there
   clips both panes' corners together automatically, no per-pane radius
   overrides needed. */
.od-split { display: flex; align-items: stretch; min-height: 0; }
/* The 240px fallback is what every existing .od-split-side already rendered
   at before this variable existed, so plain markup with no JS is untouched;
   od-resizable.js (optional) writes --od-split-side-w on the .od-split
   itself as the user drags .od-split-resize-handle below. */
.od-split-side { flex: 0 0 var(--od-split-side-w, 240px); background: var(--od-panel); border-right: 1px solid var(--od-border); overflow-y: auto; min-height: 0; }
.od-split-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--od-panel); overflow: hidden; }
.od-split-main-body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px; }
.od-split--panel { border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); overflow: hidden; }
.od-split-file-list { display: flex; flex-direction: column; }
/* Resize handle: an optional third flex child between .od-split-side and
   .od-split-main — see od-resizable.js (data-od-resizable on .od-split).
   Purely presentational without the script: a static 1px divider. */
.od-split-resize-handle { flex-shrink: 0; width: 9px; margin: 0 -4px; cursor: col-resize; position: relative; z-index: 1; touch-action: none; }
.od-split-resize-handle::after { content: ""; position: absolute; top: 0; bottom: 0; left: 4px; width: 1px; background: var(--od-border); }
.od-split-resize-handle:hover::after, .od-split-resize-handle.is-dragging::after { left: 3px; width: 2px; background: var(--od-accent); }
.od-split-file { display: flex; align-items: center; gap: 8px; padding: 9px 12px; font-family: var(--od-font-mono); font-size: 12.5px; color: var(--od-text-secondary); border-bottom: 1px solid var(--od-border-light); cursor: pointer; }
.od-split-file:hover { background: var(--od-nav-hover); }
.od-split-file.is-active { background: var(--od-accent-bg); color: var(--od-accent-text); font-weight: 700; }
@media (max-width: 820px) {
    /* Below the breakpoint, stop fighting over a fixed height (a .od-split
       mounted in .od-main--flush only gets flex:1 sizing from a desktop-only
       rule you write yourself, see the Agent workflow demo) and let both
       panes size to their own content instead, each with its own internal
       scrolling turned off — .od-main--flush now scrolls the whole page on
       mobile (section 3), so a nested scroller here would just fight that
       one for the same swipe gesture.
       .od-split itself also needs resetting here, not just its consumer-set
       flex:1: the base rule above sets `min-height: 0` (so a *desktop*
       .od-split can shrink and hand scrolling to its panes) which — with no
       explicit `flex` at all once the desktop-only flex:1 rule stops
       applying — still lets the default `flex-shrink: 1` squeeze it down to
       whatever's left in .od-main--flush, silently clipping the rest behind
       `.od-split--panel`'s `overflow: hidden` with no way to reach it, since
       .od-main--flush has nothing left to scroll either (.od-split already
       "fit"). `flex: none` + `min-height: auto` restores plain content-based
       sizing, so .od-split grows to fit both panes and .od-main--flush's
       page scroll actually reaches everything. */
    .od-split { flex: none; min-height: auto; flex-direction: column; }
    .od-split-side { flex-basis: auto; max-height: none; overflow-y: visible; border-right: 0; border-bottom: 1px solid var(--od-border); }
    .od-split-main { overflow: visible; }
    .od-split-main-body { overflow-y: visible; }
    .od-split-resize-handle { display: none; }
}

/* ============================== 78. Console (agent/CI activity log) ============================== */
/* A running transcript of commands and narrative text — an AI agent
   transcript, a CI job log. Compose expandable output with Disclosure
   (section 67):
     <details class="od-disclosure od-console-result">
       <summary><span class="od-caret"></span> wynik narzędzia</summary>
       <div class="od-console-result-body">...</div>
     </details>
   .od-console-group nests a labeled sub-run (e.g. a multi-step "Explore")
   inside the same transcript. */
.od-console { display: flex; flex-direction: column; gap: 12px; }
.od-console-cmd { display: flex; align-items: baseline; gap: 8px; font-family: var(--od-font-mono); font-size: 12.5px; flex-wrap: wrap; }
.od-console-tool { flex-shrink: 0; font-weight: 800; color: var(--od-accent-text); }
.od-console-code { color: var(--od-text-secondary); overflow-x: auto; white-space: pre; min-width: 0; }
.od-console-text { font-size: 13px; line-height: 1.65; color: var(--od-text-secondary); }
.od-console-text strong { color: var(--od-text); }
.od-console-result summary { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--od-text-muted); }
.od-console-result-body { margin-top: 6px; padding: 10px 12px; background: var(--od-border-light); border-radius: var(--od-radius); font-family: var(--od-font-mono); font-size: 12px; color: var(--od-text-secondary); overflow-x: auto; }
.od-console-group { border: 1px solid var(--od-border); border-radius: var(--od-radius); padding: 10px 12px; }
.od-console-group-title { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 800; }
.od-console-group-meta { font-size: 11.5px; color: var(--od-text-muted); margin-top: 4px; }

/* ============================== 79. Composer (chat/agent input bar) ============================== */
/* A bottom action bar for a chat/agent interface — a row of contextual
   buttons above a growable textarea + send button, plus a small hint line.
   Its own border-top already separates it from scrolled content above, so
   it sits flush at the bottom of a .od-split-main or .od-main--flush
   column with no extra positioning. */
.od-composer { flex-shrink: 0; border-top: 1px solid var(--od-border); background: var(--od-panel); padding: 12px 16px; }
.od-composer-actions { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.od-composer-row { display: flex; align-items: flex-end; gap: 8px; }
.od-composer-row .od-textarea { flex: 1; min-width: 0; resize: none; }
.od-composer-hint { font-size: 11px; color: var(--od-text-muted); margin-top: 6px; }

/* ============================== 80. Diff viewer ============================== */
/* A unified diff — added/removed/context lines with their own gutters, next
   to (or composed inside) Code block (section 66). Fixed palette, same
   reasoning as the syntax tokens there: the surface is always dark
   regardless of page theme, so the add/remove tints don't need a light-mode
   variant. Each line is its own flex row rather than one <pre> so an
   added/removed background never has to line up with monospace padding by
   hand:
     <div class="od-diff">
       <div class="od-diff-line od-diff-line--hunk"><span class="od-diff-text">@@ -12,6 +12,7 @@</span></div>
       <div class="od-diff-line"><span class="od-diff-gutter">12</span><span class="od-diff-gutter">12</span><span class="od-diff-marker"> </span><span class="od-diff-text">  const x = 1;</span></div>
       <div class="od-diff-line od-diff-line--del"><span class="od-diff-gutter">13</span><span class="od-diff-gutter"></span><span class="od-diff-marker">-</span><span class="od-diff-text">  return x + 1;</span></div>
       <div class="od-diff-line od-diff-line--add"><span class="od-diff-gutter"></span><span class="od-diff-gutter">13</span><span class="od-diff-marker">+</span><span class="od-diff-text">  return x + 2;</span></div>
     </div>
   Wrap in .od-code-block (with a .od-code-toolbar for a file name + Copy
   to clipboard button) the same way a plain .od-code does — .od-diff picks
   up the same flush-corner treatment automatically. */
.od-diff { background: var(--od-dark); color: var(--od-dark-text); border-radius: var(--od-radius-lg); overflow-x: auto; font-family: var(--od-font-mono); font-size: 12px; line-height: 1.55; padding: 6px 0; }
.od-diff-line { display: flex; white-space: pre; min-width: max-content; }
.od-diff-gutter { flex-shrink: 0; width: 38px; padding: 0 6px; text-align: right; color: var(--od-dark-text); opacity: .35; user-select: none; }
.od-diff-marker { flex-shrink: 0; width: 16px; text-align: center; opacity: .7; user-select: none; }
.od-diff-text { flex: 1; min-width: 0; padding-right: 16px; }
.od-diff-line--add { background: rgba(63, 185, 80, .15); }
.od-diff-line--add .od-diff-marker, .od-diff-line--add .od-diff-text { color: #7EE2A8; }
.od-diff-line--del { background: rgba(248, 81, 73, .15); }
.od-diff-line--del .od-diff-marker, .od-diff-line--del .od-diff-text { color: #FF9492; }
.od-diff-line--hunk { color: var(--od-accent-text); background: rgba(88, 166, 255, .08); padding: 3px 14px; opacity: .9; }
.od-code-block .od-diff, .od-code-tabs .od-diff { border-radius: 0; }

/* ============================== 81. Bulk action bar ============================== */
/* Replaces a table's own toolbar (section 9) while rows are selected — show
   it yourself (toggle .is-active, or swap it in for .od-table-toolbar) once
   a checkbox in the table body is checked, and update the count text as the
   selection changes; presentational only, no bundled JS, since "how rows
   get selected" varies too much to standardize (a plain checkbox column, a
   whole-row click, Sortable's own drag handles, ...). */
.od-bulk-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--od-accent-bg); border-bottom: 1px solid var(--od-border); flex-wrap: wrap; }
/* The plain `hidden` attribute has very low (UA-stylesheet) priority, so
   the `display: flex` above would otherwise win and keep it visible —
   same fix as .od-dropdown-menu[hidden] (section 12). Toggle the `hidden`
   attribute itself (`bar.hidden = ...`), not inline `style.display`. */
.od-bulk-bar[hidden] { display: none; }
.od-bulk-bar-info { font-size: 13px; font-weight: 700; color: var(--od-accent-text); }
.od-bulk-bar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-wrap: wrap; }

/* ============================== 82. File tree ============================== */
/* Nested, expandable folders — native <details>/<summary> per folder, same
   zero-JS mechanism as Disclosure (section 67) and Sidebar submenu (section
   33); reuse .od-caret (section 67) for the rotating arrow. A denser,
   hierarchical alternative to the flat Split view file list (section 77)
   for a real project tree. */
.od-tree { font-size: 12.5px; }
.od-tree-node > summary.od-tree-toggle { list-style: none; }
.od-tree-node > summary.od-tree-toggle::-webkit-details-marker { display: none; }
.od-tree-toggle { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: var(--od-radius-sm); color: var(--od-text-secondary); font-weight: 600; cursor: pointer; }
.od-tree-toggle:hover { background: var(--od-nav-hover); }
.od-tree-toggle .od-i { color: var(--od-text-muted); }
.od-tree-children { display: flex; flex-direction: column; gap: 1px; margin: 1px 0 2px 9px; padding-left: 12px; border-left: 1px solid var(--od-border-light); }
.od-tree-file { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: var(--od-radius-sm); color: var(--od-text-secondary); font-family: var(--od-font-mono); font-size: 12px; cursor: pointer; }
.od-tree-file:hover { background: var(--od-nav-hover); }
.od-tree-file.is-active { background: var(--od-accent-bg); color: var(--od-accent-text); font-weight: 700; }
.od-tree-file .od-i { color: var(--od-text-muted); flex-shrink: 0; }
.od-tree-file.is-active .od-i { color: var(--od-accent-text); }

/* ============================== 83. Meter (budget / usage) ============================== */
/* Progress bar (section 21) plus the label row a budget/usage reading
   almost always needs next to it — "how much" and "of what". Add
   .od-meter--warning/--over yourself once the value crosses your own
   thresholds; .od-meter doesn't know what the numbers mean, only how to
   color them once you've decided. */
.od-meter-track { height: 8px; border-radius: 999px; background: var(--od-border-light); overflow: hidden; }
.od-meter-fill { height: 100%; border-radius: inherit; background: var(--od-accent); transition: width .2s ease; }
.od-meter--warning .od-meter-fill { background: var(--od-warning); }
.od-meter--over .od-meter-fill { background: var(--od-danger); }
.od-meter-labels { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); margin-top: 5px; }
.od-meter--warning .od-meter-labels .od-meter-value { color: var(--od-warning-text); }
.od-meter--over .od-meter-labels .od-meter-value { color: var(--od-danger-text); }
.od-meter-value { font-weight: 800; color: var(--od-text); }

/* ============================== 84. Input group ============================== */
/* A text input with a fixed prefix/suffix "addon" and/or a trailing button
   glued to it — `https://` + host, subdomain + `.example.com`, a size + `MB`,
   an API key + "Copy". Children sit flush (overlapping 1px borders, outer
   corners only) so the whole group reads as one control:
     <div class="od-input-group">
       <span class="od-input-addon">https://</span>
       <input type="text" class="od-input" placeholder="www">
       <span class="od-input-addon">.example.com</span>
       <button type="button" class="od-btn-ghost">Check</button>
     </div>
   Any button variant (.od-btn, .od-btn-ghost, .od-btn-neutral, .od-btn-danger)
   works as a trailing/leading child — it stretches to the input's height. */
.od-input-group { display: flex; align-items: stretch; width: 100%; }
.od-input-group > * { border-radius: 0; margin: 0; }
.od-input-group > .od-input, .od-input-group > .od-select { flex: 1; min-width: 0; width: auto; }
.od-input-group > * + * { margin-left: -1px; }
.od-input-group > :first-child { border-top-left-radius: var(--od-radius); border-bottom-left-radius: var(--od-radius); }
.od-input-group > :last-child { border-top-right-radius: var(--od-radius); border-bottom-right-radius: var(--od-radius); }
.od-input-group > .od-input:focus, .od-input-group > .od-select:focus, .od-input-group > :focus-visible { position: relative; z-index: 1; }
.od-input-addon { display: inline-flex; align-items: center; padding: 0 11px; border: 1px solid var(--od-border); background: var(--od-border-light); color: var(--od-text-muted); font-size: 13px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.od-input-addon .od-i { font-size: 14px; }
.od-input-group .od-btn-icon { width: 36px; height: auto; }
.od-field.has-error .od-input-group .od-input { border-color: var(--od-danger); }

/* ============================== 85. Input sizes & leading icon ============================== */
/* Size modifiers for .od-input/.od-select (a toolbar search, a dense filter
   row) and a wrapper that puts an icon inside the input's left padding:
     <div class="od-input-wrap">
       <i class="od-i od-i-search" aria-hidden="true"></i>
       <input type="search" class="od-input od-input--sm" placeholder="Search…">
       <button type="button" class="od-input-clear" aria-label="Clear">&times;</button>
     </div>
   .od-input-clear is presentational — clear the value yourself (or with
   `type="search"` and the browser's own clear). */
.od-input--sm, .od-select--sm { font-size: 12.5px; padding: 6px 10px; }
.od-input--lg, .od-select--lg { font-size: 15px; padding: 11px 14px; }
.od-input-wrap { position: relative; display: flex; align-items: center; }
.od-input-wrap > .od-i:first-child { position: absolute; left: 10px; color: var(--od-text-muted); font-size: 14px; pointer-events: none; }
.od-input-wrap > .od-i:first-child ~ .od-input { padding-left: 32px; }
.od-input-wrap > .od-i:first-child ~ .od-input--sm { padding-left: 30px; }
.od-input-clear { position: absolute; right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 0; border-radius: 999px; background: none; color: var(--od-text-muted); font-size: 16px; line-height: 1; cursor: pointer; padding: 0; }
.od-input-clear:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-input-wrap:has(.od-input-clear) .od-input { padding-right: 32px; }
.od-input[type="search"]::-webkit-search-cancel-button { display: none; }

/* ============================== 86. Password field ============================== */
/* A password input with a show/hide toggle, an optional "Generate" button
   and a 4-segment strength bar, driven by od-password.js — the script
   scores the value (length + character classes, minus obvious repeats)
   and writes data-od-strength="0"–"4" on the root; this CSS only colors
   the segments for whichever score it finds:
     <div class="od-password" data-od-password data-od-password-length="16">
       <div class="od-input-group">
         <input type="password" class="od-input" data-od-password-input>
         <button type="button" class="od-btn-ghost od-btn-icon" data-od-password-toggle aria-label="Show password"><i class="od-i od-i-eye" aria-hidden="true"></i></button>
         <button type="button" class="od-btn-ghost" data-od-password-generate>Generate</button>
       </div>
       <div class="od-password-strength" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
       <div class="od-field-help" data-od-password-label></div>
     </div> */
.od-password-strength { display: flex; gap: 4px; margin-top: 8px; }
.od-password-strength span { flex: 1; height: 4px; border-radius: 999px; background: var(--od-border); transition: background .15s ease; }
[data-od-strength="1"] .od-password-strength span:nth-child(-n+1) { background: var(--od-danger); }
[data-od-strength="2"] .od-password-strength span:nth-child(-n+2) { background: var(--od-warning); }
[data-od-strength="3"] .od-password-strength span:nth-child(-n+3) { background: var(--od-accent); }
[data-od-strength="4"] .od-password-strength span:nth-child(-n+4) { background: var(--od-success); }
[data-od-strength="1"] [data-od-password-label] { color: var(--od-danger-text); }
[data-od-strength="2"] [data-od-password-label] { color: var(--od-warning-text); }
[data-od-strength="4"] [data-od-password-label] { color: var(--od-success-text); }
.od-password .od-input { font-family: var(--od-font-mono); letter-spacing: .2px; }

/* ============================== 87. Tag input ============================== */
/* Free-text multi-value entry — domain aliases, an IP allowlist, forward
   addresses — as chips inside one bordered control (od-tags.js). Reuses
   Chip / tag (section 42) for each value, and mirrors the values into
   hidden inputs so they submit with the form:
     <div class="od-tags" data-od-tags data-od-tags-name="aliases[]" data-od-tags-value="www.example.com,mail.example.com">
       <input type="text" class="od-tags-input" placeholder="Add alias…">
     </div> */
.od-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; min-height: 38px; padding: 5px 8px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); cursor: text; }
.od-tags.is-focused { border-color: var(--od-accent); }
.od-tags.is-invalid { border-color: var(--od-danger); }
.od-tags-input { flex: 1; min-width: 80px; border: 0; background: none; font: inherit; font-size: 13.5px; color: var(--od-text); outline: none; padding: 3px 2px; }
.od-tags-input::placeholder { color: var(--od-text-muted); }
.od-tags .od-chip { font-family: var(--od-font-mono); font-size: 11.5px; }
.od-tags.is-disabled { background: var(--od-border-light); cursor: not-allowed; }

/* ============================== 88. Repeater (dynamic rows) ============================== */
/* Add/remove rows of fields — DNS records, env variables, firewall rules
   (od-repeater.js). New rows are cloned from a <template>; `{i}` in any
   name/id/for attribute becomes a running index. A header row of labels
   uses the same .od-repeater-cell sizing as the rows below so the columns
   line up:
     <div class="od-repeater" data-od-repeater data-od-repeater-min="1">
       <div class="od-repeater-head"><div class="od-repeater-cell od-repeater-cell--sm">Type</div><div class="od-repeater-cell">Name</div><div class="od-repeater-cell">Value</div><div class="od-repeater-cell od-repeater-cell--actions"></div></div>
       <div data-od-repeater-rows>
         <div class="od-repeater-row" data-od-repeater-row>
           <div class="od-repeater-cell od-repeater-cell--sm"><select class="od-select" name="dns[0][type]">…</select></div>
           <div class="od-repeater-cell"><input class="od-input" name="dns[0][name]"></div>
           <div class="od-repeater-cell"><input class="od-input" name="dns[0][value]"></div>
           <div class="od-repeater-cell od-repeater-cell--actions"><button type="button" class="od-btn-ghost od-btn-icon" data-od-repeater-remove aria-label="Remove">&times;</button></div>
         </div>
       </div>
       <template data-od-repeater-template><div class="od-repeater-row" data-od-repeater-row>… name="dns[{i}][type]" …</div></template>
       <button type="button" class="od-btn-ghost od-btn-sm" data-od-repeater-add>+ Add record</button>
     </div> */
.od-repeater-head, .od-repeater-row { display: flex; align-items: flex-start; gap: 8px; }
.od-repeater-row { margin-bottom: 8px; }
.od-repeater-row.is-leaving { opacity: 0; transform: translateX(6px); transition: opacity .12s ease, transform .12s ease; }
.od-repeater-head { margin-bottom: 6px; font-size: 11px; font-weight: 700; letter-spacing: .4px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-repeater-cell { flex: 1; min-width: 0; }
.od-repeater-cell .od-field { margin-bottom: 0; }
.od-repeater-cell--sm { flex: 0 0 110px; }
.od-repeater-cell--xs { flex: 0 0 72px; }
.od-repeater-cell--actions { flex: 0 0 36px; display: flex; justify-content: flex-end; }
.od-repeater-cell--actions .od-btn-icon { width: 36px; height: 36px; }
.od-repeater-empty { padding: 14px; text-align: center; font-size: 13px; color: var(--od-text-muted); border: 1px dashed var(--od-border); border-radius: var(--od-radius); margin-bottom: 8px; }
.od-repeater-empty[hidden] { display: none; }
@media (max-width: 640px) {
    .od-repeater-head { display: none; }
    .od-repeater-row { flex-wrap: wrap; padding-bottom: 8px; border-bottom: 1px solid var(--od-border-light); }
    .od-repeater-cell, .od-repeater-cell--sm { flex: 1 1 45%; }
    .od-repeater-cell--actions { flex: 0 0 36px; margin-left: auto; }
}

/* ============================== 89. Horizontal form layout ============================== */
/* Label column on the left, control on the right — the classic control-
   panel settings form (HestiaCP, Plesk, cPanel), for pages with many short
   fields where a stacked .od-field per row wastes vertical space. Rows
   carry a light separator; sections group them under a heading. Collapses
   to stacked labels below 640px:
     <form class="od-form">
       <div class="od-form-section">
         <div class="od-form-section-title">General</div>
         <div class="od-form-row">
           <label class="od-form-row-label" for="f1">Domain name <span class="od-field-help">Without www</span></label>
           <div class="od-form-row-control"><input id="f1" class="od-input"></div>
         </div>
         <div class="od-form-row od-form-row--center">
           <span class="od-form-row-label">SSL</span>
           <div class="od-form-row-control"><label class="od-switch">…</label></div>
         </div>
       </div>
     </form> */
.od-form-row { display: grid; grid-template-columns: minmax(140px, 200px) minmax(0, 1fr); gap: 4px 24px; align-items: start; padding: 12px 0; border-bottom: 1px solid var(--od-border-light); }
.od-form-row:last-child { border-bottom: 0; }
.od-form-row--center { align-items: center; }
.od-form-row--center .od-form-row-label { padding-top: 0; }
.od-form-row-label { display: block; padding-top: 8px; font-size: 13px; font-weight: 700; color: var(--od-text-secondary); }
.od-form-row-label .od-field-help { display: block; margin-top: 2px; font-weight: 500; }
.od-form-row-control { min-width: 0; }
.od-form-row-control .od-field { margin-bottom: 0; }
.od-form-row-control > .od-field-help { margin-top: 6px; }
.od-form-row--narrow .od-form-row-control { max-width: 320px; }
.od-form-section { margin-top: 26px; }
.od-form-section:first-child { margin-top: 0; }
.od-form-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--od-border); font-size: 13px; font-weight: 800; }
.od-form-section-desc { font-size: 12.5px; color: var(--od-text-muted); margin-top: 6px; }
@media (max-width: 640px) {
    .od-form-row { grid-template-columns: 1fr; gap: 6px; }
    .od-form-row-label { padding-top: 0; }
}

/* ============================== 90. Number stepper ============================== */
/* A numeric input with −/+ buttons (od-number.js) for quotas and counts:
   mailboxes, databases, GB of disk. The script respects the input's own
   min/max/step and disables the button at each bound:
     <div class="od-number" data-od-number>
       <button type="button" data-od-number-dec aria-label="Decrease">&minus;</button>
       <input type="number" class="od-input" value="5" min="0" max="50" step="1">
       <button type="button" data-od-number-inc aria-label="Increase">+</button>
     </div>
   Add .od-number--suffix + a trailing <span class="od-number-unit">GB</span>
   for a unit label after the value. */
.od-number { display: inline-flex; align-items: stretch; }
.od-number > button { display: inline-flex; align-items: center; justify-content: center; width: 34px; border: 1px solid var(--od-border); background: var(--od-panel); color: var(--od-text-secondary); font: inherit; font-size: 16px; line-height: 1; cursor: pointer; padding: 0; }
.od-number > button:first-child { border-radius: var(--od-radius) 0 0 var(--od-radius); }
.od-number > button:last-child { border-radius: 0 var(--od-radius) var(--od-radius) 0; }
.od-number > button:hover:not(:disabled) { background: var(--od-nav-hover); color: var(--od-text); }
.od-number > button:disabled { opacity: .4; cursor: not-allowed; }
.od-number > .od-input { width: 64px; text-align: center; border-radius: 0; margin: 0 -1px; -moz-appearance: textfield; appearance: textfield; position: relative; }
.od-number > .od-input:focus { z-index: 1; }
.od-number > .od-input::-webkit-outer-spin-button, .od-number > .od-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.od-number-unit { display: inline-flex; align-items: center; padding: 0 10px; border: 1px solid var(--od-border); border-left: 0; background: var(--od-border-light); color: var(--od-text-muted); font-size: 12.5px; font-weight: 600; }
.od-number--suffix > button:last-of-type { border-radius: 0; }
.od-number--suffix > .od-number-unit { border-radius: 0 var(--od-radius) var(--od-radius) 0; }

/* ============================== 91. Save bar (unsaved changes) ============================== */
/* A sticky footer that appears once a form has unsaved edits — "You have
   unsaved changes · Discard / Save". Sticks to the bottom of whatever
   scrolls (.od-main by default), so it stays in view however long the
   form is. od-dirty.js shows/hides it by diffing the form against a
   snapshot taken at load; toggle its `hidden` attribute yourself if you'd
   rather track dirtiness some other way:
     <form data-od-dirty data-od-dirty-warn>
       …fields…
       <div class="od-save-bar" data-od-dirty-bar hidden>
         <i class="od-i od-i-info" aria-hidden="true"></i>
         <span class="od-save-bar-text">You have unsaved changes</span>
         <div class="od-save-bar-actions">
           <button type="reset" class="od-btn-ghost">Discard</button>
           <button type="submit" class="od-btn">Save changes</button>
         </div>
       </div>
     </form> */
.od-save-bar { position: sticky; bottom: 0; z-index: 20; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px; padding: 12px 18px; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); box-shadow: 0 -8px 30px rgba(17, 22, 31, .10); animation: od-save-bar-in .18s ease; }
.od-save-bar[hidden] { display: none; }
.od-save-bar > .od-i { color: var(--od-warning); font-size: 16px; }
.od-save-bar-text { font-size: 13px; font-weight: 700; }
.od-save-bar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
@keyframes od-save-bar-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* Stuck to the very bottom of .od-main, the bar would otherwise sit flush
   against the scroller's edge — a small bottom offset lets it float a
   little above it, like a toast. */
.od-main > form > .od-save-bar, .od-main > .od-save-bar { bottom: 12px; }

/* ============================== 92. Tool grid (icon launcher) ============================== */
/* A grid of icon tiles linking to tools — "File Manager · Databases · SSL ·
   PHP · Cron", the per-domain launcher every hosting panel has. Each tile
   is a plain <a>/<button>; an optional .od-tool-sub carries a one-line
   status ("Let's Encrypt · 82 days"). .od-tool-grid--inline switches to a
   compact wrapping row (icon left, label right) for a card footer:
     <div class="od-tool-grid">
       <a href="#" class="od-tool"><span class="od-tool-icon"><i class="od-i od-i-folder" aria-hidden="true"></i></span><span class="od-tool-label">File Manager</span></a>
       <a href="#" class="od-tool"><span class="od-tool-icon od-tool-icon--success"><i class="od-i od-i-shield-check" aria-hidden="true"></i></span><span class="od-tool-label">SSL/TLS</span><span class="od-tool-sub">Let's Encrypt</span></a>
     </div> */
.od-tool-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 8px; }
.od-tool { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 6px; padding: 14px 10px 12px; border: 1px solid transparent; border-radius: var(--od-radius); background: transparent; color: var(--od-text); font: inherit; text-align: center; text-decoration: none; cursor: pointer; }
.od-tool:hover { background: var(--od-nav-hover); border-color: var(--od-border-light); }
.od-tool:focus-visible { outline: 2px solid var(--od-accent); outline-offset: -2px; }
.od-tool.is-disabled { opacity: .5; pointer-events: none; }
.od-tool-icon { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 12px; background: var(--od-accent-bg); color: var(--od-accent-text); font-size: 18px; flex-shrink: 0; }
.od-tool-icon--success { background: var(--od-success-bg); color: var(--od-success-text); }
.od-tool-icon--warning { background: var(--od-warning-bg); color: var(--od-warning-text); }
.od-tool-icon--danger { background: var(--od-danger-bg); color: var(--od-danger-text); }
.od-tool-icon--neutral { background: var(--od-border-light); color: var(--od-text-secondary); }
.od-tool-label { font-size: 12.5px; font-weight: 700; line-height: 1.3; }
.od-tool-sub { font-size: 11px; font-weight: 600; color: var(--od-text-muted); line-height: 1.3; }
.od-tool-badge { position: absolute; top: 8px; right: 8px; }
.od-tool { position: relative; }
.od-tool-grid--inline { display: flex; flex-wrap: wrap; gap: 4px 6px; }
.od-tool-grid--inline .od-tool { flex-direction: row; gap: 8px; padding: 6px 10px 6px 6px; text-align: left; }
.od-tool-grid--inline .od-tool-icon { width: 28px; height: 28px; border-radius: var(--od-radius-sm); font-size: 14px; }
.od-tool-grid--inline .od-tool-sub { display: none; }

/* ============================== 93. Resource card ============================== */
/* A domain / server / database "card" — an icon, a title with status pill,
   a one-line meta row and trailing actions in the head; a body (typically
   an inline Tool grid, section 92) and a footer (Quota rows, section 94)
   below. The optional color modifiers paint a left accent border, same
   idea as Status card (section 69):
     <div class="od-resource-card od-resource-card--success">
       <div class="od-resource-head">
         <div class="od-resource-icon"><i class="od-i od-i-globe" aria-hidden="true"></i></div>
         <div class="od-resource-title-wrap">
           <div class="od-resource-title"><a href="#">example.com</a> <span class="od-pill od-pill-success">Active</span></div>
           <div class="od-resource-meta"><span>PHP 8.3</span><span>Let's Encrypt</span><span>2 mailboxes</span></div>
         </div>
         <div class="od-resource-actions">…buttons / dropdown…</div>
       </div>
       <div class="od-resource-body">…</div>
       <div class="od-resource-foot">…</div>
     </div> */
.od-resource-card { position: relative; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); margin-bottom: 16px; overflow: hidden; }
.od-resource-card--success { border-left: 3px solid var(--od-success); }
.od-resource-card--warning { border-left: 3px solid var(--od-warning); }
.od-resource-card--danger { border-left: 3px solid var(--od-danger); }
.od-resource-card--accent { border-left: 3px solid var(--od-accent); }
.od-resource-card--muted { opacity: .75; }
.od-resource-head { display: flex; align-items: center; gap: 14px; padding: 14px 18px; flex-wrap: wrap; }
.od-resource-icon { display: flex; align-items: center; justify-content: center; width: 42px; height: 42px; border-radius: var(--od-radius); background: var(--od-accent-bg); color: var(--od-accent-text); font-size: 20px; flex-shrink: 0; }
.od-resource-title-wrap { flex: 1; min-width: 180px; }
.od-resource-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; font-weight: 800; }
.od-resource-title a { color: inherit; text-decoration: none; }
.od-resource-title a:hover { color: var(--od-accent-text); }
.od-resource-meta { display: flex; align-items: center; gap: 4px 0; flex-wrap: wrap; margin-top: 3px; font-size: 12px; font-weight: 600; color: var(--od-text-muted); }
.od-resource-meta > span + span::before { content: "·"; margin: 0 7px; color: var(--od-text-muted-2); }
.od-resource-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.od-resource-body { padding: 8px 12px 12px; border-top: 1px solid var(--od-border-light); }
.od-resource-foot { padding: 12px 18px; border-top: 1px solid var(--od-border-light); background: var(--od-bg); }
.od-resource-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.od-resource-grid .od-resource-card { margin-bottom: 0; }

/* ============================== 94. Quota list ============================== */
/* Several usage readings stacked as label · bar · "used / limit" rows —
   disk, traffic, mailboxes, databases. A denser, aligned sibling of Meter
   (section 83), which is one big reading with its own label row. Add
   --warning/--danger per row once a value crosses your own threshold;
   --unlimited draws a neutral striped bar for a "no limit" plan:
     <div class="od-quota">
       <div class="od-quota-row">
         <div class="od-quota-label"><i class="od-i od-i-database" aria-hidden="true"></i> Disk space</div>
         <div class="od-quota-bar"><div class="od-quota-fill" style="width:32%"></div></div>
         <div class="od-quota-value">3.2 GB <span class="od-quota-limit">/ 10 GB</span></div>
       </div>
     </div> */
.od-quota { display: flex; flex-direction: column; }
.od-quota-row { display: grid; grid-template-columns: minmax(110px, 150px) minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 7px 0; }
.od-quota-row + .od-quota-row { border-top: 1px solid var(--od-border-light); }
.od-quota--tight .od-quota-row { padding: 4px 0; }
.od-quota--tight .od-quota-row + .od-quota-row { border-top: 0; }
.od-quota-label { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 700; color: var(--od-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-quota-label .od-i { color: var(--od-text-muted); font-size: 14px; flex-shrink: 0; }
.od-quota-bar { height: 6px; border-radius: 999px; background: var(--od-border-light); overflow: hidden; }
.od-quota-fill { height: 100%; border-radius: inherit; background: var(--od-accent); transition: width .2s ease; }
.od-quota-row--warning .od-quota-fill { background: var(--od-warning); }
.od-quota-row--danger .od-quota-fill { background: var(--od-danger); }
.od-quota-row--warning .od-quota-value { color: var(--od-warning-text); }
.od-quota-row--danger .od-quota-value { color: var(--od-danger-text); }
.od-quota-row--unlimited .od-quota-fill { width: 100% !important; background: repeating-linear-gradient(135deg, var(--od-border) 0 4px, transparent 4px 8px); }
.od-quota-value { font-size: 12.5px; font-weight: 800; white-space: nowrap; font-variant-numeric: tabular-nums; }
.od-quota-limit { font-weight: 600; color: var(--od-text-muted); }
@media (max-width: 560px) {
    .od-quota-row { grid-template-columns: 1fr auto; }
    .od-quota-bar { grid-column: 1 / -1; }
}

/* ============================== 95. Service list ============================== */
/* Server daemons with a state light, a version, a one-line description
   (uptime) and start/stop/restart actions — the "Services" page of a
   control panel. States: --running (green), --stopped (red), --degraded
   (amber), --pending (pulsing, while a restart is in flight), default
   grey = unknown:
     <div class="od-service-list">
       <div class="od-service">
         <span class="od-service-state od-service-state--running" aria-label="Running"></span>
         <div class="od-service-body">
           <div class="od-service-name">nginx <span class="od-service-version">1.26.2</span></div>
           <div class="od-service-desc">Web server · up 14d 3h</div>
         </div>
         <div class="od-service-stats"><span>CPU <b>0.4%</b></span><span>RAM <b>42 MB</b></span></div>
         <div class="od-service-actions">…Button group…</div>
       </div>
     </div> */
.od-service-list { display: flex; flex-direction: column; }
.od-service { display: flex; align-items: center; gap: 14px; padding: 12px 16px; border-bottom: 1px solid var(--od-border-light); flex-wrap: wrap; }
.od-service:last-child { border-bottom: 0; }
.od-service:hover { background: var(--od-row-hover); }
.od-service-state { position: relative; width: 10px; height: 10px; border-radius: 999px; background: var(--od-text-muted-2); flex-shrink: 0; }
.od-service-state::after { content: ""; position: absolute; inset: -4px; border-radius: 999px; background: currentColor; opacity: .18; color: inherit; }
.od-service-state--running { background: var(--od-success); color: var(--od-success); }
.od-service-state--stopped { background: var(--od-danger); color: var(--od-danger); }
.od-service-state--degraded { background: var(--od-warning); color: var(--od-warning); }
.od-service-state--pending { background: var(--od-accent); color: var(--od-accent); animation: od-service-pulse 1s ease-in-out infinite; }
@keyframes od-service-pulse { 50% { opacity: .35; } }
.od-service-body { flex: 1; min-width: 160px; }
.od-service-name { font-size: 13.5px; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.od-service-version { font-family: var(--od-font-mono); font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); }
.od-service-desc { font-size: 12px; font-weight: 600; color: var(--od-text-muted); margin-top: 1px; }
.od-service-stats { display: flex; gap: 14px; font-size: 12px; font-weight: 600; color: var(--od-text-muted); font-variant-numeric: tabular-nums; }
.od-service-stats b { color: var(--od-text-secondary); font-weight: 800; }
.od-service-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }

/* ============================== 96. Gauge ============================== */
/* A half-circle dial for a load reading — CPU, RAM, disk I/O. Set the
   value with the --value custom property (0–100), same pattern as Donut
   chart (section 50); the arc is a conic-gradient clipped to a half box,
   so it needs no SVG. --od-gauge-size sets the width; the color modifiers
   match Progress bar's:
     <div class="od-gauge od-gauge--warning" style="--value:72">
       <div class="od-gauge-arc"></div>
       <div class="od-gauge-value">72%</div>
       <div class="od-gauge-label">CPU</div>
     </div> */
.od-gauge { --od-gauge-size: 132px; --od-gauge-thickness: 12px; --od-gauge-color: var(--od-accent); width: var(--od-gauge-size); display: flex; flex-direction: column; align-items: center; }
.od-gauge-arc { position: relative; width: 100%; aspect-ratio: 2 / 1; overflow: hidden; }
.od-gauge-arc::before { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 200%; border-radius: 50%; background: conic-gradient(from 270deg, var(--od-gauge-color) calc(var(--value, 0) * 0.5%), var(--od-border-light) 0 50%, transparent 50%); -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--od-gauge-thickness) - .5px), #000 calc(100% - var(--od-gauge-thickness) + .5px)); mask: radial-gradient(farthest-side, transparent calc(100% - var(--od-gauge-thickness) - .5px), #000 calc(100% - var(--od-gauge-thickness) + .5px)); transition: background .2s ease; }
.od-gauge-value { margin-top: -22px; font-size: 22px; font-weight: 800; letter-spacing: -.5px; line-height: 1; font-variant-numeric: tabular-nums; }
.od-gauge-label { margin-top: 5px; font-size: 11px; font-weight: 700; letter-spacing: .4px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-gauge--success { --od-gauge-color: var(--od-success); }
.od-gauge--warning { --od-gauge-color: var(--od-warning); }
.od-gauge--danger { --od-gauge-color: var(--od-danger); }
.od-gauge--sm { --od-gauge-size: 96px; --od-gauge-thickness: 9px; }
.od-gauge--sm .od-gauge-value { font-size: 17px; margin-top: -18px; }
.od-gauge-row { display: flex; flex-wrap: wrap; gap: 20px 32px; justify-content: space-around; }

/* ============================== 97. Uptime strip ============================== */
/* The status-page row of thin bars, one per day (or hour) — green up,
   amber degraded, red down, grey no data. Give each bar a data-od-tooltip
   for the exact reading:
     <div class="od-uptime" role="img" aria-label="90-day uptime: 99.98%">
       <span class="od-uptime-bar" data-od-tooltip="Sep 1 — 100%"></span>
       <span class="od-uptime-bar od-uptime-bar--down" data-od-tooltip="Sep 2 — 43 min outage"></span>
       …
     </div>
     <div class="od-uptime-legend"><span>90 days ago</span><span>99.98% uptime</span><span>Today</span></div> */
.od-uptime { display: flex; gap: 2px; align-items: stretch; height: 28px; }
.od-uptime-bar { flex: 1; min-width: 2px; border-radius: 2px; background: var(--od-success); transition: opacity .1s ease; }
.od-uptime-bar:hover { opacity: .7; }
.od-uptime-bar--degraded { background: var(--od-warning); }
.od-uptime-bar--down { background: var(--od-danger); }
.od-uptime-bar--none { background: var(--od-border); }
.od-uptime--sm { height: 16px; }
.od-uptime-legend { display: flex; justify-content: space-between; gap: 8px; margin-top: 6px; font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); }
.od-uptime-legend > :nth-child(2) { color: var(--od-text-secondary); font-weight: 700; }

/* ============================== 98. File browser (path bar + file table) ============================== */
/* A file-manager listing: a path bar of clickable segments above a Table
   (section 9) with the --files modifier — tighter rows, an icon before
   each name, monospace permissions/size columns:
     <div class="od-path-bar">
       <a href="#" class="od-path-seg" aria-label="Root"><i class="od-i od-i-home" aria-hidden="true"></i></a>
       <a href="#" class="od-path-seg">var</a>
       <a href="#" class="od-path-seg">www</a>
       <span class="od-path-seg is-current">httpdocs</span>
     </div>
     <table class="od-table od-table--files">
       <tr><td><span class="od-file-name"><i class="od-i od-i-folder od-file-icon od-file-icon--dir" aria-hidden="true"></i> wp-content</span></td><td class="od-file-size">—</td><td class="od-file-perms">drwxr-xr-x</td>…</tr> */
.od-path-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; padding: 6px 8px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); font-family: var(--od-font-mono); font-size: 12.5px; }
.od-path-seg { display: inline-flex; align-items: center; gap: 4px; padding: 3px 7px; border-radius: var(--od-radius-sm); color: var(--od-text-secondary); text-decoration: none; font-weight: 600; }
.od-path-seg:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-path-seg.is-current { color: var(--od-text); font-weight: 800; }
.od-path-seg + .od-path-seg::before { content: "/"; margin-right: 4px; color: var(--od-text-muted-2); font-weight: 400; }
.od-path-bar-actions { margin-left: auto; display: flex; gap: 4px; }
.od-table--files td { padding: 8px 14px; }
.od-table--files th { padding: 9px 14px; }
.od-file-name { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; }
.od-file-name a { color: inherit; text-decoration: none; }
.od-file-name a:hover { color: var(--od-accent-text); }
.od-file-icon { font-size: 15px; color: var(--od-text-muted); flex-shrink: 0; }
.od-file-icon--dir { color: var(--od-warning); }
.od-file-icon--code { color: var(--od-accent-text); }
.od-file-icon--image { color: var(--od-success); }
.od-file-icon--archive { color: var(--od-text-secondary); }
.od-file-size, .od-file-perms, .od-file-date { font-family: var(--od-font-mono); font-size: 12px; color: var(--od-text-muted); white-space: nowrap; }
.od-file-size { text-align: right; font-variant-numeric: tabular-nums; }
.od-table--files tr.is-selected td { background: var(--od-accent-bg); }

/* ============================== 99. Chmod editor ============================== */
/* Unix permissions as a 3×3 checkbox grid plus the octal value, kept in
   sync both ways by od-chmod.js (the element starts empty; the script
   renders it). A hidden input named data-od-chmod-name submits the octal:
     <div class="od-chmod" data-od-chmod data-od-chmod-value="755" data-od-chmod-name="perms"></div> */
.od-chmod { display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
.od-chmod-grid { border-collapse: collapse; font-size: 12.5px; }
.od-chmod-grid th { padding: 4px 10px; font-size: 11px; font-weight: 700; letter-spacing: .4px; color: var(--od-text-muted-2); text-transform: uppercase; text-align: center; }
.od-chmod-grid th:first-child { text-align: left; }
.od-chmod-grid td { padding: 5px 10px; text-align: center; border-top: 1px solid var(--od-border-light); }
.od-chmod-grid td:first-child { text-align: left; font-weight: 700; color: var(--od-text-secondary); }
.od-chmod-grid .od-checkbox { gap: 0; }
.od-chmod-octal { display: flex; flex-direction: column; gap: 4px; }
.od-chmod-octal label { font-size: 11px; font-weight: 700; letter-spacing: .4px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-chmod-octal .od-input { width: 76px; font-family: var(--od-font-mono); font-size: 15px; font-weight: 700; text-align: center; letter-spacing: 2px; }
.od-chmod-symbolic { font-family: var(--od-font-mono); font-size: 12.5px; color: var(--od-text-muted); }

/* ============================== 100. Table row expansion ============================== */
/* Click a row to reveal a details row underneath — subdomains under a
   domain, aliases under a mailbox, a log excerpt under a job
   (od-expand.js). The toggle row gets .is-expanded; the details row is a
   normal <tr> spanning all columns, hidden until then:
     <tr data-od-expand-toggle>
       <td><span class="od-caret" aria-hidden="true"></span> example.com</td>…
     </tr>
     <tr class="od-table-expansion" hidden><td colspan="4">…anything…</td></tr>
   Give the toggle row data-od-expand-toggle="some-id" to target a specific
   details row by id instead of the next sibling. */
.od-table tbody tr[data-od-expand-toggle] { cursor: pointer; }
.od-table tbody tr[data-od-expand-toggle] .od-caret { margin-right: 4px; vertical-align: -1px; }
.od-table tbody tr.is-expanded .od-caret { transform: rotate(90deg); }
.od-table tbody tr.is-expanded td { border-bottom-color: transparent; background: var(--od-row-hover); }
.od-table tbody tr.od-table-expansion, .od-table tbody tr.od-table-expansion:hover { background: var(--od-bg); }
.od-table tbody tr.od-table-expansion > td { padding: 12px 16px 16px 44px; font-size: 13px; }
.od-table tbody tr.od-table-expansion[hidden] { display: none; }
.od-table-expansion .od-desc-list { max-width: 520px; }

/* ============================== 101. Log viewer ============================== */
/* A dark, monospace transcript of log lines with a level column, filtered
   by od-log.js: level toggles, a substring search, a "follow" switch that
   keeps the newest line in view (and turns itself off once you scroll up
   to read). A sibling of Console (section 78), which is a narrative
   command/output transcript rather than leveled records:
     <div class="od-log" data-od-log data-od-log-follow>
       <div class="od-log-toolbar">
         <div class="od-log-levels">
           <button type="button" class="od-log-level-btn is-active" data-od-log-level="error">Error</button>
           <button type="button" class="od-log-level-btn is-active" data-od-log-level="warn">Warn</button>
           <button type="button" class="od-log-level-btn is-active" data-od-log-level="info">Info</button>
           <button type="button" class="od-log-level-btn is-active" data-od-log-level="debug">Debug</button>
         </div>
         <div class="od-input-wrap"><i class="od-i od-i-search" aria-hidden="true"></i><input type="search" class="od-input od-input--sm" data-od-log-search placeholder="Filter…"></div>
         <span class="od-log-count" data-od-log-count></span>
         <label class="od-switch"><input type="checkbox" data-od-log-follow-toggle checked><span class="od-switch-track"></span>Follow</label>
       </div>
       <div class="od-log-body" data-od-log-body>
         <div class="od-log-line" data-od-log-level="info"><span class="od-log-time">12:00:01</span><span class="od-log-level">INFO</span><span class="od-log-msg">Server started</span></div>
       </div>
     </div>
   Fixed dark palette, same reasoning as Code block's syntax tokens. */
.od-log { display: flex; flex-direction: column; border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); overflow: hidden; background: var(--od-panel); }
.od-log-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 12px; border-bottom: 1px solid var(--od-border-light); }
.od-log-toolbar .od-input-wrap { flex: 1; min-width: 160px; max-width: 280px; }
.od-log-toolbar .od-switch { margin-left: auto; }
.od-log-levels { display: inline-flex; gap: 4px; }
.od-log-level-btn { appearance: none; border: 1px solid var(--od-border); border-radius: 999px; padding: 3px 10px; background: transparent; font: inherit; font-size: 11.5px; font-weight: 700; color: var(--od-text-muted); cursor: pointer; }
.od-log-level-btn.is-active { background: var(--od-nav-hover); color: var(--od-text); border-color: transparent; }
.od-log-level-btn.is-active[data-od-log-level="error"] { color: var(--od-danger-text); background: var(--od-danger-bg); }
.od-log-level-btn.is-active[data-od-log-level="warn"] { color: var(--od-warning-text); background: var(--od-warning-bg); }
.od-log-level-btn.is-active[data-od-log-level="info"] { color: var(--od-accent-text); background: var(--od-accent-bg); }
.od-log-count { font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); font-variant-numeric: tabular-nums; }
.od-log-body { max-height: var(--od-log-height, 320px); overflow: auto; background: var(--od-dark); color: var(--od-dark-text); font-family: var(--od-font-mono); font-size: 12px; line-height: 1.55; padding: 6px 0; }
.od-log-line { display: flex; align-items: baseline; gap: 10px; padding: 1px 14px; white-space: pre-wrap; word-break: break-word; }
.od-log-line:hover { background: rgba(255, 255, 255, .04); }
.od-log-line[hidden] { display: none; }
.od-log-ln { flex-shrink: 0; width: 34px; text-align: right; opacity: .35; user-select: none; }
.od-log-time { flex-shrink: 0; opacity: .55; }
.od-log-level { flex-shrink: 0; width: 46px; font-weight: 700; font-size: 10.5px; letter-spacing: .4px; }
.od-log-line[data-od-log-level="error"] .od-log-level { color: #FF7B72; }
.od-log-line[data-od-log-level="error"] .od-log-msg { color: #FFB3AE; }
.od-log-line[data-od-log-level="warn"] .od-log-level { color: #F2C94C; }
.od-log-line[data-od-log-level="info"] .od-log-level { color: #58A6FF; }
.od-log-line[data-od-log-level="debug"] .od-log-level, .od-log-line[data-od-log-level="debug"] .od-log-msg { opacity: .6; }
.od-log-msg { flex: 1; min-width: 0; }
.od-log-msg mark { background: rgba(242, 201, 76, .35); color: inherit; border-radius: 2px; }
.od-log-empty { padding: 20px 14px; text-align: center; opacity: .5; }
.od-log-empty[hidden] { display: none; }

/* ============================== 102. Cron builder ============================== */
/* A schedule editor rendered by od-cron.js: a preset select (every minute
   … monthly, or custom), the five crontab fields and a plain-language
   summary of the result. A hidden input named data-od-cron-name carries
   the expression:
     <div class="od-cron" data-od-cron data-od-cron-name="schedule" data-od-cron-value="0 3 * * *"></div> */
.od-cron { display: flex; flex-direction: column; gap: 10px; }
.od-cron-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.od-cron-top .od-select { width: auto; min-width: 160px; }
.od-cron-fields { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; max-width: 480px; }
.od-cron-field .od-input { font-family: var(--od-font-mono); font-size: 13.5px; text-align: center; padding: 7px 4px; }
.od-cron-field label { display: block; margin-top: 4px; font-size: 10.5px; font-weight: 700; letter-spacing: .3px; color: var(--od-text-muted-2); text-transform: uppercase; text-align: center; }
.od-cron-summary { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; color: var(--od-text-secondary); }
.od-cron-summary code { font-family: var(--od-font-mono); font-size: 12px; padding: 1px 6px; border-radius: var(--od-radius-sm); background: var(--od-border-light); color: var(--od-text); }
.od-cron.is-invalid .od-cron-summary { color: var(--od-danger-text); }

/* ============================== 103. Secret field ============================== */
/* A read-only sensitive value shown masked — an API key, a DB password, a
   token — with a reveal toggle and (composed with Copy to clipboard,
   section 36) a copy button. od-secret.js swaps the mask for the real
   value on click and back again; add data-od-secret-timeout="5000" to
   re-mask automatically:
     <div class="od-secret" data-od-secret>
       <code class="od-secret-value" data-od-secret-value="sk_live_51H…">••••••••••••</code>
       <button type="button" class="od-btn-ghost od-btn-icon od-btn-sm" data-od-secret-toggle aria-label="Reveal"><i class="od-i od-i-eye" aria-hidden="true"></i></button>
       <button type="button" class="od-btn-ghost od-btn-icon od-btn-sm" data-od-copy="sk_live_51H…" aria-label="Copy"><i class="od-i od-i-copy" aria-hidden="true"></i></button>
     </div> */
.od-secret { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; }
.od-secret-value { display: inline-block; min-width: 120px; max-width: 100%; padding: 5px 10px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-border-light); font-family: var(--od-font-mono); font-size: 12.5px; color: var(--od-text-secondary); letter-spacing: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-secret.is-revealed .od-secret-value { color: var(--od-text); letter-spacing: 0; user-select: all; }
.od-secret .od-btn-icon.od-btn-sm { width: 28px; height: 28px; }

/* ============================== 104. Sidebar rail (collapsed sidebar) ============================== */
/* A desktop "collapse to icons" mode for the shell's sidebar — add
   .od-shell--rail to .od-shell (od-sidebar.js toggles and remembers it
   from any <button data-od-sidebar-collapse>; html.od-rail-pending, set
   early from localStorage, applies the same styles before the script runs). Only the leading icon of
   each nav link stays visible; its label shows as a tooltip on hover via
   data-od-rail-label, which the script copies from the link text. Below
   the 820px mobile breakpoint the off-canvas sidebar takes over and the
   rail is ignored. Nav links need a first child that IS the icon (the
   demo's <span class="demo-icon">, an .od-nav-icon, an .od-i…) — a
   text-only link collapses to its first letter. */
[data-od-sidebar-collapse] .od-i { transition: transform .18s ease; }
@media (max-width: 820px) { [data-od-sidebar-collapse] { display: none; } }
@media (min-width: 821px) {
    :is(.od-shell--rail, .od-rail-pending .od-shell) { --od-sidebar-width: 64px; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-sidebar { overflow: visible; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-brand { justify-content: center; padding-left: 0; padding-right: 0; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-brand-name, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-section-label, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-count, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-sublist, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-account-link > div:last-child { display: none; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-group { padding-left: 10px; padding-right: 10px; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-group + .od-nav-group { border-top: 1px solid var(--od-border-light); margin-top: 4px; padding-top: 8px; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-link, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-toggle { justify-content: center; padding-left: 0; padding-right: 0; position: relative; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-link > :not(:first-child), :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-toggle > :not(:first-child) { display: none; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-toggle::after { display: none; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-link > :first-child, :is(.od-shell--rail, .od-rail-pending .od-shell) .od-nav-toggle > :first-child { font-size: 17px; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) [data-od-rail-label]::before { content: attr(data-od-rail-label); position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%); padding: 5px 9px; border-radius: var(--od-radius-sm); background: var(--od-dark); color: var(--od-dark-text); font-size: 11.5px; font-weight: 600; white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s ease; z-index: 50; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) [data-od-rail-label]:hover::before, :is(.od-shell--rail, .od-rail-pending .od-shell) [data-od-rail-label]:focus-visible::before { opacity: 1; visibility: visible; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-sidebar-footer { flex-direction: column; padding: 12px 10px; gap: 6px; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) .od-account-link { padding: 6px; justify-content: center; }
    :is(.od-shell--rail, .od-rail-pending .od-shell) [data-od-sidebar-collapse] .od-i { transform: rotate(180deg); }
}

/* ============================== 105. Scope switcher ============================== */
/* A topbar control for "which server / account / tenant am I looking at",
   composed with Dropdown (section 12) — Plesk's server picker, Hestia's
   "login as user". The trigger shows a small label over the current value;
   the menu is a normal .od-dropdown-menu whose selected item gets
   .is-selected (which also works for any other dropdown that behaves like
   a picker):
     <div class="od-dropdown od-scope" data-od-dropdown>
       <button type="button" class="od-scope-btn" data-od-dropdown-trigger aria-haspopup="menu" aria-expanded="false">
         <span class="od-scope-icon"><i class="od-i od-i-server" aria-hidden="true"></i></span>
         <span class="od-scope-text"><span class="od-scope-label">Server</span><span class="od-scope-value">web-01.example.net</span></span>
         <i class="od-i od-i-chevrons-up-down od-scope-caret" aria-hidden="true"></i>
       </button>
       <div class="od-dropdown-menu" data-od-dropdown-menu role="menu" hidden>
         <button type="button" class="od-dropdown-item is-selected" role="menuitem">web-01.example.net</button>
         …
       </div>
     </div> */
.od-scope-btn { display: inline-flex; align-items: center; gap: 10px; max-width: 100%; padding: 5px 10px 5px 6px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); color: var(--od-text); font: inherit; text-align: left; cursor: pointer; }
.od-scope-btn:hover { background: var(--od-nav-hover); }
.od-scope-icon { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--od-radius-sm); background: var(--od-accent-bg); color: var(--od-accent-text); font-size: 14px; flex-shrink: 0; }
.od-scope-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.od-scope-label { font-size: 10px; font-weight: 700; letter-spacing: .5px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-scope-value { font-size: 12.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.od-scope-caret { color: var(--od-text-muted); font-size: 12px; flex-shrink: 0; }
.od-scope .od-dropdown-menu { min-width: 240px; }
.od-dropdown-item.is-selected { color: var(--od-accent-text); }
.od-dropdown-item.is-selected::after { content: "\2713"; margin-left: auto; font-weight: 800; }
.od-dropdown-label { padding: 6px 10px 4px; font-size: 10.5px; font-weight: 700; letter-spacing: .6px; color: var(--od-text-muted-2); text-transform: uppercase; }
@media (max-width: 820px) { .od-scope-text { display: none; } .od-scope-btn { padding-right: 6px; } }

/* ============================== 106. System bar ============================== */
/* A full-width strip above the topbar for a whole-session state — "You
   are logged in as user X", "Maintenance mode is on", "Trial ends in 3
   days". Put it as the first child of .od-main-col so it spans the
   content column:
     <div class="od-system-bar od-system-bar--warning">
       <i class="od-i od-i-users" aria-hidden="true"></i>
       <span class="od-system-bar-text">Logged in as <strong>john@example.com</strong></span>
       <a href="#" class="od-btn-ghost od-btn-sm">Return to admin</a>
     </div> */
.od-system-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; flex-shrink: 0; padding: 7px 18px; background: var(--od-accent); color: #fff; font-size: 12.5px; font-weight: 600; }
.od-system-bar-text { flex: 1; min-width: 0; }
.od-system-bar > .od-i { font-size: 15px; }
.od-system-bar .od-btn-ghost { background: rgba(255, 255, 255, .15); border-color: rgba(255, 255, 255, .3); color: #fff; }
.od-system-bar .od-btn-ghost:hover { background: rgba(255, 255, 255, .25); color: #fff; }
.od-system-bar-close { margin-left: auto; background: none; border: 0; color: inherit; opacity: .7; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
.od-system-bar-close:hover { opacity: 1; }
.od-system-bar--warning { background: var(--od-warning); }
.od-system-bar--danger { background: var(--od-danger); }
.od-system-bar--dark { background: var(--od-dark); color: var(--od-dark-text); }
.od-system-bar--neutral { background: var(--od-border-light); color: var(--od-text-secondary); border-bottom: 1px solid var(--od-border); }
.od-system-bar--neutral .od-btn-ghost { background: var(--od-panel); border-color: var(--od-border); color: var(--od-text-secondary); }
@media (max-width: 820px) {
    .od-system-bar { padding: 6px 12px; font-size: 12px; gap: 8px; }
    .od-system-bar > .od-i { display: none; }
}

/* ============================== 107. Toolbar ============================== */
/* A generic action row — button groups, a search input, separators, and a
   spacer pushing the rest to the right. Where Table's own .od-table-toolbar
   (section 9) is search-left/actions-right only, this composes anything:
     <div class="od-toolbar">
       <div class="od-toolbar-group"><button class="od-btn od-btn-sm">New</button><button class="od-btn-ghost od-btn-sm">Upload</button></div>
       <span class="od-toolbar-sep"></span>
       <div class="od-input-wrap"><i class="od-i od-i-search"></i><input class="od-input od-input--sm" placeholder="Search…"></div>
       <span class="od-toolbar-spacer"></span>
       <div class="od-btn-group">…</div>
     </div>
   .od-toolbar--bare drops the padding/border for a toolbar floating
   above a panel rather than inside one. */
.od-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--od-border-light); }
.od-toolbar-group { display: inline-flex; align-items: center; gap: 6px; }
.od-toolbar-sep { width: 1px; height: 22px; background: var(--od-border); margin: 0 4px; flex-shrink: 0; }
.od-toolbar-spacer { flex: 1; }
.od-toolbar-title { font-size: 13px; font-weight: 800; margin-right: 4px; }
.od-toolbar .od-input, .od-toolbar .od-select { width: auto; }
.od-toolbar .od-input-wrap { min-width: 180px; }
.od-toolbar--bare { padding: 0; border-bottom: 0; margin-bottom: 14px; }
.od-toolbar--bottom { border-bottom: 0; border-top: 1px solid var(--od-border-light); }

/* ============================== 108. Button group ============================== */
/* Buttons glued into one segment — view switchers (list/grid), paged
   prev/next, a "Start / Stop / Restart" trio. Any .od-btn* variant works
   as a child; give a ghost/neutral child .is-active for the current
   choice:
     <div class="od-btn-group" role="group">
       <button type="button" class="od-btn-ghost od-btn-sm is-active">List</button>
       <button type="button" class="od-btn-ghost od-btn-sm">Grid</button>
     </div> */
.od-btn-group { display: inline-flex; align-items: stretch; }
.od-btn-group > * { border-radius: 0; position: relative; }
.od-btn-group > * + * { margin-left: -1px; }
.od-btn-group > :first-child { border-top-left-radius: var(--od-radius); border-bottom-left-radius: var(--od-radius); }
.od-btn-group > :last-child { border-top-right-radius: var(--od-radius); border-bottom-right-radius: var(--od-radius); }
.od-btn-group > :hover, .od-btn-group > :focus-visible, .od-btn-group > .is-active { z-index: 1; }
.od-btn-group > .od-btn + .od-btn { border-left-color: rgba(255, 255, 255, .28); }
.od-btn-group > .od-btn-ghost.is-active, .od-btn-group > .od-btn-neutral.is-active { background: var(--od-accent-bg); color: var(--od-accent-text); border-color: color-mix(in srgb, var(--od-accent) 45%, var(--od-border)); }
.od-btn-group--block { display: flex; }
.od-btn-group--block > * { flex: 1; }

/* ============================== 109. Popover ============================== */
/* A click-opened panel anchored to a trigger, for content richer than a
   Tooltip (section 23) and freer than a Dropdown's list of items (section
   12): a mini form, a stats card, a confirmation with two buttons
   (od-popover.js). Clicking inside keeps it open; outside click or Escape
   closes. data-od-popover-placement: bottom (default) | top | right | left,
   with "-end" for the far-side alignment (e.g. "bottom-end"). The script
   flips top/bottom when there's no room:
     <div class="od-popover" data-od-popover data-od-popover-placement="bottom">
       <button type="button" class="od-btn-ghost" data-od-popover-trigger aria-expanded="false">Details</button>
       <div class="od-popover-panel" data-od-popover-panel role="dialog" hidden>
         <div class="od-popover-title">Certificate</div>
         …
       </div>
     </div> */
.od-popover { position: relative; display: inline-block; }
.od-popover-panel { position: absolute; z-index: 40; min-width: 220px; max-width: 320px; padding: 12px 14px; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius); box-shadow: 0 12px 28px rgba(17, 22, 31, .16); font-size: 13px; line-height: 1.5; color: var(--od-text-secondary); outline: none; }
.od-popover-panel[hidden] { display: none; }
.od-popover-panel::before { content: ""; position: absolute; width: 10px; height: 10px; background: var(--od-panel); border: 1px solid var(--od-border); transform: rotate(45deg); }
.od-popover-title { font-size: 13px; font-weight: 800; color: var(--od-text); margin-bottom: 6px; }
.od-popover-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
.od-popover-panel--wide { max-width: 420px; }
/* Placement: [data-od-popover-side] is written by the script (the
   requested placement, flipped if needed); the arrow sits on the edge
   facing the trigger, clipped by the panel's own border on the far side. */
.od-popover-panel[data-od-popover-side="bottom"] { top: calc(100% + 10px); left: 0; }
.od-popover-panel[data-od-popover-side="bottom"]::before { top: -6px; left: 16px; border-right: 0; border-bottom: 0; }
.od-popover-panel[data-od-popover-side="top"] { bottom: calc(100% + 10px); left: 0; }
.od-popover-panel[data-od-popover-side="top"]::before { bottom: -6px; left: 16px; border-left: 0; border-top: 0; }
.od-popover-panel[data-od-popover-side="bottom"][data-od-popover-align="end"], .od-popover-panel[data-od-popover-side="top"][data-od-popover-align="end"] { left: auto; right: 0; }
.od-popover-panel[data-od-popover-side="bottom"][data-od-popover-align="end"]::before, .od-popover-panel[data-od-popover-side="top"][data-od-popover-align="end"]::before { left: auto; right: 16px; }
.od-popover-panel[data-od-popover-side="right"] { left: calc(100% + 10px); top: 0; }
.od-popover-panel[data-od-popover-side="right"]::before { left: -6px; top: 12px; border-right: 0; border-top: 0; }
.od-popover-panel[data-od-popover-side="left"] { right: calc(100% + 10px); top: 0; }
.od-popover-panel[data-od-popover-side="left"]::before { right: -6px; top: 12px; border-left: 0; border-bottom: 0; }

/* ============================== 110. Filter bar ============================== */
/* Active filters as removable chips above a table, with an "add filter"
   affordance and a "clear all" link — pairs with Chip / tag (section 42)
   and, while rows are selected, Bulk action bar (section 81). Presentational:
   apply the filters yourself:
     <div class="od-filter-bar">
       <span class="od-filter-bar-label"><i class="od-i od-i-filter" aria-hidden="true"></i> Filters</span>
       <span class="od-chip od-chip--accent"><span class="od-chip-key">Status:</span> Active <button type="button" class="od-chip-remove" aria-label="Remove">&times;</button></span>
       <button type="button" class="od-filter-add">+ Add filter</button>
       <button type="button" class="od-filter-clear">Clear all</button>
     </div> */
.od-filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--od-border-light); }
.od-filter-bar[hidden] { display: none; }
.od-filter-bar-label { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700; color: var(--od-text-muted); margin-right: 2px; }
.od-chip-key { opacity: .7; font-weight: 600; }
.od-filter-add { appearance: none; border: 1px dashed var(--od-border); border-radius: 999px; padding: 4px 10px; background: transparent; font: inherit; font-size: 12px; font-weight: 700; color: var(--od-text-secondary); cursor: pointer; }
.od-filter-add:hover { border-color: var(--od-accent); color: var(--od-accent-text); }
.od-filter-clear { appearance: none; border: 0; background: none; margin-left: auto; padding: 4px 6px; font: inherit; font-size: 12px; font-weight: 700; color: var(--od-text-muted); cursor: pointer; }
.od-filter-clear:hover { color: var(--od-danger-text); }
.od-filter-bar .od-select--sm { width: auto; }

/* ============================== 111. Date range picker ============================== */
/* An input-like trigger that opens a panel with quick presets ("Last 7
   days") next to a Mini calendar in range mode (section 45/72). Driven by
   od-daterange.js (which needs od-calendar.js); two hidden inputs named
   <name>_start / <name>_end submit the picked ISO dates:
     <div class="od-daterange" data-od-daterange data-od-daterange-name="period" data-od-daterange-start="2026-09-01" data-od-daterange-end="2026-09-08">
       <button type="button" class="od-daterange-btn" data-od-daterange-trigger aria-expanded="false">
         <i class="od-i od-i-calendar" aria-hidden="true"></i><span data-od-daterange-label>Pick a range</span>
       </button>
     </div>
   The panel itself is rendered by the script. */
.od-daterange { position: relative; display: inline-block; }
.od-daterange-btn { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); color: var(--od-text); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }
.od-daterange-btn:hover { background: var(--od-nav-hover); }
.od-daterange.is-open .od-daterange-btn { border-color: var(--od-accent); }
.od-daterange-btn .od-i { color: var(--od-text-muted); font-size: 14px; }
.od-daterange-panel { position: absolute; top: calc(100% + 6px); left: 0; z-index: 40; display: flex; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); box-shadow: 0 12px 28px rgba(17, 22, 31, .16); overflow: hidden; }
.od-daterange-panel[hidden] { display: none; }
.od-daterange--right .od-daterange-panel { left: auto; right: 0; }
.od-daterange-presets { display: flex; flex-direction: column; gap: 2px; padding: 8px; border-right: 1px solid var(--od-border-light); min-width: 130px; }
.od-daterange-preset { appearance: none; border: 0; border-radius: var(--od-radius-sm); padding: 7px 10px; background: transparent; font: inherit; font-size: 12.5px; font-weight: 600; color: var(--od-text-secondary); text-align: left; cursor: pointer; white-space: nowrap; }
.od-daterange-preset:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-daterange-preset.is-active { background: var(--od-accent-bg); color: var(--od-accent-text); }
.od-daterange-main { display: flex; flex-direction: column; padding: 12px; gap: 10px; }
.od-daterange-main .od-calendar { width: 252px; max-width: none; }
.od-daterange-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 10px; border-top: 1px solid var(--od-border-light); font-size: 12px; font-weight: 600; color: var(--od-text-muted); }
@media (max-width: 560px) { .od-daterange-panel { flex-direction: column; } .od-daterange-presets { flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--od-border-light); } }

/* ============================== 112. Anchor nav (scrollspy) ============================== */
/* A sticky table of contents for a long settings page — links to section
   ids; od-scrollspy.js marks the one currently in view .is-active and
   scrolls smoothly on click. The two-column layout helper puts the nav
   beside the content and drops it above (as a horizontal strip) on narrow
   screens:
     <div class="od-anchor-layout">
       <nav class="od-anchor-nav" data-od-scrollspy aria-label="On this page">
         <a href="#general" class="od-anchor-link is-active">General</a>
         <a href="#php" class="od-anchor-link">PHP</a>
       </nav>
       <div>…sections with matching ids…</div>
     </div> */
.od-anchor-layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 28px; align-items: start; }
.od-anchor-nav { position: sticky; top: 0; display: flex; flex-direction: column; gap: 2px; }
.od-anchor-nav-title { padding: 0 12px 6px; font-size: 10.5px; font-weight: 700; letter-spacing: .6px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-anchor-link { display: block; padding: 6px 12px; border-left: 2px solid var(--od-border-light); font-size: 13px; font-weight: 600; color: var(--od-text-secondary); text-decoration: none; }
.od-anchor-link:hover { color: var(--od-text); border-left-color: var(--od-border); }
.od-anchor-link.is-active { color: var(--od-accent-text); border-left-color: var(--od-accent); }
.od-anchor-link--sub { padding-left: 24px; font-size: 12.5px; }
@media (max-width: 820px) {
    .od-anchor-layout { grid-template-columns: 1fr; gap: 16px; }
    .od-anchor-nav { position: static; flex-direction: row; overflow-x: auto; gap: 4px; padding-bottom: 6px; border-bottom: 1px solid var(--od-border-light); }
    .od-anchor-nav-title { display: none; }
    .od-anchor-link { border-left: 0; border-bottom: 2px solid transparent; white-space: nowrap; padding: 6px 10px; }
    .od-anchor-link.is-active { border-bottom-color: var(--od-accent); }
}

/* ============================== 113. Inline edit ============================== */
/* Click-to-edit text — a title, a label, a note — that turns into an input
   in place and commits on Enter/blur, cancels on Escape (od-inline-edit.js).
   Nothing is saved anywhere: listen for `od:inline-edit-change` and call
   your backend:
     <span class="od-inline-edit" data-od-inline-edit data-od-inline-edit-name="title" tabindex="0">Project Apollo</span> */
.od-inline-edit { display: inline-block; padding: 1px 4px; border-radius: var(--od-radius-sm); border-bottom: 1px dashed transparent; cursor: text; }
.od-inline-edit[hidden] { display: none; }
.od-inline-edit:hover, .od-inline-edit:focus-visible { background: var(--od-nav-hover); border-bottom-color: var(--od-text-muted-2); outline: none; }
.od-inline-edit.is-empty { color: var(--od-text-muted); font-style: italic; font-weight: 500; }
.od-inline-edit-input { font: inherit; color: inherit; letter-spacing: inherit; padding: 0 4px; border: 1px solid var(--od-accent); border-radius: var(--od-radius-sm); background: var(--od-panel); outline: none; min-width: 120px; max-width: 100%; box-sizing: content-box; }
textarea.od-inline-edit-input { display: block; width: 100%; box-sizing: border-box; resize: vertical; line-height: inherit; }
.od-inline-edit-hint { font-size: 11px; color: var(--od-text-muted); font-weight: 600; margin-left: 8px; }

/* ============================== 114. Transfer (dual list) ============================== */
/* Two lists with move buttons between them — assign roles, packages, IPs;
   od-transfer.js moves selected items, filters each side, mirrors the
   target side into hidden inputs (data-od-transfer-name):
     <div class="od-transfer" data-od-transfer data-od-transfer-name="roles[]">
       <div class="od-transfer-box">
         <div class="od-transfer-head">Available <span class="od-transfer-count" data-od-transfer-count="source"></span></div>
         <input type="search" class="od-input od-input--sm" data-od-transfer-filter="source" placeholder="Filter…">
         <ul class="od-transfer-list" data-od-transfer-list="source">
           <li class="od-transfer-item" data-od-transfer-value="mail">Mail</li>
         </ul>
       </div>
       <div class="od-transfer-controls">
         <button type="button" class="od-btn-ghost od-btn-icon" data-od-transfer-move="target" aria-label="Add">&rsaquo;</button>
         <button type="button" class="od-btn-ghost od-btn-icon" data-od-transfer-move="source" aria-label="Remove">&lsaquo;</button>
         <button type="button" class="od-btn-ghost od-btn-icon" data-od-transfer-move-all="target" aria-label="Add all">&raquo;</button>
         <button type="button" class="od-btn-ghost od-btn-icon" data-od-transfer-move-all="source" aria-label="Remove all">&laquo;</button>
       </div>
       <div class="od-transfer-box">…same for "target"…</div>
     </div> */
.od-transfer { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 12px; align-items: stretch; }
.od-transfer-box { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 10px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); }
.od-transfer-head { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; font-weight: 800; }
.od-transfer-count { font-size: 11px; font-weight: 700; color: var(--od-text-secondary); background: var(--od-border-light); border-radius: 999px; padding: 1px 7px; }
.od-transfer-list { list-style: none; margin: 0; padding: 0; height: var(--od-transfer-height, 200px); overflow-y: auto; border: 1px solid var(--od-border-light); border-radius: var(--od-radius-sm); }
.od-transfer-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); cursor: pointer; user-select: none; }
.od-transfer-item:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-transfer-item.is-selected { background: var(--od-accent-bg); color: var(--od-accent-text); }
.od-transfer-item[hidden] { display: none; }
.od-transfer-item .od-i { color: var(--od-text-muted); }
.od-transfer-empty { padding: 20px 10px; text-align: center; font-size: 12.5px; color: var(--od-text-muted); }
.od-transfer-empty[hidden] { display: none; }
.od-transfer-controls { display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.od-transfer-controls .od-btn-icon { font-size: 18px; line-height: 1; }
@media (max-width: 560px) { .od-transfer { grid-template-columns: 1fr; } .od-transfer-controls { flex-direction: row; justify-content: center; } .od-transfer-controls .od-btn-icon { transform: rotate(90deg); } }

/* ============================== 115. Checklist card (setup progress) ============================== */
/* "Finish setting up your site — 3 of 5 done": independent steps, each
   with a check, a title/description and an optional action, under a
   progress summary. Unlike Stepper (section 32) the steps aren't ordered
   — mark any of them .is-done:
     <div class="od-checklist">
       <div class="od-checklist-head">
         <div><div class="od-checklist-title">Finish setting up</div><div class="od-checklist-sub">3 of 5 complete</div></div>
         <div class="od-progress od-checklist-progress"><div class="od-progress-bar od-progress-bar--success" style="width:60%"></div></div>
       </div>
       <ul class="od-checklist-items">
         <li class="od-checklist-item is-done"><span class="od-checklist-check" aria-hidden="true"></span><div class="od-checklist-body"><div class="od-checklist-item-title">Add a domain</div></div></li>
         <li class="od-checklist-item"><span class="od-checklist-check" aria-hidden="true"></span><div class="od-checklist-body"><div class="od-checklist-item-title">Install SSL</div><div class="od-checklist-item-desc">Free with Let's Encrypt.</div></div><a href="#" class="od-btn od-btn-sm">Install</a></li>
       </ul>
     </div> */
.od-checklist { background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); margin-bottom: 20px; }
.od-checklist-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 14px 18px; border-bottom: 1px solid var(--od-border-light); }
.od-checklist-title { font-size: 14px; font-weight: 800; }
.od-checklist-sub { font-size: 12px; font-weight: 600; color: var(--od-text-muted); margin-top: 2px; }
.od-checklist-progress { width: 160px; flex-shrink: 0; }
.od-checklist-items { list-style: none; margin: 0; padding: 0; }
.od-checklist-item { display: flex; align-items: center; gap: 12px; padding: 12px 18px; border-bottom: 1px solid var(--od-border-light); }
.od-checklist-item:last-child { border-bottom: 0; }
.od-checklist-check { position: relative; width: 20px; height: 20px; flex-shrink: 0; border: 2px solid var(--od-border); border-radius: 999px; }
.od-checklist-item.is-done .od-checklist-check { border-color: var(--od-success); background: var(--od-success); }
.od-checklist-item.is-done .od-checklist-check::after { content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); }
.od-checklist-body { flex: 1; min-width: 0; }
.od-checklist-item-title { font-size: 13.5px; font-weight: 700; }
.od-checklist-item.is-done .od-checklist-item-title { color: var(--od-text-muted); text-decoration: line-through; }
.od-checklist-item-desc { font-size: 12.5px; color: var(--od-text-muted); margin-top: 1px; }
.od-checklist-item.is-done .od-checklist-item-desc, .od-checklist-item.is-done > .od-btn, .od-checklist-item.is-done > .od-btn-ghost { display: none; }

/* ============================== 116. Heatmap calendar ============================== */
/* The GitHub-style grid of day cells — backups, deploys, logins per day.
   Cells flow column-first, seven per column (one week each); set each
   cell's intensity with --level (0–4). Pair with data-od-tooltip for the
   exact count:
     <div class="od-heatmap">
       <span class="od-heatmap-cell" style="--level:0" data-od-tooltip="Sep 1: no backups"></span>
       <span class="od-heatmap-cell" style="--level:3" data-od-tooltip="Sep 2: 3 backups"></span>
       …
     </div>
     <div class="od-heatmap-legend">Less <span class="od-heatmap-cell" style="--level:0"></span>…<span class="od-heatmap-cell" style="--level:4"></span> More</div> */
.od-heatmap { display: grid; grid-template-rows: repeat(7, var(--od-heatmap-cell, 11px)); grid-auto-flow: column; grid-auto-columns: var(--od-heatmap-cell, 11px); gap: 3px; overflow-x: auto; padding-bottom: 2px; }
.od-heatmap-cell { display: inline-block; width: var(--od-heatmap-cell, 11px); height: var(--od-heatmap-cell, 11px); border-radius: 2px; background: color-mix(in srgb, var(--od-heatmap-color, var(--od-accent)) calc(var(--level, 0) * 25%), var(--od-border-light)); }
.od-heatmap-cell[data-od-tooltip]:hover { outline: 1px solid var(--od-text-muted); outline-offset: -1px; }
/* --od-heatmap-color may also be set on any ancestor (so a legend next to
   the grid picks up the same hue) — the modifiers below are shorthands. */
.od-heatmap--success, .od-heatmap-legend--success { --od-heatmap-color: var(--od-success); }
.od-heatmap--warning, .od-heatmap-legend--warning { --od-heatmap-color: var(--od-warning); }
.od-heatmap--danger, .od-heatmap-legend--danger { --od-heatmap-color: var(--od-danger); }
.od-heatmap--lg { --od-heatmap-cell: 14px; }
.od-heatmap-legend { display: flex; align-items: center; gap: 3px; margin-top: 8px; font-size: 11px; font-weight: 600; color: var(--od-text-muted); }
.od-heatmap-legend > :first-child, .od-heatmap-legend > :last-child { margin: 0 3px; }
.od-heatmap-months { display: flex; gap: 0; font-size: 10.5px; font-weight: 600; color: var(--od-text-muted-2); margin-bottom: 4px; }
.od-heatmap-months span { flex: 1; }

/* ============================== 117. Shortcuts list ============================== */
/* A "keyboard shortcuts" reference — grouped rows of description + Kbd
   (section 25) — sized for a Modal body. od-modal.js opens any modal with
   data-od-modal-hotkey="?" when that key is pressed outside an input:
     <div class="od-shortcuts">
       <div class="od-shortcuts-group">
         <div class="od-shortcuts-title">Navigation</div>
         <div class="od-shortcut"><span>Command palette</span><span class="od-shortcut-keys"><kbd class="od-kbd">Ctrl</kbd><kbd class="od-kbd">K</kbd></span></div>
       </div>
     </div> */
.od-shortcuts { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 28px; }
.od-shortcuts-title { font-size: 11px; font-weight: 700; letter-spacing: .5px; color: var(--od-text-muted-2); text-transform: uppercase; padding: 8px 0 4px; }
.od-shortcut { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--od-border-light); font-size: 13px; font-weight: 600; color: var(--od-text-secondary); }
.od-shortcut:last-child { border-bottom: 0; }
.od-shortcut-keys { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; }
.od-shortcut-keys .od-kbd { min-width: 22px; }
.od-shortcut-keys .od-shortcut-plus { font-size: 11px; color: var(--od-text-muted-2); }

/* ============================== 118. Table column chooser & density ============================== */
/* Let a viewer hide columns and tighten row height (od-columns.js). The
   chooser is a Dropdown of checkboxes, each naming a 1-based column index;
   density is any button/segment naming "comfortable" or "dense". Both
   remember their state under data-od-columns-key in localStorage:
     <div class="od-dropdown" data-od-dropdown>
       <button type="button" class="od-btn-ghost od-btn-sm" data-od-dropdown-trigger>Columns</button>
       <div class="od-dropdown-menu od-dropdown-menu--right" data-od-dropdown-menu hidden data-od-columns="#my-table" data-od-columns-key="my-table">
         <label class="od-dropdown-check"><input type="checkbox" data-od-column="3" checked> Owner</label>
       </div>
     </div>
     <div class="od-segmented" data-od-segmented data-od-density="#my-table">…data-od-segment="dense"…</div> */
.od-dropdown-check { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: var(--od-radius-sm); font-size: 13px; font-weight: 600; color: var(--od-text-secondary); cursor: pointer; user-select: none; }
.od-dropdown-check:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-dropdown-check input { accent-color: var(--od-accent); margin: 0; }
.od-table--dense th { padding: 7px 16px; }
.od-table--dense td { padding: 6px 16px; font-size: 13px; }
.od-table--dense .od-pill { padding: 2px 8px; font-size: 11px; }
.od-table th[hidden], .od-table td[hidden] { display: none; }

/* ============================== 119. Sticky table header ============================== */
/* A scrolling table body with the header row pinned — wrap the table in
   .od-table-wrap--scroll (set --od-table-max-height to taste) and add
   .od-table--sticky. The <th> background is already opaque, so nothing
   shows through as rows scroll under it. */
.od-table-wrap--scroll { max-height: var(--od-table-max-height, 360px); overflow-y: auto; }
.od-table--sticky thead th { position: sticky; top: 0; z-index: 2; box-shadow: inset 0 -1px 0 var(--od-border); border-bottom: 0; }

/* ============================== 120. Vertical tabs ============================== */
/* Tabs (section 10) with the nav running down the left — settings pages
   with many sections. Same markup and od-tabs.js, plus the modifier on
   .od-tabs; falls back to the horizontal strip below 820px. */
.od-tabs--vertical { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 0 28px; align-items: start; }
.od-tabs--vertical .od-tabs-nav { flex-direction: column; gap: 2px; border-bottom: 0; border-right: 1px solid var(--od-border); margin-bottom: 0; padding-right: 8px; position: sticky; top: 0; }
.od-tabs--vertical .od-tab-button { justify-content: flex-start; text-align: left; border-bottom: 0; border-radius: var(--od-radius); margin: 0; padding: 8px 12px; }
.od-tabs--vertical .od-tab-button.is-active, .od-tabs--vertical .od-tab-button[aria-selected="true"] { background: var(--od-accent-bg); }
.od-tabs--vertical .od-tab-button:focus-visible { border-radius: var(--od-radius); }
.od-tabs--vertical .od-tab-count { margin-left: auto; }
@media (max-width: 820px) {
    .od-tabs--vertical { grid-template-columns: 1fr; }
    .od-tabs--vertical .od-tabs-nav { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--od-border); margin-bottom: 20px; padding-right: 0; position: static; }
    .od-tabs--vertical .od-tab-button { white-space: nowrap; }
}

/* ============================== 121. Plan card (pricing / packages) ============================== */
/* Hosting packages, subscription tiers — a name, a price with period, a
   feature list with check marks, and a call to action. .od-plan--featured
   highlights the recommended one with a ribbon; .is-current marks the
   plan the account is on:
     <div class="od-plan-grid">
       <div class="od-plan od-plan--featured">
         <span class="od-plan-ribbon">Popular</span>
         <div class="od-plan-name">Business</div>
         <div class="od-plan-price">$12<span class="od-plan-period">/mo</span></div>
         <div class="od-plan-desc">For growing sites.</div>
         <ul class="od-plan-features"><li>50 GB SSD</li><li>Unlimited mailboxes</li><li class="is-off">Dedicated IP</li></ul>
         <button type="button" class="od-btn od-btn-block">Choose Business</button>
       </div>
     </div> */
.od-plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; align-items: stretch; }
.od-plan { position: relative; display: flex; flex-direction: column; gap: 6px; padding: 22px 20px 20px; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); }
.od-plan--featured { border-color: var(--od-accent); box-shadow: 0 0 0 1px var(--od-accent); }
.od-plan.is-current { background: var(--od-bg); }
.od-plan-ribbon { position: absolute; top: -11px; left: 20px; padding: 3px 10px; border-radius: 999px; background: var(--od-accent); color: #fff; font-size: 11px; font-weight: 800; letter-spacing: .3px; }
.od-plan-name { font-size: 13px; font-weight: 800; letter-spacing: .3px; text-transform: uppercase; color: var(--od-text-secondary); }
.od-plan-price { font-size: 30px; font-weight: 800; letter-spacing: -1px; line-height: 1.1; }
.od-plan-period { font-size: 13px; font-weight: 600; letter-spacing: 0; color: var(--od-text-muted); margin-left: 2px; }
.od-plan-desc { font-size: 12.5px; color: var(--od-text-muted); min-height: 1.5em; }
.od-plan-features { list-style: none; margin: 12px 0 16px; padding: 0; display: flex; flex-direction: column; gap: 8px; flex: 1; font-size: 13px; font-weight: 600; color: var(--od-text-secondary); }
.od-plan-features li { display: flex; align-items: flex-start; gap: 8px; }
.od-plan-features li::before { content: "\2713"; color: var(--od-success); font-weight: 800; flex-shrink: 0; width: 14px; }
.od-plan-features li.is-off { color: var(--od-text-muted-2); }
.od-plan-features li.is-off::before { content: "\2013"; color: var(--od-text-muted-2); }
.od-plan-foot { margin-top: auto; font-size: 11.5px; color: var(--od-text-muted); text-align: center; }

/* ============================== 122. Swatches (color picker) ============================== */
/* A row of color choices over real radio inputs — a theme accent, a tag
   color, a label. Set each swatch's color with --swatch; the checked one
   gets a ring. CSS only:
     <div class="od-swatches" role="radiogroup">
       <label class="od-swatch" style="--swatch:#2A6FDB"><input type="radio" name="color" value="blue" checked><span class="od-swatch-dot"></span></label>
       …
     </div> */
.od-swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.od-swatch { position: relative; display: inline-flex; cursor: pointer; }
.od-swatch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.od-swatch-dot { width: 26px; height: 26px; border-radius: 999px; background: var(--swatch, var(--od-accent)); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12); transition: transform .1s ease, box-shadow .1s ease; }
.od-swatch:hover .od-swatch-dot { transform: scale(1.08); }
.od-swatch input:checked ~ .od-swatch-dot { box-shadow: 0 0 0 2px var(--od-panel), 0 0 0 4px var(--swatch, var(--od-accent)); }
.od-swatch input:checked ~ .od-swatch-dot::after { content: ""; position: absolute; left: 9px; top: 6px; width: 6px; height: 11px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); }
.od-swatch input:focus-visible ~ .od-swatch-dot { outline: 2px solid var(--od-accent); outline-offset: 3px; }
.od-swatch--sm .od-swatch-dot { width: 18px; height: 18px; }
.od-swatch--sm input:checked ~ .od-swatch-dot::after { display: none; }

/* ============================== 123. Avatar variants ============================== */
/* Sizes, an image fill and a presence dot for the existing .od-avatar
   (section 3):
     <span class="od-avatar od-avatar--lg od-avatar--online"><img src="…" alt=""></span> */
.od-avatar { position: relative; overflow: visible; }
.od-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; display: block; }
.od-avatar--sm { width: 22px; height: 22px; font-size: 10px; }
.od-avatar--lg { width: 40px; height: 40px; font-size: 15px; }
.od-avatar--xl { width: 64px; height: 64px; font-size: 22px; }
.od-avatar--square { border-radius: var(--od-radius); }
.od-avatar--online::after, .od-avatar--away::after, .od-avatar--busy::after, .od-avatar--offline::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 30%; height: 30%; min-width: 8px; min-height: 8px; border-radius: 999px; border: 2px solid var(--od-panel); background: var(--od-text-muted-2); }
.od-avatar--online::after { background: var(--od-success); }
.od-avatar--away::after { background: var(--od-warning); }
.od-avatar--busy::after { background: var(--od-danger); }

/* ============================== 124. Prose ============================== */
/* Readable defaults for a block of authored text — a help article, a
   changelog, a rendered README — inside a panel. Scoped so headings,
   lists and code inside get sensible rhythm without touching the rest of
   the app (the base reset deliberately zeroes list padding elsewhere). */
.od-prose { font-size: 14px; line-height: 1.65; color: var(--od-text); max-width: 72ch; }
.od-prose > :first-child { margin-top: 0; }
.od-prose > :last-child { margin-bottom: 0; }
.od-prose h1, .od-prose h2, .od-prose h3, .od-prose h4 { font-weight: 800; letter-spacing: -.2px; line-height: 1.3; margin: 1.6em 0 .5em; }
.od-prose h1 { font-size: 24px; } .od-prose h2 { font-size: 19px; } .od-prose h3 { font-size: 16px; } .od-prose h4 { font-size: 14px; }
.od-prose p, .od-prose ul, .od-prose ol, .od-prose blockquote, .od-prose pre, .od-prose table { margin: 0 0 1em; }
.od-prose ul, .od-prose ol { padding-left: 1.5em; }
.od-prose li + li { margin-top: .3em; }
.od-prose a { color: var(--od-accent-text); text-decoration: underline; text-underline-offset: 2px; }
.od-prose code { font-family: var(--od-font-mono); font-size: .88em; padding: 1px 5px; border-radius: 4px; background: var(--od-border-light); }
.od-prose pre { padding: 14px 16px; border-radius: var(--od-radius); background: var(--od-dark); color: var(--od-dark-text); font-family: var(--od-font-mono); font-size: 12.5px; line-height: 1.55; overflow-x: auto; }
.od-prose pre code { padding: 0; background: none; font-size: inherit; }
.od-prose blockquote { padding: 4px 16px; border-left: 3px solid var(--od-border); color: var(--od-text-secondary); }
.od-prose hr { border: 0; height: 1px; background: var(--od-border); margin: 1.6em 0; }
.od-prose table { width: 100%; border-collapse: collapse; font-size: 13px; }
.od-prose th, .od-prose td { padding: 7px 10px; border-bottom: 1px solid var(--od-border-light); text-align: left; }
.od-prose th { font-weight: 700; color: var(--od-text-secondary); }
.od-prose img { max-width: 100%; border-radius: var(--od-radius); }
.od-prose kbd { font-family: var(--od-font-mono); font-size: .85em; padding: 1px 5px; border: 1px solid var(--od-border); border-bottom-width: 2px; border-radius: 4px; background: var(--od-nav-hover); }

/* ============================== 125. Truncate utilities ============================== */
/* Single-line ellipsis and a 2/3-line clamp — pair with data-od-tooltip
   to show the full text. The element needs a bounded width (a flex/grid
   child with min-width: 0, or an explicit max-width). */
.od-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.od-truncate-2, .od-truncate-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.od-truncate-2 { -webkit-line-clamp: 2; }
.od-truncate-3 { -webkit-line-clamp: 3; }

/* ============================== 126. Kanban board ============================== */
/* Columns of cards with horizontal overflow — tickets, deploy stages,
   onboarding pipeline. Compose each column's card list with Sortable
   (section 58): give every list the same data-od-sortable-group and cards
   can be dragged between columns as well as reordered within one:
     <div class="od-kanban">
       <div class="od-kanban-col">
         <div class="od-kanban-head"><span class="od-kanban-title">To do</span><span class="od-kanban-count">3</span></div>
         <div class="od-kanban-cards od-sortable" data-od-sortable data-od-sortable-group="board">
           <div class="od-kanban-card od-sortable-item" data-od-sortable-id="t-1">
             <div class="od-kanban-card-title">Renew wildcard cert</div>
             <div class="od-kanban-card-meta"><span class="od-pill od-pill-warning">SSL</span><span class="od-avatar od-avatar--sm">KZ</span></div>
           </div>
         </div>
       </div>
     </div> */
.od-kanban { display: flex; gap: 14px; align-items: flex-start; overflow-x: auto; padding-bottom: 8px; }
.od-kanban-col { flex: 0 0 264px; display: flex; flex-direction: column; max-height: var(--od-kanban-height, 70vh); background: var(--od-bg); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); }
.od-kanban-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 12.5px; font-weight: 800; }
.od-kanban-title { flex: 1; min-width: 0; }
.od-kanban-count { font-size: 11px; font-weight: 700; color: var(--od-text-secondary); background: var(--od-border-light); border-radius: 999px; padding: 1px 7px; }
.od-kanban-cards { display: flex; flex-direction: column; gap: 8px; padding: 0 8px 8px; overflow-y: auto; min-height: 40px; flex: 1; }
/* .od-kanban-cards > .od-kanban-card outranks .od-sortable-item's own
   flex-row layout (section 58) so a card stacks title / desc / meta. */
.od-kanban-cards > .od-kanban-card, .od-kanban-card { display: block; padding: 10px 12px; margin: 0; background: var(--od-panel); border: 1px solid var(--od-border); border-radius: var(--od-radius); cursor: grab; }
.od-kanban-card:hover { border-color: var(--od-text-muted-2); }
.od-kanban-card.is-dragging { opacity: .5; }
.od-kanban-card.is-drag-over { outline: 2px solid var(--od-accent); outline-offset: 1px; }
.od-kanban-cards.is-drag-over { background: var(--od-accent-bg); border-radius: var(--od-radius); }
.od-kanban-card-title { font-size: 13px; font-weight: 700; line-height: 1.4; }
.od-kanban-card-desc { font-size: 12px; color: var(--od-text-muted); margin-top: 3px; }
.od-kanban-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); }
.od-kanban-add { appearance: none; border: 0; border-radius: var(--od-radius-sm); margin: 0 8px 8px; padding: 7px 10px; background: transparent; font: inherit; font-size: 12.5px; font-weight: 700; color: var(--od-text-muted); text-align: left; cursor: pointer; }
.od-kanban-add:hover { background: var(--od-nav-hover); color: var(--od-text); }

/* ============================== 127. Comparison table ============================== */
/* Plan features side by side — a Table (section 9) with the --compare
   modifier: centered value columns, a sticky first column, and yes/no
   marks:
     <table class="od-table od-table--compare"><thead><tr><th></th><th>Starter</th><th class="is-highlight">Business</th></tr></thead>
     <tbody><tr><td>Mailboxes</td><td>5</td><td>Unlimited</td></tr>
     <tr><td>Dedicated IP</td><td><span class="od-compare-no">&ndash;</span></td><td><span class="od-compare-yes">&#10003;</span></td></tr></tbody></table> */
.od-table--compare th:not(:first-child), .od-table--compare td:not(:first-child) { text-align: center; }
.od-table--compare th:first-child, .od-table--compare td:first-child { position: sticky; left: 0; background: var(--od-panel); font-weight: 700; color: var(--od-text-secondary); z-index: 1; }
.od-table--compare th:first-child { background: var(--od-border-light); }
.od-table--compare th.is-highlight, .od-table--compare td.is-highlight { background: var(--od-accent-bg); }
.od-table--compare th.is-highlight { color: var(--od-accent-text); }
.od-compare-yes { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 999px; background: var(--od-success-bg); color: var(--od-success-text); font-size: 13px; font-weight: 800; }
.od-compare-no { color: var(--od-text-muted-2); font-weight: 700; }

/* ============================== 128. Mobile bottom nav ============================== */
/* A fixed icon bar along the bottom of small screens — the four or five
   destinations a phone user actually taps, instead of opening the
   off-canvas sidebar. Hidden at desktop widths; .od-main gets extra bottom
   padding so content isn't hidden behind it. Place it as the last child of
   .od-shell:
     <nav class="od-bottom-nav" aria-label="Primary">
       <a href="#" class="od-bottom-nav-link is-active"><i class="od-i od-i-dashboard" aria-hidden="true"></i><span>Home</span></a>
       …
     </nav> */
.od-bottom-nav { display: none; }
@media (max-width: 820px) {
    .od-bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; display: flex; justify-content: space-around; padding: 4px 6px calc(4px + env(safe-area-inset-bottom, 0px)); background: var(--od-panel); border-top: 1px solid var(--od-border); }
    .od-bottom-nav-link { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 6px 4px; border-radius: var(--od-radius); color: var(--od-text-muted); font-size: 10.5px; font-weight: 700; text-decoration: none; position: relative; }
    .od-bottom-nav-link .od-i { font-size: 19px; }
    .od-bottom-nav-link.is-active { color: var(--od-accent-text); }
    .od-bottom-nav-link:hover { background: var(--od-nav-hover); }
    .od-bottom-nav-link .od-nav-count { position: absolute; top: 2px; right: calc(50% - 20px); margin: 0; padding: 0 5px; font-size: 10px; background: var(--od-danger); color: #fff; }
    .od-shell:has(.od-bottom-nav) .od-main { padding-bottom: 88px; }
}

/* ============================== 129. Relative time & countdown ============================== */
/* od-time.js keeps <time data-od-relative> elements reading "3 minutes
   ago" / "in 2 days" (refreshed every minute, the absolute date in the
   title attribute) and counts <time data-od-countdown> down as hh:mm:ss.
   The only style is a tabular-numeral countdown so the digits don't
   jitter as they tick. */
[data-od-countdown] { font-variant-numeric: tabular-nums; }
[data-od-countdown].is-ended { color: var(--od-danger-text); }

/* ============================== 130. Wizard (multi-step form) ============================== */
/* A Stepper (section 32) driving a set of panels, with Back / Next /
   Finish buttons — "Add domain", "Create mailbox", onboarding
   (od-wizard.js). Only the current panel is shown; Next runs the browser's
   own validation on the fields inside it before moving on:
     <div class="od-wizard" data-od-wizard>
       <div class="od-stepper">
         <div class="od-step" data-od-wizard-step="1">…</div>
         <div class="od-step" data-od-wizard-step="2">…</div>
       </div>
       <div class="od-wizard-panels">
         <div class="od-wizard-panel" data-od-wizard-panel="1">…fields…</div>
         <div class="od-wizard-panel" data-od-wizard-panel="2">…</div>
       </div>
       <div class="od-wizard-actions">
         <button type="button" class="od-btn-ghost" data-od-wizard-prev>Back</button>
         <span class="od-wizard-progress" data-od-wizard-progress></span>
         <button type="button" class="od-btn" data-od-wizard-next>Next</button>
       </div>
     </div> */
.od-wizard .od-stepper { margin-bottom: 20px; }
.od-wizard-panel { display: none; }
.od-wizard-panel.is-active { display: block; animation: od-wizard-in .16s ease; }
@keyframes od-wizard-in { from { opacity: 0; transform: translateX(6px); } to { opacity: 1; transform: none; } }
.od-wizard-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--od-border-light); }
.od-wizard-actions [data-od-wizard-prev][hidden], .od-wizard-actions [data-od-wizard-next][hidden], .od-wizard-actions [data-od-wizard-finish][hidden] { display: none; }
.od-wizard-progress { font-size: 12px; font-weight: 700; color: var(--od-text-muted); margin: 0 auto; }
.od-wizard .od-step { cursor: default; }
.od-wizard .od-step.is-complete { cursor: pointer; }
.od-wizard-summary { display: flex; flex-direction: column; gap: 0; }
.od-modal-body .od-wizard-actions { margin-bottom: -4px; }

/* ============================== 131. Tour (coach marks) ============================== */
/* A guided walk-through: a dimmed overlay with a spotlight cut-out around
   the current target and a bubble beside it, stepping through elements
   marked data-od-tour-step (od-tour.js). Starts from any
   [data-od-tour-start] button or OpenDashboard.Tour.start('name');
   remembers completion in localStorage so it only auto-runs once:
     <button type="button" data-od-tour-start="hosting">Take the tour</button>
     <div data-od-tour="hosting" data-od-tour-step="1" data-od-tour-title="Your domains" data-od-tour-text="Every site you host…">…</div>
   The overlay and bubble are rendered by the script. */
.od-tour-overlay { position: fixed; inset: 0; z-index: 90; pointer-events: none; }
.od-tour-spot { position: fixed; z-index: 91; border-radius: var(--od-radius); box-shadow: 0 0 0 9999px var(--od-overlay), 0 0 0 3px var(--od-accent); transition: top .18s ease, left .18s ease, width .18s ease, height .18s ease; pointer-events: none; }
/* Appended to <body>, outside any .od-shell, so it sets its own font stack. */
.od-tour-bubble { position: fixed; z-index: 92; width: 300px; max-width: calc(100vw - 24px); padding: 14px 16px 12px; background: var(--od-panel); color: var(--od-text); border: 1px solid var(--od-border); border-radius: var(--od-radius-lg); box-shadow: 0 16px 40px rgba(17, 22, 31, .3); font-family: var(--od-font-sans); font-size: 13px; line-height: 1.5; box-sizing: border-box; }
.od-tour-bubble *, .od-tour-bubble *::before, .od-tour-bubble *::after { box-sizing: border-box; }
.od-tour-bubble-step { font-size: 10.5px; font-weight: 700; letter-spacing: .6px; color: var(--od-accent-text); text-transform: uppercase; margin-bottom: 4px; }
.od-tour-bubble-title { font-size: 14px; font-weight: 800; margin-bottom: 4px; }
.od-tour-bubble-text { color: var(--od-text-secondary); }
.od-tour-bubble-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
.od-tour-bubble-actions .od-tour-skip { margin-right: auto; appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: 12px; font-weight: 700; color: var(--od-text-muted); cursor: pointer; }
.od-tour-bubble-actions .od-tour-skip:hover { color: var(--od-text); }
.od-tour-dots { display: inline-flex; gap: 4px; margin-right: 6px; }
.od-tour-dots span { width: 6px; height: 6px; border-radius: 999px; background: var(--od-border); }
.od-tour-dots span.is-active { background: var(--od-accent); }
body.od-tour-active { overflow: hidden; }

/* ============================== 132. Terminal ============================== */
/* A web console with a prompt line — SSH-in-the-browser, a database
   shell, a package manager runner (od-terminal.js). The script echoes
   what's typed, keeps ↑/↓ history and fires `od:terminal-command`; you run
   the command however you like and print the result back with
   OpenDashboard.Terminal.print(). Fixed dark palette like Console:
     <div class="od-terminal" data-od-terminal data-od-terminal-prompt="web1@web-01:~$">
       <div class="od-terminal-bar"><span class="od-terminal-title">web-01.example.net</span><span class="od-pill od-pill-success">Connected</span></div>
       <div class="od-terminal-body" data-od-terminal-body></div>
       <div class="od-terminal-line"><span class="od-terminal-prompt"></span><input class="od-terminal-input" data-od-terminal-input autocomplete="off" spellcheck="false"></div>
     </div> */
.od-terminal { display: flex; flex-direction: column; background: var(--od-dark); color: var(--od-dark-text); border-radius: var(--od-radius-lg); overflow: hidden; font-family: var(--od-font-mono); font-size: 12.5px; line-height: 1.55; }
.od-terminal-bar { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-bottom: 1px solid var(--od-dark-border); font-family: var(--od-font-sans); font-size: 12px; font-weight: 700; }
.od-terminal-bar .od-pill { margin-left: auto; }
.od-terminal-title { opacity: .8; }
.od-terminal-body { flex: 1; min-height: 120px; max-height: var(--od-terminal-height, 300px); overflow-y: auto; padding: 12px 14px 4px; white-space: pre-wrap; word-break: break-word; cursor: text; }
.od-terminal-body:empty { min-height: 60px; }
.od-terminal-out { color: var(--od-dark-text); }
.od-terminal-out--err { color: #FF9492; }
.od-terminal-out--ok { color: #7EE2A8; }
.od-terminal-out--muted { opacity: .55; }
.od-terminal-echo .od-terminal-prompt { margin-right: 8px; }
.od-terminal-line { display: flex; align-items: center; gap: 8px; padding: 6px 14px 12px; }
.od-terminal-prompt { color: #7EE2A8; font-weight: 700; white-space: nowrap; flex-shrink: 0; }
.od-terminal-input { flex: 1; min-width: 0; border: 0; background: none; color: inherit; font: inherit; outline: none; padding: 0; caret-color: #7EE2A8; }
.od-terminal.is-busy .od-terminal-input { opacity: .5; }
.od-terminal.is-busy .od-terminal-prompt::after { content: ""; display: inline-block; width: 8px; height: 8px; margin-left: 6px; border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent; animation: od-spin .6s linear infinite; vertical-align: -1px; }

/* ============================== 133. Activity feed ============================== */
/* Who did what, when — an avatar-led list of events with optional
   attached content (a comment, a diff excerpt, a file). Distinct from
   Timeline (section 28), which is a dotted vertical line of status
   events without actors:
     <div class="od-feed">
       <div class="od-feed-item">
         <span class="od-avatar">AN</span>
         <div class="od-feed-body">
           <div class="od-feed-text"><strong>Anna</strong> renewed the certificate for <a href="#">shop-example.pl</a></div>
           <div class="od-feed-meta"><time data-od-relative datetime="…"></time> · Automatic</div>
           <div class="od-feed-attachment">…</div>
         </div>
       </div>
     </div> */
.od-feed { display: flex; flex-direction: column; }
.od-feed-item { position: relative; display: flex; gap: 12px; padding: 12px 0; }
.od-feed-item + .od-feed-item { border-top: 1px solid var(--od-border-light); }
.od-feed-item > .od-avatar { flex-shrink: 0; margin-top: 1px; }
.od-feed-icon { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 999px; background: var(--od-border-light); color: var(--od-text-secondary); font-size: 13px; flex-shrink: 0; }
.od-feed-icon--success { background: var(--od-success-bg); color: var(--od-success-text); }
.od-feed-icon--danger { background: var(--od-danger-bg); color: var(--od-danger-text); }
.od-feed-icon--warning { background: var(--od-warning-bg); color: var(--od-warning-text); }
.od-feed-icon--accent { background: var(--od-accent-bg); color: var(--od-accent-text); }
.od-feed-body { flex: 1; min-width: 0; }
.od-feed-text { font-size: 13.5px; line-height: 1.5; color: var(--od-text-secondary); }
.od-feed-text strong { color: var(--od-text); font-weight: 700; }
.od-feed-text a { color: var(--od-accent-text); text-decoration: none; font-weight: 600; }
.od-feed-meta { font-size: 12px; font-weight: 600; color: var(--od-text-muted); margin-top: 2px; }
.od-feed-attachment { margin-top: 8px; padding: 10px 12px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-bg); font-size: 13px; color: var(--od-text-secondary); }
.od-feed-attachment .od-code, .od-feed-attachment .od-diff { margin: 0; border-radius: var(--od-radius-sm); }
.od-feed-day { padding: 14px 0 4px; font-size: 11px; font-weight: 700; letter-spacing: .5px; color: var(--od-text-muted-2); text-transform: uppercase; }
.od-feed-more { align-self: center; margin-top: 8px; }

/* ============================== 134. Notification list ============================== */
/* An inbox of notifications — for a topbar Dropdown, a Drawer, or a full
   page. Unread items carry a dot and a tinted background; the header
   offers "mark all read". Toggle .is-unread yourself:
     <div class="od-notif-list">
       <div class="od-notif-head"><span>Notifications</span><span class="od-notif-count">3</span><button type="button" class="od-notif-mark">Mark all read</button></div>
       <a href="#" class="od-notif is-unread">
         <span class="od-notif-icon od-notif-icon--danger"><i class="od-i od-i-alert-triangle" aria-hidden="true"></i></span>
         <span class="od-notif-body"><span class="od-notif-title">Disk 96% full on shop-example.pl</span><span class="od-notif-desc">Add space or clean up backups.</span><time class="od-notif-time" data-od-relative datetime="…"></time></span>
       </a>
     </div> */
.od-notif-list { display: flex; flex-direction: column; min-width: 0; }
.od-dropdown-menu .od-notif-list { width: 360px; max-width: calc(100vw - 32px); margin: -6px; }
.od-notif-head { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--od-border-light); font-size: 13px; font-weight: 800; }
.od-notif-count { font-size: 11px; font-weight: 700; color: var(--od-accent-text); background: var(--od-accent-bg); border-radius: 999px; padding: 1px 7px; }
.od-notif-mark { margin-left: auto; appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: 12px; font-weight: 700; color: var(--od-accent-text); cursor: pointer; }
.od-notif-mark:hover { text-decoration: underline; }
.od-notif { position: relative; display: flex; gap: 12px; padding: 11px 14px 11px 22px; color: inherit; text-decoration: none; border-bottom: 1px solid var(--od-border-light); }
.od-notif:last-child { border-bottom: 0; }
.od-notif:hover { background: var(--od-nav-hover); }
.od-notif.is-unread { background: var(--od-accent-bg); }
.od-notif.is-unread::before { content: ""; position: absolute; left: 9px; top: 18px; width: 6px; height: 6px; border-radius: 999px; background: var(--od-accent); }
.od-notif-icon { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 999px; background: var(--od-border-light); color: var(--od-text-secondary); font-size: 14px; flex-shrink: 0; }
.od-notif-icon--success { background: var(--od-success-bg); color: var(--od-success-text); }
.od-notif-icon--danger { background: var(--od-danger-bg); color: var(--od-danger-text); }
.od-notif-icon--warning { background: var(--od-warning-bg); color: var(--od-warning-text); }
.od-notif-icon--accent { background: var(--od-panel); color: var(--od-accent-text); border: 1px solid var(--od-border); }
.od-notif-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.od-notif-title { font-size: 13px; font-weight: 700; }
.od-notif-desc { font-size: 12.5px; color: var(--od-text-secondary); }
.od-notif-time { font-size: 11.5px; font-weight: 600; color: var(--od-text-muted); margin-top: 2px; }
.od-notif-foot { padding: 8px 14px; text-align: center; border-top: 1px solid var(--od-border-light); font-size: 12.5px; font-weight: 700; }
.od-notif-foot a { color: var(--od-accent-text); text-decoration: none; }
.od-notif-empty { padding: 28px 14px; text-align: center; font-size: 13px; color: var(--od-text-muted); }

/* ============================== 135. Job card (background task) ============================== */
/* A long-running task in flight — a backup, a migration, a restore, a
   bulk import — with a progress bar, a live status line, an ETA and a
   cancel/retry action. Add --done / --failed when it finishes:
     <div class="od-job">
       <div class="od-job-head">
         <span class="od-spinner od-spinner--sm od-job-spinner" aria-hidden="true"></span>
         <div class="od-job-title">Full backup · example.com</div>
         <div class="od-job-pct">64%</div>
       </div>
       <div class="od-progress"><div class="od-progress-bar" style="width:64%"></div></div>
       <div class="od-job-foot"><span class="od-job-status">Archiving wp-content/uploads… (8,120 / 14,203 files)</span><span class="od-job-eta">~2 min left</span><button type="button" class="od-btn-ghost od-btn-sm">Cancel</button></div>
     </div> */
.od-job { padding: 12px 14px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-panel); }
.od-job + .od-job { margin-top: 10px; }
.od-job-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.od-job-title { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-job-pct { font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; }
.od-job-foot { display: flex; align-items: center; gap: 10px; margin-top: 8px; font-size: 12px; font-weight: 600; color: var(--od-text-muted); }
.od-job-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-job-eta { flex-shrink: 0; font-variant-numeric: tabular-nums; }
.od-job--done .od-job-spinner, .od-job--failed .od-job-spinner { display: none; }
.od-job--done .od-progress-bar { background: var(--od-success); }
.od-job--done .od-job-pct { color: var(--od-success-text); }
.od-job--failed .od-progress-bar { background: var(--od-danger); }
.od-job--failed .od-job-pct, .od-job--failed .od-job-status { color: var(--od-danger-text); }
.od-job-icon { font-size: 15px; color: var(--od-text-muted); }
.od-job--done .od-job-icon { color: var(--od-success-text); }
.od-job--failed .od-job-icon { color: var(--od-danger-text); }
.od-job--queued .od-progress-bar { background: repeating-linear-gradient(135deg, var(--od-border) 0 4px, transparent 4px 8px); width: 100% !important; }

/* ============================== 136. Upload queue ============================== */
/* The list under a File dropzone (section 47) while files upload — icon,
   name, size, a thin progress bar and a per-file state/cancel. Set each
   bar's width and the row's modifier as your uploads progress:
     <ul class="od-upload-list">
       <li class="od-upload">
         <i class="od-i od-i-image od-upload-icon" aria-hidden="true"></i>
         <div class="od-upload-body">
           <div class="od-upload-row"><span class="od-upload-name">hero.png</span><span class="od-upload-size">2.4 MB</span></div>
           <div class="od-progress"><div class="od-progress-bar" style="width:70%"></div></div>
         </div>
         <button type="button" class="od-upload-cancel" aria-label="Cancel">&times;</button>
       </li>
       <li class="od-upload od-upload--done">…</li>
       <li class="od-upload od-upload--failed">…<span class="od-upload-error">File too large</span>…</li>
     </ul> */
.od-upload-list { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.od-upload { display: flex; align-items: center; gap: 12px; padding: 8px 10px; border: 1px solid var(--od-border-light); border-radius: var(--od-radius); background: var(--od-panel); }
.od-upload-icon { font-size: 18px; color: var(--od-text-muted); flex-shrink: 0; }
.od-upload-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.od-upload-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12.5px; }
.od-upload-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.od-upload-size { flex-shrink: 0; font-weight: 600; color: var(--od-text-muted); font-variant-numeric: tabular-nums; }
.od-upload .od-progress { height: 4px; }
.od-upload--done .od-progress-bar { background: var(--od-success); }
.od-upload--done .od-upload-icon { color: var(--od-success-text); }
.od-upload--failed .od-progress-bar { background: var(--od-danger); }
.od-upload--failed .od-upload-name { color: var(--od-danger-text); }
.od-upload-error { font-size: 11.5px; font-weight: 600; color: var(--od-danger-text); }
.od-upload-cancel { flex-shrink: 0; appearance: none; width: 24px; height: 24px; border: 0; border-radius: 999px; background: none; color: var(--od-text-muted); font-size: 16px; line-height: 1; cursor: pointer; padding: 0; }
.od-upload-cancel:hover { background: var(--od-nav-hover); color: var(--od-text); }
.od-upload--done .od-upload-cancel { visibility: hidden; }
