/* Design System CSS Variables */
:root {
    /* ========================================================================
       v0.8.0 page chrome — single source of truth for the analyze + compliance
       section page widths. Consumed by:
         - .device-detail-page, .devices-page  (device-detail.css)
         - .fw-page                            (compliance/framework_page.css)
         - .compliance-landing                 (compliance/landing.css)
       Change here once → every page updates. Add new analyze pages
       (.gateways-page, .gateway-detail-page) by referencing these vars.
       ======================================================================== */
    --page-max-width: 110rem; /* PR-8: bumped from 88rem so the 7-col devices
                                 table fits beside the 240px rail. Affects all
                                 v0.8.0 chrome pages (compliance + analyze) —
                                 they all gain ~22rem of horizontal breathing
                                 room on wide monitors. */
    --page-padding: 1rem 1.25rem; /* shorthand: top/bottom 1rem, sides 1.25rem */
    --page-rail-width: 240px; /* main + rail two-column stage; PR-7=320, PR-8=240 */

    /* Spacing System */
    --primitives-spacing-0: 0px;
    --primitives-spacing-0-5: 2px;
    --primitives-spacing-1: 4px;
    --primitives-spacing-1-duplicate: 4px;
    --primitives-spacing-5: 20px;
    --primitives-spacing-11: 44px;
    --primitives-spacing-14: 56px;

    /* Border Radius */
    --themes-border-border-radius-rounded-sm: 4px;

    /* Background Colors */
    --themes-colors-background-bg-secondary-soft: var(--bg-page);
    --themes-colors-background-bg-brand-softer: var(--row-highlight);
    --themes-colors-background-bg-warning-soft: var(--sev-medium-bg);

    /* Border Colors */
    --themes-colors-border-border-base-medium: var(
        --border-medium
    ); /* kept: no core equivalent (dE 3.67 from --border-medium) */
    --themes-colors-border-border-brand-subtle: var(
        --hex-gw-1
    ); /* kept: no core equivalent (dE 3.07 from --sev-low-border) */
    --themes-colors-border-border-warning-subtle: var(--sev-medium-border);

    /* Text Colors */
    --themes-colors-text-text-fg-brand-strong: var(--sev-low-fg);
    --themes-colors-text-text-fg-warning: var(
        --danger-darkest-fg
    ); /* kept: genuinely unique (dE 17.0 from anything in core) */

    /* Brand Colors (Figma: Green/100)
       --color-brand-green / -hover / -deep are NOT redefined here. They are
       defined once in design-tokens.css, which is vendored from the design
       system itself (.claude/skills/patitur-design/colors_and_type.css).
       This file used to re-declare all three and, because base.html loads it
       AFTER design-tokens.css, its copies won. Two were byte-identical and
       harmless; -deep was var(--coverage-strong-fg) (emerald-800) against the design system's
       var(--color-brand-green-deep) (emerald-700), so every internal-link hover rendered a colour the
       design system does not define and nothing pointed at the disagreement.
       Add brand tokens to design-tokens.css, not here. */
    --color-brand-green-light: var(--coverage-strong-bg);
    --color-brand-green-focus-ring: color-mix(
        in srgb,
        var(--color-brand-green) 40%,
        transparent
    );

    /* Panel Headers (Figma: tenant info panel header tint) */
    --color-panel-header-tinted: var(--status-online-bg);

    /* Chart Colors (Figma: donut charts) */
    --color-chart-status-online: var(--chart-status-online);
    --color-chart-status-offline: var(--chart-status-offline);
    --color-chart-importance-low: var(--chart-importance-low);
    --color-chart-importance-medium: var(--chart-importance-medium);
    --color-chart-importance-high: var(--chart-importance-high);

    /* Chart Colors (Figma: bar charts) */
    --color-chart-alert-bar: var(
        --chart-alert-bar
    ); /* Figma Primary/300 teal */

    /* Dashboard Figma Alignment (Phase 1) */
    /* Banner gradient backgrounds */
    --color-banner-gradient-start: var(--banner-soft-start);
    --color-banner-gradient-end: var(--banner-soft-end);

    /* Tenant Scope Banner (Figma TenantBanner.tsx -- dark gradient)
       Distinct from --color-banner-gradient-* (light green, org dashboard). */
    --color-tenant-banner-gradient-start: var(--banner-tenant-start);
    --color-tenant-banner-gradient-end: var(--color-brand-green);
    --color-tenant-banner-border: var(--banner-tenant-border);

    /* Alert severity dot colors (8px circles) */
    --color-severity-dot-high: var(--sev-dot-high); /* coral red */
    --color-severity-dot-medium: var(--sev-dot-medium); /* warm orange */
    --color-severity-dot-info: var(--sev-dot-info); /* sky blue */

    /* Status Pill Colors (Figma: pill-shaped status badges) */
    --color-status-pill-bg-online: var(--status-online-bg);
    --color-status-pill-text-online: var(--status-online-fg);
    --color-status-pill-bg-danger: var(--status-danger-bg);
    --color-status-pill-text-danger: var(--status-danger-fg);
    --color-status-pill-bg-warning: var(--status-warning-bg);
    --color-status-pill-text-warning: var(--status-warning-fg);
    --color-status-pill-bg-info: var(--status-info-bg);
    --color-status-pill-text-info: var(--status-info-fg);
    --color-status-pill-bg-neutral: var(--status-neutral-bg);
    --color-status-pill-text-neutral: var(--fg-4);

    /* Filter Pill Colors (Figma: toggle filter buttons) */
    --color-filter-pill-bg-active: var(--color-brand-green);
    --color-filter-pill-text-active: var(--fg-on-dark);
    --color-filter-pill-border-inactive: var(--border-medium);
    --color-filter-pill-text-inactive: var(--fg-body-strong);

    /* Viewing Context Dropdown (Sidebar — Dark Theme, Figma Sidebar.tsx) */
    --color-vc-section-setup-bg: var(--bg-sidebar-setup);
    --color-vc-section-connected-bg: var(--bg-sidebar);
}

/* Flowbite Badge Base Styles - High specificity */
table .flowbite-badge,
.flowbite-badge {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 4px !important;
    padding: 2px 4px !important;
    position: relative !important;
    border-radius: var(--radius-xs) !important;
    border: 1px solid !important;
    white-space: nowrap !important;
    font-family: var(--font-display) !important;
    font-weight: var(--fw-medium) !important;
    font-size: var(--fs-sm) !important;
    text-align: center !important;
    letter-spacing: 0 !important;
    line-height: 16px !important;
}

/* Flowbite Severity Badge Variants - High specificity - Design System Colors */
table .flowbite-badge-severity-low,
.flowbite-badge-severity-low {
    border-radius: 6px !important;
    border: 1px solid var(--sev-low-border) !important;
    background: var(--sev-low-bg) !important;
    color: var(--sev-low-fg) !important;
    text-align: center !important;
    font-family: var(--font-display) !important;
    font-size: var(--fs-sm) !important;
    font-style: normal !important;
    font-weight: 500 !important;
    line-height: 16px !important; /* 133.333% */
}

table .flowbite-badge-severity-medium,
.flowbite-badge-severity-medium {
    border-radius: 6px !important;
    border: 1px solid var(--sev-medium-border) !important;
    background: var(--sev-medium-bg) !important;
    color: var(--sev-medium-fg) !important;
    text-align: center !important;
    font-family: var(--font-display) !important;
    font-size: var(--fs-sm) !important;
    font-style: normal !important;
    font-weight: 500 !important;
    line-height: 16px !important; /* 133.333% */
}

table .flowbite-badge-severity-high,
.flowbite-badge-severity-high {
    border-radius: 6px !important;
    border: 1px solid var(--sev-high-border) !important;
    background: var(--sev-high-bg) !important;
    color: var(--sev-high-fg) !important;
    text-align: center !important;
    font-family: var(--font-display) !important;
    font-size: var(--fs-sm) !important;
    font-style: normal !important;
    font-weight: 500 !important;
    line-height: 16px !important; /* 133.333% */
}

table .flowbite-badge-severity-critical,
.flowbite-badge-severity-critical {
    border-radius: 6px !important;
    border: 1px solid var(--sev-critical-border) !important;
    background: var(--sev-critical-bg) !important;
    color: var(--sev-critical-fg) !important;
    text-align: center !important;
    font-family: var(--font-display) !important;
    font-size: var(--fs-sm) !important;
    font-style: normal !important;
    font-weight: 500 !important;
    line-height: 16px !important; /* 133.333% */
}

/* Flowbite Status Badge Variants - No dot icon, rounded shape */
table .flowbite-badge-status-active,
.flowbite-badge-status-active {
    background-color: var(--bg-secondary-medium) !important;
    border-color: var(--themes-colors-border-border-base-medium) !important;
    color: var(--fg-2) !important;
    border-radius: 6px !important;
    padding: 2px 6px !important;
}

table .flowbite-badge-status-resolved,
.flowbite-badge-status-resolved {
    background-color: var(--border-base) !important;
    border-color: var(--border-medium) !important;
    color: var(--chart-axis-fg) !important;
    border-radius: 6px !important;
    padding: 2px 6px !important;
}

table .flowbite-badge-status-acknowledged,
.flowbite-badge-status-acknowledged {
    background-color: var(
        --themes-colors-background-bg-brand-softer
    ) !important;
    border-color: var(--themes-colors-border-border-brand-subtle) !important;
    color: var(--themes-colors-text-text-fg-brand-strong) !important;
    border-radius: 6px !important;
    padding: 2px 6px !important;
}

