/* HostMGR component layer.
 *
 * Shipped as-is (no build step) and loaded AFTER the Tailwind bundle, so
 * anything here wins. Everything resolves through the tokens declared in
 * tailwind.css, which is what lets the reseller white-label layer (injected at
 * runtime, after this file) restyle the whole panel by overriding variables.
 *
 * ---------------------------------------------------------------------------
 * THE SIGNATURE: lit edges
 *
 * State is shown by a lit edge, never by flooding an area with colour. The
 * active nav item gets a 2px ember rail; a stat tile carries its status as a
 * 2px strip along its top; a focused field gets an ember underline; a hovered
 * table row lights from its left edge.
 *
 * One idea, applied everywhere. It keeps large colour fills out of a dense
 * interface, which is what makes most admin panels tiring to read, and it
 * borrows honestly from the subject: equipment tells you what it is doing with
 * a lit indicator, not by changing colour all over.
 *
 * ---------------------------------------------------------------------------
 * THE SECOND IDEA: machined surfaces
 *
 * Lit edges say what something is DOING. Nothing said what something IS. Every
 * surface was the same flat white behind the same hairline, sunk was 1.5% off
 * white and therefore invisible, and a modal, a menu and a panel were the same
 * object at different sizes. That is what made a well-behaved interface feel
 * cheap.
 *
 * So raised things are now milled rather than printed: a hairline of light
 * along the top edge, a shadow measured in single pixels, and a wash down the
 * face. Three surface steps (sunk, surface, raised) and one shadow scale carry
 * the whole product. Nothing grew: no larger type, no more padding, no less
 * information per screen.
 *
 * The frame follows from the same thought. The sidebar was already chassis and
 * the header was already exactly as tall as the sidebar's wordmark band, so the
 * header is now cut from the chassis too. Dark equipment holding a light
 * readout, which is the honest shape of the thing being operated.
 * ------------------------------------------------------------------------ */

:root {
    /* Declared unconditionally so the dark block below can ever apply. The
       document ships <meta name="color-scheme" content="light">, which pins the
       page's used colour scheme, and prefers-color-scheme resolves against the
       used scheme rather than the raw system setting. A CSS declaration on the
       root outranks the meta, which is the only lever available from a
       stylesheet. */
    color-scheme: light dark;

    /* Radii. Four steps, in one place, because the file had grown five
       different values for the same kind of corner. */
    --hm-r-sm: 0.375rem;   /* buttons, fields, menu items */
    --hm-r-md: 0.5rem;     /* tabs, search, choices */
    --hm-r-lg: 0.625rem;   /* panels, tiles, cards, popovers */
    --hm-r-xl: 0.875rem;   /* modals */

    /* Elevation. Tinted with the chassis rather than with black: a grey shadow
       under a cool white surface is the thing that reads as a stock template. */
    --hm-e1:
        0 1px 2px -1px color-mix(in oklch, var(--color-chassis) 14%, transparent),
        0 1px 3px -2px color-mix(in oklch, var(--color-chassis) 10%, transparent);
    --hm-e2:
        0 10px 24px -10px color-mix(in oklch, var(--color-chassis) 34%, transparent),
        0 2px 6px -3px color-mix(in oklch, var(--color-chassis) 18%, transparent);
    --hm-e3:
        0 24px 60px -18px color-mix(in oklch, var(--color-chassis) 45%, transparent),
        0 4px 12px -6px color-mix(in oklch, var(--color-chassis) 22%, transparent);

    /* The machined highlight: a single pixel of light along the top edge of a
       raised surface. Invisible on white by design, which is correct, since in
       light the shadow does the work and in dark the highlight does. */
    --hm-edge-light: color-mix(in oklch, white 90%, transparent);

    /* Fills that have to invert. Every one of these was a literal #fff
       somewhere in this file, and every one of them was a reason dark mode
       could not exist. */
    --hm-hover: var(--color-surface-sunk);      /* rows and quiet controls */
    --hm-wash-top: oklch(1 0 0);                /* control gradient, top stop */
    --hm-wash-bottom: var(--color-surface-sunk);
    --hm-track-off: var(--color-slate-300);     /* toggle, off */
    --hm-knob: oklch(1 0 0);                    /* toggle, the knob itself */

    /* The frame. Aliased rather than used directly so a white-label layer can
       move the chrome without moving the sign-in screen with it. */
    --hm-chrome: var(--color-chassis);
    --hm-chrome-raised: var(--color-chassis-raised);
    --hm-chrome-line: var(--color-chassis-line);

    /* The primary control, derived rather than hand-mixed.
       ---------------------------------------------------------------------
       These were raw oklch() literals, which meant the single most visible
       control in the product was the one thing a reseller overriding
       --color-brand-* could not reach. Everything is now a mix of brand-600
       with black or white, which reproduces the tuned original to within a
       thousandth of a lightness step and follows the brand ramp when it moves.

       Deliberately built on 600 and not on 700: 700 and 800 are ink, and ink
       has to lighten in dark mode. A fill built on ink would turn pale exactly
       when it needed to stay solid.

       The whole ramp also sits about seven points lower than the hand-mixed
       version it replaces, because that version put white 13px text on a
       3.8:1 fill at the top of its gradient and 4.3:1 on the destructive one.
       Both now clear 4.5:1 across every stop and every state, hover included,
       which is the reason hover brightens by a bloom in the button's own hue
       rather than by lightening the fill past the ceiling. */
    --hm-primary-top: color-mix(in oklch, var(--color-brand-600) 87%, black);
    --hm-primary-bottom: color-mix(in oklch, var(--color-brand-600) 79%, black);
    --hm-primary-top-hover: color-mix(in oklch, var(--color-brand-600) 90%, black);
    --hm-primary-bottom-hover: color-mix(in oklch, var(--color-brand-600) 82.5%, black);
    --hm-primary-pressed: color-mix(in oklch, var(--color-brand-600) 74%, black);
    --hm-primary-edge: color-mix(in oklch, var(--color-brand-600) 68%, black);

    --hm-destruct-top: color-mix(in oklch, var(--color-danger) 88%, black);
    --hm-destruct-bottom: color-mix(in oklch, var(--color-danger) 80%, black);
    --hm-destruct-top-hover: color-mix(in oklch, var(--color-danger) 92%, black);
    --hm-destruct-bottom-hover: color-mix(in oklch, var(--color-danger) 84%, black);
    --hm-destruct-pressed: color-mix(in oklch, var(--color-danger) 75%, black);
    --hm-destruct-edge: color-mix(in oklch, var(--color-danger) 69%, black);
    /* Shadows have to be translucent as well as dark, so they are mixed twice:
       once toward black for the hue, once toward transparent for the alpha. */
    --hm-destruct-shade: color-mix(in oklch,
        color-mix(in oklch, var(--color-danger) 63%, black) 55%, transparent);
    --hm-destruct-shade-deep: color-mix(in oklch,
        color-mix(in oklch, var(--color-danger) 48%, black) 50%, transparent);
}

body {
    font-feature-settings: "cv05" 1; /* Plex's single-storey g, steadier at 13px */
    -webkit-font-smoothing: antialiased;
    /* Six thousandths of an em. Not a style: Plex Sans is drawn on a generous
       sidebearing for print, and at 13px on a screen full of table rows it sets
       loose enough to look like a default. */
    letter-spacing: -0.006em;
}

/* Data is monospace everywhere: domains, paths, usernames, sizes, TTLs. True
 * to an audience that reads terminals all day, and it makes a column of values
 * scan as a column rather than as prose. */
.hm-data {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 0.8125rem;
    letter-spacing: -0.01em;
}

.hm-label {
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

/* ---------------------------------------------------------------------------
 * Focus. Visible, ember, never removed. An operator working a keyboard through
 * a long form during an incident is exactly who this is for.
 * ------------------------------------------------------------------------ */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--color-brand-500);
    outline-offset: 2px;
    border-radius: 0.25rem;
}

/* ---------------------------------------------------------------------------
 * Panels. Hairline seams, not floating shadows. Equipment has seams, and at
 * this density a page of drop-shadowed cards turns into visual noise.
 * ------------------------------------------------------------------------ */
.hm-panel {
    position: relative;
    background: var(--color-surface);
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-lg);
    overflow: hidden;
    /* Machined, not floating. The shadow is small but it has to be visible:
       with the canvas only two percent off white and the shadow at one pixel,
       a panel was indistinguishable from the page it sat on and the whole
       surface hierarchy may as well not have existed. The canvas is now four
       percent down and this lifts a little further off it. */
    box-shadow:
        0 1px 2px -1px color-mix(in oklch, var(--color-chassis) 22%, transparent),
        0 2px 6px -3px color-mix(in oklch, var(--color-chassis) 18%, transparent),
        inset 0 1px 0 0 var(--hm-edge-light);
}

/* The lip. A wash down the face rather than a flat fill, so the head reads as a
   milled step in the panel rather than as a coloured band pasted on it. */
.hm-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.625rem 1rem;
    border-bottom: 1px solid var(--color-line);
    background:
        linear-gradient(180deg,
            var(--color-surface) 0%,
            var(--color-surface-sunk) 100%);
}

.hm-panel-title {
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: -0.005em;
    color: var(--color-slate-800);
}

.hm-panel-body { padding: 1rem; }

/* Form panels: header, body, footer.
   ------------------------------------------------------------------------
   Every form in the panel takes this shape. The header says what the form is
   for, the body holds the fields, and the submit lives in a footer with a
   tinted background so the commit point is in the same place on every form
   rather than wherever the last field happened to end. */

.hm-panel-header {
    padding: 0.875rem 1.25rem;
    border-bottom: 1px solid var(--color-line);
    /* The same wash as .hm-panel-head, so a form and a listing open with the
       same lip. They were two different objects for no reason anyone could
       name. */
    background:
        linear-gradient(180deg,
            var(--color-surface) 0%,
            var(--color-surface-sunk) 100%);
}

.hm-panel-header h3,
.hm-panel-header .hm-panel-title {
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: -0.012em;
    color: var(--color-slate-900);
}

.hm-panel-header p {
    margin-top: 0.1875rem;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--color-slate-500);
}

.hm-form-body {
    padding: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 1.125rem;
}

.hm-panel-footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.875rem 1.25rem;
    border-top: 1px solid var(--color-line);
    /* Recessed, so the footer reads as the base of the panel rather than as one
       more row of content, and lit along its top edge so the seam between body
       and base is a machined joint rather than a drawn line. */
    background: var(--color-surface-sunk);
    box-shadow: inset 0 1px 0 0 var(--hm-edge-light);
    border-bottom-left-radius: var(--hm-r-lg);
    border-bottom-right-radius: var(--hm-r-lg);
}

/* A footer inside a modal follows the dialog's own rounded corners.
 *
 * It used to carry margin: 0 -1.25rem -1.25rem, from when the panel had padding
 * for the footer to bleed out through. The footer's parent is now an unpadded
 * form, so that margin did two visible things: it made the bar stick twenty
 * pixels out of each side of the dialog, and, because the bottom margin is
 * negative, it pulled the panel's own height down by twenty pixels so the
 * footer hung past the bottom edge and its buttons were cut in half.
 *
 * That was the whole bug. It survived being chased through flex-shrink,
 * min-height, max-height, overflow and scrollbar reflow because none of those
 * were ever involved. */
.hm-modal .hm-panel-footer {
    border-radius: 0 0 var(--hm-r-xl) var(--hm-r-xl);
}

@media (min-width: 640px) {
    .hm-modal .hm-panel-footer { border-radius: 0 0 var(--hm-r-xl) var(--hm-r-xl); }
}

/* Anything the footer needs on the left, such as a delete action, goes here
   and the submit stays hard right. */
.hm-panel-footer-start { margin-right: auto; }

/* A footer directly after a header paints two lines a pixel apart: the header's
 * bottom border and the footer's top border, with no body between them to
 * justify either. One panel, one divider. */
.hm-panel-header + .hm-panel-footer { border-top: 0; }


/* ---------------------------------------------------------------------------
 * Stat tiles. The lit edge at work: a 2px strip along the top carries state, so
 * a row of tiles reads from across a desk without any of them being a block of
 * colour.
 * ------------------------------------------------------------------------ */
.hm-tile {
    position: relative;
    background:
        linear-gradient(180deg,
            var(--color-surface) 55%,
            var(--color-surface-sunk) 100%);
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-lg);
    padding: 0.875rem 1rem 1rem;
    overflow: hidden;
    box-shadow: var(--hm-e1);
}

.hm-tile::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 2px;
    background: var(--color-idle);
}

.hm-tile-ok::before     { background: var(--color-ok); }
.hm-tile-warn::before   { background: var(--color-warn); }
.hm-tile-danger::before { background: var(--color-danger); }
.hm-tile-live::before   { background: var(--color-brand-500); }

.hm-tile-value {
    font-size: 1.5rem;
    font-weight: 600;
    line-height: 1.15;
    color: var(--color-slate-900);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

.hm-tile-detail { font-size: 0.75rem; color: var(--color-slate-500); margin-top: 0.25rem; }

/* ---------------------------------------------------------------------------
 * Buttons. Three weights and no more: a panel where every action looks equally
 * urgent is a panel where nothing does.
 * ------------------------------------------------------------------------ */
.hm-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    padding: 0.375rem 0.6875rem;
    border-radius: var(--hm-r-sm);
    border: 1px solid transparent;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.25rem;
    white-space: nowrap;
    cursor: pointer;
    transition:
        background-color 120ms ease,
        border-color 120ms ease,
        color 120ms ease,
        box-shadow 120ms ease,
        transform 60ms ease;
}

/* Half a pixel of travel on press. A transform cannot reflow anything, so this
   is the one way a control can physically move without the row it lives in
   moving with it. */
.hm-btn:active:not(:disabled) { transform: translateY(0.5px); }

.hm-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* 14px, not 16px. An icon set at the same size as the label competes with it;
 * set slightly smaller it reads as a mark beside the word, which is what an
 * icon in a button is for. */
/* Every icon has a size.
 *
 * An <svg> with only a viewBox has no intrinsic size, so one used outside a
 * button, inline in a table cell or a heading, rendered at the replaced
 * element default and dwarfed the text beside it. Sized in em so it tracks
 * whatever it sits in.
 *
 * Every rule below that sizes an icon for a particular control is more specific
 * than this and still wins. */
.hm-icon { width: 1.15em; height: 1.15em; flex: none; }

.hm-btn svg { width: 0.875rem; height: 0.875rem; flex: none; }

