/* Component styles — corporate design pass (W8).
 *
 * @relation(REQ-27, scope=file, role=Impl)
 * @relation(REQ-29, scope=file, role=Impl)
 * @relation(REQ-95, scope=file, role=Impl)
 * @relation(REQ-37, scope=file, role=Impl)
 * @relation(REQ-41, scope=file, role=Impl)
 * @relation(REQ-43, scope=file, role=Impl)
 * @relation(REQ-44, scope=file, role=Impl)
 * @relation(REQ-45, scope=file, role=Impl)
 * @relation(REQ-46, scope=file, role=Impl)
 * @relation(REQ-47, scope=file, role=Impl)
 * @relation(REQ-50, scope=file, role=Impl)
 * @relation(REQ-54, scope=file, role=Impl)
 * @relation(REQ-55, scope=file, role=Impl)
 * @relation(REQ-53, scope=file, role=Impl)
 * @relation(REQ-56, scope=file, role=Impl)
 * @relation(REQ-57, scope=file, role=Impl)
 * @relation(REQ-59, scope=file, role=Impl)
 * @relation(REQ-60, scope=file, role=Impl)
 * @relation(REQ-61, scope=file, role=Impl)
 * @relation(REQ-62, scope=file, role=Impl)
 * @relation(REQ-63, scope=file, role=Impl)
 * @relation(REQ-64, scope=file, role=Impl)
 * @relation(REQ-65, scope=file, role=Impl)
 * @relation(REQ-20, scope=file, role=Impl)
 * @relation(REQ-70, scope=file, role=Impl)
 * @relation(REQ-78, scope=file, role=Impl)
 * @relation(REQ-79, scope=file, role=Impl)
 * @relation(REQ-88, scope=file, role=Impl)
 *
 * Branding values come exclusively from the tokens in theme.css (var(--st-*));
 * nothing here hardcodes a brand color or font. Selectors match the DOM
 * built by the templates and js/ — class hooks are pinned by the E2E tests.
 */

/* Reserve the scrollbar gutter always (REQ-43): without it, opening a tall
 * editor can add the page scrollbar, narrowing the viewport and triggering
 * the one reflow/re-render path the two-column grid below doesn't cover.
 * The page wash (REQ-44) lives on html, not body: the root's background
 * paints the whole canvas (including the gutter strip), while body's box
 * is only content-height — a gradient there would tile/clip unpredictably
 * and a min-height hack would be needed. The wash's last stop equals
 * --st-bg, so short pages fall back to the flat color with no seam. */
html {
    scrollbar-gutter: stable;
    background-color: var(--st-bg);
    background-image: var(--st-page-wash);
    background-repeat: no-repeat;
}

/* Sticky footer (REQ-62): body is the flex column so <main> can grow to fill
 * any leftover viewport height, pushing the footer to the bottom edge on
 * short pages. On tall pages main simply exceeds the viewport and the footer
 * follows the content and scrolls with it -- no `position: fixed` (rejected:
 * it would cost viewport height on every page and need an opaque backing to
 * stop content showing through, both fighting REQ-41/REQ-57). 100dvh (not
 * 100vh) so mobile browser chrome resizing doesn't leave a gap below the
 * footer; `scrollbar-gutter: stable` on html above avoids that reserving a
 * scrollbar on short, non-scrolling pages. */
body {
    margin: 0;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    color: var(--st-ink);
    line-height: 1.5;
}

/* keyboard focus stays visible everywhere (accessibility) */
:focus-visible {
    outline: var(--st-focus);
    outline-offset: 2px;
}

h1 {
    font-size: var(--st-text-l);
    font-weight: 650;
    margin: var(--st-space-m) 0;
}

/* Standard qualifier next to the PKI name (REQ-46): subordinate to the name,
 * never part of #pki-name's textContent. --st-ink-muted stays >= 4.5:1 on
 * the page wash (REQ-44), so this reaches WCAG AA without a new token. */
#pki-standard {
    font-size: var(--st-text-base);
    font-weight: 400;
    color: var(--st-ink-muted);
}

a {
    color: var(--st-blue);
}

/* --- header (REQ-28: logo on every main interface via base.html) ---------- */

.st-header {
    display: flex;
    align-items: center;
    gap: var(--st-space-m);
    /* Positioned and stacked above <main>'s in-flow content so the
     * notification (REQ-60) slides out from *underneath* it: the header's
     * opaque gradient and its shadow hide the box while it is still above its
     * resting position. The only z-index in this stylesheet — an in-flow
     * element cannot be raised, so the header has to be the positioned one. */
    position: relative;
    z-index: 1;
    background: var(--st-header-bg);
    color: var(--st-surface);
    padding: var(--st-space-m) var(--st-space-l);
    border-bottom: 3px solid var(--st-orange);
    box-shadow: var(--st-elev-header);
}

.st-header .st-logo-link {
    display: inline-flex;
}

/* Horizontal lockup (3:1) — the pixel-fitting height is a raw rem value on
 * purpose (not a spacing token); it sets the logo's visual size, not layout
 * rhythm. */
.st-header .st-logo {
    height: 2.25rem;
}

.st-header .st-title {
    font-weight: 600;
    letter-spacing: 0.01em;
}

/* narrow viewports (REQ-108): unwrapped, the three header items shrink until
 * the title and the switch labels break inside themselves, so the header eats
 * three rows of an already small screen. Wrapping the *container* instead puts
 * logo + title on row one and the standard switch (which keeps its
 * margin-left: auto and so stays right-aligned) on row two -- two rows at
 * most -- and nowrap on the title and the switch links keeps every element's
 * own text on a single line. Width-only query on purpose: a narrowed desktop
 * window benefits from the same compaction.
 * @relation(REQ-108, scope=range_start, role=Impl) */
@media (max-width: 48rem) {
    .st-header {
        flex-wrap: wrap;
    }

    .st-header .st-title,
    .standard-nav a {
        white-space: nowrap;
    }
}
/* @relation(REQ-108, scope=range_end, role=Impl) */

/* --- phone hint banner (REQ-107) --------------------------------------------
 * The whole display gate is here: hidden everywhere by default, shown only
 * where the viewport is narrow *and* the primary pointer is coarse, so a
 * desktop window dragged narrow stays clear of it. mobile-hint.js contributes
 * nothing but the --dismissed class. In flow between header and <main>, so it
 * is part of the initial layout and shifts nothing after load (REQ-43).
 * @relation(REQ-107, scope=range_start, role=Impl) */
.mobile-hint {
    display: none;
}

@media (max-width: 48rem) and (pointer: coarse) {
    .mobile-hint {
        display: flex;
        align-items: flex-start;
        gap: var(--st-space-s);
        margin: 0;
        padding: var(--st-space-s) var(--st-space-m);
        border-bottom: 1px solid var(--st-orange-deep);
        background: var(--st-orange-tint);
        color: var(--st-orange-deep-2);
        font-size: var(--st-text-s);
    }

    .mobile-hint.mobile-hint--dismissed {
        display: none;
    }

    .mobile-hint p {
        margin: 0;
    }

    /* same glyph token as .overlay-close -- the other "x" in this UI */
    .mobile-hint-close {
        flex: none;
        margin-left: auto;
        padding: 0 var(--st-space-xs);
        border: none;
        background: transparent;
        color: inherit;
        font-size: var(--st-icon-glyph-l);
        line-height: 1;
    }
}
/* @relation(REQ-107, scope=range_end, role=Impl) */

