/* ===================================================================
   The identity hub — "night lobby".

   Mockup A, approved by Milo on 2026-09-11. The hub is not a panel: it
   has one job, which is to say who you are and which servers you can
   open, and it should feel like standing outside the worlds at night
   rather than like a fourth dashboard.

   This file lives in app/static/css and NOT in app/hub/, deliberately.
   Every guard in the suite that reads first-party CSS globs this one
   directory — the colour-token contract, the radius contract, the gray
   sweep, the contrast floors. A second stylesheet root would be outside
   all four of them, and a stylesheet nobody's guard reads is how the
   palette came to exist six times before tokens.css.

   Three rules follow from that, and none of them is style preference:

   * Every colour is a :root token. Where a step has no ready form (only
     bloc-300/500/600/700 do) the channel spelling `rgb(var(--c-NAME))`
     is used, which is what the offline empty-state icon already writes.
   * This file declares NO custom property of its own. scripts/check_vars.py
     attributes a declaration to the `.bloc-*` scope it sits in; a token
     declared under `.hub-night` belongs to no scope it recognises, so
     every call site of it would be reported as unresolvable. Consume
     tokens here, declare them in tokens.css.
   * Radii are --radius-card / --radius-control / --radius-pill, or 50%
     for a circle. tests/test_radius_contract.py holds that.

   The two faces come from tokens.css's own @font-face blocks, which are
   self-hosted for a privacy reason stated there. The hub reaches them by
   serving app/static — see app/hub/__init__.py — rather than by keeping
   a second copy of two woff2 files.
   =================================================================== */

.hub-night *,
.hub-night *::before,
.hub-night *::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* --- the ground ----------------------------------------------------
   A flat surface-950 with two violet washes laid over it as an IMAGE,
   in that order and not as one shorthand. The flat step is what
   tests/test_contrast_floors.py can read, and the washes are bounded
   against it there: both peak DARKER than surface-800, so every ink
   this file measures on a card also clears its floor out here.

   `background-attachment: fixed` is not used. The washes are anchored
   to the viewport by the ::before starfield instead, which is the layer
   that has to hold still while the page scrolls. */
.hub-night {
    min-height: 100%;
    /* Tells the browser this page is dark, which is not the same as painting
       it dark: it is what makes the scrollbar, a UA default focus ring and —
       the one that shows on the admin page — a `<select>`'s OPEN popup use
       the dark palette instead of the light one. Those widgets are drawn by
       the operating system and no stylesheet reaches inside them, so without
       this the grant dropdowns open white on a violet-black page. */
    color-scheme: dark;
    font-family: 'Space Grotesk', system-ui, sans-serif;
    background: var(--surface-950);
    background-image:
        radial-gradient(1200px 700px at 78% -10%, rgb(var(--c-bloc-500) / .14), transparent 60%),
        radial-gradient(900px 600px at -10% 110%, rgb(var(--c-bloc-700) / .18), transparent 55%);
    color: var(--surface-100);
    overflow-x: hidden;
}

html {
    height: 100%;
}

/* The star grid: the world at night, drifting a viewport's width every
   four minutes. Two dot fields at different pitches so it never reads
   as a repeat, masked to fade out down the page so it never competes
   with a card. `pointer-events: none` because it covers everything. */
.hub-night::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    background-image:
        radial-gradient(rgb(var(--c-bloc-300) / .35) 1px, transparent 1.5px),
        radial-gradient(rgb(var(--c-surface-100) / .18) 1px, transparent 1.5px);
    background-size: 190px 170px, 310px 260px;
    background-position: 0 0, 90px 60px;
    mask-image: linear-gradient(180deg,
        rgb(var(--c-shadow) / .9), rgb(var(--c-shadow) / .25) 55%, transparent);
    animation: hub-drift 240s linear infinite;
}

@keyframes hub-drift {
    to { background-position: 190px 170px, 400px 320px; }
}