/* Table Header Cell - Standard table cell, not flex container */
table thead th.table-header-cell {
    height: 44px;
    padding: 12px 20px;
    border-top: 1px solid var(--border-base);
    border-bottom: 1px solid var(--border-base);
    background: var(--bg-page);
    color: var(--fg-3);
    font-family: var(--font-base);
    font-size: var(--fs-base);
    font-weight: 500;
    line-height: var(--lh-normal);
    text-transform: capitalize;
    vertical-align: middle;
    text-align: left;
}

/* Table Header Link - Flex container for text and icon alignment */
table thead th.table-header-cell .table-header-link {
    color: var(--fg-3);
    font-family: var(--font-base);
    font-size: var(--fs-base);
    font-weight: 500;
    text-transform: capitalize;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}

/* Table header text */
table thead th.table-header-cell .table-header-link .table-header-text {
    display: inline;
}

/* SVG icons in table headers */
table thead th.table-header-cell .table-header-link svg {
    display: inline-block;
    flex-shrink: 0;
    width: 12px;
    height: 12px;
    vertical-align: middle;
}

.table-header-link:hover {
    color: var(--fg-1);
}

/* Table Cell Links - clickable items in table rows (green by default, underline on hover) */
.table-cell-link {
    color: var(--color-brand-green);
    text-decoration: none;
    transition:
        color var(--t-fast),
        text-decoration var(--t-fast);
}

.table-cell-link:hover {
    color: var(--color-brand-green-hover);
    text-decoration: underline;
}

.table-cell-link:focus {
    outline: 2px solid var(--color-brand-green);
    outline-offset: 1px;
    border-radius: 2px;
}

/*
 * ─── Link affordance convention ────────────────────────────────────────────
 *
 * Three icon-decorated link classes:
 *
 *   .link-external  → box-with-diagonal-arrow glyph (::after).
 *                     For URLs pointing OUTSIDE the Patitur app
 *                     (mitre.org, sensus.com, hhs.gov, etc.). Always pair
 *                     with target="_blank" rel="noopener noreferrer".
 *
 *   .link-internal  → plain diagonal-arrow glyph (::after).
 *                     For in-app navigation where we want a "go to details"
 *                     affordance (e.g. "view framework", "view product model").
 *                     Do NOT add target="_blank".
 *
 *   .link-glossary  → book-open glyph PREFIX (::before).
 *                     For in-app navigation that opens a *definition* in the
 *                     glossary. Use whenever the destination is /glossary/...
 *                     The prefix glyph disambiguates "look up the term" from
 *                     "navigate to the peer object", which matters when the
 *                     link text is itself the framework / object name and
 *                     could otherwise be confused with the framework page.
 *
 * `.table-cell-link-external` is kept as a legacy alias of `.link-external`.
 *
 * Sidebar entries, breadcrumbs, or other structural navigation where the
 * user doesn't need a visual "jump to" cue should omit both classes.
 */
.link-external,
.table-cell-link-external {
    color: var(--color-brand-green);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    transition:
        color var(--t-fast),
        text-decoration var(--t-fast);
}

.link-external::after,
.table-cell-link-external::after {
    /* Box-with-arrow external-link glyph (Heroicons arrow-top-right-on-square).
       Semantically means "opens in a new tab / goes off-site" — reserve for
       actual external links only. Internal navigation should omit this. */
    content: "";
    display: inline-block;
    width: 0.8rem;
    height: 0.8rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2325af88'%3E%3Cpath fill-rule='evenodd' d='M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z' clip-rule='evenodd' /%3E%3Cpath fill-rule='evenodd' d='M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z' clip-rule='evenodd' /%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    flex-shrink: 0;
}

.link-external:hover,
.table-cell-link-external:hover {
    color: var(--color-brand-green-hover);
    text-decoration: underline;
}

.link-external:hover::after,
.table-cell-link-external:hover::after {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%231e9a73'%3E%3Cpath fill-rule='evenodd' d='M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z' clip-rule='evenodd' /%3E%3Cpath fill-rule='evenodd' d='M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z' clip-rule='evenodd' /%3E%3C/svg%3E");
}

.link-external:focus,
.table-cell-link-external:focus {
    outline: 2px solid var(--color-brand-green);
    outline-offset: 1px;
    border-radius: 2px;
}

/*
 * ─── Code chip ─────────────────────────────────────────────────────────────
 *
 * Monospace pill used for IDs, element codes, technique IDs, OUI prefixes —
 * anywhere we want a gray "token" to stand out in text.
 *
 * Combine with `.link-external` when the chip is a clickable off-site link
 * (e.g. `Art. 21(2)(a)` → EUR-Lex) to get the box-with-arrow glyph and the
 * hover/focus green border that signals clickability.
 */
.code-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    padding: 0.125rem 0.5rem;
    border-radius: var(--radius-xs);
    background-color: var(--status-neutral-bg);
    border: 1px solid transparent;
    color: var(--fg-3);
    transition:
        background-color var(--t-fast),
        border-color var(--t-fast),
        color var(--t-fast);
    text-decoration: none;
}

a.code-chip:hover,
a.code-chip:focus {
    background-color: var(--border-base);
    border-color: var(--color-brand-green);
    color: var(--fg-1);
    text-decoration: none;
    outline: none;
}

/* When a code chip is itself an external link, skip the gap between the code
   text and the chip border so the ::after glyph sits neatly inside. */
a.code-chip.link-external,
a.code-chip.table-cell-link-external {
    gap: 0.25rem;
}

/* Internal "jump to details" link — diagonal arrow, no box */
.link-internal {
    color: var(--link-internal-fg);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    transition:
        color var(--t-fast),
        text-decoration var(--t-fast);
}

.link-internal::after {
    content: "";
    display: inline-block;
    width: 0.7rem;
    height: 0.7rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%230e9f6e'%3E%3Cpath fill-rule='evenodd' d='M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    flex-shrink: 0;
}

.link-internal:hover {
    color: var(--color-brand-green-deep);
    text-decoration: underline;
}

.link-internal:hover::after {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23047857'%3E%3Cpath fill-rule='evenodd' d='M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
}

.link-internal:focus {
    outline: 2px solid var(--link-internal-fg);
    outline-offset: 1px;
    border-radius: 2px;
}

/* Color override for link-internal used with Tailwind text-red-600
   (e.g. alert count deep-links).  The doubled-class selector beats
   the single `.link-internal` rule above without resorting to !important. */
.link-internal.text-danger {
    color: var(--danger-fg); /* Tailwind red-600 */
}
.link-internal.text-danger::after {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23dc2626'%3E%3Cpath fill-rule='evenodd' d='M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
}
.link-internal.text-danger:hover {
    color: var(--danger-strong-fg); /* Tailwind red-700 */
}
.link-internal.text-danger:hover::after {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23b91c1c'%3E%3Cpath fill-rule='evenodd' d='M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
}

/* Glossary "look up the term" link — book-open glyph prefix.
   Distinguishes definition lookups from peer-object navigation; see the
   "Link affordance convention" block above. */
.link-glossary {
    color: var(--link-internal-fg);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    transition:
        color var(--t-fast),
        text-decoration var(--t-fast);
}

.link-glossary::before {
    content: "";
    display: inline-block;
    width: 0.85rem;
    height: 0.85rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.75' stroke='%230e9f6e'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25'/%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    flex-shrink: 0;
}

.link-glossary:hover {
    color: var(--color-brand-green-deep);
    text-decoration: underline;
}

.link-glossary:hover::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.75' stroke='%23047857'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25'/%3E%3C/svg%3E");
}

.link-glossary:focus {
    outline: 2px solid var(--link-internal-fg);
    outline-offset: 1px;
    border-radius: 2px;
}

/*
 * ─── LAF code token ────────────────────────────────────────────────────────
 *
 * Monospace styling for LAF_xxx tokens (LAF_401, LAF_700, etc.) when they
 * appear inside a ``.link-glossary`` anchor — alerts table, analytics
 * dashboard widgets, recent-alerts list. Transparent background so the
 * link's hover / focus chrome reads cleanly; ``color: inherit`` so the
 * brand-green link colour applies. Use ``.code-chip`` instead when you
 * want a grey mono pill (standalone token, not inside a link).
 *
 * Usage:
 *   <a href="/glossary/alerts/LAF-401" class="link-glossary">
 *     <code class="laf-code">LAF_401</code>
 *   </a>
 */
.laf-code {
    font-family: var(--font-mono);
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);
    color: inherit;
    background: transparent;
    padding: 0;
    letter-spacing: 0.01em;
}

/* Status Pill Variant Classes (Figma: pill-shaped status badges) */
.status-pill-online {
    background-color: var(--color-status-pill-bg-online);
    color: var(--color-status-pill-text-online);
}

.status-pill-danger {
    background-color: var(--color-status-pill-bg-danger);
    color: var(--color-status-pill-text-danger);
}

.status-pill-warning {
    background-color: var(--color-status-pill-bg-warning);
    color: var(--color-status-pill-text-warning);
}

.status-pill-info {
    background-color: var(--color-status-pill-bg-info);
    color: var(--color-status-pill-text-info);
}

.status-pill-neutral {
    background-color: var(--color-status-pill-bg-neutral);
    color: var(--color-status-pill-text-neutral);
}

/* Filter Pill Variant Classes (Figma: toggle filter buttons) */
.filter-pill-active {
    background-color: var(--color-filter-pill-bg-active);
    /* Transparent rather than absent: both states carry a border, so toggling
       one does not shift the row by a pixel. */
    border-color: transparent;
    color: var(--color-filter-pill-text-active);
}

.filter-pill-inactive {
    background-color: var(--bg-card);
    border-color: var(--color-filter-pill-border-inactive);
    color: var(--color-filter-pill-text-inactive);
}

/* The unselected pill firms up toward the brand on hover. This was a Tailwind
   arbitrary value with the brand hex interpolated into it from Python —
   ``hover:border-[{COLOR_BRAND_GREEN}]`` — which put a colour in a class
   string built server-side. */