/* fluid full width (REQ-41) — the diagram/tree views use the whole viewport.
 * `flex: 1` (REQ-62) makes main the flexible column that absorbs the gap
 * between header and footer on short pages; header/footer keep their
 * intrinsic height as the other two flex items. An explicit `width` is
 * needed alongside that: a flex item with the tree view's
 * `margin-inline: auto` below stops stretching to the line's cross size
 * (auto margins on the cross axis opt it out of `align-items: stretch`) and
 * reverts to shrink-to-fit sizing instead, so before REQ-43's pre-paint tree
 * layout had any content it measured a few hundred px narrower than after --
 * the exact horizontal snap that requirement forbids. `calc(100% - 2 *
 * --st-space-l)` reproduces the old width:auto content-box math (fill the
 * container, then let padding add back on top) instead of switching to
 * border-box, so the tree view's 72rem cap below still lands on the same
 * content width and the rendered column geometry is unchanged from before
 * this rule existed.
 *
 * KEEP IN SYNC: the calc subtracts exactly the inline padding on the next
 * line. Changing one without the other makes main's border box overflow (or
 * under-fill) the body by the delta -- a horizontal scrollbar or a visible
 * gap at the right edge. Both values are --st-space-l on purpose. */
main {
    flex: 1;
    width: calc(100% - 2 * var(--st-space-l));
    padding: var(--st-space-m) var(--st-space-l);
}

/* tree view (REQ-41): the tree does not benefit from a wide screen — restore
 * the pre-REQ-41 centered column so tree and editor stay grouped instead of
 * drifting to opposite screen edges. Diagram view stays full width. */
body[data-view="tree"] main {
    max-width: 72rem;
    margin-inline: auto;
}

/* --- notification (REQ-60/61: API errors / success notes) -----------------
 * A flex item in the sub-header row (see _notification.html), not a row of its
 * own and not a floating box. It takes the space that already exists between
 * "Manage PKIs" and the view toggle, which means:
 *   - no vertical space is spent (the band this replaced reserved 4.5rem that
 *     was empty most of the time and only ever used at its right end),
 *   - nothing moves when it appears or disappears (a `display: none` flex item
 *     does not participate, so the two ends stay put) -- REQ-60/REQ-43,
 *   - nothing is covered (it is in the flow, unlike the float before that,
 *     which sat on the view toggle at every viewport width),
 *   - and it gets the full width of that gap (~40rem at 1600px against the
 *     float's 24rem cap), so a message wrapping at all is the exception.
 * A long message scrolls inside instead of growing the row: #message is capped
 * at one line (see .message below). That is the one trade left -- no new space,
 * no covering and no cropping cannot all hold for arbitrary-length text. The
 * box's own 2.5rem cap alone was NOT enough: the row's height is derived from
 * its buttons (~37px) and the cap is 40px, so a message wrapping to a second
 * line grew the row by the difference and shifted the diagram 2.8px -- the
 * exact move REQ-60 forbids, measured in Playwright 2026-08-18 once REQ-81's
 * 18-character EMAID default made wrapping reachable on a default PKI. */
.notification {
    display: none;
    /* The element keeps `popover` for the promoted case below, and the UA
     * stylesheet styles `[popover]` whether or not it is open: `position:
     * fixed; inset: 0; margin: auto; border: solid` would take it out of the
     * flex row entirely, so all of it is undone here, not just in
     * .is-promoted. */
    position: static;
    margin: 0;
    border: none;
    flex: 1 1 auto;
    /* Stretched to the row's height rather than centred in it, so the slide
     * offset below is exact: the box's top then sits at the row's top, i.e.
     * main's padding plus the row's margin below the header, with no
     * centring slack to guess at. */
    align-self: stretch;
    min-width: 0;
    /* Capped rather than filling the whole gap: at a wide viewport the gap is
     * ~40rem and a one-line message stretched across all of it read as thin and
     * over-wide. Still well above the 24rem the floating version was capped to,
     * so a message wrapping at all stays the exception. */
    max-width: 30rem;
    max-height: 2.5rem;
    box-sizing: border-box;
    align-items: flex-start;
    gap: var(--st-space-s);
    padding: var(--st-space-xs) 0.75rem;
    border-radius: var(--st-radius-sm);
    background: var(--st-ok-bg);
    color: var(--st-ok-text);
    box-shadow: var(--st-elev-1);
    /* Starts hidden behind the header and slides *down* into the row. The
     * offset is its own height plus exactly the distance from the row to the
     * header's bottom edge — main's top padding plus the row's own margin — so
     * at rest-minus-one-slide the box's bottom sits on that edge and the header
     * covers it completely. Expressed in the tokens that create the offset, not
     * a magic number, so it follows if either changes. */
    transform: translateY(calc(-100% - var(--st-space-m) - var(--st-space-s)));
    /* `display` must be in the transition with `allow-discrete`, and the
     * open state needs an @starting-style, or the slide does not happen at
     * all: an element coming out of `display: none` has no previous
     * rendered style to interpolate from, so it lands on translateX(0) in
     * the first frame. Measured in Chromium -- without these two lines the
     * computed transform is the identity matrix on every frame after
     * `.is-open` is added; with them it walks from 120% down to 0. */
    transition: transform 0.4s ease-out, display 0.4s allow-discrete;
}

.notification.is-open {
    display: flex;
    transform: translateY(0);
}

/* The one sanctioned exception (REQ-60's COMMENT): while a modal <dialog> is
 * open the sub-header sits behind its ::backdrop, so api.js promotes the
 * notification to the top layer, where it is positioned against the viewport
 * instead of the row. `inset` must name all four sides -- UA `inset: 0`
 * otherwise resolves `left`/`bottom` too and an over-constrained box goes to
 * the left edge in LTR. */
.notification.is-promoted {
    position: fixed;
    inset: 5.5rem var(--st-space-l) auto auto;
    flex: none;
    max-width: 24rem;
    max-height: none;
}

@starting-style {
    .notification.is-open {
        transform: translateY(calc(-100% - var(--st-space-m) - var(--st-space-s)));
    }
}

/* Reduced motion (REQ-44's design-review culture expects this everywhere an
 * animation is purely decorative): the notification still appears/
 * disappears, just without the slide. */
@media (prefers-reduced-motion: reduce) {
    .notification {
        transition: none;
    }
}

/* The error/ok background+text color lives on the container, keyed off
 * #message's own "error" class via :has() -- #message keeps toggling that
 * class exactly as before (unchanged JS semantics); the container just
 * reads it, so the whole coloured box (text + dismiss button) is one
 * surface instead of a coloured line inside an uncoloured card. */
.notification:has(.message.error) {
    background: var(--st-error-bg);
    color: var(--st-error-text);
}

.message {
    flex: 1;
    min-width: 0;
    margin: 0;
    /* One visible line, scroll for the rest (REQ-60): one line plus the box's
     * padding stays below the sub-header row's button-derived height whatever
     * the button metrics, so a wrapping message can never grow the row. */
    max-height: 1lh;
    overflow-y: auto;
    overflow-wrap: anywhere;
}

/* Promoted (modal open) floats against the viewport, off the row -- there the
 * full message may show; the notification's own max-height is already lifted
 * in .is-promoted above. */
.notification.is-promoted .message {
    max-height: none;
}

.notification-dismiss {
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: inherit;
    font-size: var(--st-icon-glyph-l);
    line-height: 1;
    padding: 0;
    cursor: pointer;
}

/* Promoted means a modal <dialog> is open, which makes everything outside it
 * inert -- so this button cannot be clicked or focused however solid it looks.
 * REQ-61 sanctions the message persisting undismissed in that state, but a
 * control painted at full opacity that silently refuses every click is the
 * dead-control state the same requirement condemns one clause earlier. Hide it
 * rather than offer it: the message stays readable, and nothing on screen
 * promises an action that cannot happen. It returns as soon as the dialog
 * closes, because recomputeNotificationPlacement() drops .is-promoted. */