.hub-page {
    max-width: 1080px;
    margin: 0 auto;
    padding: 56px 32px 96px;
    position: relative;
}

/* Every page that is one card rather than a lobby: sign in, choose a
   password, no access, the handoff. */
.hub-page--narrow {
    max-width: 520px;
    padding-top: 72px;
}

/* --- masthead ------------------------------------------------------ */
.hub-masthead {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 24px;
    flex-wrap: wrap;
}

.hub-wordmark {
    display: flex;
    align-items: center;
    gap: 18px;
}

/* One font-size drives the drawing: mark.css sets the glyph to 1em tall
   with an auto width, which is what lets the three viewBox ratios sit on
   one baseline. Nothing here restates the geometry. */
.hub-wordmark__glyph {
    display: flex;
    font-size: 64px;
    filter: drop-shadow(0 6px 24px rgb(var(--c-bloc-500) / .35));
}

.hub-wordmark__name {
    display: block;
    font-size: clamp(40px, 6vw, 64px);
    font-weight: 640;
    letter-spacing: -.015em;
    line-height: .95;
}

/* The one-card pages. Both steps have a call site: the lobby passes neither
   and sign in / choose a password / no access pass `compact`. */
.hub-wordmark--compact .hub-wordmark__glyph { font-size: 40px; }
.hub-wordmark--compact .hub-wordmark__name { font-size: 30px; }

.hub-kicker {
    display: block;
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 12px;
    letter-spacing: .32em;
    text-transform: uppercase;
    color: var(--bloc-300);
    margin-bottom: 6px;
}

.hub-session {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 13px;
    color: var(--surface-300);
    text-align: right;
    line-height: 1.7;
    padding-bottom: 8px;
}

.hub-session__name {
    color: var(--surface-100);
    font-weight: 600;
}

/* Sign out is a POST — the session is cleared server side and the form
   carries a CSRF token — so it is a real <button> wearing a link, never
   an <a>. A GET link here would be one prefetch away from signing the
   family out of their own hub. */
.hub-linkbtn {
    font: inherit;
    color: var(--bloc-300);
    background: none;
    border: 0;
    border-bottom: 1px solid rgb(var(--c-bloc-300) / .35);
    padding: 0;
    cursor: pointer;
}

.hub-rule {
    height: 1px;
    background: linear-gradient(90deg, var(--surface-600), transparent 85%);
    margin: 40px 0 14px;
    border: 0;
}

.hub-lede {
    font-size: 17px;
    color: var(--surface-300);
    max-width: 44ch;
    margin-bottom: 56px;
}

/* --- the server cards ---------------------------------------------- */
.hub-cards {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 22px;
    list-style: none;
}

@media (max-width: 880px) {
    .hub-cards { grid-template-columns: 1fr; }
}

/* The flat step is declared first and the shade over it second, so the
   contrast file can read a ground. The gradient runs 800 -> 900, i.e.
   away from the ink, so surface-800 is its LIGHTEST end and measuring
   there measures the worst case rather than an average nobody sees. */
.hub-card {
    position: relative;
    display: block;
    text-decoration: none;
    color: var(--surface-100);
    background: var(--surface-800);
    background-image: linear-gradient(175deg, var(--surface-800), var(--surface-900) 82%);
    border: 1px solid var(--surface-600);
    border-radius: var(--radius-card);
    padding: 30px 26px 24px;
    transition: transform .28s cubic-bezier(.2, .9, .25, 1.2), border-color .28s, box-shadow .28s;
    overflow: hidden;
}

/* The violet horizon inside a card, revealed on approach. */
.hub-card::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--bloc-700), var(--bloc-300));
    opacity: 0;
    transition: opacity .28s;
}

.hub-card:hover {
    transform: translateY(-6px);
    border-color: var(--bloc-500);
    box-shadow: 0 18px 44px -18px rgb(var(--c-bloc-500) / .45);
}

.hub-card:hover::after,
.hub-card:focus-visible::after {
    opacity: 1;
}