.filter-pill-inactive:hover {
    border-color: var(--color-brand-green);
}

/* Active row highlight for table row selection */
:root {
    --color-row-highlight: var(--row-highlight);
}

#product-models-table tr.pm-row-active td,
#tenant-pm-tab-container tr.pm-row-active td {
    background-color: var(--color-row-highlight);
}

/* Subtle pulsing animation for attention-drawing buttons (V53) */
@keyframes pulse-orange {
    0%,
    100% {
        background-color: var(--sev-major-dot);
    }
    50% {
        background-color: var(--pulse-orange-mid);
    }
}

.animate-pulse-orange {
    animation: pulse-orange 2.5s ease-in-out infinite;
}

/* Leaflet popup overrides — keep close button inside the white box */
.leaflet-popup-content-wrapper {
    border-radius: 10px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
    padding: 12px 14px;
}

.leaflet-popup-content {
    margin: 0;
    font-family: var(--font-base);
    font-size: var(--fs-base);
}

.leaflet-container a.leaflet-popup-close-button {
    top: 8px;
    right: 10px;
    z-index: 10;
    font-size: 20px;
    font-weight: var(--fw-regular);
    color: var(--fg-5);
    width: 20px;
    height: 20px;
    line-height: 20px;
    padding: 0;
}

.leaflet-container a.leaflet-popup-close-button:hover {
    color: var(--fg-body-strong);
}

/* Tab underline — ensure no border-radius on active tab indicator.
   Tailwind's border-b-2 produces a flat bottom border; this rule guards
   against any CDN stylesheet or future browser default introducing rounding
   on the bottom corners of tab buttons. (LF-2) */
[role="tablist"] [role="tab"] {
    border-radius: 0;
}

/* ============================================================
   Alert details modal — inline status-edit form + resolution
   card.  Lifted out of templates/alerts/_alert_details_modal.html
   so the modal's chrome lives in one place and design-token
   tweaks (greens, grays, focus rings) don't require touching
   the Jinja partial.  The modal is htmx-swapped into
   #main-modal-content from any page that triggers
   ``alerts.alert_details_modal``, so the rules need to be
   loaded globally — design-system.css is the global include in
   base.html.
   ============================================================ */
.alert-resolved-card {
    margin-top: 0.5rem;
    padding: 0.625rem 0.75rem;
    background: var(--status-resolved-bg);
    border: 1px solid var(--policy-stricter-bg);
    border-radius: 0.375rem;
}
.alert-resolved-row {
    display: flex;
    gap: 0.625rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    padding: 0.125rem 0;
}
.alert-resolved-label {
    flex-shrink: 0;
    width: 7rem;
    font-weight: var(--fw-semibold);
    color: var(--color-brand-green-deep);
}
.alert-resolved-value {
    color: var(--fg-1);
}
.alert-resolved-value-placeholder {
    color: var(--fg-5);
    font-style: italic;
}
.alert-status-row {
    display: flex;
    align-items: flex-end;
    gap: 1.5rem;
    flex-wrap: wrap;
}
.alert-status-cell {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

/* The gateway calibration card's diagnostic rows. Same shape as the alert
   status cell — a label the component should own rather than leave to the
   loop that renders it. */
.cal-diagnostics dt {
    font-size: var(--fs-base);
    color: var(--fg-4);
}

/* The component styles its own label. Its three call sites each wrote
   "text-sm font-medium text-subtle" instead, which is what .detail-pair__label
   already means — a component that leaves its label to the call site invites
   three spellings of one thing. */
.alert-status-cell dt {
    font-size: var(--fs-base);
    font-weight: 500;
    color: var(--fg-4);
}
.alert-status-cell-value {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 1.75rem;
}
.alert-status-id {
    font-family: var(--font-mono);
    background: var(--status-neutral-bg);
    padding: 0.125rem 0.5rem;
    border-radius: var(--radius-xs);
    font-size: 0.8125rem;
    color: var(--fg-1);
}
.alert-status-edit-toggle {
    margin-left: auto;
}
.alert-status-edit-select {
    padding: 0.25rem 0.5rem;
    font-size: var(--fs-sm);
    line-height: 1.4;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-sm);
    background: var(--bg-app);
    color: var(--fg-1);
    cursor: pointer;
    min-width: 9rem;
}
.alert-status-edit-select:focus {
    outline: none;
    border-color: var(--color-brand-green-hover);
    box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.15);
}
.alert-status-edit-form {
    margin-top: 0.875rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border-base);
}
.alert-status-edit-form-label {
    display: block;
    margin-bottom: 0.375rem;
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--fg-body-strong);
}
.alert-status-edit-form-hint {
    font-weight: var(--fw-regular);
    color: var(--fg-5);
    margin-left: 0.25rem;
}
.alert-status-edit-textarea {
    width: 100%;
    padding: 0.4375rem 0.625rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-sm);
    background: var(--bg-app);
    color: var(--fg-1);
    resize: vertical;
    min-height: 2.75rem;
}
.alert-status-edit-textarea:focus {
    outline: none;
    border-color: var(--color-brand-green-hover);
    box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.15);
}
.alert-status-edit-form-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.625rem;
}
.alert-status-edit-form-prototype-note {
    flex: 1;
    font-size: var(--fs-xs);
    color: var(--fg-5);
}
.alert-status-edit-save-btn {
    padding: 0.4375rem 1rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--fg-on-dark);
    background: var(--color-brand-green-hover);
    border: 1px solid var(--color-brand-green-hover);
    border-radius: 0.375rem;
    cursor: pointer;
}
.alert-status-edit-save-btn:hover {
    background: var(--color-brand-green-deep);
    border-color: var(--color-brand-green-deep);
}

/* In-modal quick actions — one-click Acknowledge / False positive / Resolve,
   the same tinted flat pills as the list's bulk toolbar (no bevel/shadow), fed
   by one shared comment box. */
.alert-modal-quick-actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-top: 0.75rem;
}
.alert-modal-quick-comment {
    width: 100%;
    padding: 0.375rem 0.625rem;
    border: 1px solid var(--border-medium);
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    resize: none;
}
.alert-modal-quick-comment:focus {
    outline: none;
    border-color: var(--color-brand-green);
}
.alert-modal-quick-btns {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.alert-modal-quick-btn {
    flex-shrink: 0;
    padding: 0.375rem 0.875rem;
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.alert-modal-quick-btn-ack {
    background: var(--sev-medium-bg);
    border: 1px solid var(--sev-medium-border);
    color: var(--status-warning-fg);
}
.alert-modal-quick-btn-ack:hover {
    background: var(--warning-softer-bg);
}
.alert-modal-quick-btn-fp {
    background: var(--status-neutral-bg);
    border: 1px solid var(--border-medium);
    color: var(--fg-body-strong);
}
.alert-modal-quick-btn-fp:hover {
    background: var(--border-base);
}
.alert-modal-quick-btn-resolve {
    background: var(--coverage-strong-bg);
    border: 1px solid var(--policy-stricter-bg);
    color: var(--coverage-strong-fg);
}
.alert-modal-quick-btn-resolve:hover {
    background: var(--policy-stricter-bg);
}

/* ============================================================
   Alert details modal -- in-product chrome (plan 03 / handoff
   .generated/handoff-alerts). Layered ON TOP of the status-edit
   + resolved-card rules above (which are intentionally left
   untouched so alert-modal-status-edit.js keeps working and the
   existing explicit mono stack / prototype-note rule are not
   regressed -- the handoff snippet re-bundled and drifted those,
   so only the genuinely-new .alert-modal-* rules are taken).

   Replaces the old bg-gray-800 / bright-amber inline panels with
   the card look: tinted-green header, deep-green display title,
   soft-neutral content blocks, warning-soft reason block. No
   left-bevel accent stripes (explicit design call). Font tokens
   carry fallbacks per the Phase-1 graceful-degradation convention.
   ============================================================ */

/* The shared modal shell (base.html #modal-placeholder) always
   renders its own #modal-header-bar (title "Details: <id>" + a
   close X). This modal ships its own in-product .alert-modal-header,
   so suppress the shell bar -- one header, not two stacked with a
   duplicate close. Scoped via :has() to the .alert-modal-header
   marker, so it auto-reverts for every other modal (which never
   emit that class). The .alert-modal-close below is then the sole
   close affordance and uses Flowbite's data-modal-hide. */
#modal-placeholder:has(#main-modal-content .alert-modal-header)
    #modal-header-bar {
    display: none;
}

/* -- Modal header -- */
.alert-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 20px;
    background: var(--color-panel-header-tinted);
    border-bottom: 1px solid var(--coverage-strong-bg);
}
.alert-modal-header-title {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-family: var(--font-display);
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    margin: 0;
    color: var(--color-brand-green-deep);
}
.alert-modal-header-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 7px;
    background: var(--coverage-strong-bg);
    color: var(--color-brand-green-deep);
    font-size: 13px;
    flex: none;
}
.alert-modal-header-id {
    font-family: var(
        --font-mono,
        ui-monospace,
        SFMono-Regular,
        Menlo,
        Monaco,
        Consolas,
        monospace
    );
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--color-brand-green-deep);
    background: color-mix(in srgb, var(--fg-on-dark) 70%, transparent);
    border: 1px solid var(--coverage-strong-bg);
    padding: 2px 8px;
    border-radius: 4px;
    margin-left: 4px;
}
.alert-modal-close {
    background: transparent;
    border: 0;
    color: var(--fg-4);
    width: 28px;
    height: 28px;
    border-radius: 6px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition:
        background 0.15s,
        color 0.15s;
}
.alert-modal-close:hover {
    background: color-mix(in srgb, var(--coverage-strong-fg) 8%, transparent);
    color: var(--color-brand-green-deep);
}