.notification.is-promoted .notification-dismiss {
    display: none;
}

/* --- buttons ---------------------------------------------------------------
 * Secondary (default): quiet outline. Primary (form submits): filled blue.
 * Scoped to the form/list contexts so component buttons (.tab, .node,
 * .action-icon, .overlay-close, #legend-open) keep their own styling. */

button {
    font: inherit;
    cursor: pointer;
}

.create-form button,
.import-form button,
.pki-row button,
.editor button,
.add-cert-form button,
.export-bar a {
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    color: var(--st-blue);
    font-weight: 600;
}

.create-form button:hover,
.import-form button:hover,
.pki-row button:hover,
.editor button:hover,
.add-cert-form button:hover,
.export-bar a:hover {
    border-color: var(--st-blue);
    background: var(--st-blue-tint);
}

.create-form button[type="submit"],
.import-form button[type="submit"],
.editor button[type="submit"],
.add-cert-form button[type="submit"] {
    border-color: var(--st-blue);
    background: var(--st-blue);
    color: var(--st-surface);
}

.create-form button[type="submit"]:hover,
.import-form button[type="submit"]:hover,
.editor button[type="submit"]:hover,
.add-cert-form button[type="submit"]:hover {
    border-color: var(--st-blue-dark);
    background: var(--st-blue-dark);
}

button.danger {
    color: var(--st-error-text);
}

button.danger:hover {
    border-color: var(--st-error-text);
    background: var(--st-error-bg);
}

/* --- text inputs / selects -------------------------------------------------- */

input,
select {
    font: inherit;
    color: var(--st-ink);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    padding: 0.35rem 0.5rem;
}

input:focus-visible,
select:focus-visible {
    outline: none;
    border-color: var(--st-blue);
    box-shadow: 0 0 0 2px var(--st-blue-tint);
}

/* --- standard switch (REQ-25): segmented control in the header --------------
 * Sits on the blue header bar, so it inverts the palette: surface-on-blue,
 * with the current segment filled surface. */

.standard-nav {
    display: inline-flex;
    margin-left: auto;
    border: 1px solid var(--st-surface);
    border-radius: var(--st-radius-sm);
    overflow: hidden;
}

.standard-nav a {
    padding: 0.25rem 0.75rem;
    border: none;
    background: transparent;
    color: var(--st-surface);
    font: inherit;
    font-weight: 600;
    text-decoration: none;
}

.standard-nav a:hover:not([aria-current="page"]) {
    background: var(--st-blue-dark);
}

.standard-nav [aria-current="page"] {
    background: var(--st-surface);
    color: var(--st-blue);
}

/* --- per-standard sub-header: overview-overlay trigger (REQ-21) -------------- */

.sub-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--st-space-s);
    margin-top: var(--st-space-s);
}

/* narrow windows (REQ-41): on ~360px phones the view toggle no longer fits
 * beside the Manage-PKIs button and pushed the whole page into sideways
 * scroll -- let it wrap onto its own row instead.
 * @relation(REQ-41, scope=range_start, role=Impl) */
@media (max-width: 48rem) {
    .sub-header {
        flex-wrap: wrap;
    }
}
/* @relation(REQ-41, scope=range_end, role=Impl) */

.overview-open {
    /* WebKit crops flex-shrunk buttons — never shrink or wrap this one */
    flex: none;
    white-space: nowrap;
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    color: var(--st-blue);
    font-weight: 600;
}

.overview-open:hover {
    border-color: var(--st-blue);
    background: var(--st-blue-tint);
}

/* --- empty standard view (no PKI open / -20 not in v1) ----------------------- */

.empty-state {
    max-width: 28rem;
    margin: 3rem auto;
    padding: var(--st-space-l);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    box-shadow: var(--st-elev-2);
    text-align: center;
}

.empty-state p {
    color: var(--st-ink-muted);
}

/* --- overview PKI list -------------------------------------------------------- */

/* REQ-93: the public-mode limits notice. Muted hint tokens (same pair as
 * .field-hint) on purpose -- these are the sandbox's rules, not a warning. */
.sandbox-limits {
    margin: 0 0 var(--st-space-m);
    font-size: var(--st-text-s);
    color: var(--st-ink-muted);
}

.create-form {
    display: flex;
    gap: var(--st-space-s);
    margin-bottom: var(--st-space-m);
}

.pki-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--st-space-s);
}

.pki-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: var(--st-space-s) var(--st-space-m);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    box-shadow: var(--st-elev-1);
}

.pki-row a {
    color: var(--st-blue);
    font-weight: 600;
    margin-right: auto;
    text-decoration: none;
}

.pki-row a:hover {
    text-decoration: underline;
}

/* phone widths (REQ-41 bug): a row's name, timestamp and four action buttons
 * (open/rename/export/delete) cannot fit one line inside the overview dialog
 * -- unwrapped they shrank past their labels and ran out of the dialog, so
 * Export and Delete sat off screen (measured at 390px: content 532px in a
 * 337px body). Wrapping stacks them inside instead. The create form is two
 * items and overflowed by only ~22px, but for the same reason.
 * Scoped to the breakpoint on purpose, not applied unconditionally: at desktop
 * width these rows do fit (the name flex-shrinks and wraps its own text), and
 * wrapping them there would grow every row from 66px to 104px.
 * @relation(REQ-41, scope=range_start, role=Impl) */
@media (max-width: 48rem) {
    .pki-row,
    .create-form {
        flex-wrap: wrap;
    }
}
/* @relation(REQ-41, scope=range_end, role=Impl) */

.muted {
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
}

/* --- tree page layout ---------------------------------------------------------- */

/* Two-column grid (REQ-43): the editor track is a fixed 26rem and exists
 * whether or not the editor is displayed, so opening/closing it never
 * resizes the visualization (no ResizeObserver re-render, no node jump).
 * The reserved column simply stays blank while nothing is selected. */
.tree-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 26rem;
    gap: var(--st-space-l);
    align-items: start;
}

/* the visualization panel (REQ-44): raised off the page background. #diagram
 * and #tree are mutually exclusive (the inactive one is display: none), but
 * #pki-empty can be shown alongside whichever is active once the forest is
 * empty (REQ-58/59: the "+" controls stay reachable instead of the panel
 * turning into a dead end) -- each auto-placed into its own row of this
 * grid's single 1fr column, so this never shows as an empty white box. */
.tree-layout > section {
    grid-column: 1;
    background: var(--st-surface);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    box-shadow: var(--st-elev-2);
}

/* @relation(REQ-66, scope=file, role=Impl)
 * #diagram and #tree now sit inside #viz (.viz-panel), which carries the
 * frame the rule above used to give each of them directly -- one frame
 * around whichever visualization is active plus the anchor strip above it,
 * instead of the strip needing a frame of its own. `overflow: hidden` (not
 * `auto`) so the panel itself never grows a second scrollbar; #diagram's own
 * `overflow-x: auto` still scrolls its content, and #diagram keeps
 * `position: relative` so #diagram-edges' absolute coordinates are unaffected
 * by this wrapper. No padding, no box-sizing change here -- the frame move
 * is width-neutral and stays that way. */
.viz-panel {
    grid-column: 1;
    min-width: 0;
    background: var(--st-surface);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    box-shadow: var(--st-elev-2);
    overflow: hidden;
}