/* Primary: the one action this screen exists for. At most one per view.
 *
 * A flat, fully saturated orange rectangle was the first version and it looked
 * cheap, for a reason worth writing down: it broke this design system's own
 * rule that colour marks a lit EDGE rather than filling an area. A large vivid
 * fill also reads as a warning, because everywhere else in this panel a big
 * warm area means something is wrong.
 *
 * So the fill is deeper and less saturated than the indicator ember, and it is
 * given physical depth instead of loudness: a one-pixel light line along the
 * top edge, a darker line beneath, and a shadow tinted with the button's own
 * hue rather than grey. That is how a real illuminated control looks, and it
 * belongs to the instrument-panel direction the rest of the panel follows.
 *
 * Every value is now derived from --color-brand-600 (see the token block at the
 * top) rather than hand-mixed, so overriding the brand ramp actually reaches
 * this control. It was previously the one thing a white-label layer could not
 * touch, which is a poor thing for the most-clicked button in the product. */
.hm-btn-primary {
    background: linear-gradient(180deg,
        var(--hm-primary-top) 0%,
        var(--hm-primary-bottom) 100%);
    color: #fff;
    border-color: var(--hm-primary-edge);
    box-shadow:
        inset 0 1px 0 0 color-mix(in oklch, white 22%, transparent),
        0 1px 2px -1px color-mix(in oklch, var(--color-brand-900) 55%, transparent);
    text-shadow: 0 1px 0 color-mix(in oklch, var(--color-brand-950) 45%, transparent);
}

/* Hover adds a low bloom in the button's own hue. A lit control that is about
 * to be pressed should look like it is putting out light, not like it has been
 * repainted. */
.hm-btn-primary:hover {
    background: linear-gradient(180deg,
        var(--hm-primary-top-hover) 0%,
        var(--hm-primary-bottom-hover) 100%);
    border-color: color-mix(in oklch, var(--color-brand-600) 72%, black);
    box-shadow:
        inset 0 1px 0 0 color-mix(in oklch, white 26%, transparent),
        0 1px 2px -1px color-mix(in oklch, var(--color-brand-900) 55%, transparent),
        0 3px 12px -5px color-mix(in oklch, var(--color-brand-500) 65%, transparent);
}

/* Pressed loses the top highlight and gains an inner shadow, so the control
 * reads as pushed in rather than merely darker. */
.hm-btn-primary:active {
    background: var(--hm-primary-pressed);
    box-shadow: inset 0 1px 3px 0 color-mix(in oklch, var(--color-brand-950) 55%, transparent);
}

.hm-btn-secondary {
    background: linear-gradient(180deg, var(--hm-wash-top) 0%, var(--hm-wash-bottom) 100%);
    border-color: var(--color-line-strong);
    color: var(--color-slate-700);
    box-shadow:
        var(--hm-e1),
        inset 0 1px 0 0 var(--hm-edge-light);
}
.hm-btn-secondary:hover {
    background: linear-gradient(180deg, var(--hm-wash-bottom) 0%, var(--color-slate-100) 100%);
    border-color: var(--color-slate-400);
    color: var(--color-slate-900);
}
.hm-btn-secondary:active {
    background: var(--color-slate-100);
    box-shadow: inset 0 1px 2px 0 color-mix(in oklch, var(--color-chassis) 16%, transparent);
}

/* Quiet: row-level actions. Lighter than secondary, but still a button. A
 * transparent control that only reveals itself on hover reads as text, and a
 * row action nobody recognises as clickable may as well not be there.
 *
 * The border is on the resting state rather than appearing on hover, and the
 * base .hm-btn already reserves 1px of transparent border, so nothing shifts. */
.hm-btn-quiet {
    background: var(--color-surface);
    border-color: var(--color-line);
    color: var(--color-slate-700);
    padding-inline: 0.5rem;
}
.hm-btn-quiet:hover {
    background: var(--hm-hover);
    border-color: var(--color-slate-400);
    color: var(--color-slate-900);
}
.hm-btn-quiet:active { background: var(--color-slate-100); }

/* Destructive: red only on hover. A permanently red button in a row of actions
 * pulls the eye straight to the one thing nobody should click by accident. */
.hm-btn-danger { background: transparent; color: var(--color-danger); padding-inline: 0.5rem; }
.hm-btn-danger:hover { background: color-mix(in oklch, var(--color-danger) 10%, transparent); }

/* Confirmed destruction inside a modal, where it IS the primary action. */
/* Confirmed destruction, given the same physicality as the primary so the two
 * read as the same class of control rather than two different design eras. */
.hm-btn-danger-solid {
    background: linear-gradient(180deg,
        var(--hm-destruct-top) 0%,
        var(--hm-destruct-bottom) 100%);
    color: #fff;
    border-color: var(--hm-destruct-edge);
    box-shadow:
        inset 0 1px 0 0 color-mix(in oklch, white 20%, transparent),
        0 1px 2px -1px var(--hm-destruct-shade);
    text-shadow: 0 1px 0 var(--hm-destruct-shade-deep);
}

.hm-btn-danger-solid:hover {
    background: linear-gradient(180deg,
        var(--hm-destruct-top-hover) 0%,
        var(--hm-destruct-bottom-hover) 100%);
}

.hm-btn-danger-solid:active {
    background: var(--hm-destruct-pressed);
    box-shadow: inset 0 1px 3px 0 var(--hm-destruct-shade-deep);
}

.hm-btn-sm { padding: 0.3125rem 0.5rem; font-size: 0.75rem; }

/* ---------------------------------------------------------------------------
 * Icon-only row actions.
 *
 * A row of text buttons ("Pause" "Delete" "Renew") is a wall of words that gives
 * the eye nothing to land on, and the words get longer in every translation. An
 * icon with a tooltip is scannable and a fixed width.
 *
 * The border is transparent rather than absent so that showing it on hover does
 * not change the button's size and shove the rest of the row sideways.
 */
.hm-btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.875rem;
    height: 1.875rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 0.375rem;
    background: transparent;
    color: var(--color-slate-600);
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.hm-btn-icon:hover {
    background: var(--hm-hover);
    border-color: var(--color-slate-400);
    color: var(--color-slate-900);
}
.hm-btn-icon:focus-visible {
    outline: 2px solid var(--color-brand-600);
    outline-offset: 1px;
}
.hm-btn-icon svg { width: 1rem; height: 1rem; }

/* Destructive stays neutral until hover, same reasoning as .hm-btn-danger. */
.hm-btn-icon-danger:hover {
    background: color-mix(in oklch, var(--color-danger) 10%, transparent);
    border-color: color-mix(in oklch, var(--color-danger) 40%, transparent);
    color: var(--color-danger);
}

.hm-btn-icon[disabled] { opacity: 0.4; cursor: not-allowed; }

/* ---------------------------------------------------------------------------
 * Table toolbar: filter box and the "showing x of y" counter.
 */
.hm-tabletools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}
.hm-tablesearch {
    position: relative;
    flex: 1 1 16rem;
    max-width: 22rem;
}
.hm-tablesearch input {
    width: 100%;
    padding: 0.375rem 0.5rem 0.375rem 2rem;
    font-size: 0.8125rem;
    border: 1px solid var(--color-line);
    border-radius: 0.375rem;
    background: var(--color-surface);
    color: var(--color-slate-900);
}
.hm-tablesearch input:focus {
    outline: none;
    border-color: var(--color-brand-600);
}
.hm-tablesearch svg {
    position: absolute;
    left: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    width: 0.9375rem;
    height: 0.9375rem;
    color: var(--color-slate-500);
    pointer-events: none;
}
.hm-filtercount {
    font-size: 0.75rem;
    color: var(--color-slate-600);
    white-space: nowrap;
}
/* Only meaningful once a filter is active; hidden otherwise so an unfiltered
 * table does not carry a redundant "showing 12 of 12". */
.hm-filtercount:not([data-filtering]) { display: none; }

/* A filtered-away row. Kept in the DOM rather than removed so the selection
 * state survives clearing the box. */
.hm-row[data-filtered] { display: none; }

.hm-filterempty {
    padding: 1.25rem 0.75rem;
    text-align: center;
    font-size: 0.8125rem;
    color: var(--color-slate-600);
}

/* ---------------------------------------------------------------------------
 * Fields. The lit edge again: focus thickens the bottom border to ember rather
 * than drawing a ring around the whole control.
 * ------------------------------------------------------------------------ */
.hm-field {
    width: 100%;
    background: var(--color-surface);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--hm-r-sm);
    padding: 0.4375rem 0.625rem;
    font-size: 0.8125rem;
    color: var(--color-slate-900);
    /* A recess, not a rectangle. One pixel of shadow under the top edge is the
       difference between a field you can type into and an outlined div. */
    box-shadow: inset 0 1px 1.5px -1px color-mix(in oklch, var(--color-chassis) 30%, transparent);
    transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}

/* Placeholder text is still text somebody has to read. 400 on white is
   2.63:1; 500 is 4.77:1 and still reads as a placeholder next to the
   slate-900 of an actual value. */
.hm-field::placeholder { color: var(--color-slate-500); }

/* There was no hover state at all, so a form of eleven fields gave no feedback
   until something was clicked. */
.hm-field:hover:not(:focus):not(:disabled) { border-color: var(--color-slate-400); }

.hm-field:focus {
    outline: none;
    border-color: var(--color-brand-500);
    box-shadow: inset 0 -2px 0 0 var(--color-brand-500);
}

.hm-field:disabled { background: var(--color-surface-sunk); color: var(--color-slate-500); cursor: not-allowed; }

/* A select carries the browser's own arrow inside its right padding, and at
   0.625rem the longest option's text runs straight into it. The extra room is
   on the right only, so the text still lines up with the inputs above and
   below it in the same form. */
select.hm-field { padding-right: 2rem; }

select.hm-field-sm { padding-right: 1.75rem; }

.hm-field-data { font-family: var(--font-mono); }

/* The field that pairs with .hm-btn-sm, so a control and its button match when
   they sit in the same table row. */
.hm-field-sm { padding: 0.3125rem 0.5rem; font-size: 0.75rem; }

.hm-hint { font-size: 0.75rem; color: var(--color-slate-500); margin-top: 0.375rem; line-height: 1.45; }

.hm-field-label {
    display: block;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--color-slate-700);
    margin-bottom: 0.3125rem;
}

/* ---------------------------------------------------------------------------
 * Navigation drawer and rail.
 *
 * The breakpoint behaviour lives entirely in CSS. Below lg the sidebar is
 * off-canvas and JavaScript adds .hm-drawer-open; from lg up it is a static
 * column and the JavaScript state is ignored completely. Resizing a window
 * while the drawer is open therefore cannot strand the desktop layout in a
 * state only JavaScript knows how to undo.
 * ------------------------------------------------------------------------ */
.hm-drawer {
    position: fixed;
    inset-block: 0;
    left: 0;
    z-index: 40;
    width: 15.5rem;
    background: var(--hm-chrome);
    /* The seam. There was none: the dark column met the light workspace with
       nothing between them, which is the one place a frame most needs an edge.
       Border-box sizing means this costs no width. */
    border-right: 1px solid var(--hm-chrome-line);
    transform: translateX(-100%);
    transition: transform 200ms cubic-bezier(0.32, 0.72, 0, 1);
}

.hm-drawer.hm-drawer-open { transform: translateX(0); }

@media (min-width: 1024px) {
    .hm-drawer { position: static; z-index: auto; flex: none; transform: none; }
}

.hm-nav {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin-inline: 0.5rem;
    padding: 0.4375rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    color: var(--color-chassis-text);
    transition: background-color 120ms ease, color 120ms ease;
}

.hm-nav svg {
    width: 1.0625rem;
    height: 1.0625rem;
    flex: none;
    opacity: 0.7;
    transition: opacity 120ms ease, color 120ms ease;
}

.hm-nav:hover { background: var(--color-chassis-raised); color: #fff; }
.hm-nav:hover svg { opacity: 1; }

.hm-nav-active { background: var(--color-chassis-raised); color: #fff; font-weight: 500; }
.hm-nav-active svg { opacity: 1; color: var(--color-brand-500); }

/* The rail: 2px ember, inset from the rounded corners so it reads as an
 * indicator rather than as a border. */
.hm-nav-active::before {
    content: "";
    position: absolute;
    left: -0.5rem;
    top: 0.3125rem;
    bottom: 0.3125rem;
    width: 2px;
    border-radius: 0 2px 2px 0;
    background: var(--color-brand-500);
    /* The rail finally behaves like a light source rather than like a coloured
       rectangle. Two pixels of ember with a short bloom is what the signature
       has been describing all along. */
    box-shadow: 0 0 8px 0 color-mix(in oklch, var(--color-brand-500) 55%, transparent);
}

/* Section headings encode something true: the split between what belongs to one
 * customer and what belongs to the whole server. Not decorative grouping.
 *
 * With six groups rather than two the old 18px gap above each heading spent
 * more than a hundred pixels of a sidebar on air. The gap is tighter and the
 * separation is carried by a hairline instead, which is both smaller and
 * clearer about where one section ends. */
.hm-nav-section + .hm-nav-section {
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid color-mix(in oklch, var(--hm-chrome-line) 60%, transparent);
}

.hm-nav-group {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0.625rem 0.5rem 0.25rem;
    padding: 0.25rem 0.5rem;
    border-radius: 0.3125rem;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--color-chassis-dim);
    cursor: pointer;
    user-select: none;
    /* The default triangle is replaced by the caret below, and it renders on
       the wrong side at the wrong size on every browser. */
    list-style: none;
}

.hm-nav-group::-webkit-details-marker { display: none; }

.hm-nav-group:hover { color: #fff; background: var(--color-chassis-raised); }

/* An open section's heading is a shade brighter than a closed one, so the shape
   of the menu is readable without opening anything. */
.hm-nav-section[open] > .hm-nav-group { color: var(--color-chassis-text); }

/* A collapsed section still owns the page you are on. Without this the only
   indication of where you are disappears the moment its group is closed. */
.hm-nav-section:not([open]):has(.hm-nav-active) > .hm-nav-group { color: var(--color-brand-400); }

.hm-nav-caret {
    display: block;
    width: 0.75rem;
    height: 0.75rem;
    flex: none;
    opacity: 0.6;
    transition: transform 140ms ease;
}

.hm-nav-caret svg { width: 100%; height: 100%; }

/* Points down when open, right when closed, which is the direction people
   expect from a disclosure. */
.hm-nav-section:not([open]) .hm-nav-caret { transform: rotate(-90deg); }

/* The filter searches the whole menu, so a collapsed group must not hide a
   match. panel.js opens every section while a filter is active. */
.hm-nav-section[data-filtering] .hm-nav-items { display: block; }

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

/* ---------------------------------------------------------------------------
 * Status dots and chips. Shared everywhere, so a green dot means the same thing
 * on every screen. Never colour alone: every chip carries a word, because
 * colour-only status fails for the ~8% of men with a colour vision deficiency
 * and this audience is overwhelmingly men.
 * ------------------------------------------------------------------------ */
.hm-dot {
    display: inline-block;
    width: 0.4375rem;
    height: 0.4375rem;
    border-radius: 9999px;
    flex: none;
}

.hm-dot-ok     { background: var(--color-ok);     box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-ok) 18%, transparent); }
.hm-dot-warn   { background: var(--color-warn);   box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-warn) 22%, transparent); }
.hm-dot-danger { background: var(--color-danger); box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-danger) 18%, transparent); }
.hm-dot-idle   { background: var(--color-idle);   box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-idle) 15%, transparent); }
.hm-dot-live   { background: var(--color-brand-500); animation: hm-pulse 1.8s ease-in-out infinite; }