/* Keyboard focus is drawn everywhere something is reachable by tab, in one
   place, so a control added later inherits it by joining the list rather
   than by remembering. `:focus-visible` and not `:focus`: a pointer click on
   a card should not leave a ring behind it. */
.hub-card:focus-visible,
.hub-linkbtn:focus-visible,
.hub-back:focus-visible,
.hub-field:focus-visible,
.hub-select:focus-visible,
.hub-submit:focus-visible,
.hub-btn:focus-visible {
    outline: 2px solid var(--bloc-300);
    outline-offset: 3px;
}

.hub-card__glyph {
    display: flex;
    align-items: center;
    height: 64px;
    font-size: 56px;
    margin-bottom: 22px;
}

.hub-card__name {
    font-size: 21px;
    font-weight: 620;
    letter-spacing: .14em;
    text-transform: uppercase;
    margin-bottom: 6px;
}

/* The panel's own hostname, read off the configured origin. It is not
   the Minecraft address: the hub is never told one, and typing three
   down here would be three strings that drift the day a DNS record
   moves. What this line says is where the card takes you. */
.hub-card__addr {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 12.5px;
    color: var(--surface-300);
    letter-spacing: .04em;
    margin-bottom: 20px;
}

.hub-card__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.hub-card .role {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 11px;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: rgb(var(--c-bloc-200));
    background: rgb(var(--c-bloc-500) / .14);
    border: 1px solid rgb(var(--c-bloc-500) / .35);
    border-radius: var(--radius-pill);
    padding: 4px 10px 3px;
}

/* The dot is a ::before rather than an element, and that is worth a
   line: the state is carried by the WORD beside it — Up, Auth error,
   Unknown — so the colour is a second reading of a fact already in
   text, never the only one. Nothing here says anything by hue alone. */
.hub-card .status {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 12px;
    color: var(--surface-300);
    white-space: nowrap;
}

.hub-card .status::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
    background: var(--warning);
    box-shadow: 0 0 10px rgb(var(--c-warning-500) / .6);
}

.hub-card .status-up::before {
    background: var(--success);
    box-shadow: 0 0 10px rgb(var(--c-success-500) / .8);
}

.hub-card .status-auth-rejected::before {
    background: var(--danger);
    box-shadow: 0 0 10px rgb(var(--c-danger-500) / .7);
}

/* The cards arrive rather than appear. The opacity: 0 start is the trap
   in this pattern: kill only the animation and the cards stay invisible
   forever, so the reduced-motion block below restores BOTH ends. */
.hub-card {
    opacity: 0;
    transform: translateY(14px);
    animation: hub-rise .6s cubic-bezier(.2, .8, .3, 1) forwards;
}

.hub-cards li:nth-child(1) .hub-card { animation-delay: .08s; }
.hub-cards li:nth-child(2) .hub-card { animation-delay: .18s; }
.hub-cards li:nth-child(3) .hub-card { animation-delay: .28s; }

@keyframes hub-rise {
    to { opacity: 1; transform: none; }
}

/* --- one-card pages ------------------------------------------------- */
.hub-panel {
    background: var(--surface-800);
    color: var(--surface-100);
    border: 1px solid var(--surface-600);
    border-radius: var(--radius-card);
    padding: 32px 30px;
    margin-top: 40px;
}

.hub-panel__title {
    font-size: 22px;
    font-weight: 620;
    letter-spacing: .12em;
    text-transform: uppercase;
    margin-bottom: 10px;
}

.hub-note {
    color: var(--surface-300);
    font-size: 15px;
    line-height: 1.6;
    margin-bottom: 22px;
}

/* The one thing on the page that must be read before anything else, so
   it is a real border and real weight, not a tint that reads as decor. */
.hub-alert {
    color: var(--danger-text);
    background: rgb(var(--c-danger-500) / .10);
    border: 1px solid rgb(var(--c-danger-500) / .35);
    border-radius: var(--radius-control);
    padding: 10px 14px;
    margin-bottom: 20px;
    font-size: 14px;
}