/* whole-PKI export beneath the panel (REQ-94). .viz-column is the grid item;
 * it exists so the bar is NOT one — a sibling bar advanced grid
 * auto-placement past row 1 and dropped the open #editor a full row below
 * the panel (measured 558px/832px, round 18; the wrapper is what keeps
 * #editor beside the panel). min-width: 0 was measured NOT load-bearing
 * today — the column track is minmax(0, 1fr), so -20's wide grid stays put
 * without it — and is kept as the same shrink guard .viz-panel carries, for
 * the day the track loses its 0 floor. Flex gap, not the grid's, separates
 * panel and bar, so the bar hugs the panel however tall the open editor
 * makes the row. */
.viz-column {
    grid-column: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--st-space-l);
}

/* The link wears the secondary-button outfit via the shared button lists
 * above -- it is an <a> (plain navigation to the REQ-1 attachment URL), so
 * only the two anchor-specific resets live here. */
.export-bar a {
    display: inline-block;
    text-decoration: none;
}

/* One header strip shared by both views (REQ-66's COMMENT: co-located with
 * the visualizations, not the sub-header's view toggle). Deliberately a
 * separate block from .view-toggle*, even though the two look alike, so
 * either can be restyled without touching the other. No [hidden] rule here:
 * the strip is omitted from the document server-side (pki.html) when
 * anchor_strip is absent, rather than rendered-then-hidden, so there is
 * nothing to override the UA default for. */
/* REQ-66's "visually subordinate to the certificates the panel exists to
 * show": muted small text, no pill, no fill, and pushed to the trailing edge
 * of a row that itself sits along the panel's bottom edge -- so the
 * certificates lead and the settings trail, in both axes. border-top (not
 * -bottom) because the rule now separates the strip from the content above
 * it. justify-content
 * rather than margin-inline-start on the first item, because a wrapped second
 * row would otherwise indent under the first (and margin-inline: auto on a
 * flex item opts it out of stretch -- the REQ-43 defect of round 11). */
.anchor-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--st-space-s) var(--st-space-l);
    padding: var(--st-space-s) var(--st-space-m);
    border-top: 1px solid var(--st-line);
}

/* The label is the whole control: it names the domain and the anchor, so
 * there is no separate caption and nothing to clip. white-space: nowrap keeps
 * a sentence from breaking mid-phrase; measured, the labels only wrap
 * internally below a ~320px viewport, so this is legibility insurance at
 * sizes the app does not target rather than the thing that keeps the strip
 * usable when it narrows -- the strip wrapping between the two toggles
 * (at ~540px) does that. */
.anchor-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--st-space-xs);
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
    white-space: nowrap;
    cursor: pointer;
}

/* The grouped strip's shared anchor name (amended REQ-66: named "once as a
 * shared prefix"). Same muted tokens as the controls it introduces -- the
 * prefix is part of the same subordinate sentence, not a heading. */
.anchor-prefix {
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
    white-space: nowrap;
}

.anchor-toggle:hover {
    color: var(--st-ink);
}

/* accent-color tints the native box with the brand blue without replacing the
 * control -- an appearance: none rebuild would cost the platform's own focus
 * and checked rendering for no requirement. */
.anchor-toggle input {
    accent-color: var(--st-blue);
    cursor: pointer;
}

/* Restore the global :focus-visible outline. Making this control a checkbox
 * moved it under the `input:focus-visible` rule above, which is written for
 * text fields: it drops the outline for a border tint a checkbox has no
 * border to show, leaving the focused box indistinguishable from the
 * unfocused one -- on the one control where Space opens a destructive
 * confirmation (REQ-67). */
.anchor-toggle input:focus-visible {
    outline: var(--st-focus);
    outline-offset: 2px;
    box-shadow: none;
}

.editor {
    grid-column: 2;
    position: sticky;
    top: var(--st-space-m);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    box-shadow: var(--st-elev-2);
    padding: var(--st-space-m);
}

.editor h2 {
    margin: 0 0 var(--st-space-s);
    font-size: var(--st-text-m);
    color: var(--st-blue);
}

.editor-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--st-space-s);
}

/* narrow windows (REQ-41): side-by-side no longer fits — stack the editor
 * full-width below the visualization instead of squeezing both columns */
@media (max-width: 60rem) {
    .tree-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .editor {
        grid-column: 1;
        position: static;
    }
}

/* --- nested certificate forest (REQ-22) ---------------------------------------- */

.forest,
.forest ul {
    list-style: none;
    padding-left: 1.25rem;
    border-left: 1px solid var(--st-line);
}

.forest {
    margin: 0;
    padding-left: 0;
    border-left: none;
}

/* connector parity (REQ-45): the branch leading to a selected/affected node
 * carries the same accent, colour only -- width stays 1px (REQ-43). */
.forest ul.affected {
    border-left-color: var(--st-orange-deep);
}

.node {
    display: inline-flex;
    gap: var(--st-space-s);
    align-items: center;
    margin: 0.15rem 0;
    padding: var(--st-space-xs) var(--st-space-s);
    border: 1px solid transparent;
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-ink);
}

/* the standard marks this certificate optional (REQ-78): dashed outline in the
 * neutral line colour, no tint -- that is what keeps it distinct from
 * .node.affected below, which dashes in orange over a tint. The colour is
 * load-bearing, not decoration: .node's base border is `1px solid transparent`,
 * so a style-only rule would draw nothing. Placed *before* .node.affected on
 * purpose -- equal specificity, so the later rule wins and a node that is both
 * still reads as affected. */
.node.optional {
    border-style: dashed;
    border-color: var(--st-line);
}

/* OCSP signer (REQ-95): monochrome — the role name (and the badge, greyed in
 * help.js's roleIcon) lose their cluster colour, so the chains lead by
 * chroma. Third treatment, and deliberately colour-only in the tree: a fill
 * read as a highlight inside the white panel and a fade read as disabled
 * (both shipped and reverted, round 18) — grey keeps full contrast and needs
 * no state exceptions, the emphases paint over it like over any node. */
.node.ocsp .node-role {
    color: var(--st-ink-muted);
}

/* weaker emphasis for descendants of the selection (REQ-45): dashed border is
 * the non-colour cue, survives hover below since hover never touches style */
.node.affected {
    border-style: dashed;
    border-color: var(--st-orange-deep);
    background: var(--st-affect-tint);
}

.node:hover {
    border-color: var(--st-blue);
    background: var(--st-blue-tint);
}

.node.selected {
    border-color: var(--st-orange-deep);
    background: var(--st-orange-tint);
    box-shadow: inset 3px 0 0 var(--st-orange-deep);
}

/* the "+" that creates a missing root (REQ-59) or a missing child (REQ-63),
 * listed after the tree's existing roots/children; dashed for the same "not
 * there yet" reading as their diagram counterparts (.diagram-add-root /
 * .diagram-add-child). Hover/focus come from the shared `.node` rules above. */
.node.add-root-node,
.node.add-child-node {
    border-style: dashed;
    border-color: var(--st-line);
    color: var(--st-blue);
    font-weight: 700;
}