/* -- Body wrapper + divider -- */
.alert-modal-body {
    padding: 20px 22px 24px;
}
.alert-modal-divider {
    margin: 18px 0 16px;
    border: 0;
    border-top: 1px solid var(--status-neutral-bg);
}

/* -- Details grid -- */
.alert-modal-details {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px 20px;
}
.alert-modal-details .span-2 {
    grid-column: 1 / -1;
}
.alert-modal-details dt {
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    color: var(--fg-4);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: 0 0 6px;
}
.alert-modal-details dd {
    font-size: var(--fs-base);
    color: var(--fg-1);
    margin: 0;
    line-height: var(--lh-normal);
}
.alert-modal-details a {
    color: var(--color-brand-green);
}
.alert-modal-details a:hover {
    text-decoration: underline;
}

/* -- Content blocks -- soft neutral, NO left-bevel accent stripes -- */
.alert-modal-info-block {
    background: var(--bg-page);
    border: 1px solid var(--border-base);
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    font-size: var(--fs-base);
    line-height: 1.55;
    color: var(--fg-1);
    white-space: pre-wrap;
}
.alert-modal-code-block {
    background: var(--bg-page);
    border: 1px solid var(--border-base);
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    font-size: var(--fs-sm);
    line-height: var(--lh-normal);
    color: var(--fg-1);
    overflow-x: auto;
}
.alert-modal-code-block pre {
    margin: 0;
    font-family: var(
        --font-mono,
        ui-monospace,
        SFMono-Regular,
        Menlo,
        Monaco,
        Consolas,
        monospace
    );
    font-size: var(--fs-sm);
    white-space: pre-wrap;
    word-break: break-word;
    color: inherit;
}

/* Reason -- warning-soft pattern from the design system. */
.alert-modal-reason {
    background: var(
        --themes-colors-background-bg-warning-soft,
        var(--sev-medium-bg)
    );
    border: 1px solid var(--themes-colors-border-border-warning-subtle);
    color: var(--themes-colors-text-text-fg-warning);
    padding: 10px 12px;
    border-radius: 6px;
    font-weight: 500;
    font-size: var(--fs-base);
    line-height: 1.55;
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.alert-modal-reason svg,
.alert-modal-reason i {
    color: var(--status-warning-fg);
    font-size: 13px;
    margin-top: 2px;
    flex: none;
}

/* Shared page shell for legacy table pages (product-models, groups-and-labels,
   integrations, and any future Manage/Analyze list without .dd-* chrome).
   Width + padding come from the :root page-shell tokens above — same chrome
   as .gateways-page, .ci-page, and .alerts-page. */
.app-page-shell {
    max-width: var(--page-max-width);
    margin: 0;
    padding: var(--page-padding);
    width: 100%;
    box-sizing: border-box;
}

/*
 * ─── Design-system table (.ds-table*) ──────────────────────────────────────
 *
 * Shared table chrome used by Cluster Integrity, the cluster member table,
 * and any future surface. Hoisted here from cluster_integrity.css so it's
 * available globally (this sheet loads on every page). 14px body cells /
 * 12px left-aligned headers to match the gateways + devices tables.
 */

/* ─── Design-system table ─────────────────────────────────────────── */

.ds-table-card {
    background: var(--bg-card);
    border: 1px solid var(--border-base);
    border-radius: var(--radius-lg);
    overflow: hidden;
    margin-bottom: 18px;
}

.ds-table-card-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--border-base);
}

.ds-table-card-title {
    font-family: var(--font-display);
    font-weight: var(--fw-medium);
    font-size: var(--fs-md);
    color: var(--fg-1);
    margin: 0;
}

.ds-table-card-meta {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--fg-5);
    text-align: right;
}

/* Card footer — "Showing X of Y" + the shared cursor-pagination nav
   (mirrors flowbite's table footer chrome on the bespoke card). */
.ds-table-card-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border-top: 1px solid var(--border-base);
}

.ds-table-scroll {
    overflow-x: auto;
}

.ds-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-base);
    /* 14px body cells to match the standard app tables (gateways/devices
       use Tailwind text-sm = 14px); headers stay 12px via the shared
       `table thead th` rule in section-improvements.css. */
    font-size: var(--fs-base);
    color: var(--fg-2);
}

.ds-table thead {
    background: var(--bg-page);
}

.ds-table th {
    /* Match the standard app tables (gateways/devices): 12px headers over
       14px body cells. `.ds-table th` out-specifies the shared
       `table thead th` rule, so the header size is pinned here rather
       than inherited from .ds-table's 14px base. */
    text-align: left;
    padding: 9px 14px;
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--fg-4);
}

.ds-table tbody tr {
    border-top: 1px solid var(--border-base);
}

.ds-table tbody tr:hover {
    background: var(--bg-page);
}

.ds-table td {
    padding: 10px 14px;
    vertical-align: middle;
}

.ds-table-tenant {
    font-weight: var(--fw-semibold);
    color: var(--fg-2);
    white-space: nowrap;
}

.ds-table-device {
    font-weight: var(--fw-semibold);
    color: var(--color-brand-green-deep);
    white-space: nowrap;
}

.ds-table-detail {
    color: var(--fg-3);
    font-size: var(--fs-xs);
    line-height: 1.4;
}

.ds-table-detail code {
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    background: var(--bg-page);
    padding: 1px 5px;
    border-radius: var(--radius-xs);
    color: var(--fg-2);
}

.ds-table-time {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--fg-4);
    white-space: nowrap;
}

.ds-table-action {
    color: var(--fg-3);
    font-size: var(--fs-xs);
    line-height: 1.4;
}

/* ============================================
   TERRAIN STAGE ZOOM
   ============================================
   The stage scale rides a custom property the fragment sets INLINE on the
   [data-terrain-zoom] container, so the initial zoom applies at parse time —
   no unscaled first frame ("blip") after an HTMX swap. terrain-zoom.js
   updates the same property on zoom clicks; containers without the inline
   var (alerts scenes) fall back to scale 1. */

[data-terrain-zoom] > svg {
    transform-origin: center center;
    transform: scale(var(--terrain-zoom, 1));
}

/* ============================================================================
   Standard card
   ----------------------------------------------------------------------------
   The design system's card, in one place: "border-radius: 12px, border: 1px
   solid #e5e7eb, box-shadow: 0 1px 0.5px 0.05px rgba(29,41,61,0.02) (almost
   invisible)" — README, Borders/shadows/radii.

   Every one of those values already had a token, and --radius-lg is even
   commented "cards". The markup used none of them: 56 elements across 34 files
   wrote the card out as five utilities each, and settled on an 8px radius and
   Tailwind's shadow-sm rather than the 12px and near-invisible shadow the
   system specifies.

   Layout stays in the markup — padding and overflow vary per card. This owns
   what makes it a card.
   ============================================================================ */

.patitur-card {
    background-color: var(--bg-card);
    border: 1px solid var(--border-base);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xs);
}

/* Table header cell -----------------------------------------------------------
   90 <th> elements carried 37 distinct class strings. The four commonest were
   one component spelled four ways, disagreeing on padding (px-3 vs px-4),
   weight (medium vs semibold), colour (--fg-3 vs --fg-4), and one of them
   forgot `uppercase` altogether. This is the majority spelling of the four.

   Deliberately sets NO text-align. Under Tailwind's preflight a <th> without
   an explicit alignment renders CENTRED, and 21 of those cells rely on that
   while 18 say text-left. Setting either here would silently re-align half the
   tables, so alignment stays an explicit per-column choice in the markup.
   ---------------------------------------------------------------------------- */

.patitur-th {
    padding: 0.75rem 1rem;
    font-size: var(--fs-sm);
    line-height: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--fg-3);
}

/* =============================================================================
   TABLE CELLS. .patitur-th already existed; the body cell did not, so 130 <td>
   elements spelled their own padding, size and colour in 65 distinct class
   strings — while 48 of the 89 <th> in the same tables were already on the
   component and 41 were not.

   The default padding is 0.75rem 1rem, which is what .patitur-th sets and what
   59 of the 130 cells already wrote as px-4 py-3, so header and body line up
   without either naming a number.

   Two size modifiers, because the tree genuinely has three densities: a tight
   cell for dense in-page tables (22 cells wrote px-3 py-3) and a roomy one for
   full-page lists (15 wrote px-6 py-4). Alignment, wrapping and numerics stay
   as utilities — text-right and tabular-nums say what they mean and carry no
   role worth naming.
   ============================================================================= */
.patitur-td {
    padding: 0.75rem 1rem;
    font-size: var(--fs-base);
    line-height: 1.25rem;

    /* Not --fg-2. Every uncoloured cell in the tree already inherits
       --fg-body-strong from `body`, and that is also exactly what .text-body
       sets — which is why the 6 cells carrying .text-body were painting
       themselves the colour they already had. Naming the inherited value here
       keeps the rendering identical and makes .text-body droppable. */
    color: var(--fg-body-strong);
}

.patitur-td--tight {
    padding: 0.75rem;
}

.patitur-td--roomy {
    padding: 1rem 1.5rem;
}

/* A cell holding an identifier — a EUI, an OUI, a rule code. Monospace and
   slightly dimmer, so the value reads as data rather than prose. */
.patitur-td--code {
    font-family: var(--font-mono);
    white-space: nowrap;
}