/* Motion in this interface always means "something is happening right now". */
@keyframes hm-pulse {
    0%, 100% { box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-brand-500) 22%, transparent); }
    50%      { box-shadow: 0 0 0 5px   color-mix(in oklch, var(--color-brand-500) 8%, transparent); }
}

.hm-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.1875rem 0.5rem;
    border-radius: 0.3125rem;
    border: 1px solid transparent;
    font-size: 0.6875rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
    white-space: nowrap;
}

/* The tint is mixed into whatever the surface happens to be rather than into
   white, and the ink is a token. Those two changes are the whole reason a chip
   needs no second definition in dark mode. */
.hm-chip-ok     { background: color-mix(in oklch, var(--color-ok) 10%, var(--color-surface));      color: var(--color-ok-ink);     border-color: color-mix(in oklch, var(--color-ok) 25%, transparent); }
.hm-chip-warn   { background: color-mix(in oklch, var(--color-warn) 15%, var(--color-surface));    color: var(--color-warn-ink);   border-color: color-mix(in oklch, var(--color-warn) 32%, transparent); }
.hm-chip-danger { background: color-mix(in oklch, var(--color-danger) 8%, var(--color-surface));   color: var(--color-danger-ink); border-color: color-mix(in oklch, var(--color-danger) 22%, transparent); }
.hm-chip-idle   { background: var(--color-slate-100); color: var(--color-slate-600); border-color: var(--color-slate-200); }
.hm-chip-live   { background: color-mix(in oklch, var(--color-brand-500) 10%, var(--color-surface)); color: var(--color-brand-800); border-color: color-mix(in oklch, var(--color-brand-500) 28%, transparent); }

/* ---------------------------------------------------------------------------
 * Tables. Flush inside the panel, hairline rows, and a hover that lights the
 * row from its left edge rather than tinting the whole thing.
 * ------------------------------------------------------------------------ */
.hm-table {
    width: 100%;
    font-size: 0.8125rem;
    border-collapse: collapse;
    /* Sizes, counts, ports and TTLs read down a column rather than jittering
       row to row. The single highest-value line in this section. */
    font-variant-numeric: tabular-nums;
}

.hm-table thead th {
    padding: 0.5rem 1rem;
    text-align: left;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-slate-500);
    background:
        linear-gradient(180deg,
            var(--color-surface) 0%,
            var(--color-surface-sunk) 100%);
    border-bottom: 1px solid var(--color-line);
}

.hm-table tbody td { padding: 0.625rem 1rem; border-bottom: 1px solid var(--color-line); vertical-align: middle; }
.hm-table tbody tr:last-child td { border-bottom: 0; }

.hm-row { position: relative; transition: background-color 120ms ease; }
.hm-row:hover { background: var(--hm-hover); }

.hm-row::after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--color-brand-500);
    opacity: 0;
    transition: opacity 120ms ease;
}

.hm-row:hover::after { opacity: 1; }

/* Keyboard equivalence: tabbing into a row's controls lights the same edge the
   pointer would, so an operator working without a mouse is never guessing which
   row is about to be acted on. */
.hm-row:focus-within { background: var(--hm-hover); }
.hm-row:focus-within::after { opacity: 1; }

/* A transparent border in the base state so a hover edge never reflows. */
.hm-hoverable { border: 1px solid transparent; transition: border-color 120ms ease, background-color 120ms ease; }
.hm-hoverable:hover { border-color: var(--color-line-strong); background: var(--hm-hover); }

/* ---------------------------------------------------------------------------
 * Empty states. An empty screen is an invitation to act, so it names the step.
 * ------------------------------------------------------------------------ */
.hm-empty { padding: 3rem 1.5rem; text-align: center; }

/* A decorative icon above the message, if a caller supplies one.
 *
 * Direct child only. As a descendant selector this also caught the icon inside
 * an empty state's own call-to-action button, sizing it 2rem and giving it auto
 * side margins, which turned "Create The First Account" into an enormous
 * mis-shapen block. EmptyState renders no icon of its own, so the rule was
 * styling nothing it was written for and only doing damage. */
.hm-empty > svg {
    width: 2rem;
    height: 2rem;
    margin: 0 auto 0.75rem;
    color: var(--color-slate-300);
}

.hm-empty-title { font-size: 0.875rem; font-weight: 500; color: var(--color-slate-700); }
.hm-empty-body  { font-size: 0.8125rem; color: var(--color-slate-500); margin-top: 0.25rem; }

/* ---------------------------------------------------------------------------
 * Banners
 * ------------------------------------------------------------------------ */
.hm-banner {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    padding: 0.625rem 1rem;
    border-bottom: 1px solid;
    font-size: 0.8125rem;
}

.hm-banner-ok     { background: color-mix(in oklch, var(--color-ok) 8%, var(--color-surface));     border-color: color-mix(in oklch, var(--color-ok) 22%, transparent);     color: var(--color-ok-ink); }
.hm-banner-warn   { background: color-mix(in oklch, var(--color-warn) 14%, var(--color-surface));  border-color: color-mix(in oklch, var(--color-warn) 32%, transparent);   color: var(--color-warn-ink); }
.hm-banner-danger { background: color-mix(in oklch, var(--color-danger) 7%, var(--color-surface)); border-color: color-mix(in oklch, var(--color-danger) 22%, transparent); color: var(--color-danger-ink); }

/* ---------------------------------------------------------------------------
 * Tooltips. Fixed elements on <body>, positioned in JS. Deliberately NOT
 * ::after pseudo-elements: a tooltip on a control inside a table cell, a panel
 * with overflow-hidden, or a scrolling log pane gets clipped by the ancestor,
 * and most of this panel's controls live in exactly those places.
 * ------------------------------------------------------------------------ */
.hm-tooltip {
    position: fixed;
    z-index: 9999;
    max-width: 20rem;
    padding: 0.375rem 0.5rem;
    border-radius: var(--hm-r-sm);
    background: var(--color-chassis);
    border: 1px solid var(--color-chassis-line);
    color: oklch(0.95 0.005 260);
    font-size: 0.75rem;
    line-height: 1.1rem;
    pointer-events: none;
    opacity: 0;
    transform: translateY(2px);
    transition: opacity 120ms ease, transform 120ms ease;
    box-shadow: var(--hm-e2);
}

.hm-tooltip[data-show] { opacity: 1; transform: translateY(0); }

/* ---------------------------------------------------------------------------
 * Modals. Hidden by the plain HTML `hidden` attribute, not by a class or a
 * marker a script has to remove. `display: none` from `hidden` loses to the
 * `flex` utility on the same element, so it is restated with the specificity to
 * win. Getting this wrong once left dialogs covering the whole panel.
 * ------------------------------------------------------------------------ */
.hm-modal[hidden] { display: none !important; }

/* Two pixels of blur, not eight. Enough that the page behind is clearly out of
   reach, little enough that an operator can still read the row they are about
   to delete. A full-bleed rectangle with no radius, so it does not meet the
   WebKit bug where a blurred element loses its rounded corners. */
.hm-modal-backdrop { backdrop-filter: blur(2px); }

/* Centred on every screen size.
 *
 * The markup carries "items-end sm:items-center", which made a dialog slide up
 * from the bottom edge on a phone. That is a common pattern but the wrong one
 * here: these are short confirmations and two-field forms, not sheets of
 * content, and pinned to the bottom edge they sit under the thumb and read as
 * something that half-appeared. Overridden here rather than in a dozen
 * templates so it cannot drift apart again. */
/* The dialog's container scrolls; the dialog itself is never height-limited.
 *
 * This was a flex container centring a panel that carried max-height and its
 * own overflow, and it clipped the footer by exactly twenty pixels at every
 * window size. Measured in a real browser: the same panel cloned into a plain
 * block container laid out at its full 341px and fitted, while inside the flex
 * container it was given 321px and its buttons hung past the bottom edge. No
 * combination of flex-shrink, min-height, align-items or max-height changed
 * that; only leaving the flex context did.
 *
 * So the panel is a plain block that is always exactly as tall as its content,
 * and when a dialog is genuinely taller than a short window this container
 * scrolls to reach it. Nothing anywhere in a dialog now has a height limit, so
 * there is nothing left that can cut a footer off.
 *
 * The cost is that a dialog sits near the top rather than being vertically
 * centred. Centring is what the flex container was for, and it is not worth a
 * submit button nobody can reach. */