/* The role identifier, shared by the tree node and the diagram card. It
 * truncates for the same reason .diagram-node-cn does (REQ-37/REQ-29: a card is
 * a bordered box, and text painting outside that border is not one). Measured
 * before the fix: V2G_OCSP_LEAF wanted 103px of the 68px the -2 OCSP lane left
 * it and painted 28px past the border; today -20's VEHICLE_SUB_CA_2 is the
 * case that still needs it, at 120px in a 106px span.
 *
 * `overflow: hidden` is the one load-bearing declaration, and not only for the
 * clipping: per the flexbox spec a box with overflow other than `visible` has
 * an automatic minimum size of 0, so it also cancels the `min-width: auto` that
 * would otherwise stop this flex item shrinking below its content. An explicit
 * `min-width: 0` was measured to be redundant here and is deliberately absent.
 * Dropping `overflow` is the only edit to these three lines that a test
 * catches; the other two were measured not to change anything today and are
 * kept for the reasons below, not because they are load-bearing.
 *
 * `text-overflow: ellipsis` is cosmetic -- an ellipsis rather than a hard cut.
 * `white-space: nowrap` cannot be observed at all right now: role identifiers
 * are A_B_C with no spaces, and an underscore is not a break opportunity, so
 * the text has nowhere to wrap. It stays because truncation-not-wrapping is a
 * deliberate choice -- card heights feed the edge-clearance arithmetic that
 * web-layer.md section 10 D4 records, so a role that wrapped to two lines would
 * move every downstream measurement -- and because `text-overflow` only applies
 * to non-wrapping text in the general case.
 *
 * The full identifier stays reachable either way: diagram.js and tree.js both
 * set it as `title`. */
.node-role {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--st-text-xs);
    font-weight: 600;
    color: var(--st-blue);
}

/* --- edit mask / add-child form (REQ-11 / REQ-6 UI) ----------------------------- */

.field {
    display: block;
    margin: var(--st-space-s) 0;
}

/* REQ-54: the label must never render smaller than the input it labels (the
 * input inherits the base size via font: inherit) -- so the label sits at
 * the base step too, and gets its own visual weight from colour alone. */
.field span {
    display: block;
    margin-bottom: 0.1rem;
    font-size: var(--st-text-base);
    color: var(--st-ink-muted);
}

.field input,
.field select {
    width: 100%;
    box-sizing: border-box;
}

/* REQ-88: the expected format of a field the server can reject on syntax,
 * under the input it belongs to. Muted small text at the same step as the
 * other supplementary notes (.resign-notice) -- --st-ink-muted is held at
 * >= 4.5:1 on --st-surface by the theme, so it passes AA at this size
 * (REQ-29); it never carries the label, which stays at the base step for
 * REQ-54's sake. */
.field-hint {
    display: block;
    margin-top: 0.1rem;
    font-size: var(--st-text-s);
    color: var(--st-ink-muted);
}

/* Consequence notices in the editor: the save re-sign warning and REQ-47's
 * delete warning — two sentences on a selected CA's panel. Small text
 * (0.85em) on the white panel, so it takes the deep orange
 * — 4.76:1 against --st-surface, where the plain accent orange reaches only
 * 3.30:1 and misses AA (REQ-29). */
.resign-notice {
    font-size: var(--st-text-s);
    color: var(--st-orange-deep);
}

/* the delete section (REQ-47) is separated like the other editor sections;
 * its button keeps the shared .danger treatment (overview PKI delete). Kept
 * as its own rule (not shared with a dead ".add-child" any more, REQ-65
 * moved the add form out of the editor panel entirely) so this separator
 * survives on its own. */
.delete-cert {
    margin-top: var(--st-space-m);
    border-top: 1px solid var(--st-line-soft);
    padding-top: var(--st-space-s);
}

/* --- role icons (REQ-7) ---------------------------------------------------------
 * Shape = kind (shield root / diamond sub-CA / square leaf); the glyph
 * (cluster letter + tier for sub-CAs, role letters for leaves) makes each
 * role distinct on its own — the cluster background color (set inline from
 * the help.js map) is a redundant cue, not load-bearing (color-blind safe). */

.role-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.4rem;
    flex: none;
    color: var(--st-surface);
    font-size: var(--st-icon-glyph-s);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1;
}

.role-icon.kind-root {
    clip-path: polygon(50% 0, 100% 18%, 100% 55%, 50% 100%, 0 55%, 0 18%);
}

.role-icon.kind-sub {
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

.role-icon.kind-leaf {
    border-radius: var(--st-radius-sm);
}

/* --- per-node action icons: magnifying glass (REQ-12) + "i" (REQ-26) ------------
 * hand-drawn in CSS — no icon fonts, no external assets */

.node-actions {
    display: inline-flex;
    gap: 0.15rem;
    margin-left: var(--st-space-xs);
    vertical-align: middle;
}

.action-icon {
    position: relative;
    display: inline-block;
    width: 1.4rem;
    height: 1.4rem;
    flex: none;
    padding: 0;
    border: none;
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-blue);
}

button.action-icon:hover {
    background: var(--st-blue-tint);
}

.icon-view::before {
    /* lens */
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    width: 8px;
    height: 8px;
    border: 2px solid currentColor;
    border-radius: 50%;
    box-sizing: border-box;
}

.icon-view::after {
    /* handle */
    content: "";
    position: absolute;
    top: 10.5px;
    left: 10.5px;
    width: 6px;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transform: rotate(45deg);
    transform-origin: 0 50%;
}

.icon-info::before {
    content: "i";
    position: absolute;
    inset: 3px;
    display: grid;
    place-content: center;
    border: 1.5px solid currentColor;
    border-radius: 50%;
    font-size: var(--st-icon-glyph-m);
    font-weight: 700;
    font-style: italic;
    line-height: 1;
}

/* "+" opening the add-certificate dialog (REQ-64/REQ-65), shown only where
 * addable_child_roles is non-empty -- same circled-glyph treatment as
 * .icon-info so the third action reads as one of the same family. */
.icon-add::before {
    content: "+";
    position: absolute;
    inset: 3px;
    display: grid;
    place-content: center;
    border: 1.5px solid currentColor;
    border-radius: 50%;
    font-size: var(--st-icon-glyph-m);
    font-weight: 700;
    line-height: 1;
}

/* --- modal overlays: cert viewer (REQ-12), role info (REQ-26), legend (REQ-9) --- */

dialog.overlay {
    width: min(92vw, 48rem);
    max-height: 82vh;
    overflow: auto;
    border: none;
    border-radius: var(--st-radius);
    background: var(--st-surface);
    color: var(--st-ink);
    box-shadow: var(--st-elev-3);
    padding: var(--st-space-m) 1.25rem;
}

dialog.overlay::backdrop {
    background: var(--st-scrim);
}

.overlay-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--st-space-m);
    padding-bottom: var(--st-space-s);
    border-bottom: 2px solid var(--st-orange);
}

/* Overlay heading ladder (REQ-54): the dialog title sits a step above the
   section headings, which in turn sit above the content they head. Before, the
   h3 sections were the *smallest* text in the dialog while the field values and
   extension names below them ran at the base step -- the same inversion as a
   label smaller than its value. */
.overlay-head h2 {
    margin: 0;
    font-size: var(--st-text-l);
    color: var(--st-blue);
}

.overlay-close {
    border: none;
    border-radius: var(--st-radius-sm);
    background: transparent;
    font-size: var(--st-icon-glyph-l);
    line-height: 1;
    color: var(--st-ink-muted);
}

.overlay-close:hover {
    background: var(--st-blue-tint);
    color: var(--st-ink);
}

/* Space below the orange divider regardless of what the body starts with
 * (form, list, ...); margin collapsing makes the h3-first case (legend
 * overlay) fall out for free instead of stacking on top of h3's own
 * margin-top. */
.overlay-body {
    margin-top: var(--st-space-m);
}

.overlay-body h3 {
    margin: var(--st-space-m) 0 var(--st-space-xs);
    font-size: var(--st-text-m);
    color: var(--st-blue);
}