/* =============================================================================
   TEXT ROLES — the closed set. Every piece of text in the product picks one.

   Before this, 829 elements spelled out their own size and weight across 38
   combinations, 94 of which appeared exactly once. Nobody chose 38 typographic
   treatments; they accumulated one call site at a time, and four of the sizes
   in use — 20px, 11px, 13px, 9px — were not steps on the type scale at all.

   Nine roles cover 93% of that on the evidence, and the rest were the off-scale
   sizes, which collapse onto their nearest step.

   A role fixes size, weight, case and tracking. It deliberately does NOT fix
   colour: the same role is used in --fg-1 on a card and --fg-4 in a caption
   beneath it, and the semantic colour classes already cover that well (839
   semantic uses against 144 palette ones). Role for shape, .text-* for tone.

   Put the class on the element that already exists — a <td>, a <dt>, an <h3> —
   or reach for text() when you are creating one. The macro exists so the
   common case is short; the class exists because a wrapper <span> would break
   heading semantics and cannot go on an element the template did not create.
   ============================================================================= */
.t-heading {
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    line-height: 1.4;
}

.t-subheading {
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);
    line-height: 1.4;
}

.t-body {
    font-size: var(--fs-base);
    font-weight: var(--fw-regular);
    line-height: 1.5;
}

.t-label {
    font-size: var(--fs-base);
    font-weight: var(--fw-medium);
    line-height: 1.4;
}

.t-caption {
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    line-height: 1.4;
}

.t-caption-strong {
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.4;
}

/* A small tracked capital above the thing it names. */
.t-eyebrow {
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* The smallest step: a unit, a footnote under a figure, a count. 11px folds in
   here — the scale has 10 and 12, and 11 was never one of them. */
.t-micro {
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    line-height: 1.4;
}

/* The big figure on a dashboard or KPI card. Its own role because 30px bold
   is not a heading — it is a number meant to be read across a room. */
.t-display {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    line-height: 1.2;
}

/* Inline emphasis inside running text. Inherits its size on purpose: it marks
   a word inside a sentence, so it must not resize it. */
.t-strong {
    font-weight: var(--fw-semibold);
}

/* The counterpart to .t-strong: keep the size, drop the weight. For a span
   inside something bold that should not be — the denominator of a fraction in
   a KPI figure, a unit after a number. */
.t-regular {
    font-weight: var(--fw-regular);
}

/* Numbers that line up in a column. Not a role of its own — it composes onto
   one, because a figure is still a body or a caption. */
.t-figure {
    font-variant-numeric: tabular-nums;
}

/* =============================================================================
   BUTTONS — the canonical set. One base, six intents, three sizes, one shape.
   Every button in the app is a combination of these; nothing else is allowed,
   and scripts/lint/check_markup_drift.py enforces that.

   Intent classes set COLOUR ONLY and require .btn alongside them for geometry:
       <button class="btn btn-primary">        <button class="btn btn-neutral btn-sm">
       <button class="btn btn-danger btn-icon" aria-label="Delete">

   The base carries a 1px transparent border so solid and outline intents have
   identical box size — swapping intent never shifts layout. Radius is 8px
   (--radius-md) for every button: the classes this replaces disagreed, using
   8px and 12px for the same role. No shadow, no gradient, no transform: hover
   moves colour only.
   ============================================================================= */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    font-family: inherit;
    font-weight: 500;
    line-height: 1.25rem;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    padding: 8px 16px;
    font-size: var(--fs-base);
    transition:
        background-color 0.15s ease,
        border-color 0.15s ease,
        color 0.15s ease;
}

.btn:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

.btn:disabled,
.btn[aria-disabled="true"],
.btn.is-disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

/* --- sizes ---------------------------------------------------------------- */
.btn-sm {
    padding: 6px 12px;
    font-size: var(--fs-sm);
}

.btn-lg {
    padding: 10px 20px;
    font-size: var(--fs-base);
}

/* --- shape ---------------------------------------------------------------- */
.btn-icon {
    width: 32px;
    height: 32px;
    padding: 0;
    gap: 0;
}

.btn-icon.btn-sm {
    width: 28px;
    height: 28px;
}

.btn-icon.btn-lg {
    width: 36px;
    height: 36px;
}

/* --- intents -------------------------------------------------------------- */
.btn-primary {
    background: var(--color-brand-green);
    border-color: var(--color-brand-green);
    color: var(--fg-on-dark);
}

.btn-primary:hover {
    background: var(--color-brand-green-hover);
    border-color: var(--color-brand-green-hover);
}

.btn-neutral {
    background: var(--bg-card);
    border-color: var(--border-medium);
    color: var(--fg-body-strong);
}

.btn-neutral:hover {
    background: var(--border-base);
}

.btn-danger {
    background: var(--bg-card);
    border-color: var(--danger-fg);
    color: var(--danger-fg);
}

.btn-danger:hover {
    background: var(--status-danger-bg);
}

.btn-warning {
    background: var(--banner-warning-fg);
    border-color: var(--warning-strong-fg);
    color: var(--fg-on-dark);
}

.btn-warning:hover {
    background: var(--warning-strong-fg);
}

.btn-info {
    background: var(--chart-line-info);
    border-color: var(--chart-line-info);
    color: var(--fg-on-dark);
}

.btn-info:hover {
    background: var(--info-deep-fg);
    border-color: var(--info-deep-fg);
}

.btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--fg-3);
}

.btn-ghost:hover {
    background: var(--bg-page);
    color: var(--fg-body-strong);
}

/* The sidebar and the auth pages sit on a dark ground, where .btn-ghost's
   --fg-3 is invisible. Same shape, inverted foreground. */
.btn-ghost-dark {
    background: transparent;
    border-color: transparent;
    color: var(--fg-5);
}

.btn-ghost-dark:hover {
    background: color-mix(in srgb, var(--fg-on-dark) 8%, transparent);
    color: var(--fg-on-dark);
}

/* =============================================================================
   Alert-map toolbar — a segmented mode switch and severity toggles. Their own
   family, not buttons: they carry aria-pressed and alert-geography.js paints the
   active state with inline styles (which outrank these rules). What is here is
   the rest state, which previously lived as utilities in the markup.
   ============================================================================= */
.ag-mode-btn {
    padding: 6px 12px;
    font-weight: 500;
    background: transparent;
    border: 0;
    cursor: pointer;
}

.ag-mode-btn + .ag-mode-btn {
    border-left: 1px solid var(--border-base);
}

.ag-mode-btn:disabled {
    color: var(--fg-5);
    cursor: not-allowed;
}

.ag-sev-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 1px solid var(--border-base);
    border-radius: 6px;
    padding: 6px 10px;
    background: transparent;
    cursor: pointer;
}

/* =============================================================================
   FRAMEWORK SECTION — a block on the alert detail page showing how that alert
   maps to one compliance framework.

   Each section used a hand-picked Tailwind family as its identity: blue for
   NIS2, amber for IEC-62443, indigo for NIST-CSF. That is not a status — it is
   the framework's own colour — but it was chosen by whoever wrote the section,
   in a family the framework itself knows nothing about. Adding a framework
   meant picking a new one by hand and hoping it did not collide.

   The accent now comes from framework config through --section-accent, so a
   new framework arrives with its colour already set.
   ============================================================================= */
.framework-section {
    --section-accent: var(--color-brand-green);
}

.framework-section__title {
    color: var(--section-accent);
}

/* The numbered glyph beside a function or requirement — GV, FR 3, Article 21. */
.framework-section__marker {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--radius-xs);
    background: color-mix(in srgb, var(--section-accent) 12%, transparent);
    color: var(--section-accent);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
}

/* A card holding one requirement, washed with the framework's accent. */
.framework-section__card {
    padding: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--section-accent) 25%, transparent);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--section-accent) 6%, var(--bg-card));
}

.framework-section__label {
    color: var(--section-accent);
    font-weight: 500;
}

/* =============================================================================
   DETAIL HERO — the banner across the top of a glossary detail page, washed
   with a little of that entity's accent.

   Three pages carried this as a gradient with the alpha byte glued onto the
   colour, and they did not agree on the byte: 15 on compliance, 12 on device
   and manufacturer, against 1a and 20 on the tiles beside them. Five spellings
   of "a faint tint of the accent", none of which said so.

   Flat, not a gradient: a page header is chrome, and chrome in this system
   does not carry one. color-mix also takes its alpha from the colour rather
   than from the spelling, so it cannot break the way appending a byte does.
   ============================================================================= */
.detail-hero {
    padding: 1.25rem 1.5rem;
    border-bottom: 1px solid var(--border-base);
    background: color-mix(
        in srgb,
        var(--hero-accent, var(--color-brand-green)) 7%,
        var(--bg-card)
    );
}

/* =============================================================================
   GLYPH TILES — a framework's icon on a tint of its own colour.

   Replaces a string-concatenation trick that was one config edit away from
   breaking:

       style="background: {{ entry.color }}1a; color: {{ entry.color }};"

   Gluing "1a" onto the value produces #RRGGBB1a — a hex with an alpha byte —
   and works only while every framework's colour happens to be a six-digit
   hex. ComplianceDisplay.color is an unvalidated ``str`` read from framework
   config, so a framework declaring rgb(...) or a named colour would yield
   "rgb(37,175,136)1a", which is not a colour at all: the tint would silently
   disappear and nothing would fail.

   color-mix() takes the alpha from the colour rather than the spelling, so it
   holds for any format the config supplies.
   ============================================================================= */
.glyph-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: var(--glyph-size, 32px);
    height: var(--glyph-size, 32px);
    border-radius: var(--radius-md);
    background: color-mix(
        in srgb,
        var(--glyph-colour, var(--color-brand-green)) 10%,
        transparent
    );
    color: var(--glyph-colour, var(--color-brand-green));
    font-size: var(--fs-base);
}

.glyph-tile--lg {
    --glyph-size: 56px;

    border-radius: var(--radius-lg);
    font-size: var(--fs-xl);
}

.glyph-tile--round {
    --glyph-size: 40px;

    border-radius: var(--radius-pill);
}

.glyph-tile--sm {
    --glyph-size: 24px;

    font-size: var(--fs-sm);
}