.hub-form {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.hub-label {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 11px;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--bloc-300);
    margin-top: 14px;
}

.hub-field,
.hub-select {
    font: inherit;
    font-size: 15px;
    color: var(--surface-100);
    background: var(--surface-700);
    border: 1px solid var(--surface-600);
    border-radius: var(--radius-control);
    padding: 11px 13px;
    width: 100%;
}

.hub-submit {
    font: inherit;
    font-weight: 600;
    letter-spacing: .06em;
    color: var(--white);
    background: var(--bloc-600);
    border: 0;
    border-radius: var(--radius-control);
    padding: 12px 18px;
    margin-top: 24px;
    cursor: pointer;
    transition: background .18s;
}

.hub-submit:hover {
    background: var(--bloc-700);
}

/* The quiet control: everything on the users page that is not the one
   primary action of a form. */
.hub-btn {
    font: inherit;
    font-size: 13px;
    color: var(--surface-100);
    background: var(--surface-700);
    border: 1px solid var(--surface-600);
    border-radius: var(--radius-control);
    padding: 7px 12px;
    cursor: pointer;
    transition: background .18s;
}

.hub-btn:hover {
    background: var(--surface-500);
}

/* `inline-block` and a top margin because the reset above zeroes every
   margin in this tree, and this link is the last thing on two pages — on the
   admin page it follows a card directly, with nothing else to space it. */
.hub-back {
    display: inline-block;
    margin-top: 20px;
    color: var(--bloc-300);
    text-decoration: none;
    border-bottom: 1px solid rgb(var(--c-bloc-300) / .35);
    font-size: 14px;
}

/* --- the admin table ------------------------------------------------
   Function over flourish. This page is read by whoever is fixing an
   account at an awkward moment, so the shape it needs is a legible grid
   and controls that are obviously controls — not the lobby's drama. */
.hub-page--wide {
    max-width: 1180px;
}

.hub-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

.hub-table th {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 11px;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--bloc-300);
    text-align: left;
    padding: 0 14px 10px;
    border-bottom: 1px solid var(--surface-600);
}

.hub-table td {
    padding: 16px 14px;
    border-bottom: 1px solid var(--surface-600);
    vertical-align: top;
}

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

.hub-account__login {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 14px;
}

.hub-account__display {
    display: block;
    color: var(--surface-300);
    font-size: 13px;
    margin-top: 3px;
}

/* Each grant is its own POST, so each is its own form. Laid out as a
   row rather than stacked, because the point of the column is reading
   three panels at a glance. */
.hub-grant {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

.hub-grant__panel {
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 12px;
    letter-spacing: .1em;
    min-width: 8ch;
}

.hub-select {
    width: auto;
    font-size: 13px;
    padding: 6px 10px;
}

.hub-flags {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
}

/* A label, a field and a button on one line — the password reset. The
   stacked form's top margin on a label is what separates one field from the
   next, and there is no next one here. */
.hub-inline {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.hub-inline .hub-label {
    margin-top: 0;
}

.hub-inline .hub-field {
    width: auto;
    flex: 1 1 16ch;
}

/* --- foot ----------------------------------------------------------- */
.hub-foot {
    margin-top: 64px;
    font-family: 'Overpass Mono', ui-monospace, monospace;
    font-size: 12px;
    letter-spacing: .06em;
    color: var(--surface-300);
}

/* --- reduced motion --------------------------------------------------
   Both animations are removed AND both are landed on their finished
   state. `animation: none` alone would leave every card at the
   `opacity: 0` its entrance starts from — a page that renders nothing
   at all for the visitor who asked for less movement, which is the
   worst outcome this media query can produce. */
@media (prefers-reduced-motion: reduce) {
    .hub-night::before {
        animation: none;
    }

    .hub-card {
        animation: none;
        opacity: 1;
        transform: none;
        transition: border-color .28s, box-shadow .28s;
    }

    .hub-card:hover {
        transform: none;
    }
}