/* --- add-certificate dialog (REQ-65) ---------------------------------------
 * Reuses the overlay shell; dialog.overlay's 48rem cap stays shared with the
 * viewer/legend/role-info dialogs (test-pinned), so only the parent line
 * gets dedicated styling here -- the field/button rules are the existing
 * unscoped `.field` and the `.add-cert-form` additions above. */
.add-parent {
    margin: 0 0 var(--st-space-s);
    color: var(--st-ink-muted);
}

/* certificate viewer key/value listing (macOS-preview-like sections) */
.kv {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.15rem 0.75rem;
    margin: 0;
}

/* REQ-54: the label must never render smaller than its value (.kv dd, which
 * has no font-size of its own and so sits at the base step) -- base size
 * here too, distinguished by colour alone. */
.kv dt {
    color: var(--st-ink-muted);
    font-size: var(--st-text-base);
}

.kv dd {
    margin: 0;
    /* wrap at natural breaks first; only break a run that genuinely can't
       fit (long DNs/serials), instead of chopping mid-token (REQ-53) */
    overflow-wrap: anywhere;
}

/* technical values (DNs, serials, OIDs, hex) get a monospace face (REQ-53);
   applied selectively via keyValueList()'s `mono` flag and to .ext-value --
   never to the human-readable fields (curve, hash, version, dates). */
.mono {
    font-family: var(--st-font-mono);
}

.ext-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* same row family as .kv above (REQ-53): generous padding on the
   --st-space scale instead of the old cramped 0.3rem. */
.ext-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--st-space-xs) var(--st-space-s);
    padding: var(--st-space-s) 0;
    border-bottom: 1px solid var(--st-line-soft);
}

/* extension names in brand blue, not --st-ink (REQ-53/REQ-29): at base size
   and bold they were the darkest, heaviest text in the viewer and read as a
   wall of near-black. Blue lightens them and ties the names to the section
   headings, which use the same token. 9.62:1 on --st-surface, well clear of
   AA, so nothing is lost in legibility. */
.ext-list li strong {
    color: var(--st-blue);
}

/* criticality marker (REQ-53): its own badge, not text appended to the name.
   --st-orange-deep, not --st-orange, for AA contrast on white (see the
   .resign-notice comment above for the same reasoning). */
.ext-critical {
    font-size: var(--st-text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--st-surface);
    background: var(--st-orange-deep);
    padding: 0.05rem 0.4rem;
    border-radius: var(--st-radius-sm);
}

.ext-value {
    flex-basis: 100%;
    font-size: var(--st-text-s);
    color: var(--st-ink-muted);
    overflow-wrap: anywhere; /* see .kv dd above */
}

.pem-download {
    display: inline-block;
    margin: var(--st-space-xs) var(--st-space-m) 0 0;
    color: var(--st-blue);
    font-weight: 600;
}

/* --- legend (REQ-9) -------------------------------------------------------------- */

.acronym-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.15rem 0.75rem;
    margin: 0;
}

.acronym-list dt {
    font-weight: 700;
    color: var(--st-blue);
}

.acronym-list dd {
    margin: 0;
}

.icon-legend {
    list-style: none;
    margin: 0;
    padding: 0;
}

.icon-legend li {
    display: flex;
    align-items: center;
    gap: var(--st-space-s);
    padding: 0.15rem 0;
}

/* the dashed sample box explaining the optional outline (REQ-78); mirrors
 * .node.optional / .diagram-card.optional at legend scale */
.legend-sample.optional {
    flex: none;
    width: 1.6rem;
    height: 1.1rem;
    border: 1px dashed var(--st-line);
    border-radius: var(--st-radius-sm);
}

/* --- footer: legend link + tool version + copyright notice (REQ-57, REQ-91) ----- */

/* legal boilerplate, not content: no top border competing with the page above
 * it, muted text throughout. Flex row keeps the legend button (when present)
 * on the left; the version's own auto margin pushes the version + copyright
 * pair to the lower right regardless of whether the button precedes them
 * (landing.html has no button). */
/* margin-top only (REQ-62): a bottom margin here would leave a gap between
 * the footer and the true viewport edge on short pages, undercutting "rests
 * at the bottom edge". The top gap still separates it from the content
 * above, on both short pages (main's flex-grow already fills the rest) and
 * tall ones (footer follows the content when scrolled to). */
.st-footer {
    margin-top: var(--st-space-m);
    padding: var(--st-space-s) var(--st-space-l);
    display: flex;
    align-items: center;
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
}

#legend-open {
    border: none;
    background: transparent;
    color: var(--st-blue);
    font: inherit;
    text-decoration: underline;
    padding: 0;
}

#legend-open:hover {
    color: var(--st-blue-dark);
}

/* @relation(REQ-91, scope=range_start, role=Impl) — the tool version, styled
 * identically to the notice it sits beside: same muted token, same xs size, so
 * the footer's line height is unchanged and REQ-57's "unobtrusive" still holds
 * for both. The auto margin moved here from .st-copyright: it belongs on the
 * first of the pair, so version and notice travel to the lower right together
 * instead of the free space being split between them. */
.st-version {
    margin: 0 0 0 auto;
    color: var(--st-ink-muted);
    font-size: var(--st-text-xs);
}

/* separator drawn in CSS, not in the template, so neither element's text
 * content carries punctuation a test would have to match */
.st-copyright::before {
    content: "·";
    margin: 0 0.4em;
}

/* margin 0, not the auto it used to carry: the auto margin moved to
 * .st-version above, which now positions the pair. */
.st-copyright {
    margin: 0;
    color: var(--st-ink-muted);
    font-size: var(--st-text-xs);
}
/* @relation(REQ-91, scope=range_end, role=Impl) */

/* @relation(REQ-101, scope=range_start, role=Impl) — public-mode legal links,
 * same muted/xs treatment as the version+copyright pair so REQ-57's
 * "unobtrusive" holds for the whole group. The nav takes over the auto margin
 * that right-aligns the group; the sibling selectors switch .st-version back
 * to a group member and draw its separator, so local mode — where the nav is
 * not rendered — needs no template or CSS branching. */
.st-legal {
    margin-left: auto;
    font-size: var(--st-text-xs);
}

.st-legal a {
    color: var(--st-ink-muted);
}

/* separator between the two links; inline-block keeps the link underline off
 * the dot (text decorations do not propagate into atomic inline boxes) */
.st-legal a + a::before {
    content: "·";
    display: inline-block;
    margin: 0 0.4em;
}

.st-legal ~ .st-version {
    margin-left: 0;
}

.st-legal ~ .st-version::before {
    content: "·";
    margin: 0 0.4em;
}
/* @relation(REQ-101, scope=range_end, role=Impl) */

/* @relation(REQ-103, scope=range_start, role=Impl) — public-mode SEVENSTAX
 * link, same muted/xs treatment as the rest of the group. It sits one element
 * left of the legal nav, so the auto margin moves onto it and the sibling
 * selectors hand the nav over to a group member — additive to REQ-101's own
 * handover: .st-legal keeps its auto margin for a public footer without the
 * promo link, and local mode still renders neither. */
.st-promo {
    margin-left: auto;
    color: var(--st-ink-muted);
    font-size: var(--st-text-xs);
}

.st-promo ~ .st-legal {
    margin-left: 0;
}

/* separator between the promo link and the nav's first link; inline-block
 * for the same underline containment as .st-legal a + a::before */
.st-promo ~ .st-legal a:first-child::before {
    content: "·";
    display: inline-block;
    margin: 0 0.4em;
}
/* @relation(REQ-103, scope=range_end, role=Impl) */