/* =============================================================================
   ITEM ROW — a row of centre-aligned items with a small gap between them.

   The most repeated piece of markup in the product: 111 uses of
   "inline-flex items-center gap-1.5" and its near-identical siblings at three
   different gaps. Most are a glyph and the text it labels — a swatch beside a
   legend entry, an icon beside a metadata line — and the rest are the
   block-level equivalent holding a label beside its control. One shape, so
   one name.

   The default is inline at .375rem because that is what most of them already
   were; reach for a modifier only to move away from it.

   The three gaps are the steps the markup itself used (.25 / .375 / .5rem).
   They are literals here because the product has no spacing scale yet — when
   one lands, these are the first three tokens it should absorb.
   ============================================================================= */
.item-row {
    display: inline-flex;
    align-items: center;
    gap: var(--item-row-gap, 0.375rem);
}

.item-row--tight {
    --item-row-gap: 0.25rem;
}

.item-row--roomy {
    --item-row-gap: 0.5rem;
}

.item-row--block {
    display: flex;
}

.item-row--wrap {
    flex-wrap: wrap;
}

/* =============================================================================
   STAT CARDS — a small label over a large figure, in a card.

   Distinct from summary_card(), which is a bigger card carrying an icon, a
   subtitle and a link. This is the compact form: the policy and tenant
   overview strips, where six cards said the same thing six ways.

   One of the six also carried shadow-sm, a bevel the system does not use, and
   spelled its warning border as border-amber-200 rather than the banner token
   the rest of the product warns with.

   The figure is --fs-xl. The call sites wrote text-xl, which is 20px in
   Tailwind and is not a step on this scale — --fs-lg is 18 and --fs-xl is 24 —
   so adopting the scale moves it up four pixels. That is the scale doing its
   job rather than an accident.
   ============================================================================= */
.stat-card {
    padding: 0.75rem;
}

.stat-card__label {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--fg-4);
}

.stat-card__value {
    margin-top: 0.25rem;
    font-size: var(--fs-xl);
    font-weight: 700;
    color: var(--fg-1);
}

/* The line under the figure — "across 4 rules", "no overrides". */
.stat-card__note {
    margin-top: 0.125rem;
    font-size: var(--fs-2xs);
    color: var(--fg-5);
}

/* A card whose figure is itself the warning: tenants diverging from policy. */
.stat-card--warning {
    border-color: var(--banner-warning-border);
}

.stat-card--warning .stat-card__label,
.stat-card--warning .stat-card__value {
    color: var(--status-warning-fg);
}

.stat-card--brand .stat-card__value {
    color: var(--color-brand-green);
}

.stat-card--muted .stat-card__value {
    color: var(--fg-2);
}

/* =============================================================================
   DETAIL PAIRS — a label with its value beneath, and the grid they sit in.

   Distinct from info_row(), which puts the label and value side by side in a
   two-column row. This is the stacked form: the label above, the value under
   it, several pairs across a responsive grid. Key Information on a key modal,
   the numeric limits under a policy parameter, a manufacturer's OUI block.

   The tree held 24 of these in 12 distinct class strings, at two sizes that
   nobody had named: 14px for a field, 10px uppercase for a metric. They are
   one component and a modifier, not two dozen spellings — and the 10px ones
   were also feeding the off-scale font-size gate, since text-[10px] is not on
   the type scale.
   ============================================================================= */
.detail-grid {
    display: grid;
    grid-template-columns: repeat(
            auto-fit,
            minmax(var(--detail-min, 11rem), 1fr)
        );
    gap: 1rem;
}

/* A denser grid for compact metric pairs, which are short enough to sit
   several across. */
.detail-grid--compact {
    --detail-min: 6rem;

    gap: 0.5rem 1rem;
}

.detail-pair__label {
    font-size: var(--fs-base);
    font-weight: 500;
    color: var(--fg-4);
}

.detail-pair__value {
    margin-top: 0.25rem;
    font-size: var(--fs-base);
    color: var(--fg-1);
}

/* The metric variant: a small tracked capital label over a value that is
   usually a figure. */
.detail-pair--compact .detail-pair__label {
    font-size: var(--fs-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.detail-pair--compact .detail-pair__value {
    font-size: var(--fs-sm);
}

/* Label and value on one line rather than stacked — the characteristics loops
   on the device and manufacturer glossary pages, where each row is one short
   key and one short value. */
.detail-pair--inline {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
}

.detail-pair--inline .detail-pair__value {
    margin-top: 0;
}

/* A pair whose value IS the warning — a compliance penalty, a breach window.
   The tone paints both halves, so the label and value cannot disagree the way
   text-amber-700 over text-amber-900 did. */
.detail-pair--warning .detail-pair__label,
.detail-pair--warning .detail-pair__value {
    color: var(--status-warning-fg);
}

.detail-pair--danger .detail-pair__label,
.detail-pair--danger .detail-pair__value {
    color: var(--status-danger-fg);
}

/* A value that is an identifier rather than prose — a key id, a prefix, an
   OUI. Monospace on a muted ground so it reads as data to be copied. */
.detail-pair__value--code {
    display: inline-block;
    padding: 0.25rem 0.5rem;
    border-radius: var(--radius-xs);
    background: var(--status-neutral-bg);
    font-family: var(--font-mono);
}

.detail-pair__value--mono {
    font-family: var(--font-mono);
}

/* =============================================================================
   TOGGLES AND SEGMENTED CONTROLS — two controls that look alike and are not
   the same thing.

   A TOGGLE is independent: the map's Signal-zones, Computed-cells and
   Predicted-cells layers can each be on or off, in any combination. It is
   aria-pressed, and pressing one says nothing about the others.

   A SEGMENTED control is one-of-N: the terrain view is Terrain or Building use
   or Signal overlay, never two. It is a radiogroup, and choosing one
   deselects the rest.

   The terrain modes were built as three aria-pressed buttons in a role="group",
   which announces three independently-pressable buttons for what is really a
   single choice. That is why these are two components and not one with a
   modifier: the difference is in what a screen reader is told, not in the paint.

   The map's toggles were worse off again — assembled in JavaScript from inline
   style attributes, so they carried no class at all and no drift gate could
   see them.

   The active fill is a tint rather than a solid, because a toggle can carry a
   colour swatch (the map's layer keys do) and a solid brand fill would fight
   it. .filter-pill fills solid for the same on-state; unifying the two is a
   separate decision and is filed, not assumed.
   ============================================================================= */
.toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    border: 1.5px solid var(--border-base);
    border-radius: var(--radius-pill);
    background: var(--bg-card);
    color: var(--fg-2);
    font-size: var(--fs-sm);
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
}

.toggle[aria-pressed="true"] {
    border-color: var(--color-brand-green);
    background: color-mix(in srgb, var(--color-brand-green) 10%, transparent);
    color: var(--fg-1);
}

.toggle:disabled {
    opacity: 0.6;
    cursor: default;
}

/* The layer key inside a toggle: grey when off, the layer's colour when on. */
.toggle__key {
    display: inline-block;
    flex-shrink: 0;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--border-base);
}

.toggle[aria-pressed="true"] .toggle__key {
    background: var(--toggle-key-colour, var(--color-brand-green));
}

/* Segmented ------------------------------------------------------------------
   One choice of several, drawn as joined cells. The container is the
   radiogroup; each cell is a radio. */
.segmented {
    display: inline-flex;
    overflow: hidden;
    border: 1px solid var(--border-base);
    border-radius: var(--radius-md);
}

.segmented--pill {
    border-radius: var(--radius-pill);
}

.segmented__option {
    padding: 0.375rem 0.75rem;
    border: 0;
    border-left: 1px solid var(--border-base);
    background: var(--bg-card);
    color: var(--fg-2);
    font-size: var(--fs-base);
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
}

.segmented__option:first-child {
    border-left: 0;
}

.segmented__option:hover:not([aria-checked="true"]):not(:disabled) {
    background: color-mix(in srgb, var(--color-brand-green) 6%, transparent);
}

/* Selected is a light green with deep-green text; pressing goes deeper. The
   terrain modes this replaced were near-black (bg-gray-900), which made them
   the only selected control in the product not wearing the brand colour.

   A tint rather than a solid fill, unlike .btn-primary: a segmented control
   often sits over a map, and a saturated block competes with what is drawn
   underneath it. The text carries --color-brand-green-deep rather than the
   base green so it stays legible against its own tint.

   This is also why there is no --subtle modifier any more. It existed to give
   the map rail a quieter selected state than the near-black default; now that
   the default is the quiet one, the modifier had nothing left to say. */
.segmented__option[aria-checked="true"] {
    background: color-mix(in srgb, var(--color-brand-green) 14%, transparent);
    color: var(--color-brand-green-deep);
    font-weight: 600;
}

/* A cell that cannot be chosen. The component had no disabled state at all, so
   an inert option was indistinguishable from a live one — on the cluster shape
   filter that meant "twin blob 0" and "arc 12" looked equally clickable, and
   clicking the dead one did nothing with no explanation. Matches .toggle's own
   disabled treatment so the two controls fail the same way. */
.segmented__option:disabled {
    opacity: 0.45;
    cursor: default;
}

.segmented__option:disabled:hover {
    background: var(--bg-card);
}

/* Press feedback — the same green, deeper, on the way down. */
.segmented__option:active {
    background: color-mix(in srgb, var(--color-brand-green) 28%, transparent);
    color: var(--color-brand-green-deep);
}

.segmented--sm .segmented__option {
    padding: 0.1875rem 0.5625rem;
    font-size: var(--fs-sm);
}