div.hm-modal {
    /* Centred, and that is all it does. The container does not scroll, so it
       has no scrollbar to take fifteen pixels off one side and push the dialog
       off the window's centre line. */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

.hm-modal-panel {
    position: relative;
    width: 100%;
    /* Rounded on every side, because it is no longer meeting an edge. */
    border-radius: var(--hm-r-xl);
    background: var(--color-surface-raised);
    box-shadow: var(--hm-e3), inset 0 1px 0 0 var(--hm-edge-light);
    animation: hm-modal-in 180ms cubic-bezier(0.32, 0.72, 0, 1);


    /* Bounded by the window and scrolls its own content when a dialog is
       taller than that. Safe now that the footer no longer carries a negative
       bottom margin: that margin was subtracting twenty pixels from this
       panel's height, which is what pushed the buttons past its bottom edge. */
    max-height: calc(100dvh - 2rem);
    overflow-y: auto;
    overflow-x: hidden;
}

/* Nothing inside a dialog may force it wider than it is.
 *
 * A grid or flex child defaults to min-width:auto, which refuses to shrink
 * below its content: one long unbroken string, a wide input or a monospace
 * path then pushes the whole dialog out. These are the elements that carry
 * such content. */
.hm-modal-panel .hm-panel-body > *,
.hm-modal-panel .hm-field,
.hm-modal-panel pre,
.hm-modal-panel textarea {
    min-width: 0;
    max-width: 100%;
}

/* A path, a URL or a key has no spaces to break at, so it has to be told it
   may break anywhere rather than being allowed to set the dialog's width. */
.hm-modal-panel .hm-data,
.hm-modal-panel pre {
    overflow-wrap: anywhere;
}


/* Full-width stacked buttons on a phone: a row of three at the bottom of a
 * sheet gives each one a tap target narrower than a thumb. */
@media (max-width: 639px) {
    .hm-modal .hm-panel-footer {
        flex-direction: column-reverse;
        align-items: stretch;
        gap: 0.5rem;
    }
    .hm-modal .hm-panel-footer .hm-btn {
        width: 100%;
        justify-content: center;
        min-height: 2.75rem;
    }
}

@media (min-width: 640px) {
    .hm-modal-panel {
        /* 34rem, not 28. A dialog of paired fields, a path or a repository URL
           had to wrap at 28rem, and every wrap is another line of height in a
           box that is already fighting for it. Wider means shorter. */
        max-width: 34rem;
        border-radius: var(--hm-r-xl);

    }
}

@keyframes hm-modal-in {
    from { opacity: 0; transform: translateY(6px) scale(0.99); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* ---------------------------------------------------------------------------
 * HTMX feedback. A panel action can take 30 seconds (issuing a certificate,
 * pulling an image, restoring a snapshot), so the in-flight state must be
 * obvious without moving anything: opacity and cursor only.
 * ------------------------------------------------------------------------ */
.htmx-request, .htmx-request.hm-busy { opacity: 0.55; cursor: progress; pointer-events: none; }

/* A panel that is working says so along its own top edge.
 *
 * The signature used as a progress indicator: the same two pixels of ember that
 * mark the active nav item, sweeping. Nothing moves, nothing resizes and no
 * spinner is inserted into the layout, which matters because these requests
 * genuinely take thirty seconds and the operator is looking at the panel that
 * is doing the work, not at a button somewhere else on the page.
 *
 * The bar exists in the resting state and only the sweep is motion, so with
 * reduced motion the panel still shows a lit edge rather than nothing at all. */
.hm-panel.htmx-request::after,
.hm-panel.hm-busy::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 2px;
    z-index: 1;
    background-image: linear-gradient(90deg,
        transparent 0%,
        var(--color-brand-500) 50%,
        transparent 100%);
    background-size: 45% 100%;
    background-repeat: no-repeat;
    background-position: 50% 0;
}

@media (prefers-reduced-motion: no-preference) {
    .hm-panel.htmx-request::after,
    .hm-panel.hm-busy::after { animation: hm-sweep 1.1s ease-in-out infinite; }
}

@keyframes hm-sweep {
    from { background-position: -45% 0; }
    to   { background-position: 145% 0; }
}

.hm-indicator { display: none; }
.htmx-request .hm-indicator, .htmx-request.hm-indicator { display: inline-flex; }

.hm-progress { height: 3px; border-radius: 9999px; background: var(--color-slate-100); overflow: hidden; }
.hm-progress-bar { height: 100%; background: var(--color-brand-500); transition: width 300ms ease; }

/* ---------------------------------------------------------------------------
 * Terminal, editor, logs, charts
 * ------------------------------------------------------------------------ */
.hm-terminal { height: 100%; min-height: 24rem; padding: 0.5rem; border-radius: 0.5rem; background: var(--color-chassis); }

.hm-terminal .xterm-viewport {
    scrollbar-width: thin;
    /* Chromium ignores the shorthand unless both stops are given. */
    scrollbar-color: var(--color-chassis-line) transparent;
}

.hm-editor .cm-editor { height: 100%; font-family: var(--font-mono); font-size: 0.8125rem; }
.hm-editor .cm-editor.cm-focused { outline: none; }

.hm-logtail {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1.15rem;
    white-space: pre-wrap;
    word-break: break-word;
    overflow-anchor: none; /* our own autoscroll, not the browser's */
}

.hm-logtail .lvl-error { color: var(--color-danger); }
.hm-logtail .lvl-warn  { color: var(--color-warn); }

.hm-chart .u-legend { font-size: 0.75rem; }
.hm-chart .u-select { background: color-mix(in oklch, var(--color-brand-500) 15%, transparent); }

/* ---------------------------------------------------------------------------
 * Touch. Operators reach for a panel from a phone when something is already
 * broken, so controls need to be hittable with a thumb. 44px is the platform
 * minimum on both iOS and Android.
 * ------------------------------------------------------------------------ */
@media (max-width: 640px) {
    .hm-btn, button, [role="button"] { min-height: 2.5rem; }
    .hm-nav { min-height: 2.75rem; }

    /* Hover tooltips are meaningless on touch and can stick open after a tap,
     * covering the control that was just pressed. */
    .hm-tooltip { display: none; }

    /* iOS zooms when a focused input is under 16px and never zooms back out.
     * Every field here is 13px by design, so the floor is set here rather than
     * fought with a viewport meta that would also disable pinch zoom. */
    input[type="text"], input[type="email"], input[type="password"],
    input[type="number"], input[type="search"], select, textarea, .hm-field {
        font-size: 16px;
    }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ---------------------------------------------------------------------------
 * Print. Invoices, account summaries and audit exports get printed.
 * ------------------------------------------------------------------------ */
@media print {
    /* The header joined the chassis, so it is now a solid dark bar across the
       top of every printed page and has to come off with the sidebar. */
    .hm-no-print, aside, header, .hm-tooltip, .hm-modal { display: none !important; }
    .hm-panel { break-inside: avoid; border: 1px solid #ddd; box-shadow: none; }
    .hm-panel-head, .hm-panel-header, .hm-table thead th { background: none; }
}

/* ---------------------------------------------------------------------------
 * Fields on the chassis
 *
 * Only the sign-in screen sits on the dark background. It is the one moment in
 * the product with no data to show, so it is the one place the brand gets to
 * speak; these are the field styles for that context.
 * ------------------------------------------------------------------------ */
.hm-field-dark {
    width: 100%;
    background: color-mix(in oklch, var(--color-chassis-raised) 70%, black);
    border: 1px solid var(--color-chassis-line);
    border-radius: 0.375rem;
    padding: 0.5rem 0.6875rem;
    font-size: 0.875rem;
    color: #fff;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}

.hm-field-dark::placeholder { color: var(--color-chassis-dim); }

.hm-field-dark:focus {
    outline: none;
    border-color: var(--color-brand-500);
    box-shadow: inset 0 -2px 0 0 var(--color-brand-500);
}

/* The brand dot without the pulse: a lit indicator that is simply on. Used by
 * the wordmark, where constant motion would be noise rather than information. */
.hm-dot-brand {
    background: var(--color-brand-500);
    box-shadow: 0 0 0 2.5px color-mix(in oklch, var(--color-brand-500) 22%, transparent);
}

/* ---------------------------------------------------------------------------
 * Sign-in
 *
 * The one screen with no data to show, and the only one that may show none:
 * nothing about this server, not the hostname, not service status, not even the
 * build version, may appear before someone authenticates. Anything shown here
 * is shown to the whole internet. So the left half is identity rather than
 * telemetry, and it is the one place the product gets to state what it is.
 * ------------------------------------------------------------------------ */

.hm-signin {
    position: relative;
    min-height: 100vh;
    min-height: 100dvh;
    background: var(--color-chassis);
    overflow: hidden;
}

/* Perforation. The material of the subject's world: a rack faceplate is a
 * punched metal sheet. Kept at an opacity where you register the texture
 * without being able to point at it, because a dark half-screen with nothing on
 * it is what made the old sign-in feel like a placeholder. */
.hm-signin::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image: radial-gradient(
        circle at center,
        color-mix(in oklch, white 6%, transparent) 1px,
        transparent 1px
    );
    background-size: 22px 22px;
    mask-image: radial-gradient(ellipse 90% 70% at 30% 40%, black, transparent 75%);
    pointer-events: none;
}

/* The seam. The signature made atmospheric: light escaping the top edge of the
 * sign-in plate, the same ember that marks the active nav item and the running
 * job. One effect on this screen, not a set of them. */
.hm-signin-glow {
    position: absolute;
    top: -14rem;
    left: 50%;
    width: min(46rem, 120vw);
    height: 28rem;
    transform: translateX(-50%);
    background: radial-gradient(
        ellipse at center,
        color-mix(in oklch, var(--color-brand-500) 22%, transparent) 0%,
        transparent 68%
    );
    pointer-events: none;
}

@media (min-width: 1024px) {
    .hm-signin-glow { left: auto; right: -6rem; transform: none; }
}

/* The plate the form sits on. An ember rail across its top edge, matching the
 * rail on the active nav item. */
.hm-plate {
    position: relative;
    background: color-mix(in oklch, var(--color-chassis-raised) 62%, black);
    border: 1px solid var(--color-chassis-line);
    border-radius: 0.625rem;
    padding: 1.75rem;
}

.hm-plate::before {
    content: "";
    position: absolute;
    top: -1px;
    left: 1.25rem;
    right: 1.25rem;
    height: 2px;
    border-radius: 0 0 2px 2px;
    background: linear-gradient(
        90deg,
        transparent,
        var(--color-brand-500) 20%,
        var(--color-brand-400) 50%,
        var(--color-brand-500) 80%,
        transparent
    );
}

/* The statement. Type carries this screen, so it is set tight and large and
 * given room, rather than being a subtitle under a logo. */
.hm-signin-eyebrow {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-brand-400);
}

.hm-signin-display {
    font-size: clamp(1.75rem, 4.2vw, 2.625rem);
    font-weight: 600;
    line-height: 1.12;
    letter-spacing: -0.03em;
    color: #fff;
    text-wrap: balance;
}

/* What the panel manages, as a hairline-separated list in mono. Honest and
 * specific: these are the real modules, not a marketing list, and nothing here
 * reveals anything about the machine itself. */
.hm-signin-list { border-top: 1px solid var(--color-chassis-line); }

.hm-signin-list li {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0;
    border-bottom: 1px solid var(--color-chassis-line);
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    color: var(--color-chassis-text);
}

.hm-signin-list li svg {
    width: 1rem;
    height: 1rem;
    flex: none;
    color: var(--color-brand-500);
    opacity: 0.8;
}

/* ---------------------------------------------------------------------------
 * Bordered destructive button
 *
 * .hm-btn-danger is deliberately borderless: in a table of twenty rows, a
 * bordered button per row turns the page into a wall of boxes, so row actions
 * read as quiet text until hovered.
 *
 * That is the wrong call where the action IS the point of the screen. Stopping
 * nginx is not an incidental row action, and a control that looks like a link
 * is one people either miss or click by accident. This variant is for those:
 * an unmistakable button that still reads as dangerous.
 * ------------------------------------------------------------------------ */
.hm-btn-danger-outline {
    background: var(--color-surface);
    border-color: color-mix(in oklch, var(--color-danger) 35%, transparent);
    color: var(--color-danger);
}

.hm-btn-danger-outline:hover {
    background: color-mix(in oklch, var(--color-danger) 8%, var(--color-surface));
    border-color: color-mix(in oklch, var(--color-danger) 55%, transparent);
}

/* A group of related controls, joined so they read as one set of choices for
 * one thing rather than three unrelated buttons that happen to be adjacent. */
.hm-btn-group {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}

/* Icon-only, for controls whose meaning is unambiguous and whose label would
 * only add width. Always carries a tooltip and an aria-label. */
.hm-btn-icon {
    padding: 0.4375rem;
    gap: 0;
}

.hm-btn-icon svg { width: 1.0625rem; height: 1.0625rem; }

/* ---------------------------------------------------------------------------
 * Toggle switches
 *
 * A real <input type="checkbox"> underneath, styled as a switch. Not a div with
 * a click handler: the native input keeps keyboard focus, space to toggle, form
 * submission and screen reader semantics for free, and it still works if the
 * panel's JavaScript never loads.
 *
 * The knob is drawn with ::before on the track rather than as a second element,
 * so the markup stays one input and one label.
 * ------------------------------------------------------------------------ */
.hm-toggle {
    display: inline-flex;
    align-items: flex-start;
    gap: 0.625rem;
    cursor: pointer;
}

.hm-toggle input {
    /* Kept in the layout rather than display:none, so it remains focusable and
     * announced. Opacity zero and a zero-size box put it under the track. */
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    margin: 0;
}

.hm-toggle-track {
    position: relative;
    flex: none;
    width: 2.25rem;
    height: 1.25rem;
    margin-top: 0.0625rem;
    border-radius: 9999px;
    background: var(--hm-track-off);
    box-shadow: inset 0 1px 2px -1px color-mix(in oklch, var(--color-chassis) 45%, transparent);
    transition: background-color 150ms ease;
}

.hm-toggle-track::before {
    content: "";
    position: absolute;
    top: 0.1875rem;
    left: 0.1875rem;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 9999px;
    background: var(--hm-knob);
    box-shadow: 0 1px 2px color-mix(in oklch, var(--color-chassis) 30%, transparent);
    transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
}

.hm-toggle input:checked + .hm-toggle-track {
    background: var(--color-brand-500);
}

.hm-toggle input:checked + .hm-toggle-track::before {
    transform: translateX(1rem);
}

/* Row selection.
 *
 * The same switch, without a label and a size down, so a column of them in a
 * table reads as a column of controls rather than a column of text. Selection
 * is a state the row is in, which is what a switch says and what a tick box
 * ("I agree") does not.
 */
.hm-toggle-select {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    vertical-align: middle;
}

/* The real checkbox, hidden under the track.
 *
 * This has to be repeated for the select variant rather than inherited: the
 * rule above is scoped to .hm-toggle, and a row switch carries .hm-toggle-select
 * on its own. Without it the browser draws its native tick box beside the track,
 * which is exactly the "checkbox next to a toggle" this class exists to replace.
 *
 * Kept in the layout rather than display:none, so it stays focusable and is
 * still announced by a screen reader.
 */
.hm-toggle-select input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    margin: 0;
    pointer-events: none;
}
.hm-toggle-select .hm-toggle-track {
    width: 1.875rem;
    height: 1.0625rem;
    margin-top: 0;
}
.hm-toggle-select .hm-toggle-track::before {
    top: 0.1875rem;
    left: 0.1875rem;
    width: 0.6875rem;
    height: 0.6875rem;
}
.hm-toggle-select input:checked + .hm-toggle-track::before {
    transform: translateX(0.8125rem);
}
/* Focus has to be visible on the track, because the input itself is a 1px box
 * parked underneath it. */
.hm-toggle-select input:focus-visible + .hm-toggle-track {
    outline: 2px solid var(--color-brand-600);
    outline-offset: 2px;
}
/* Partially selected: neither on nor off, so the track shows a midpoint rather
 * than claiming either. */
.hm-toggle-select input:indeterminate + .hm-toggle-track {
    background: color-mix(in oklch, var(--color-brand-500) 45%, var(--hm-track-off));
}
.hm-toggle-select input:indeterminate + .hm-toggle-track::before {
    transform: translateX(0.40625rem);
}

/* Focus lands on the track, since the input itself is visually hidden. */
.hm-toggle input:focus-visible + .hm-toggle-track {
    outline: 2px solid var(--color-brand-500);
    outline-offset: 2px;
}

.hm-toggle input:disabled + .hm-toggle-track {
    opacity: 0.5;
    cursor: not-allowed;
}

/* A destructive toggle reads red when on, because "force kill" being armed
 * should not look the same as "read only" being armed. */
.hm-toggle-danger input:checked + .hm-toggle-track {
    background: var(--color-danger);
}

.hm-toggle-label {
    font-size: 0.8125rem;
    color: var(--color-slate-700);
    line-height: 1.25rem;
}

.hm-toggle-hint {
    display: block;
    font-size: 0.75rem;
    color: var(--color-slate-500);
    margin-top: 0.125rem;
}

/* Type picker.
   ------------------------------------------------------------------------
   A choice between kinds of thing, not a dropdown of values. Cards make the
   options and their consequences visible at once, which matters here because
   the type decides which fields below are even relevant. The selected state
   uses the same lit edge as every other stateful surface in the panel rather
   than filling the card with brand colour. */

.hm-typegrid {
    display: grid;
    gap: 0.625rem;
    grid-template-columns: 1fr;
}

@media (min-width: 40rem) {
    .hm-typegrid { grid-template-columns: repeat(3, 1fr); }
}

.hm-typecard {
    position: relative;
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
        "icon label"
        "desc desc";
    align-items: center;
    gap: 0.125rem 0.5rem;
    padding: 0.8125rem 0.875rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-lg);
    background: var(--color-surface);
    cursor: pointer;
    /* Only the fast properties. Animating the border box here would make the
       whole grid reflow on every hover. */
    transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}

.hm-typecard:hover { border-color: var(--color-brand-300); }

/* The radio itself stays in the accessibility tree and stays focusable. It is
   moved out of sight rather than display:none, which would remove it from the
   tab order and break keyboard selection entirely. */
.hm-typecard-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    margin: 0;
    pointer-events: none;
}

.hm-typecard:has(.hm-typecard-input:checked) {
    border-color: var(--color-brand-400);
    background: color-mix(in oklch, var(--color-brand-500) 4%, var(--color-surface));
    box-shadow: inset 0 2px 0 -1px var(--color-brand-500);
}

.hm-typecard:has(.hm-typecard-input:focus-visible) {
    outline: 2px solid var(--color-brand-500);
    outline-offset: 2px;
}

.hm-typecard-icon {
    grid-area: icon;
    display: inline-flex;
    color: var(--color-slate-400);
    transition: color 120ms ease;
}

.hm-typecard-icon svg { width: 1rem; height: 1rem; }

.hm-typecard:has(.hm-typecard-input:checked) .hm-typecard-icon {
    color: var(--color-brand-600);
}

.hm-typecard-label {
    grid-area: label;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--color-slate-900);
}

.hm-typecard-desc {
    grid-area: desc;
    margin-top: 0.25rem;
    font-size: 0.75rem;
    line-height: 1.45;
    color: var(--color-slate-500);
}

/* Field groups that belong to one type only. Hidden by the attribute so a
   page with JavaScript disabled shows every field rather than none: the
   server clears whatever does not apply to the chosen type anyway. */
[data-typeonly][hidden],
[data-roleonly][hidden] { display: none; }

/* Tabs.
   ------------------------------------------------------------------------
   A segmented control: a sunk track holding pills, with the active one raised
   out of it on a lit surface. A lone underline is quiet enough to be missed on
   a dense settings page, and this reads as a control you operate rather than a
   caption you read.

   The track is inline-flex so it hugs its tabs instead of drawing a rule the
   full width of the page, which is what made the old underline look like a
   section divider rather than a switch. */