/* @relation(REQ-102, scope=range_start, role=Impl) — the privacy page: plain
 * prose column, same width feel as the landing hero; main already pads the
 * sides. */
.legal-page {
    max-width: 44rem;
    margin: 2rem auto;
}
/* @relation(REQ-102, scope=range_end, role=Impl) */

/* --- landing page hero (REQ-38) -------------------------------------------------------- */

/* raised surface (REQ-44): the landing page has no other panel, so it needs
 * its own tier-2 card rather than sitting bare on the page background. */
.landing-hero {
    max-width: 34rem;
    margin: 3rem auto;
    padding: var(--st-space-l);
    background: var(--st-surface);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    box-shadow: var(--st-elev-2);
    text-align: center;
}

.landing-logo {
    width: min(70%, 20rem);
}

.landing-hero h1 {
    font-size: var(--st-text-xl);
}

/* testing-only disclaimer (REQ-56): its own callout, not intro prose, so it
 * reads as a notice. Filled with --st-orange-tint, bordered in
 * --st-orange-deep -- but the text sits on the tint, not on --st-surface, so
 * it needs --st-orange-deep-2: --st-orange-deep only reaches 4.19:1 on the
 * tint (below AA), while --st-orange-deep-2 clears 4.97:1 there. */
.landing-notice {
    margin: var(--st-space-m) 0;
    padding: var(--st-space-s) var(--st-space-m);
    border: 1px solid var(--st-orange-deep);
    border-radius: var(--st-radius-sm);
    background: var(--st-orange-tint);
    color: var(--st-orange-deep-2);
    font-size: var(--st-text-s);
    font-weight: 600;
}

.landing-intro {
    color: var(--st-ink-muted);
}

.landing-actions {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: var(--st-space-l);
    margin-top: var(--st-space-l);
}

.landing-button {
    display: inline-block;
    padding: 0.6rem 1.6rem;
    border: 1px solid var(--st-blue);
    border-radius: var(--st-radius-sm);
    background: var(--st-blue);
    color: var(--st-surface);
    font: inherit;
    font-size: var(--st-text-m);
    font-weight: 600;
    text-decoration: none;
}

a.landing-button:hover {
    border-color: var(--st-blue-dark);
    background: var(--st-blue-dark);
}

/* @relation(REQ-104, scope=range_start, role=Impl) — public-mode SEVENSTAX
 * promo: its own card below the hero (design decision 2026-08-26, preview
 * variant A1), split into a stylized website-preview window and a text
 * block. Left-aligned on purpose — a different kind of content than the
 * centered hero. flex-wrap lets the text drop under the window on narrow
 * screens. The text link takes the orange brand accent, in
 * --st-orange-deep-2 for the same AA-contrast reason the REQ-56 notice text
 * does — --st-surface is lighter than the notice tint, so the shade that
 * clears AA there clears it here too. */
.landing-promo {
    max-width: 34rem;
    margin: var(--st-space-l) auto 3rem;
    padding: var(--st-space-l);
    background: var(--st-surface);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius);
    box-shadow: var(--st-elev-2);
    display: flex;
    flex-wrap: wrap;
    gap: var(--st-space-l);
    align-items: center;
}

.promo-window {
    flex: 0 0 12rem;
    display: block;
    text-decoration: none;
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    overflow: hidden;
    box-shadow: var(--st-elev-2);
}

.promo-window-bar {
    display: flex;
    gap: 4px;
    padding: 6px 8px;
    background: var(--st-bg);
    border-bottom: 1px solid var(--st-line);
}

.promo-window-bar i {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--st-line);
}

/* A plain centered block, deliberately NOT a flex column: WebKit
 * mis-resolves a percentage-width image flex item in a column flex and
 * renders the logo vertically stretched (2.69:1 against the natural 3:1 —
 * reproduced in Playwright's WebKit after a Safari report; explicit
 * height:auto and flex:none did not cure it, dropping flex did). The
 * width/height attributes on the img are part of the same fix. */
.promo-window-body {
    display: block;
    padding: var(--st-space-m) var(--st-space-s);
    background: var(--st-surface);
    text-align: center;
}

.promo-window-body img {
    display: block;
    width: 78%;
    height: auto;
    margin: 0 auto var(--st-space-s);
}

.promo-window-body span {
    font-size: var(--st-text-xs);
    color: var(--st-ink-muted);
}

/* 1 1 16rem: grows beside the window where the card is wide enough, wraps
 * below it as a full-width row where it is not */
.promo-text {
    flex: 1 1 16rem;
}

.promo-text h2 {
    margin: 0 0 var(--st-space-xs);
    font-size: var(--st-text-l);
    color: var(--st-blue);
}

.promo-text p {
    margin: 0 0 var(--st-space-s);
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
}

.promo-text a {
    color: var(--st-orange-deep-2);
    font-weight: 600;
    font-size: var(--st-text-s);
}
/* @relation(REQ-104, scope=range_end, role=Impl) */

/* --- diagram (REQ-37) ------------------------------------------------------------------
 * Actor-domain columns with issuer edges (diagram.js). Horizontal overflow
 * stays inside the section (REQ-29: never page-level scroll); the SVG edge
 * layer shares its origin with the grid and scrolls with it. */

.view-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--st-space-s);
}

.view-toggle-label {
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
    white-space: nowrap;
}

.view-toggle-buttons {
    display: inline-flex;
    background: var(--st-surface);
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    overflow: hidden;
}

/* the inline-flex above would override the UA [hidden] rule — keep the
 * fallback-hidden toggle (renderView) actually gone */
.view-toggle[hidden] {
    display: none;
}

.view-toggle button {
    padding: 0.35rem 0.9rem;
    border: none;
    background: var(--st-surface);
    color: var(--st-blue);
    font-weight: 600;
}

.view-toggle button[aria-pressed="false"]:hover {
    background: var(--st-blue-tint);
}

.view-toggle button[aria-pressed="true"] {
    background: var(--st-blue);
    color: var(--st-surface);
}

/* padding stays 0 (REQ-44): #diagram-edges is absolutely positioned against
 * #diagram's padding box (top:0; left:0), while drawEdges() (diagram.js:203,
 * :210) computes coordinates relative to #diagram-grid's border box. Padding
 * here would offset every edge from its card; #diagram-grid gets the padding
 * instead so the two origins stay coincident. */
#diagram {
    position: relative;
    min-width: 0; /* let the grid item shrink so the overflow stays in here */
    overflow-x: auto;
    padding: 0;
}

/* same containment for the tree view — long CNs scroll here, not the page */
#tree {
    min-width: 0;
    overflow-x: auto;
    padding: var(--st-space-m);
}

/* @relation(REQ-106, scope=range_start, role=Impl) — SEVENSTAX promo strip
 * beneath the visualization panel (round-2 design decision 2026-08-26,
 * option D1): the REQ-104 preview window scaled down beside a one-liner,
 * unframed so it sits on the page background instead of adding a third
 * card. */
.viz-promo {
    display: flex;
    flex-wrap: wrap;
    gap: var(--st-space-l);
    align-items: center;
    margin-top: var(--st-space-m);
}

.viz-promo p {
    margin: 0;
    color: var(--st-ink-muted);
    font-size: var(--st-text-s);
    max-width: 34rem;
}

.viz-promo p a {
    color: var(--st-orange-deep-2);
    font-weight: 600;
}

/* the small variant of the REQ-104 preview window, with its chrome scaled
 * to match */
.promo-window-s {
    flex-basis: 9rem;
    width: 9rem;
}

.promo-window-s .promo-window-bar {
    padding: 5px 7px;
}