/* =============================================================================
   ICON BADGES — an icon on a tinted circle, above the message in a
   confirmation modal.

   This is the shape deliberately EXCLUDED from the callout sweep: a bg-red-50
   square with a fixed width and rounded-full is a circular icon badge, not a
   notice box, and converting one into a callout would have destroyed it. That
   was the right call, and it left the shape with no component of its own —
   eight uses in five spellings, at three sizes and three tones.

   Distinct from .glyph-tile, which carries a framework's own accent from
   config. This one takes a status tone: you are about to delete something.
   ============================================================================= */
.icon-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: var(--icon-badge-size, 3rem);
    height: var(--icon-badge-size, 3rem);
    border-radius: var(--radius-pill);
    background: var(--icon-badge-bg, var(--status-neutral-bg));
    color: var(--icon-badge-fg, var(--fg-3));
}

.icon-badge--sm {
    --icon-badge-size: 2.5rem;
}

.icon-badge--lg {
    --icon-badge-size: 4rem;
}

/* A rounded square rather than a circle — the collector delete modal. */
.icon-badge--square {
    border-radius: var(--radius-md);
}

.icon-badge--danger {
    --icon-badge-bg: var(--status-danger-bg);
    --icon-badge-fg: var(--status-danger-fg);
}

.icon-badge--warning {
    --icon-badge-bg: var(--banner-warning-bg);
    --icon-badge-fg: var(--status-warning-fg);
}

.icon-badge--success {
    --icon-badge-bg: var(--status-online-bg);
    --icon-badge-fg: var(--status-online-fg);
}

.icon-badge--info {
    --icon-badge-bg: var(--status-info-bg);
    --icon-badge-fg: var(--status-info-fg);
}

/* =============================================================================
   CHART SERIES — the per-gateway accent, keyed by slot.

   The signal panel colours a gateway by its position in the packet-count
   ordering. Python used to send the hex for that slot; it now sends the slot
   number and this decides what it looks like. Each class supplies the custom
   properties the existing components already read (--gw-color on the chip and
   summary, --gw on the rail swatch), so no component rule changed — only where
   the value comes from. --series is the general name for anything new.

   Slot 0 means "not in the coverage set" and gets no class, so the components'
   own fallbacks (var(--fg-5) and friends) apply unchanged.
   ============================================================================= */
.series-1 {
    --series: var(--series-1);
    --gw-color: var(--series-1);
    --gw: var(--series-1);
}

.series-2 {
    --series: var(--series-2);
    --gw-color: var(--series-2);
    --gw: var(--series-2);
}

.series-3 {
    --series: var(--series-3);
    --gw-color: var(--series-3);
    --gw: var(--series-3);
}

.series-4 {
    --series: var(--series-4);
    --gw-color: var(--series-4);
    --gw: var(--series-4);
}

.series-5 {
    --series: var(--series-5);
    --gw-color: var(--series-5);
    --gw: var(--series-5);
}

.series-6 {
    --series: var(--series-6);
    --gw-color: var(--series-6);
    --gw: var(--series-6);
}

.series-7 {
    --series: var(--series-7);
    --gw-color: var(--series-7);
    --gw: var(--series-7);
}

.series-8 {
    --series: var(--series-8);
    --gw-color: var(--series-8);
    --gw: var(--series-8);
}

.series-9 {
    --series: var(--series-9);
    --gw-color: var(--series-9);
    --gw: var(--series-9);
}

.series-10 {
    --series: var(--series-10);
    --gw-color: var(--series-10);
    --gw: var(--series-10);
}

/* =============================================================================
   SWATCHES — the colour key beside a map, a chart or a legend.

   A swatch and a dot look alike and mean different things. A dot carries a
   TONE: .dot--success says "online", and its colour is a design decision the
   stylesheet owns. A swatch carries a VALUE: it says "this exact colour, on
   the map to your left, means mostly-fragile", and that colour comes from the
   data. Painting a swatch with a tone would be a lie the moment the map
   changed its palette.

   So the colour arrives as a custom property, exactly as the meter's
   percentage does:

       <span class="swatch" style="--swatch-colour: var(--health-mixed-fg)"></span>

   Opacity is a value too, not a finish. The network-health legend encodes
   gateway count in it — one gateway is the same hue at 0.30, three or more at
   0.75 — so --swatch-alpha is data and belongs at the call site with the
   colour, while the size, shape and radius stay here.
   ============================================================================= */
.swatch {
    display: inline-block;
    flex-shrink: 0;
    width: var(--swatch-size, 12px);
    height: var(--swatch-size, 12px);
    border-radius: var(--radius-xs);
    background: var(--swatch-colour, var(--fg-4));
    opacity: var(--swatch-alpha, 1);
}

.swatch--xs {
    --swatch-size: 8px;
}

.swatch--sm {
    --swatch-size: 10px;
}

.swatch--lg {
    --swatch-size: 14px;
}

/* A colour the user picked and can see themselves — the tag preview in the
   delete-confirmation modal. Big enough to read as a choice rather than a key. */
.swatch--xl {
    --swatch-size: 24px;
}

/* A round key, for a legend whose map draws circles rather than cells. */
/* --- keyed swatches ------------------------------------------------------
   A swatch normally carries a VALUE the caller passes in. These carry a NAME
   instead: the terrain legend says "forest" and the stylesheet decides what
   forest looks like, so the cartography palette never has to travel through
   Python as a hex. One class per token, so a drift check can verify every
   reference — a var() assembled from a Jinja variable cannot be checked.
   ------------------------------------------------------------------------ */
/* Water */
.swatch--landcover-water {
    --swatch-colour: var(--landcover-water);
}

/* Forest and trees */
.swatch--landcover-forest {
    --swatch-colour: var(--landcover-forest);
}

/* Urban and built */
.swatch--landcover-urban {
    --swatch-colour: var(--landcover-urban);
}

/* Bare ground */
.swatch--landcover-bare {
    --swatch-colour: var(--landcover-bare);
}

/* Rural, crop, grass, open */
.swatch--landcover-rural {
    --swatch-colour: var(--landcover-rural);
}

/* Residential */
.swatch--building-use-residential {
    --swatch-colour: var(--building-use-residential);
}

/* Commercial */
.swatch--building-use-commercial {
    --swatch-colour: var(--building-use-commercial);
}

/* Industrial */
.swatch--building-use-industrial {
    --swatch-colour: var(--building-use-industrial);
}

/* Utility */
.swatch--building-use-utility {
    --swatch-colour: var(--building-use-utility);
}

/* Civic */
.swatch--building-use-civic {
    --swatch-colour: var(--building-use-civic);
}

/* Other use */
.swatch--building-use-other {
    --swatch-colour: var(--building-use-other);
}

/* Unclassified */
.swatch--building-use-unclassified {
    --swatch-colour: var(--building-use-unclassified);
}

/* No building */
.swatch--building-use-unknown {
    --swatch-colour: var(--building-use-unknown);
}

/* Footprint unavailable */
.swatch--building-use-unavailable {
    --swatch-colour: var(--building-use-unavailable);
}

/* RSSI excellent */
.swatch--hex-rssi-excellent {
    --swatch-colour: var(--hex-rssi-excellent);
}

/* RSSI very good */
.swatch--hex-rssi-very-good {
    --swatch-colour: var(--hex-rssi-very-good);
}

/* RSSI good */
.swatch--hex-rssi-good {
    --swatch-colour: var(--hex-rssi-good);
}

/* RSSI fair */
.swatch--hex-rssi-fair {
    --swatch-colour: var(--hex-rssi-fair);
}

/* RSSI marginal */
.swatch--hex-rssi-marginal {
    --swatch-colour: var(--hex-rssi-marginal);
}

/* RSSI poor */
.swatch--hex-rssi-poor {
    --swatch-colour: var(--hex-rssi-poor);
}

/* RSSI critical */
.swatch--hex-rssi-critical {
    --swatch-colour: var(--hex-rssi-critical);
}

.swatch--round {
    border-radius: var(--radius-pill);
}

/* An outline, for a pale swatch that would otherwise vanish into the card. */
.swatch--outlined {
    border: 1px solid var(--border-base);
}

/* A dotted outline reads as "no data" rather than a value of its own — the
   network-health legend uses it for cells with nothing in them. */
.swatch--empty {
    border: 1px dotted var(--fg-4);
}

/* A continuous scale rather than a single value: an RSSI ramp, a coverage
   gradient. The gradient is the data here, which is why this one shape is
   allowed one — it is a chart axis, not decoration on a piece of chrome. */
.swatch--ramp {
    width: 48px;
    background: var(--swatch-ramp, var(--swatch-colour, var(--fg-4)));
}

/* =============================================================================
   METERS — a track and a proportional fill.

   Three implementations of this existed: .dd-rail-bar-track (4px, 2px radius),
   .mini-bar (6px, 3px radius) and .landing-tenants-coverage-track (6px, pill),
   plus hand-rolled tracks in seven more files. All three ground colours turned
   out to be aliases of --status-neutral-bg, so they differed only in height and
   corner — three spellings of one component, and no colour disagreement to
   settle.

   The percentage is data, not appearance, so it arrives as a custom property
   rather than a width declaration:

       <div class="meter meter--success" style="--meter-fill: 62%">
         <div class="meter__fill"></div>
       </div>

   That keeps the one genuinely dynamic value inline while every appearance
   decision stays in this file. The pattern is already in the tree — the gateway
   markers pass --gw-color the same way. Reach for the meter() macro rather than
   writing the markup: a bar carrying a number needs a role and an accessible
   name, and a bare div has neither.
   ============================================================================= */
.meter {
    height: var(--meter-height, 6px);
    border-radius: var(--radius-pill);
    background: var(--status-neutral-bg);
    overflow: hidden;
}

/* 4px for a dense rail beside a label; 8px when the bar is the point. */
.meter--xs {
    --meter-height: 4px;
}

.meter--lg {
    --meter-height: 8px;
}