.hm-tabs {
    display: inline-flex;
    gap: 0.125rem;
    max-width: 100%;
    padding: 0.1875rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md);
    background: var(--color-surface-sunk);
    box-shadow: inset 0 1px 2px -1px color-mix(in oklch, var(--color-chassis) 35%, transparent);
    /* Wrapped rather than scrolled.
     *
     * This was overflow-x: auto with the scrollbar hidden, which on a desktop
     * meant a site with fourteen tabs simply cut the last few off: no scrollbar
     * to see, and horizontal scrolling with a mouse wheel is a gesture nobody
     * discovers. Wrapping shows every tab at the cost of a second row, which is
     * the right trade for a strip that is the page's navigation.
     *
     * The phone form is a drawer instead, below, because fourteen tabs wrap to
     * five rows on a narrow screen and push the content off the first screenful.
     */
    flex-wrap: wrap;
}

.hm-tabs::-webkit-scrollbar { display: none; }

.hm-tab svg { width: 0.875rem; height: 0.875rem; flex: none; }

/* ---------------------------------------------------------------------------
 * The tab drawer: the mobile form of a tab strip.
 *
 * Sixteen tabs cannot sit in a row on a phone. Scrolling hides most of them
 * behind a gesture nobody is told about, a select turns a visible set of
 * choices into a hidden one, and a grid of chips takes most of the first
 * screenful before any content appears. A drawer costs one tap and gives the
 * full list room to be readable.
 *
 * The button carries the current tab's name, so the closed state still answers
 * "which tab am I on".
 * ------------------------------------------------------------------------ */
.hm-tabdrawer-btn { display: none; }

.hm-tabdrawer,
.hm-tabdrawer-backdrop { display: none; }

@media (max-width: 767px) {
    .hm-tabs { display: none; }

    .hm-tabdrawer-btn {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        width: 100%;
        min-height: 2.75rem;
        padding: 0.5rem 0.75rem;
        font-size: 0.9375rem;
        font-weight: 600;
        color: var(--color-slate-900);
        background: var(--color-surface);
        border: 1px solid var(--color-line);
        border-radius: var(--hm-r-md);
    }

    .hm-tabdrawer-btn svg { width: 1.125rem; height: 1.125rem; color: var(--color-slate-500); }

    .hm-tabdrawer-backdrop:not([hidden]) {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 45;
        background: color-mix(in oklch, var(--color-chassis) 55%, transparent);
    }

    .hm-tabdrawer:not([hidden]) {
        display: flex;
        flex-direction: column;
        position: fixed;
        inset-block: 0;
        left: 0;
        z-index: 46;
        width: 17rem;
        max-width: 85vw;
        padding: 0.75rem;
        overflow-y: auto;
        background: var(--color-surface-raised);
        border-right: 1px solid var(--color-line);
        box-shadow: var(--hm-e3);
        animation: hm-tabdrawer-in 200ms cubic-bezier(0.32, 0.72, 0, 1);
        /* Clear of the notch and the home indicator. */
        padding-top: max(0.75rem, env(safe-area-inset-top));
        padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
    }

    .hm-tabdrawer-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0.25rem 0.25rem 0.75rem;
        font-size: 0.75rem;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--color-slate-500);
        border-bottom: 1px solid var(--color-line);
        margin-bottom: 0.5rem;
    }

    .hm-tabdrawer-item {
        display: flex;
        align-items: center;
        gap: 0.625rem;
        min-height: 2.75rem;
        padding: 0.5rem 0.625rem;
        border-radius: var(--hm-r-md);
        font-size: 0.9375rem;
        color: var(--color-slate-700);
    }

    .hm-tabdrawer-item svg { width: 1.125rem; height: 1.125rem; flex: none; color: var(--color-slate-500); }

    .hm-tabdrawer-item:active { background: var(--hm-hover); }

    .hm-tabdrawer-active {
        background: var(--color-brand-50, var(--hm-hover));
        color: var(--color-brand-700);
        font-weight: 600;
    }

    .hm-tabdrawer-active svg { color: var(--color-brand-600); }
}

@keyframes hm-tabdrawer-in {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
}


.hm-tab {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.375rem 0.75rem;
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--color-slate-500);
    white-space: nowrap;
    /* Reserved so the active pill's border never changes the pill's size and
       the row does not shift as tabs are switched. */
    border: 1px solid transparent;
    transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
}

.hm-tab:hover {
    color: var(--color-slate-900);
    background: color-mix(in oklch, var(--color-surface) 70%, transparent);
    border-color: var(--color-line);
}

/* Raised: a lit surface, a real border and a soft drop shadow, so the active
   pill sits above the track rather than being tinted inside it.

   The brand appears only here, and only as a hairline along the top edge:
   enough to say which one is live without turning the control orange.

   Both states are set in one rule because the hover selector is the more
   specific of the two. Split across two rules, the shorthand in the hover rule
   resets the background-image and the hairline disappears exactly when the
   pointer is on it. */
.hm-tab-active,
.hm-tab-active:hover {
    color: var(--color-brand-700);
    background-color: var(--color-surface);
    background-image: linear-gradient(
        to bottom,
        color-mix(in oklch, var(--color-brand-500) 55%, transparent) 0 2px,
        transparent 2px
    );
    border-color: var(--color-line-strong);
    box-shadow:
        var(--hm-e1),
        inset 0 1px 0 0 var(--hm-edge-light);
}

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

/* Notification bell.
   ------------------------------------------------------------------------
   The count is the reason this exists: a backup that stopped running is
   silent by nature, so the badge is the only thing that makes it visible on
   a page the operator was already looking at. */

.hm-bell {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 0.375rem;
    color: var(--color-slate-500);
    border: 1px solid transparent;
    transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
}

.hm-bell:hover {
    color: var(--color-slate-900);
    background: var(--hm-hover);
    border-color: var(--color-line);
}

.hm-bell svg { width: 1.0625rem; height: 1.0625rem; }

.hm-bell-count {
    position: absolute;
    top: -0.1875rem;
    right: -0.1875rem;
    min-width: 1rem;
    height: 1rem;
    padding: 0 0.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.625rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: #fff;
    /* Danger red rather than the brand colour. An unread alert is not a
       decoration, and the panel already reads a warm area as a problem. */
    background: var(--color-danger);
    border-radius: 999px;
    box-shadow: 0 0 0 2px var(--color-surface);
}

/* Page furniture.
   ------------------------------------------------------------------------
   One definition for the block that opens every page. It was hand written
   per module until now, in four variants, and the drift showed. */

.hm-page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.hm-page-title {
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--color-slate-900);
    /* A long site name must truncate rather than push the actions off the
       edge of a narrow screen. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hm-page-desc {
    font-size: 0.875rem;
    line-height: 1.45;
    color: var(--color-slate-500);
}

.hm-page-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    /* Never squeezed: the primary action stays a full-size target however
       long the title is. */
    flex: none;
}

/* Global search.
   ------------------------------------------------------------------------
   An inline dropdown under the header field rather than a full-screen
   palette: the page behind stays visible, which matters when you are
   searching for something to compare against what you are already looking
   at. */

.hm-search {
    position: relative;
    display: none;
    align-items: center;
    gap: 0.5rem;
    flex: 1 1 auto;
    max-width: 26rem;
    padding: 0 0.625rem;
    height: 2rem;
    background: var(--color-surface-sunk);
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md);
    transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
}

/* Hidden on a phone, where the header has no room for it and the sidebar
   filter is the more useful of the two. */
@media (min-width: 48rem) {
    .hm-search { display: flex; }
}

.hm-search:focus-within {
    border-color: var(--color-brand-400);
    background: var(--color-surface);
}

.hm-search svg {
    width: 0.9375rem;
    height: 0.9375rem;
    color: var(--color-slate-400);
    flex: none;
}

.hm-search input {
    flex: 1;
    min-width: 0;
    border: 0;
    background: none;
    font-size: 0.8125rem;
    color: var(--color-slate-800);
    outline: none;
}

.hm-search input::placeholder { color: var(--color-slate-500); }

/* The browser's own clear button duplicates Escape and sits at a different
   size in every engine. */
.hm-search input::-webkit-search-cancel-button { display: none; }

.hm-search-results {
    position: absolute;
    top: calc(100% + 0.375rem);
    left: 0;
    right: 0;
    max-height: 24rem;
    overflow-y: auto;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--hm-r-lg);
    box-shadow: var(--hm-e2), inset 0 1px 0 0 var(--hm-edge-light);
    padding: 0.25rem;
    z-index: 60;
}

.hm-search-results[hidden] { display: none; }

/* slate-500 rather than 400: on white, 400 is 2.63:1, which fails AA and is
   below even the 3:1 floor for large text. 500 is 4.77:1. */
.hm-search-group {
    padding: 0.4375rem 0.625rem 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

.hm-search-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4375rem 0.625rem;
    border-radius: 0.375rem;
    color: var(--color-slate-700);
}

/* One highlight style for hover and keyboard, so arrowing down looks the
   same as pointing at it. */
.hm-search-item:hover,
.hm-search-item.hm-search-active {
    background: color-mix(in oklch, var(--color-brand-500) 8%, transparent);
    color: var(--color-slate-900);
}

.hm-search-icon { display: inline-flex; color: var(--color-slate-400); flex: none; }
.hm-search-icon svg { width: 0.875rem; height: 0.875rem; }

.hm-search-label {
    display: block;
    font-size: 0.8125rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hm-search-detail {
    display: block;
    font-size: 0.6875rem;
    color: var(--color-slate-500);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hm-search-empty {
    padding: 0.75rem 0.625rem;
    font-size: 0.8125rem;
    color: var(--color-slate-500);
}

/* Sidebar filter.
   ------------------------------------------------------------------------
   Narrows the menu and nothing else. A box in a sidebar that searched
   customer data would be a box doing something other than what its position
   implies. */

.hm-navfilter { padding: 0 0.75rem 0.5rem; }

.hm-navfilter input {
    width: 100%;
    padding: 0.3125rem 0.5rem;
    font-size: 0.75rem;
    color: #fff;
    background: color-mix(in oklch, white 8%, transparent);
    border: 1px solid color-mix(in oklch, white 12%, transparent);
    border-radius: 0.375rem;
    outline: none;
}

.hm-navfilter input::placeholder { color: color-mix(in oklch, white 40%, transparent); }
.hm-navfilter input:focus { border-color: var(--color-brand-400); }
.hm-navfilter input::-webkit-search-cancel-button { display: none; }

/* A group heading whose every entry is filtered out would otherwise sit
   there labelling nothing. */
.hm-nav[hidden], .hm-nav-group[hidden] { display: none; }

/* Tables on small screens.
   ------------------------------------------------------------------------
   Below this width a row becomes a card and each cell gets its column name
   above it. Scrolling a table sideways on a phone means seeing three of six
   columns and dragging the rest into view one row at a time, which is a
   compromise rather than a design.

   The stacking is CSS so it works with JavaScript off; the labels are added
   by panel.js on top. Without them you still get cards and still no
   horizontal scroll. */

/* 47.9375rem is 767px, which tiles exactly against the 48rem rules below.
   47.99rem leaves a fractional band where neither matches and a label renders
   inline, unstyled. */
@media (max-width: 47.9375rem) {
    /* The wrapper must stop scrolling, or the card it lives in keeps its
       overflow and the whole point is lost. */
    .hm-panel .overflow-x-auto { overflow-x: visible; }

    .hm-table,
    .hm-table tbody,
    .hm-table tr,
    .hm-table td {
        display: block;
        width: 100%;
    }

    /* The header row is what the labels are built from, so it is hidden
       rather than removed. */
    .hm-table thead { display: none; }

    .hm-table tr {
        padding: 0.75rem 1rem;
        border-bottom: 1px solid var(--color-line);
    }

    .hm-table tr:last-child { border-bottom: 0; }

    .hm-table td {
        padding: 0.25rem 0;
        border: 0;
        /* A long domain, container image or connection string wraps instead
           of forcing the page wider. */
        overflow-wrap: anywhere;
    }

    /* An empty cell would otherwise leave a gap that reads as a missing
       value rather than an absent one. */
    .hm-table td:empty { display: none; }

    .hm-cell-label {
        display: block;
        font-size: 0.6875rem;
        font-weight: 600;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--color-slate-500);
        margin-bottom: 0.0625rem;
    }

    /* Actions sit as a row of buttons at the foot of the card. They carry no
       label: a row of buttons under the word "Actions" is noise. */
    .hm-table td.text-right {
        text-align: left;
        margin-top: 0.5rem;
    }

    .hm-table td.text-right .flex { justify-content: flex-start; }
}

/* Above the breakpoint the label is never shown, whatever the JS stamped. */
@media (min-width: 48rem) {
    .hm-cell-label { display: none; }
}

/* Jails.
   ------------------------------------------------------------------------
   Each jail is its own form, so they need a separator: without one the fields
   of one jail and the heading of the next read as a single block, and an
   operator saves the wrong service's settings. */
.hm-jail + .hm-jail {
    padding-top: 1rem;
    border-top: 1px solid var(--color-line);
}

/* Choices.
   ------------------------------------------------------------------------
   A radio with a title and an explanation, for decisions where the options
   differ in ways a one-word label cannot carry. The whole row is the target,
   so the explanation is clickable rather than being text beside a small
   circle. */
.hm-choice {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.75rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md);
    background: var(--color-surface);
    cursor: pointer;
    transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
}

.hm-choice:hover { border-color: var(--color-slate-400); background: var(--hm-hover); }

/* The lit edge, as everywhere else: the selected option is marked by its border,
   a faint tint and two pixels of ember down its leading edge, which is the same
   way a hovered table row announces itself. It is a row, so it lights from the
   side rather than along the top the way a type card does. */
.hm-choice:has(input:checked) {
    border-color: var(--color-brand-500);
    background: color-mix(in oklch, var(--color-brand-500) 4%, var(--color-surface));
    box-shadow: inset 2px 0 0 0 var(--color-brand-500);
}

.hm-choice input { margin-top: 0.1875rem; accent-color: var(--color-brand-500); flex: none; }

.hm-choice-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--color-slate-900);
}

.hm-choice-detail {
    display: block;
    margin-top: 0.1875rem;
    font-size: 0.75rem;
    line-height: 1.5;
    color: var(--color-slate-500);
}

/* The account menu.
   ------------------------------------------------------------------------
   A details element, so it opens with no script at all. Everything below is
   presentation; panel.js adds only what markup cannot do, which is closing when
   the pointer goes elsewhere. */
.hm-menu { position: relative; }

.hm-menu-trigger {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.5rem 0.25rem 0.25rem;
    border: 1px solid transparent;
    border-radius: 999px;
    cursor: pointer;
    user-select: none;
    list-style: none;
    transition: background-color 120ms ease, border-color 120ms ease;
}