.promo-window-s .promo-window-bar i {
    width: 6px;
    height: 6px;
}

.promo-window-s .promo-window-body {
    padding: 0.7rem 0.5rem;
}
/* @relation(REQ-106, scope=range_end, role=Impl) */

#diagram-edges {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
}

#diagram-edges .edge {
    fill: none;
    stroke: var(--st-ink-muted);
    stroke-width: 1.5;
}

/* the cascade is the signing relation (REQ-45): mark each affected node's
 * incoming edge. Stroke width (not dash) is the non-colour cue -- a dashed
 * edge reads as "optional relation" in graph conventions and the cards
 * already carry the dash. */
#diagram-edges .edge.affected {
    stroke: var(--st-orange-deep);
    stroke-width: 2.5;
}

/* every arrowhead follows its own path's stroke, so neutral and affected
 * edges each get a matching arrow with one rule (REQ-45) */
#diagram-edges marker path {
    fill: context-stroke;
}

#diagram-grid {
    display: grid;
    grid-template-columns: minmax(8rem, 0.6fr) repeat(5, minmax(9.75rem, 1fr));
    gap: var(--st-space-l) var(--st-space-m);
    align-items: start;
    padding: var(--st-space-m);
}

/* -2's own track sizing sits above: an 8rem OCSP lane plus 5 equal 9.75rem
 * tracks. Both minima are set by the widest role label they must show without
 * truncating (REQ-77's CPO_OCSP2 at 76px in the lane, CONTRACT_LEAF at 107px in
 * the MO track) -- .node-role truncates rather than overflowing, so a track too
 * narrow by one pixel costs a whole character to an ellipsis. -20 deliberately
 * keeps 9.5rem for its chain tracks: VEHICLE_SUB_CA_2 wants 120px in a 106px
 * span and is meant to truncate, since widening six tracks to fit it would cost
 * far more scroll than the ellipsis is worth.
 *
 * -20 is 10 tracks (REQ-79): four narrow unlabelled OCSP lanes, each one
 * immediately left of the chain whose signers it holds (CSO, eMSP, OEM,
 * Vehicle), interleaved with the six chain tracks CSO/CPS/eMSP/OEM/Vehicle/PE.
 * The lane is 8rem, exactly -2's (REQ-77) -- five of the twelve -20 signer
 * labels truncate at that width and that is the user's decision (2026-08-16):
 * the card's glyph badge carries the tier digit and the title the full
 * identifier, whereas a lane wide enough for VEHICLE_OCSP3 stops reading as a
 * lane against a 9.5rem chain track. 4 x 8rem + 6 x 9.5rem = 89rem minimum, so
 * the -20 diagram scrolls horizontally further than before -- accepted with
 * REQ-79, on the same grounds as the six-track step above.
 * #app carries data-version (pki.html) and is an ancestor of #diagram-grid.
 *
 * tests/backend/test_theme_diagram_grid.py pins this rule as source text --
 * keep one declaration per line with the property token starting the line, and
 * no `}` inside the rule body. */
#app[data-version="-20"] #diagram-grid {
    grid-template-columns:
        minmax(8rem, 0.6fr) repeat(2, minmax(9.5rem, 1fr))
        minmax(8rem, 0.6fr) minmax(9.5rem, 1fr)
        minmax(8rem, 0.6fr) minmax(9.5rem, 1fr)
        minmax(8rem, 0.6fr) repeat(2, minmax(9.5rem, 1fr));
}

.diagram-col-label {
    grid-row: 1;
    text-align: center;
    font-size: var(--st-text-s);
    font-weight: 600;
    color: var(--st-ink-muted);
}

.diagram-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--st-space-s);
    min-width: 0;
}

.diagram-card {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 15rem;
    box-sizing: border-box;
    border: 1px solid var(--st-line);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    box-shadow: var(--st-elev-1);
}

/* optional per the standard (REQ-78), the diagram half of .node.optional:
 * style only. .diagram-card already carries `1px solid var(--st-line)`, so
 * setting nothing else leaves colour and background to whichever state rule
 * applies -- which is what makes an optional *and* affected card read as
 * affected (orange + tint) no matter the rule order. */
.diagram-card.optional {
    border-style: dashed;
}

/* OCSP signer (REQ-95), the diagram half of .node.ocsp: monochrome role
 * text/badge plus one step quieter card — no elevation, the fainter
 * separator for a border ("mono-soft", chosen from real-app screenshots).
 * Size, slot, background and text contrast untouched. BEFORE the state rules
 * on purpose: equal specificity, so affected/hover/selected win the border
 * and background exactly as on any other card. -2's optional dash renders in
 * this fainter grey — recorded in REQ-95's COMMENT as accepted. */
.diagram-card.ocsp {
    border-color: var(--st-line-soft);
    box-shadow: none;
}

.diagram-card.ocsp .node-role {
    color: var(--st-ink-muted);
}

/* weaker emphasis for descendants of the selection (REQ-45); elevation stays
 * at --st-elev-1 (base) -- only the selected card lifts. */
.diagram-card.affected {
    border-style: dashed;
    border-color: var(--st-orange-deep);
    background: var(--st-affect-tint);
}

.diagram-card:hover {
    border-color: var(--st-blue);
    background: var(--st-blue-tint);
}

.diagram-card.selected {
    border-color: var(--st-orange-deep);
    background: var(--st-orange-tint);
    box-shadow: var(--st-elev-2), inset 3px 0 0 var(--st-orange-deep);
}

.diagram-node {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
    padding: var(--st-space-xs) var(--st-space-s);
    border: none;
    background: transparent;
    color: var(--st-ink);
    text-align: left;
}

.diagram-node-head {
    display: flex;
    align-items: center;
    gap: var(--st-space-xs);
    min-width: 0;
}

.diagram-node-cn {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--st-text-s);
    color: var(--st-ink-muted);
}

.diagram-card .node-actions {
    justify-content: flex-end;
    margin: 0;
    padding: 0 var(--st-space-xs) var(--st-space-xs);
}

/* the "+" that creates a missing root (REQ-59) or a missing child (REQ-63):
 * same footprint as a card so it sits in its slot the same way, dashed to
 * read as "not there yet" rather than as a certificate. */
.diagram-add-root,
.diagram-add-child {
    width: 100%;
    max-width: 15rem;
    box-sizing: border-box;
    padding: var(--st-space-s);
    border: 1px dashed var(--st-line);
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-blue);
    font-size: var(--st-text-l);
    font-weight: 700;
    line-height: 1;
}

.diagram-add-root:hover,
.diagram-add-child:hover {
    border-color: var(--st-blue);
    background: var(--st-blue-tint);
}

/* --- import form (REQ-2 UI) ---------------------------------------------------------- */

.import-form {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--st-space-s);
    margin-top: var(--st-space-l);
    border-top: 1px solid var(--st-line);
    padding-top: var(--st-space-m);
    color: var(--st-ink-muted);
}

/* phone widths (REQ-41 bug): the file input's intrinsic width is ~355px, wider
 * than the overview dialog's body there -- wrapping cannot help a single
 * too-wide item (the form already wraps), so it is capped. Measured at 390px
 * this was the last thing scrolling the dialog sideways. `min-width: 0` is
 * what makes the cap bite: a flex item's automatic minimum is its min-content
 * width, which otherwise overrides max-width. The control still renders in
 * full at 337px.
 * @relation(REQ-41, scope=range_start, role=Impl) */
@media (max-width: 48rem) {
    .import-form input[type="file"] {
        max-width: 100%;
        min-width: 0;
    }
}
/* @relation(REQ-41, scope=range_end, role=Impl) */