.meter__fill {
    height: 100%;
    width: var(--meter-fill, 0%);
    background: var(--meter-colour, var(--fg-4));
    transition: width 0.2s ease;
}

@media (prefers-reduced-motion: reduce) {
    .meter__fill {
        transition: none;
    }
}

.meter--neutral {
    --meter-colour: var(--fg-4);
}

.meter--info {
    --meter-colour: var(--status-info-fg);
}

.meter--success {
    --meter-colour: var(--status-online-fg);
}

.meter--warning {
    --meter-colour: var(--status-warning-fg);
}

.meter--danger {
    --meter-colour: var(--status-danger-fg);
}

.meter--brand {
    --meter-colour: var(--color-brand-green);
}

/* =============================================================================
   BADGES AND DOTS — a tinted label, and the smallest possible status marker.
   Same shape as the button set: a base carries geometry, a tone carries colour,
   and nothing is declared at the call site.

       <span class="badge badge--warning">Degraded</span>
       <span class="dot dot--success"></span>

   Reach for the macros in components/badges.html rather than writing the markup:
   badge() and dot() add the role and aria-label a bare span cannot.

   The domain badges — status_badge, severity_badge — stay as they are. They map
   a value to a meaning, which is a decision worth keeping in one place; these
   are for the cases where the template already knows the tone.
   ============================================================================= */
.badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    padding: 2px 10px;
    font-size: var(--fs-sm);
    line-height: 1rem;
    font-weight: 500;
    white-space: nowrap;
}

.badge--sm {
    padding: 1px 8px;
    font-size: var(--fs-2xs);
}

.badge--neutral {
    background: var(--status-neutral-bg);
    border-color: var(--status-neutral-bg);
    color: var(--status-neutral-fg);
}

.badge--info {
    background: var(--status-info-bg);
    border-color: var(--info-border);
    color: var(--status-info-fg);
}

.badge--success {
    background: var(--status-online-bg);
    border-color: var(--status-online-bg);
    color: var(--status-online-fg);
}

.badge--warning {
    background: var(--status-warning-bg);
    border-color: var(--banner-warning-border);
    color: var(--status-warning-fg);
}

.badge--danger {
    background: var(--status-danger-bg);
    border-color: var(--status-danger-bg);
    color: var(--status-danger-fg);
}

/* Violet is the glossary accent, already in the palette. There is no violet
   ground token, so the neutral tint carries it rather than minting a colour. */
.badge--accent {
    background: var(--status-neutral-bg);
    border-color: var(--violet-fg);
    color: var(--violet-fg);
}

/* Compliance is its own accent on this product, not a shade of success: its
   ground is --coverage-strong-bg (#d1fae5) where .badge--success is
   --status-online-bg (#e8f9f3). Naming it as a tone reuses the exact tokens
   the compliance surfaces already paint with, so the chips centralise without
   being repainted. Whether three greens this close should survive as three is
   a palette question, and a real one — it is filed, not silently resolved. */
.badge--compliance {
    background: var(--coverage-strong-bg);
    border-color: var(--success-soft-bg);
    color: var(--coverage-strong-fg);
}

.badge--brand {
    background: var(--color-brand-green);
    border-color: var(--color-brand-green);
    color: var(--fg-on-dark);
}

/* Two modifiers, not two more sizes. The tree held 25 micro-labels in 24
   distinct class strings — 9px, 10px and 11px, three weights, both radii,
   some uppercase, some monospace. Underneath were only two ideas: a label
   set in caps, and a value set in code. .badge--sm already carries the
   10px size, so these compose onto it rather than minting a fourth step. */
.badge--label {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
}

.badge--code {
    font-family: var(--font-mono);
    text-transform: none;
    letter-spacing: 0;
}

/* Dots ----------------------------------------------------------------------
   A dot carries no text, so it is invisible to a screen reader unless the macro
   gives it a label. That is the main reason to use dot() rather than a span. */
.dot {
    display: inline-block;
    flex-shrink: 0;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
}

.dot--md {
    width: 8px;
    height: 8px;
}

.dot--lg {
    width: 10px;
    height: 10px;
}

.dot--neutral {
    background: var(--fg-5);
}

.dot--info {
    background: var(--chart-line-info);
}

.dot--success {
    background: var(--success-mid);
}

.dot--warning {
    background: var(--banner-warning-fg);
}

.dot--danger {
    background: var(--danger-fg);
}

.dot--accent {
    background: var(--violet-fg);
}

.dot--brand {
    background: var(--color-brand-green);
}

/* A dot that stands for a marker drawn on a map carries the marker's outline,
   so the legend and the map agree. */
.dot--ring {
    border: 1.5px solid var(--fg-3);
}

/* =============================================================================
   ICONS — one size scale. See docs/design-system/icons.html.

   Each step sets width, height AND font-size, because this codebase sizes an
   inline SVG with width/height and a Font Awesome glyph with font-size. Two
   mechanisms for one intent meant no search could find "every 10px icon"; one
   class now covers both.

   Size only. Colour stays a semantic class (text-faint and friends), and margin
   stays at the call site — where an icon sits is the caller's business.

   Replaces 13 distinct pixel sizes, several of which were the same size spelled
   two ways: h-3.5 and h-[14px] were both 14px across 30 calls.
   ============================================================================= */
.patitur-icon {
    flex-shrink: 0;
}

.icon-xs {
    width: var(--icon-xs);
    height: var(--icon-xs);
    font-size: var(--icon-xs);
}

.icon-sm {
    width: var(--icon-sm);
    height: var(--icon-sm);
    font-size: var(--icon-sm);
}

.icon-md {
    width: var(--icon-md);
    height: var(--icon-md);
    font-size: var(--icon-md);
}

.icon-base {
    width: var(--icon-base);
    height: var(--icon-base);
    font-size: var(--icon-base);
}

.icon-lg {
    width: var(--icon-lg);
    height: var(--icon-lg);
    font-size: var(--icon-lg);
}

.icon-xl {
    width: var(--icon-xl);
    height: var(--icon-xl);
    font-size: var(--icon-xl);
}

.icon-2xl {
    width: var(--icon-2xl);
    height: var(--icon-2xl);
    font-size: var(--icon-2xl);
}

.icon-3xl {
    width: var(--icon-3xl);
    height: var(--icon-3xl);
    font-size: var(--icon-3xl);
}

/* A form that exists only to carry a POST — it wraps a single control and must
   not introduce any box of its own. */
.form-inline {
    display: inline;
    margin: 0;
    padding: 0;
}

/* A fixed-height box for a Chart.js canvas. Charts need a sized parent — the
   canvas has no intrinsic height and collapses to nothing without one — so the
   height is structural, not decorative, and belongs to a named frame rather
   than an inline style repeated per chart. */
.chart-frame {
    position: relative;
    /* 250px unless the page says otherwise. The hook exists because a second
       page wanted 160px and wrote `style="height: …"` rather than reach for
       this — a declaration in markup, where a custom property is the form the
       drift gate allows and the component keeps owning the rule. */
    height: var(--chart-height, 250px);
}

/* The tenant overview banner. A soft brand wash behind the tenant counts,
   outlined in brand green so it reads as the page's own summary rather than a
   card like the ones below it. */
.dashboard-tenant-banner {
    background: linear-gradient(
        135deg,
        var(--color-banner-gradient-start) 0%,
        var(--color-banner-gradient-end, var(--banner-soft-end)) 100%
    );
    border: 1px solid var(--color-brand-green);
}

/* The option a listbox is currently showing. Two grounds, because the tenant
   picker exists twice: once on the dashboard card and once on the dark nav
   rail. Both hand-wrote the brand colour at each option — three restatements in
   the rail's Alpine :class alone — so a change to the selected treatment had to
   be made in four places and would silently be made in fewer. */
.listbox-option-current {
    color: var(--color-brand-green);
}

/* On the rail the same state is a solid fill, because brand-green text on the
   dark ground does not carry enough contrast to read as chosen. */
.listbox-option-current-on-dark {
    background-color: var(--color-brand-green);
    color: var(--fg-on-dark);
}

/* A map filling a modal's body: as tall as the viewport allows, minus the
   chrome above and below it. The height was written inline at the call site
   along with the radius and border, so a modal that changed its header height
   had to be found by reading markup. */
.modal-map {
    height: calc(90vh - 120px);
    border: 1px solid var(--border-base);
    border-radius: var(--radius-md);
}

/* A scrolling panel body under a fixed header. Same calculation, same reason. */
.panel-scroll {
    max-height: calc(100vh - 120px);
}

/* A data table wide enough that it scrolls rather than crushes its columns.
   The two rules travel together — auto layout without a floor just crushes —
   so they belong in one name rather than two declarations at the call site. */
.table-wide {
    table-layout: auto;
    min-width: 800px;
}

/* The placeholder drawn when an icon name does not resolve. Danger-toned on
   purpose: a missing icon is a bug to notice, not a blank to skip past. */
.icon-missing {
    stroke: var(--danger-fg);
}

/* The loading dimmer over a table while HTMX swaps it. The class existed and
   carried no rules — every one of the eight declarations was written inline at
   the call site, so the class named the thing without describing it. */
.dimmer {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--fg-on-dark) 70%, transparent);
    z-index: 10;
}

/* A dot that takes the colour of the text around it — a status pill's marker,
   which must never disagree with the label beside it. */
.dot-current {
    background-color: currentColor;
}

/* A dot in a colour the server chose: a framework's own brand, an alert
   type's key. The value arrives as a custom property because only the server
   knows it; the declaration reading it belongs here. */
.dot-tinted {
    background-color: var(--dot-colour, currentColor);
}

/* Text in a colour the server chose, same contract as .dot-tinted. */
.text-tinted {
    color: var(--tint-colour, inherit);
}