.hm-menu-trigger::-webkit-details-marker { display: none; }

.hm-menu-trigger:hover { background: var(--hm-hover); border-color: var(--color-line); }
.hm-menu[open] .hm-menu-trigger { background: var(--hm-hover); border-color: var(--color-line-strong); }

/* The one place the brand appears as a filled shape rather than as an edge, and
   it is allowed because it is 26px across and it stands for a person rather
   than for a state. */
.hm-avatar {
    display: grid;
    place-items: center;
    width: 1.625rem;
    height: 1.625rem;
    flex: none;
    border-radius: 999px;
    background: linear-gradient(180deg, var(--color-brand-400), var(--color-brand-600));
    color: #fff;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    box-shadow:
        inset 0 1px 0 0 color-mix(in oklch, white 35%, transparent),
        0 1px 2px -1px color-mix(in oklch, var(--color-brand-900) 60%, transparent);
}

.hm-menu-caret { display: block; width: 0.75rem; height: 0.75rem; opacity: 0.5; flex: none; transition: transform 140ms ease; }
.hm-menu-caret svg { width: 100%; height: 100%; }
.hm-menu[open] .hm-menu-caret { transform: rotate(180deg); opacity: 0.8; }

.hm-menu-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 40;
    min-width: 13rem;
    padding: 0.3125rem;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--hm-r-lg);
    box-shadow: var(--hm-e2), inset 0 1px 0 0 var(--hm-edge-light);
    transform-origin: top right;
}

/* details renders its contents the instant it opens, so the panel can be given
   an entrance without a single line of script. */
@media (prefers-reduced-motion: no-preference) {
    .hm-menu[open] .hm-menu-panel { animation: hm-pop 120ms cubic-bezier(0.32, 0.72, 0, 1); }
}

@keyframes hm-pop {
    from { opacity: 0; transform: translateY(-4px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.hm-menu-head {
    padding: 0.5rem 0.625rem 0.625rem;
    border-bottom: 1px solid var(--color-line);
    margin-bottom: 0.3125rem;
}

.hm-menu-name { font-size: 0.8125rem; font-weight: 600; color: var(--color-slate-900); }
.hm-menu-sub  { font-size: 0.75rem; color: var(--color-slate-500); margin-top: 0.0625rem; }

.hm-menu-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.4375rem 0.625rem;
    border: 0;
    border-radius: var(--hm-r-sm);
    background: transparent;
    font-size: 0.8125rem;
    color: var(--color-slate-700);
    text-align: left;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease;
}

.hm-menu-item svg { width: 0.875rem; height: 0.875rem; flex: none; opacity: 0.7; }
.hm-menu-item:hover { background: var(--hm-hover); color: var(--color-slate-900); }
.hm-menu-item:hover svg { opacity: 1; }

.hm-menu-foot { border-top: 1px solid var(--color-line); margin-top: 0.3125rem; padding-top: 0.3125rem; }
.hm-menu-danger { color: var(--color-danger); }
.hm-menu-danger:hover { background: color-mix(in oklch, var(--color-danger) 10%, transparent); color: var(--color-danger); }

/* Cards.
   ------------------------------------------------------------------------
   .hm-card was a class with no rules behind it: five places in the product
   hand-built a panel out of utilities and each one drifted a little further
   from .hm-panel. It is the same object, so it gets the same surface, the same
   corner and the same single pixel of shadow. Declared here, after the Tailwind
   bundle, it also overrides the bg-white and rounded-xl still sitting in that
   markup, which is what stops those five from being the only white rectangles
   left in dark mode. */
.hm-card {
    background: var(--color-surface);
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-lg);
    box-shadow: var(--hm-e1), inset 0 1px 0 0 var(--hm-edge-light);
}

/* ---------------------------------------------------------------------------
 * THE CHROME
 *
 * The frame around the workspace, and the one deliberate risk in this file.
 *
 * The sidebar was already chassis. The header was white, sitting on an almost
 * white page, so the product had no frame at all and read as a document rather
 * than as an instrument. The sidebar's wordmark band is already exactly as tall
 * as the header, so cutting the header from the same material costs nothing and
 * closes the corner: one continuous L of dark equipment holding a light readout.
 *
 * The selector is header.bg-surface, which matches exactly one element in the
 * product. Everything below is scoped inside it, so the blast radius is 56px of
 * one page and any module that grows its own <header> is untouched.
 *
 * The account menu's panel is deliberately NOT restyled here: it drops out of
 * the dark bar onto the light workspace, which is where it belongs and is what
 * every operating system does with a menu bar.
 * ------------------------------------------------------------------------ */
header.bg-surface {
    background: var(--hm-chrome);
    border-color: var(--hm-chrome-line);
    color: var(--color-chassis-text);
    /* The machined seam, matching the panels. */
    box-shadow: inset 0 -1px 0 0 color-mix(in oklch, white 5%, transparent);
}

header.bg-surface h1 {
    color: #fff;
    letter-spacing: -0.012em;
}

/* The page name is the brightest thing in the bar; everything else is support. */
header.bg-surface .text-slate-800,
header.bg-surface .text-slate-700,
header.bg-surface .text-slate-600,
header.bg-surface .text-slate-500 { color: var(--color-chassis-text); }

header.bg-surface button[data-drawer-open] { color: var(--color-chassis-dim); }

header.bg-surface button[data-drawer-open]:hover {
    color: #fff;
    background: var(--hm-chrome-raised);
}

/* Search, as a command bar rather than as a form field. Sunk into the chassis,
   with the same ember underline on focus that every light field in the product
   uses, so the signature survives the change of material. */
header.bg-surface .hm-search {
    /* Milled into the bar rather than sitting on it, so it stays darker than
       the chrome in both modes and lights up on focus. */
    background: color-mix(in oklch, var(--hm-chrome) 88%, black);
    border-color: var(--hm-chrome-line);
    box-shadow: inset 0 1px 2px -1px color-mix(in oklch, black 55%, transparent);
}

header.bg-surface .hm-search:focus-within {
    background: var(--hm-chrome-raised);
    border-color: var(--color-brand-500);
    box-shadow: inset 0 -2px 0 0 var(--color-brand-500);
}

header.bg-surface .hm-search svg { color: var(--color-chassis-dim); }
header.bg-surface .hm-search input { color: #fff; }
header.bg-surface .hm-search input::placeholder { color: var(--color-chassis-dim); }

header.bg-surface .hm-bell { color: var(--color-chassis-dim); }

header.bg-surface .hm-bell:hover {
    color: #fff;
    background: var(--hm-chrome-raised);
    border-color: var(--hm-chrome-line);
}

/* The unread badge's halo is a cutout of whatever it sits on, so it has to
   become chassis or it draws a white ring in a dark bar. */
header.bg-surface .hm-bell-count { box-shadow: 0 0 0 2px var(--hm-chrome); }

header.bg-surface .hm-hoverable:hover {
    background: var(--hm-chrome-raised);
    border-color: var(--hm-chrome-line);
}

header.bg-surface .hm-menu-trigger { color: var(--color-chassis-text); }

header.bg-surface .hm-menu-trigger:hover,
header.bg-surface .hm-menu[open] .hm-menu-trigger {
    color: #fff;
    background: var(--hm-chrome-raised);
    border-color: var(--hm-chrome-line);
}

/* A hairline of light around the avatar, which is otherwise a saturated shape
   floating on a dark field with nothing to seat it. */
header.bg-surface .hm-avatar {
    box-shadow:
        inset 0 1px 0 0 color-mix(in oklch, white 35%, transparent),
        0 0 0 1px color-mix(in oklch, white 12%, transparent);
}

/* ---------------------------------------------------------------------------
 * DARK
 *
 * One block, and it is mostly variables.
 *
 * The panel's markup leans hard on Tailwind colour utilities, several hundred
 * of them, and every one compiles to color: var(--color-slate-N). This file is
 * unlayered and loads after the bundle, so a :root override here outranks the
 * theme layer unconditionally. Inverting the neutral ramp therefore restyles
 * this stylesheet and all of the markup at the same time, which is the only way
 * a complete dark mode was reachable without touching a single template.
 *
 * The ramp is inverted for CONTRAST, not mirrored. Light slate is a background
 * and dark slate is text; flipping the numbers literally would put 4:1 text on
 * half the panel. The light end lands at L 0.25 to 0.36 (fills and borders) and
 * the dark end at L 0.80 to 0.96 (text), each checked against the surface it is
 * actually used on.
 *
 * Scoped to screen so that printing an invoice or an audit export from a
 * machine set to dark still produces black ink on white paper.
 * ------------------------------------------------------------------------ */
@media screen and (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        /* The neutral ramp. Fills and borders at the low end, text at the high
           end, with a gap in the middle because nothing legitimately needs a
           mid grey on either surface. */
        --color-slate-50:  oklch(0.255 0.011 264);
        --color-slate-100: oklch(0.300 0.012 264);
        --color-slate-200: oklch(0.355 0.013 264);
        --color-slate-300: oklch(0.620 0.016 264);
        --color-slate-400: oklch(0.665 0.017 264);
        --color-slate-500: oklch(0.720 0.017 264);
        --color-slate-600: oklch(0.800 0.014 264);
        --color-slate-700: oklch(0.865 0.011 264);
        --color-slate-800: oklch(0.920 0.008 264);
        --color-slate-900: oklch(0.960 0.005 264);
        --color-slate-950: oklch(0.985 0.003 264);

        /* Four surface steps, deepest first: the chrome frames the page field,
           the page field holds panels, panels hold popovers. */
        --color-chassis: oklch(0.165 0.012 264);
        --color-chassis-raised: oklch(0.232 0.013 264);
        --color-chassis-line: oklch(0.300 0.013 264);
        --color-chassis-text: oklch(0.760 0.015 260);
        --color-chassis-dim: oklch(0.600 0.015 260);

        --color-surface-sunk: oklch(0.190 0.011 264);
        --color-surface: oklch(0.228 0.012 264);
        --color-surface-raised: oklch(0.262 0.013 264);
        --color-line: oklch(0.312 0.012 264);
        --color-line-strong: oklch(0.375 0.013 264);

        /* Status, lifted. A 0.63 red is a fine dot and a failing text colour, and
           --color-danger is both. Everything here stays on its own hue. */
        --color-ok: oklch(0.760 0.170 152);
        --color-warn: oklch(0.830 0.150 92);
        --color-danger: oklch(0.680 0.210 25);
        --color-idle: oklch(0.640 0.020 264);

        --color-ok-ink: oklch(0.870 0.140 152);
        --color-warn-ink: oklch(0.890 0.130 92);
        --color-danger-ink: oklch(0.810 0.140 25);

        /* Only the ink end of the brand ramp moves. 600 and below are fills:
           the primary button, the avatar, the rails and the focus ring are all
           built from them and must stay solid. */
        --color-brand-700: oklch(0.800 0.150 60);
        --color-brand-800: oklch(0.860 0.110 68);

        /* Now that there is a dark field behind them, the highlight does the
           work the shadow was doing, and the shadows themselves have to be
           deeper to register at all. */
        --hm-edge-light: color-mix(in oklch, white 7%, transparent);
        --hm-e1:
            0 1px 2px -1px color-mix(in oklch, black 60%, transparent),
            0 1px 3px -2px color-mix(in oklch, black 45%, transparent);
        --hm-e2:
            0 10px 24px -10px color-mix(in oklch, black 75%, transparent),
            0 2px 6px -3px color-mix(in oklch, black 55%, transparent);
        --hm-e3:
            0 24px 60px -18px color-mix(in oklch, black 85%, transparent),
            0 4px 12px -6px color-mix(in oklch, black 60%, transparent);

        /* Hover lightens in dark and darkens in light, which is the one place
           the two modes are not the same instruction. */
        --hm-hover: oklch(0.268 0.013 264);
        --hm-wash-top: oklch(0.268 0.013 264);
        --hm-wash-bottom: oklch(0.228 0.012 264);
        --hm-track-off: oklch(0.330 0.013 264);
        --hm-knob: oklch(0.930 0.005 264);
    }

    /* Pressed has to read as darker than resting, and in dark the slate-100 the
       light theme presses to is lighter than the button. */
    :root:not([data-theme="light"]) .hm-btn-secondary:active { background: var(--color-surface-sunk); }

    /* Exception 1. Fourteen install-instruction blocks are bg-chassis with
       text-slate-100 on them. Slate-100 is a fill in dark, so the code inside
       them would be near black on near black. */
    .bg-chassis.text-slate-100,
    .bg-chassis.text-slate-200,
    :root:not([data-theme="light"]) .bg-chassis.text-slate-300 { color: oklch(0.865 0.010 264); }

    /* Exception 2. The remaining literal bg-white in markup: the .hm-card rule
       above already covers five of six, and the sixth is the two-factor QR
       plate, which carries its own white rect inside the SVG and therefore
       stays scannable on a dark plate. */
    :root:not([data-theme="light"]) .bg-white { background-color: var(--color-surface); }

    /* Exception 4. Both of these are defined as "darker than the page", which is
       the one description that does not survive the page becoming dark. A
       tooltip has to float above the panel, so it becomes the raised step. */
    :root:not([data-theme="light"]) .hm-tooltip {
        background: var(--color-surface-raised);
        border-color: var(--color-line-strong);
        color: var(--color-slate-900);
    }

    /* The editor. CodeMirror injects its own stylesheet at the TOP of head
       precisely so author CSS can override it, so equal specificity is enough,
       and these are deliberately one step more specific than that. */
    :root:not([data-theme="light"]) .hm-editor .cm-editor { background: var(--color-surface); color: var(--color-slate-800); }
    :root:not([data-theme="light"]) .hm-editor .cm-editor .cm-content { caret-color: var(--color-brand-500); }
    :root:not([data-theme="light"]) .hm-editor .cm-editor .cm-cursor { border-left-color: var(--color-slate-900); }
    :root:not([data-theme="light"]) .hm-editor .cm-editor .cm-gutters {
        background: var(--color-surface-sunk);
        color: var(--color-slate-400);
        border-right-color: var(--color-line);
    }
    :root:not([data-theme="light"]) .hm-editor .cm-editor .cm-activeLine { background: color-mix(in oklch, var(--color-brand-500) 7%, transparent); }
    :root:not([data-theme="light"]) .hm-editor .cm-editor .cm-activeLineGutter { background: color-mix(in oklch, var(--color-brand-500) 10%, transparent); }
    .hm-editor .cm-editor .cm-selectionBackground,
    .hm-editor .cm-editor.cm-focused .cm-selectionBackground,
    :root:not([data-theme="light"]) .hm-editor .cm-editor ::selection { background: color-mix(in oklch, var(--color-brand-500) 28%, transparent); }
}

/* The same theme, chosen rather than inherited.
   ------------------------------------------------------------------------
   The query above applies when the operating system asks for dark. These rules
   apply when the reader asked for it, which is also the default: theme.go
   renders data-theme on the html element from a cookie, because a strict CSP
   forbids the inline script this would normally need and a deferred one would
   flash the wrong theme on every load.

   Duplicated rather than folded into one comma separated selector list with the
   query above, because a list is discarded whole if a browser fails to parse any
   part of it, and that would take the entire theme with it. */

    :root[data-theme="dark"] {
        color-scheme: dark;

        /* The neutral ramp. Fills and borders at the low end, text at the high
           end, with a gap in the middle because nothing legitimately needs a
           mid grey on either surface. */
        --color-slate-50:  oklch(0.255 0.011 264);
        --color-slate-100: oklch(0.300 0.012 264);
        --color-slate-200: oklch(0.355 0.013 264);
        --color-slate-300: oklch(0.620 0.016 264);
        --color-slate-400: oklch(0.665 0.017 264);
        --color-slate-500: oklch(0.720 0.017 264);
        --color-slate-600: oklch(0.800 0.014 264);
        --color-slate-700: oklch(0.865 0.011 264);
        --color-slate-800: oklch(0.920 0.008 264);
        --color-slate-900: oklch(0.960 0.005 264);
        --color-slate-950: oklch(0.985 0.003 264);

        /* Four surface steps, deepest first: the chrome frames the page field,
           the page field holds panels, panels hold popovers. */
        --color-chassis: oklch(0.165 0.012 264);
        --color-chassis-raised: oklch(0.232 0.013 264);
        --color-chassis-line: oklch(0.300 0.013 264);
        --color-chassis-text: oklch(0.760 0.015 260);
        --color-chassis-dim: oklch(0.600 0.015 260);

        --color-surface-sunk: oklch(0.190 0.011 264);
        --color-surface: oklch(0.228 0.012 264);
        --color-surface-raised: oklch(0.262 0.013 264);
        --color-line: oklch(0.312 0.012 264);
        --color-line-strong: oklch(0.375 0.013 264);

        /* Status, lifted. A 0.63 red is a fine dot and a failing text colour, and
           --color-danger is both. Everything here stays on its own hue. */
        --color-ok: oklch(0.760 0.170 152);
        --color-warn: oklch(0.830 0.150 92);
        --color-danger: oklch(0.680 0.210 25);
        --color-idle: oklch(0.640 0.020 264);

        --color-ok-ink: oklch(0.870 0.140 152);
        --color-warn-ink: oklch(0.890 0.130 92);
        --color-danger-ink: oklch(0.810 0.140 25);

        /* Only the ink end of the brand ramp moves. 600 and below are fills:
           the primary button, the avatar, the rails and the focus ring are all
           built from them and must stay solid. */
        --color-brand-700: oklch(0.800 0.150 60);
        --color-brand-800: oklch(0.860 0.110 68);

        /* Now that there is a dark field behind them, the highlight does the
           work the shadow was doing, and the shadows themselves have to be
           deeper to register at all. */
        --hm-edge-light: color-mix(in oklch, white 7%, transparent);
        --hm-e1:
            0 1px 2px -1px color-mix(in oklch, black 60%, transparent),
            0 1px 3px -2px color-mix(in oklch, black 45%, transparent);
        --hm-e2:
            0 10px 24px -10px color-mix(in oklch, black 75%, transparent),
            0 2px 6px -3px color-mix(in oklch, black 55%, transparent);
        --hm-e3:
            0 24px 60px -18px color-mix(in oklch, black 85%, transparent),
            0 4px 12px -6px color-mix(in oklch, black 60%, transparent);

        /* Hover lightens in dark and darkens in light, which is the one place
           the two modes are not the same instruction. */
        --hm-hover: oklch(0.268 0.013 264);
        --hm-wash-top: oklch(0.268 0.013 264);
        --hm-wash-bottom: oklch(0.228 0.012 264);
        --hm-track-off: oklch(0.330 0.013 264);
        --hm-knob: oklch(0.930 0.005 264);
    }

    /* Pressed has to read as darker than resting, and in dark the slate-100 the
       light theme presses to is lighter than the button. */
    :root[data-theme="dark"] .hm-btn-secondary:active { background: var(--color-surface-sunk); }

    /* Exception 1. Fourteen install-instruction blocks are bg-chassis with
       text-slate-100 on them. Slate-100 is a fill in dark, so the code inside
       them would be near black on near black. */
    .bg-chassis.text-slate-100,
    .bg-chassis.text-slate-200,
    :root[data-theme="dark"] .bg-chassis.text-slate-300 { color: oklch(0.865 0.010 264); }

    /* Exception 2. The remaining literal bg-white in markup: the .hm-card rule
       above already covers five of six, and the sixth is the two-factor QR
       plate, which carries its own white rect inside the SVG and therefore
       stays scannable on a dark plate. */
    :root[data-theme="dark"] .bg-white { background-color: var(--color-surface); }

    /* Exception 3. Two chips in the PHP module are built from raw palette
       classes instead of hm-chip-ok and hm-chip-danger, so they miss the token
       swap that carries every other chip. */
    /* Exception 4. Both of these are defined as "darker than the page", which is
       the one description that does not survive the page becoming dark. A
       tooltip has to float above the panel, so it becomes the raised step. */
    :root[data-theme="dark"] .hm-tooltip {
        background: var(--color-surface-raised);
        border-color: var(--color-line-strong);
        color: var(--color-slate-900);
    }

    /* The editor. CodeMirror injects its own stylesheet at the TOP of head
       precisely so author CSS can override it, so equal specificity is enough,
       and these are deliberately one step more specific than that. */
    :root[data-theme="dark"] .hm-editor .cm-editor { background: var(--color-surface); color: var(--color-slate-800); }
    :root[data-theme="dark"] .hm-editor .cm-editor .cm-content { caret-color: var(--color-brand-500); }
    :root[data-theme="dark"] .hm-editor .cm-editor .cm-cursor { border-left-color: var(--color-slate-900); }
    :root[data-theme="dark"] .hm-editor .cm-editor .cm-gutters {
        background: var(--color-surface-sunk);
        color: var(--color-slate-400);
        border-right-color: var(--color-line);
    }
    :root[data-theme="dark"] .hm-editor .cm-editor .cm-activeLine { background: color-mix(in oklch, var(--color-brand-500) 7%, transparent); }
    :root[data-theme="dark"] .hm-editor .cm-editor .cm-activeLineGutter { background: color-mix(in oklch, var(--color-brand-500) 10%, transparent); }
    .hm-editor .cm-editor .cm-selectionBackground,
    .hm-editor .cm-editor.cm-focused .cm-selectionBackground,
    :root[data-theme="dark"] .hm-editor .cm-editor ::selection { background: color-mix(in oklch, var(--color-brand-500) 28%, transparent); }


/* Light, chosen explicitly.
   ------------------------------------------------------------------------
   The base tokens are already light, so this only has to stop the media query
   winning on a dark desktop. Without it, choosing light does nothing for the
   readers most likely to want the choice. */
:root[data-theme="light"] {
    color-scheme: light;
}


/* Resource meters.
   ------------------------------------------------------------------------
   The bar repeats the percentage rather than replacing it. The number is the
   precise answer and stays; the bar is what lets a grid of tiles be read at a
   glance from across a desk, which is the whole job of a monitoring page.

   The notch is the operator's own alert level, so the page and the thing that
   sends the alert are visibly the same setting. */
.hm-meter {
    position: relative;
    height: 0.25rem;
    margin-top: 0.625rem;
    border-radius: 999px;
    background: var(--color-slate-200);
    overflow: hidden;
}

.hm-meter-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--color-ok);
    transition: width 200ms ease;
}

.hm-meter-warn   { background: var(--color-warn); }
.hm-meter-danger { background: var(--color-danger); }

/* Sits above the fill, so a bar past its limit still shows where the limit
   was. */
.hm-meter-limit {
    position: absolute;
    top: 0;
    width: 2px;
    height: 100%;
    margin-left: -1px;
    background: var(--color-slate-500);
    opacity: 0.55;
}

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

/* Tabs on a small screen are a drawer, not a wrapped strip.
   ------------------------------------------------------------------------
   This block used to make the strip wrap, which was right when a section held
   nine tabs. A site now holds sixteen, and sixteen wrapped pills fill most of
   the first screenful before any content appears.

   The rule is gone rather than edited because the drawer above replaces it
   entirely. It is worth recording why: it lived several hundred lines after the
   drawer's own media query and set display back to flex, so the strip kept
   rendering under the drawer button. Two media queries for the same element at
   the same breakpoint is the shape of the bug, not the values in either one. */

/* Paging.
   ------------------------------------------------------------------------
   Under the list, not over it: the reader arrives at the strip having finished
   the rows, which is the moment they want it. */
.hm-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 0.875rem;
    font-size: 0.75rem;
    color: var(--color-slate-500);
}

.hm-pager-count { font-variant-numeric: tabular-nums; }

.hm-pager-controls { display: flex; align-items: center; gap: 0.375rem; }

.hm-pager-at {
    padding-inline: 0.25rem;
    font-variant-numeric: tabular-nums;
    color: var(--color-slate-600);
}

/* The row size choice, as a small segmented set. */
.hm-pager-per {
    display: inline-flex;
    align-items: center;
    margin-right: 0.375rem;
    padding: 0.125rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-sm, 0.3125rem);
    background: var(--color-surface-sunk);
}

.hm-pager-size {
    padding: 0.125rem 0.375rem;
    border-radius: 0.1875rem;
    font-variant-numeric: tabular-nums;
    color: var(--color-slate-500);
}

.hm-pager-size:hover { color: var(--color-slate-900); }

.hm-pager-size-active {
    background: var(--color-surface);
    color: var(--color-slate-900);
    font-weight: 600;
    box-shadow: 0 1px 1px -1px color-mix(in oklch, var(--color-chassis) 30%, transparent);
}

/* A disabled control keeps its space so the strip does not jump as the reader
   reaches the first or last page. */
.hm-pager [aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }

@media (max-width: 47.9375rem) {
    .hm-pager { justify-content: center; }
    .hm-pager-controls { flex-wrap: wrap; justify-content: center; }
}

/* The application catalogue.
   ------------------------------------------------------------------------
   A card per application, grouped by what the application is for. Browsing by
   category is the point: somebody who wants a forum does not know they want
   phpBB, and a flat alphabetical list is a list you have to already understand
   to use. */
.hm-app {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.875rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md, 0.5rem);
    background: var(--color-surface);
    transition: border-color 120ms ease, background-color 120ms ease;
}

.hm-app:hover { border-color: var(--color-brand-500); background: var(--color-surface-sunk); }

.hm-app-icon {
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    flex: none;
    border-radius: var(--hm-r-sm, 0.375rem);
    background: var(--color-surface-sunk);
    border: 1px solid var(--color-line);
    color: var(--color-slate-600);
}

.hm-app-icon svg { width: 1rem; height: 1rem; }

.hm-app-name {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-wrap: wrap;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--color-slate-900);
}

.hm-app-version {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 400;
    color: var(--color-slate-500);
}

.hm-app-blurb {
    display: block;
    margin-top: 0.1875rem;
    font-size: 0.75rem;
    line-height: 1.45;
    color: var(--color-slate-500);
}

/* The appearance switch, in the account menu.
   ------------------------------------------------------------------------
   A segmented set of submit buttons rather than a select, because the choice
   posts to the server and a select would need script to submit on change. */
.hm-menu-theme {
    margin-top: 0.3125rem;
    padding: 0.5rem 0.625rem 0.375rem;
    border-top: 1px solid var(--color-line);
}

.hm-menu-theme-label {
    display: block;
    margin-bottom: 0.375rem;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

.hm-menu-theme-set {
    display: flex;
    gap: 0.125rem;
    padding: 0.125rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-sm, 0.3125rem);
    background: var(--color-surface-sunk);
}

.hm-menu-theme-option {
    flex: 1;
    padding: 0.25rem 0.375rem;
    border: 0;
    border-radius: 0.1875rem;
    background: transparent;
    font-size: 0.75rem;
    color: var(--color-slate-500);
    cursor: pointer;
}

.hm-menu-theme-option:hover { color: var(--color-slate-900); }

.hm-menu-theme-on {
    background: var(--color-surface);
    color: var(--color-slate-900);
    font-weight: 600;
    box-shadow: 0 1px 1px -1px color-mix(in oklch, var(--color-chassis) 30%, transparent);
}

/* ---------------------------------------------------------------------------
   Dashboard

   The landing page answers one question, so it opens with the answer written
   out rather than with a row of tiles somebody has to add up. Everything here
   exists to make that first line unmissable and the rest quiet enough that it
   is not competing with it.
   --------------------------------------------------------------------------- */

.hm-verdict {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 1.125rem 1.25rem;
    margin-bottom: 1rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md, 0.5rem);
    background: var(--color-surface);
    /* The state reads down the leading edge rather than as a wash across the
       whole block. A full-width tint on the first thing on the page turns an
       ordinary "everything is fine" into something that looks like an alert. */
    border-left: 3px solid var(--color-slate-300);
    box-shadow: 0 1px 2px -1px color-mix(in oklch, var(--color-chassis) 22%, transparent);
}

.hm-verdict-ok     { border-left-color: var(--color-ok); }
.hm-verdict-warn   { border-left-color: var(--color-warn); }
.hm-verdict-danger { border-left-color: var(--color-danger); }

.hm-verdict-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 999px;
    background: color-mix(in oklch, var(--color-slate-500) 12%, transparent);
    color: var(--color-slate-500);
}

.hm-verdict-mark svg { width: 1.125rem; height: 1.125rem; }

.hm-verdict-ok .hm-verdict-mark {
    background: color-mix(in oklch, var(--color-ok) 14%, transparent);
    color: var(--color-ok);
}

.hm-verdict-warn .hm-verdict-mark {
    background: color-mix(in oklch, var(--color-warn) 16%, transparent);
    color: var(--color-warn);
}

.hm-verdict-danger .hm-verdict-mark {
    background: color-mix(in oklch, var(--color-danger) 14%, transparent);
    color: var(--color-danger);
}

.hm-verdict-headline {
    font-size: 1.0625rem;
    font-weight: 650;
    letter-spacing: -0.011em;
    color: var(--color-slate-900);
    line-height: 1.25;
}

.hm-verdict-sub {
    font-size: 0.8125rem;
    color: var(--color-slate-500);
    margin-top: 0.125rem;
}

/* A reading, sized to be legible across a desk and aligned in a column with
   the others, which is what tabular-nums buys. */
.hm-figure {
    font-size: 0.9375rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: var(--color-slate-900);
}

.hm-figure-warn   { color: var(--color-warn); }
.hm-figure-danger { color: var(--color-danger); }

/* The estate strip: one row of counts, each a link to the thing it counts.
   Dividers between rather than borders around, so it reads as one sentence
   about the server rather than as six more cards. */
.hm-estate {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Clips the rules that each cell draws past its right and bottom edges, so
       the last column and the last row have nothing after them. */
    overflow: hidden;
}

@media (min-width: 40rem) {
    .hm-estate { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 64rem) {
    .hm-estate { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

.hm-estate-item {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.875rem 1rem;
    text-decoration: none;
    transition: background 120ms ease;

    /* Drawn outside the box rather than as a border, so the grid's overflow
       clips the outer edges. Counting cells with nth-child cannot do this: it
       has to know both the column count and the item count, and the estate has
       one row per module the server is running. */
    box-shadow:
        1px 0 0 var(--color-line),
        0 1px 0 var(--color-line);
}

.hm-estate-item:hover { background: var(--color-surface-sunk); }

.hm-estate-item:focus-visible {
    outline: 2px solid var(--color-brand-500);
    outline-offset: -2px;
}

.hm-estate-value {
    font-size: 1.375rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
    color: var(--color-slate-900);
    line-height: 1.1;
}

.hm-estate-label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

/* ---------------------------------------------------------------------------
   Faint text in light mode

   Tailwind's slate-400 measures 2.63:1 against white, which is below AA's 4.5:1
   for normal text, and nearly every use of it in this panel is at text-xs: last
   seen times, container ids, netmask suffixes. Exactly the small print somebody
   squints at, and exactly what fails on a laptop screen in daylight.

   Fixed here rather than by editing 25 templates, and by overriding the utility
   rather than the --color-slate-400 token, because that token is also the
   border and placeholder colour: darkening it globally would thicken every
   hairline seam in the panel, which is the one thing the design is built on.

   oklch(0.56 0.035 264) measures 4.66:1 on white. Dark mode is left alone: its
   slate-400 is already 5.6:1 against the panel surface.
   --------------------------------------------------------------------------- */
:root[data-theme="light"] .text-slate-400 { color: oklch(0.56 0.035 264); }

@media screen and (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .text-slate-400 { color: oklch(0.56 0.035 264); }
}

/* The facts strip along the bottom of the dashboard.
   Same dividers-between rather than borders-around idea as the estate strip, so
   the page ends on one continuous band instead of a short column with dead
   space beside it. */
.hm-facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 64rem) {
    .hm-facts { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.hm-facts-item {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.75rem 1rem;
    border-right: 1px solid var(--color-line);
    border-bottom: 1px solid var(--color-line);
    min-width: 0;
}

.hm-facts-item:nth-child(2n) { border-right: 0; }
.hm-facts-item:nth-last-child(-n + 2) { border-bottom: 0; }

@media (min-width: 64rem) {
    .hm-facts-item:nth-child(2n) { border-right: 1px solid var(--color-line); }
    .hm-facts-item:nth-child(4n) { border-right: 0; }
    .hm-facts-item:nth-last-child(-n + 2) { border-bottom: 1px solid var(--color-line); }
    .hm-facts-item:nth-last-child(-n + 4) { border-bottom: 0; }
}

.hm-facts-label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

.hm-facts-value {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    font-size: 0.8125rem;
    color: var(--color-slate-800);
    min-width: 0;
}

/* ---------------------------------------------------------------------------
 * Branding form. The colour swatch, the generated ramp, and the preview of an
 * uploaded image.
 * ------------------------------------------------------------------------ */
.hm-color-swatch {
    width: 2.75rem;
    height: 2.5rem;
    padding: 0;
    border: 1px solid var(--color-line);
    border-radius: 0.375rem;
    background: var(--color-surface);
    cursor: pointer;
}

/* Chrome and Firefox each wrap the swatch in their own box; without these the
   control renders as a small square floating inside a large grey one. */
.hm-color-swatch::-webkit-color-swatch-wrapper { padding: 3px; }
.hm-color-swatch::-webkit-color-swatch { border: 0; border-radius: 0.25rem; }
.hm-color-swatch::-moz-color-swatch { border: 0; border-radius: 0.25rem; }

.hm-ramp {
    display: flex;
    border: 1px solid var(--color-line);
    border-radius: 0.375rem;
    overflow: hidden;
}

.hm-ramp-stop {
    flex: 1 1 0;
    height: 1.75rem;
}

.hm-brand-preview {
    display: inline-flex;
    align-items: center;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--color-line);
    border-radius: 0.375rem;
    /* The chassis colour, because that is what the sidebar and the sign-in page
       put the logo on. Previewing a white-on-transparent logo against a white
       card would show it as invisible and send somebody off to fix a file that
       was fine. */
    background: var(--color-chassis);
}

/* ---------------------------------------------------------------------------
 * Bulk selection. A row of checkboxes and a bar that acts on all of them.
 * ------------------------------------------------------------------------ */
/* A table sitting directly under a panel header has no rounding of its own: the
 * panel supplies it. Without this the first header cell paints a square corner
 * over the panel's rounded one. */
.hm-panel > .overflow-x-auto > .hm-table { border-radius: 0; }
.hm-panel > .overflow-x-auto:last-child { border-bottom-left-radius: 0.5rem; border-bottom-right-radius: 0.5rem; overflow: hidden; }

/* The way out of a page that has its own tabs. Sits where the section strip
 * would, so the page below it does not shift. */
.hm-backlink {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    margin-bottom: 1.25rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--color-slate-600);
    border: 1px solid transparent;
    border-radius: 0.375rem;
    padding: 0.25rem 0.5rem;
    margin-left: -0.5rem;
    transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.hm-backlink:hover {
    color: var(--color-slate-900);
    background: var(--hm-hover);
    border-color: var(--color-slate-400);
}
.hm-backlink svg { width: 0.9375rem; height: 0.9375rem; }

.hm-bulkbar {
    margin-bottom: 0.75rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--color-line);
    border-radius: 0.5rem;
    background: var(--color-surface-sunk);
}

.hm-check {
    width: 1rem;
    height: 1rem;
    accent-color: var(--color-brand-600);
    cursor: pointer;
}

/* A disabled bulk button is the normal state, so it must not look broken: it is
   waiting for a selection rather than refusing one. */
.hm-bulkbar button[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
 * Tab group dropdowns.
 *
 * The site page grew to eighteen tabs, at which point a flat strip stops being
 * navigation and becomes a list: nothing stands out, and finding one entry
 * means reading every entry. The strip groups them instead, and a group opens
 * a menu.
 *
 * Desktop only. On a phone the whole strip is already a drawer, and a menu
 * inside a drawer would be two taps to reach anything.
 * ------------------------------------------------------------------------ */
.hm-tabmenu { position: relative; display: inline-flex; }

.hm-tabmenu-btn { cursor: pointer; }

/* The chevron is the affordance that says this opens rather than navigates, so
   it is dimmer than the label but never invisible. */
.hm-tabmenu-btn > svg:last-child {
    opacity: 0.55;
    margin-left: -0.125rem;
}

.hm-tabmenu-panel {
    position: absolute;
    top: calc(100% + 0.375rem);
    left: 0;
    z-index: 40;
    min-width: 11rem;
    padding: 0.25rem;
    border: 1px solid var(--color-line);
    border-radius: var(--hm-r-md);
    background: var(--color-surface-raised);
    box-shadow: var(--hm-e2);
    animation: hm-modal-in 120ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* A menu near the right edge would otherwise run off the page. Anchoring the
   last group's menu to its right edge keeps it on screen without measuring
   anything in script. */
.hm-tabmenu:last-child .hm-tabmenu-panel { left: auto; right: 0; }

.hm-tabmenu-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4375rem 0.625rem;
    border-radius: var(--hm-r-sm);
    font-size: 0.8125rem;
    color: var(--color-slate-700);
    white-space: nowrap;
}

.hm-tabmenu-item svg { width: 0.875rem; height: 0.875rem; flex: none; opacity: 0.7; }

.hm-tabmenu-item:hover {
    background: var(--color-surface-sunk);
    color: var(--color-slate-900);
}

.hm-tabmenu-current {
    color: var(--color-brand-700);
    font-weight: 600;
}

.hm-tabmenu-current svg { opacity: 1; }

@media (max-width: 767px) {
    .hm-tabmenu, .hm-tabmenu-panel { display: none; }
}

/* A group's heading inside the phone drawer, so the same grouping reads there
   without becoming a second layer to tap through. */
.hm-tabdrawer-group {
    padding: 0.75rem 0.875rem 0.25rem;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-slate-500);
}

/* ---------------------------------------------------------------------------
 * One height for every control in the header.
 *
 * The bar carried four adjacent controls at three different heights: the search
 * box and the bell at 32px, the account menu at 36 and the load badge at 27.
 * Each was sized by its own padding and content, so nothing lined up along
 * either edge and the row read as slightly broken without it being obvious why.
 *
 * 2rem is the bell's and the search box's existing height, so this brings the
 * other two into line rather than moving everything.
 * ------------------------------------------------------------------------ */
header .hm-menu-trigger,
header .hm-hoverable,
header .hm-bell,
header .hm-search .hm-field {
    min-height: 2rem;
    box-sizing: border-box;
}

/* The account menu is pinned rather than floored: its avatar plus padding comes
   to exactly 2rem, and its one pixel of transparent border then pushed the
   control to 34 while everything beside it sat at 32. */
header .hm-menu-trigger {
    height: 2rem;
    padding-top: 0;
    padding-bottom: 0;
}

/* The account menu's avatar is what was making it the tallest thing in the row.
   Bounded so the control follows the height above instead of setting it. */
header .hm-menu-trigger .hm-avatar {
    width: 1.5rem;
    height: 1.5rem;
    font-size: 0.625rem;
}

/* ---------------------------------------------------------------------------
 * A section heading with its actions on the right.
 *
 * Two positions, one class. Most callers put it above a panel or a table, where
 * it is a plain row. A few put it inside .hm-panel as that panel's header, and
 * .hm-panel carries no padding, so there it needs the panel's own inset and a
 * line for the content below to sit against.
 *
 * Distinguished by parentage rather than by a second class, so a caller cannot
 * pick the wrong one: where it sits already says which it is.
 * ------------------------------------------------------------------------ */
.hm-tabhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.hm-panel > .hm-tabhead {
    margin-bottom: 0;
    padding: 0.625rem 1rem;
    border-bottom: 1px solid var(--color-line);
    background:
        linear-gradient(180deg,
            var(--color-surface) 0%,
            var(--color-surface-sunk) 100%);
}

/* The two bars along the bottom of the window.
 *
 * The sidebar's licence line and the main footer sit side by side and share a
 * top border, so the moment they differ in height that border steps eight
 * pixels partway across the screen. They did: both had the same padding, but a
 * chip is taller than a line of footer text, so the sidebar came to 50px
 * against the footer's 42.
 *
 * One height for both, and the padding moves here from the two elements so
 * there is a single number to change. */
.hm-footbar {
    display: flex;
    align-items: center;
    min-height: 2.625rem;
    padding-top: 0;
    padding-bottom: 0;
}

/* ---------------------------------------------------------------------------
 * The role permission grid.
 *
 * This page used to be one form per role, each a full-height column of toggles.
 * With the permissions this build declares that is several thousand pixels of
 * scrolling per role, and the question an operator actually has, "what can a
 * reseller do that a customer cannot", could not be answered without scrolling
 * between two forms and remembering.
 *
 * One grid instead: permissions down, roles across, so the difference between
 * two roles is a glance along a row.
 * ------------------------------------------------------------------------- */

/* The wrapper scrolls, not the page. A wide grid must never make the whole
 * document scroll sideways. */
.hm-rolegrid-wrap { overflow-x: auto; }

.hm-rolegrid { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }

.hm-rolegrid thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-line);
    padding: 0.625rem 0.75rem;
    text-align: center;
    font-weight: 500;
    color: var(--color-slate-700);
}

/* The permission column is the one that carries text, so it takes the width and
 * the role columns stay narrow enough to fit several on a laptop. */
.hm-rolegrid-perm { text-align: left !important; width: 100%; min-width: 16rem; }
.hm-rolegrid-role { min-width: 7.5rem; white-space: nowrap; }

.hm-rolegrid-count { font-size: 0.6875rem; color: var(--color-slate-500); font-weight: 400; margin-top: 0.125rem; }

.hm-rolegrid tbody td { padding: 0.375rem 0.75rem; text-align: center; }
.hm-rolegrid tbody td.hm-rolegrid-perm { padding: 0.5rem 0.75rem; }

.hm-rolegrid-row { border-top: 1px solid color-mix(in oklch, var(--color-line) 55%, transparent); }
.hm-rolegrid-row:hover { background: var(--color-surface-sunk); }

.hm-rolegrid-key { font-size: 0.6875rem; color: var(--color-slate-500); font-family: var(--font-mono); }

/* The module band. Sticky under the header so the group a row belongs to is
 * still on screen after scrolling into the middle of a long one. */
.hm-rolegrid-group > th {
    position: sticky;
    top: 3.25rem;
    z-index: 1;
    text-align: left;
    padding: 0.5rem 0.75rem;
    background: var(--color-surface-sunk);
    border-top: 1px solid var(--color-line);
    border-bottom: 1px solid var(--color-line);
    font-weight: 600;
    font-size: 0.75rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--color-slate-600);
}
.hm-rolegrid-group > td {
    background: var(--color-surface-sunk);
    border-top: 1px solid var(--color-line);
    border-bottom: 1px solid var(--color-line);
}

.hm-rolegrid-all {
    font-size: 0.6875rem;
    color: var(--color-slate-500);
    padding: 0.0625rem 0.375rem;
    border: 1px solid transparent;
    border-radius: var(--hm-r-sm);
    background: transparent;
    cursor: pointer;
}
/* A visible border on hover, with the border already there in transparent form
 * so nothing shifts when it appears. */
.hm-rolegrid-all:hover { border-color: var(--color-line); color: var(--color-slate-700); }

.hm-rolegrid-cell { display: inline-flex; padding: 0.25rem; cursor: pointer; }
.hm-rolegrid-cell input {
    width: 1rem; height: 1rem; cursor: pointer;
    accent-color: var(--color-brand-600);
}

/* A row hidden by the filter. Set on the element rather than via [hidden], which
 * an author display rule would otherwise beat. */
.hm-rolegrid-row.hm-hidden,
.hm-rolegrid-group.hm-hidden { display: none; }
