/* One file, no dependency on the outside world. A public page on a tenant's
   domain must not open a connection to third parties — otherwise it needs a
   consent banner, and that costs more clicks than the design brings in. This
   file serves the admin area too, for the same reason: one stylesheet, one
   trust boundary, nothing fetched from anywhere else. */

:root {
    --ink: #0a0c11;
    --base: #0a0c11;
    --surface: #14161f;
    --surface-bright: #1b1f2b;
    --surface-alt: #1f2330;
    --border: #262b3a;
    --border-soft: #1b1f2a;
    --text: #e9ebf1;
    --muted: #98a1b5;
    --dim: #5d6579;
    --accent: #f2a93b;
    --accent-ink: #17130a;
    --good: #3ed598;
    --bad: #ff6b5a;
    --warn: #e0ac4a;
    --radius: 10px;
    --radius-sm: 6px;
    --mono: ui-monospace, "SF Mono", "IBM Plex Mono", Menlo, monospace;
    --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: light) {
    :root {
        --ink: #f4f5f9;
        --base: #f4f5f9;
        --surface: #ffffff;
        --surface-bright: #eef0f7;
        --surface-alt: #e7eaf3;
        --border: #dde1ec;
        --border-soft: #e7eaf3;
        --text: #14161d;
        --muted: #5c6478;
        --dim: #848da2;
        --accent: #b0690f;
        --accent-ink: #ffffff;
    }
}

* { box-sizing: border-box; }

/* NOTHING SCROLLS SIDEWAYS. The requirement is the whole rule: a page that can be
   pushed wider than the window is one where a column, a button or half a heading
   is somewhere the eye never goes.

   Two lines do most of the work. The first stops a stray wide child from turning
   into a page-wide scrollbar; the second breaks long words so the child is not
   wide to begin with — this application is full of strings with no spaces in
   them: ISRCs, catalogue numbers, API tokens, signed URLs, domain names.

   `clip` AND NOT `hidden`, and the difference is not cosmetic. `html`'s own
   overflow propagates to the viewport, which leaves `html` itself `visible` and
   `body`'s declaration applying to `body`. With `hidden` there, one axis hidden
   makes the other compute to `auto` — so `body` became a scroll container, a
   scrollable box that merely declines to show a scrollbar. `position: sticky`
   resolves against the nearest scroll container, so the sidebar and the topbar
   were sticking to a box that never scrolls, which is to say not sticking at
   all: measured, both sat at -1500 px after scrolling 1500. `clip` establishes
   no scroll container, so it clips the stray child exactly as before and sticky
   goes back to resolving against the viewport, where it belongs.

   `break-word` and not `anywhere` for the general case: both break a long word
   that would otherwise overflow, but `anywhere` also counts the break when the
   browser works out an element's minimum width, which makes ordinary prose in a
   flex or grid child collapse to a ragged column far narrower than it needs to
   be. Where the min-width contribution IS the point — a table cell holding an
   ISRC, a token, a signed address — `anywhere` is set on that cell instead. */
html, body { overflow-x: clip; max-width: 100%; }
body { overflow-wrap: break-word; }

/* An image or a media element never wider than what holds it. */
img, video, canvas, iframe { max-width: 100%; }

/* Inputs default to a size the browser picks, in characters, and ignore their
   container — a `size`-driven text box is one of the few things that still forces
   a page wider than the screen in 2026. */
input, select, textarea { max-width: 100%; }

body {
    margin: 0;
    background: var(--base);
    color: var(--text);
    font: 15px/1.55 var(--sans);
    -webkit-font-smoothing: antialiased;
}

h1 {
    font-size: 1.4rem;
    font-weight: 800;
    letter-spacing: .01em;
    margin: 0 0 .3rem;
}

h2 {
    font-size: .82rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .09em;
    color: var(--muted);
    margin: 0;
}

h3 { font-size: 1rem; margin: 1.25rem 0 .5rem; }
a { color: inherit; }

code { font-family: var(--mono); font-size: .85em; }

.aside { color: var(--muted); }

/* A READING WIDTH IS A PROPERTY OF TEXT, not of the page around it. Now that the
   content grows with the window, an explanatory paragraph would otherwise run the
   full width of a wide screen — and the eye loses the start of the next line
   somewhere past eighty characters. Only paragraphs: a table, a field row or a
   button strip inside an .aside container has no business being narrowed. */
p.aside { max-width: 82ch; }

.small { font-size: .85rem; }

/* ─── Public link page ────────────────────────────────────────────────────── */

.public {
    max-width: 560px;
    margin: 0 auto;
    padding: 3rem 1.25rem 4rem;
}

.linkpage header { text-align: center; margin-bottom: 2rem; }
.linkpage .avatar { border-radius: 50%; object-fit: cover; }
.linkpage h1 { font-size: 1.6rem; text-transform: none; letter-spacing: normal; font-weight: 650; margin: .75rem 0 .35rem; color: var(--text); }
.linkpage .bio { color: var(--muted); margin: 0; }

.blocks { list-style: none; margin: 0; padding: 0; display: grid; gap: .7rem; }

.blocks .block {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: 1rem 1.15rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 550;
    transition: transform .12s ease, border-color .12s ease;
}

.blocks .block:hover { transform: translateY(-2px); border-color: var(--accent); }
.blocks .block .platform { color: var(--muted); font-weight: 400; font-size: .85rem; }
.blocks .block.release { border-left: 3px solid var(--accent); }
.blocks .block.gate { border-left: 3px solid var(--good); }

/* A player is not a link — nothing to lift on hover, and the label sits above
   the control instead of beside it, the same way it would above any other
   audio player. */
.blocks .block.player { flex-direction: column; align-items: stretch; gap: .6rem; cursor: default; }
.blocks .block.player:hover { transform: none; border-color: var(--border); }
/* The player's own rules are inlined by LinkPage from ReleaseEmbedPage.CardStyle,
   so that the link page and the embed cannot drift apart. Nothing about the card
   belongs here. */

.signup { margin-top: 2.5rem; display: grid; gap: .6rem; }
.signup form { display: grid; gap: .5rem; }
.signup .wording { color: var(--muted); font-size: .85rem; margin: 0; }

/* The gate's action-before-the-form block (GAT-02/GAT-03): the prompt, an
   optional link and the code/checkbox field are siblings with nothing between
   them in the rendered markup — Razor trims the whitespace around the @if
   blocks that separate them, so without this the prompt ran straight into the
   next label's text ("...from the flyerCode"). Laid out as a grid instead of
   leaning on incidental whitespace. */
.gate .block { display: grid; gap: .5rem; }

.empty {
    text-align: center;
    padding: 3rem 1.5rem;
    color: var(--muted);
    background: var(--surface);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
}

/* ─── Administration shell ────────────────────────────────────────────────── */

.admin { display: flex; min-height: 100vh; align-items: stretch; }

/* STICKY AND EXACTLY ONE SCREEN TALL.
   Before: min-height: 100vh on the flex container, so the sidebar stretched to
   the height of the DOCUMENT. margin-top: auto then pushed the block with the
   tenant, the account and the language to the bottom of the PAGE — on a long
   page hundreds of pixels below the fold, where nobody scrolls a navigation bar
   to find it.
   align-items: flex-start stops the stretch; sticky keeps it in view; the height
   is what the screen has, not what the content wants. */
.sidebar {
    width: 232px;
    flex: 0 0 232px;
    background: var(--surface);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    align-self: flex-start;
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;   /* the mobile browser bar eats part of 100vh */
}

/* The links scroll when there are too many for the screen; the foot does not
   move. min-height: 0 is what allows a flex child to shrink below its content —
   without it overflow never engages and the foot is pushed out again. */
.sidebar .nav { flex: 1 1 auto; overflow-y: auto; min-height: 0; padding: .5rem; }
.sidebar .foot { flex: 0 0 auto; }

.sidebar .brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: 1.15rem 1.25rem 1rem;
    border-bottom: 1px solid var(--border-soft);
    font-weight: 800;
    font-size: .82rem;
    text-transform: uppercase;
    letter-spacing: .1em;
}

.sidebar .brand .led {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent);
    flex: 0 0 auto;
}

/* A label, not a link — the whole point is that a group is a visual break in
   an otherwise flat list of ten links, so it must not look clickable. */
.sidebar .group {
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--dim);
    text-transform: uppercase;
    letter-spacing: .1em;
    padding: .9rem .75rem .3rem;
}

.sidebar .item {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .5rem .75rem;
    margin: 1px 0;
    border-radius: var(--radius-sm);
    color: var(--muted);
    text-decoration: none;
    font-size: .88rem;
    font-weight: 500;
}

.sidebar .item:hover { color: var(--text); background: var(--surface-bright); }
.sidebar .item.active { color: var(--text); background: var(--surface-alt); }

.sidebar .item .dot {
    width: 5px;
    height: 5px;
    flex: 0 0 5px;
    border-radius: 50%;
    background: var(--border);
}

.sidebar .item.active .dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }

.sidebar .foot { border-top: 1px solid var(--border-soft); padding: .85rem 1.25rem; }

.sidebar .language { margin-top: .6rem; }
.sidebar .language select { font-size: .82rem; padding: .3rem .45rem; }

/* ─── Topbar ───────────────────────────────────────────────────────────────── */

/* Everything that used to crowd the bottom of the sidebar — who is signed in,
   which tenant, how to switch — now lives up here, where it stays visible
   without competing with the navigation for space. The sidebar is left to do
   one thing: get somewhere else. */
.topbar {
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: .6rem 1.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 30;
    flex-wrap: wrap;
}

.workspace { display: flex; align-items: center; gap: .6rem; }

.workspace .avatar {
    width: 28px;
    height: 28px;
    flex: 0 0 28px;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--accent-ink);
    display: grid;
    place-items: center;
    font-weight: 800;
    font-size: .72rem;
    text-transform: uppercase;
}

.workspace .name { font-weight: 650; font-size: .9rem; line-height: 1.15; }
.workspace .kind { font-family: var(--mono); font-size: .68rem; color: var(--dim); text-transform: uppercase; letter-spacing: .06em; }
.workspace .switch { font-size: .78rem; color: var(--accent); text-decoration: none; margin-left: .35rem; }

.topbar .spacer { flex: 1; }

.topbar .who {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: .82rem;
    color: var(--muted);
}

.topbar .who .name { color: var(--text); max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .who a { color: var(--accent); text-decoration: none; font-size: .8rem; }

/* Column flex: the topbar and the content stack vertically underneath it.
   The topbar sticks to the top of the PAGE (there is no scroll container of
   its own in here), the same way the sidebar already stuck to it. */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* THE WIDTH, AND WHY IT IS NOT 1180 ANY MORE.
   It was `max-width: 1180px` with no centring, inside a .main that takes whatever
   the screen has left. On a 1920 screen that left 508 px of nothing down the right
   side, and on a 2560 one, 1148 px — measured, not guessed. Every table on these
   pages was squeezed into two thirds of the window while the last third stayed
   empty.

   Filling completely is not the answer either: a rack of prose stretched to 2500 px
   is a line nobody can read back to the start of. So the content GROWS to a limit
   and is CENTRED past it — the space that is left over ends up split rather than
   piled up on one side, which is what made it read as a mistake.

   The prose keeps its own measure separately (.lede, .aside), because a reading
   width is a property of text and not of the page it sits on. */
.content {
    min-width: 0;
    width: 100%;
    max-width: 1600px;
    margin-inline: auto;
    padding: 1.75rem 2.25rem 4rem;
}

/* The switch, the button that flips it and the sheet behind the drawer. None of
   the three exists on a wide screen — see the media query below, where they
   appear and the sidebar becomes a drawer. */
.nav-switch, .nav-open-button, .nav-scrim { display: none; }

@media (max-width: 800px) {
    .admin { flex-direction: column; align-items: stretch; }

    /* A DRAWER, NOT A BAR ACROSS THE TOP. Stacking the navigation above the
       content pushed the page down by its own height — twenty-five links, most of
       a screen — so the first thing a phone showed was a menu. The sidebar now
       slides in over the page and slides back out, and the page underneath keeps
       its whole screen.

       Off-screen with a transform and not display:none, so the panel can move
       rather than blink — but `visibility: hidden` with it, which is the part that
       was missing. A transform moves a thing without hiding it: closed, the drawer
       was still twenty-five reachable links, so a Tab from the menu button walked
       the whole navigation through a panel nobody could see, and a screen reader
       read out a menu that was not open.

       The delay on `visibility` is what keeps the slide: the property flips at the
       END of the close (`0s .18s`) so the drawer stays visible while it travels,
       and at the START of the open (`0s`) so it is visible before it moves. */
    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: min(84vw, 300px);
        z-index: 60;
        border-right: 1px solid var(--border);
        border-bottom: none;
        transform: translateX(-100%);
        visibility: hidden;
        transition: transform .18s ease, visibility 0s .18s;
        overflow-y: auto;
    }
    .nav-switch:checked ~ .sidebar {
        transform: none;
        visibility: visible;
        transition: transform .18s ease, visibility 0s;
        box-shadow: 0 0 40px rgba(0, 0, 0, .35);
    }

    /* The page behind an open drawer does not scroll. Without this a swipe over
       the scrim moves the page under it, so closing the menu leaves you somewhere
       you did not choose to be. Safe here and only here: on this width the topbar
       is `position: static`, and the drawer itself is `fixed`, so nothing depends
       on the root staying a non-scroll-container. */
    html:has(.nav-switch:checked) { overflow: hidden; }

    /* The sheet over the page. It is what makes a tap anywhere outside the drawer
       close it — the gesture everybody already knows — and it is a button rather
       than a div so that closing is reachable without a pointer. */
    .nav-switch:checked ~ .nav-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 50;
        border: 0;
        padding: 0;
        background: rgba(0, 0, 0, .45);
    }

    /* Out of sight, still focusable — a menu nobody can reach by keyboard is a
       menu that needs a pointer. */
    .nav-switch {
        display: block;
        position: absolute;
        width: 1px; height: 1px;
        margin: -1px; padding: 0; border: 0;
        overflow: hidden; clip-path: inset(50%);
    }
    /* The focus ring has to show on the button, since the input it belongs to is
       the thing that is out of sight. One selector and not two: the button lives
       in .topbar, but .topbar is inside .main — only .main is a sibling of the
       switch, so a `~ .topbar` combinator matches nothing at all. */
    .nav-switch:focus-visible ~ .main .nav-open-button { outline: 2px solid var(--accent); outline-offset: 2px; }

    .nav-open-button {
        display: inline-flex;
        align-items: center;
        gap: .45rem;
        background: none;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        color: var(--text);
        padding: .35rem .6rem;
        font: inherit;
        font-size: .8rem;
        cursor: pointer;
        flex: 0 0 auto;
    }
    /* A bar at the top, and no longer sticky: on a phone one screen of navigation
       above the content is the content gone. */
    /* The drawer keeps the desktop list exactly as it is — one link per line, its
       groups intact. The old mobile rules turned it into a wrapped strip of chips
       with the group headings hidden, which only made sense while it was a bar
       across the top of the page. */
    .topbar { position: static; }
    .content { padding: 1.1rem 1.1rem 3rem; }
}

/* ─── The address a page ends up at ───────────────────────────────────────── */

.address {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin: .35rem 0 .9rem;
    font-size: .9rem;
}

.address .label { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; }
.address a { color: var(--accent); word-break: break-all; text-decoration: none; }
.address a:hover { text-decoration: underline; }
.address.missing a { color: var(--muted); }

.address .copy {
    font: inherit;
    font-size: .78rem;
    padding: .15rem .5rem;
    color: var(--muted);
    background: var(--surface-bright);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.address .copy:hover { color: var(--text); }

/* ─── View header ──────────────────────────────────────────────────────────── */

/* Every page starts with these two: what this is, in one line what it is for.
   Giving that its own block with room underneath is what actually separates
   "the page's own headline" from "the first rack's own title" below it — both
   used to be a bare <h1>/<h2> with the same weight, so the page read as one
   long undifferentiated list rather than a title over a stack of panels. */
.view-head { margin-bottom: 1.4rem; }
.view-head h1 { text-transform: uppercase; }
.view-head .lede { color: var(--muted); margin: .4rem 0 0; max-width: 74ch; font-size: .92rem; }

/* ─── Rack: the one container every section of content sits in ───────────── */

/* THE SINGLE BIGGEST CHANGE HERE. Before, a page was h2, table, h2, form, h2,
   table — one flat stream with nothing telling the eye where one piece of
   content ends and the next begins beyond a change in font size. A rack is a
   bordered panel with its own header bar; five racks down a page read as five
   distinct instruments, not one long scroll. */
.rack {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 1.1rem;
    overflow: hidden;
}

.rack-head {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .7rem .95rem;
    border-bottom: 1px solid var(--border-soft);
    flex-wrap: wrap;
}

/* 0 1 auto and not 0 0 auto: it still does not GROW — whatever sits beside it
   stays next to it rather than being pushed to the far edge — but it may shrink,
   and a heading forbidden to shrink is a heading that makes the page wider than
   the screen. That was the last horizontal scroll left at 320 px. */
.rack-head h2 { flex: 0 1 auto; min-width: 0; }
.rack-head .spacer { flex: 1; }

/* overflow-x, not visible: .rack itself clips (for the rounded corners), and
   without a scrollable escape hatch here a table wider than the panel — a
   release row's full public address plus its action buttons, easily wider
   than 1100px — is not just tight, it is GONE: cut off mid-button by the
   rack's own edge with no way to reach the rest. Scrolling loses nothing;
   overflow: hidden silently did. */
.rack-body { padding: .95rem; overflow-x: auto; min-width: 0; }

/* A flex item does not shrink below its content unless it is told it may, and a
   heading like "Stems, press texts, contracts" is one long line of content. At
   320 px it was 290 px wide inside a 283 px box — seven pixels of page scroll,
   from a heading. */
.rack-head, .rack-head > * { min-width: 0; }
.rack-body > :first-child { margin-top: 0; }
.rack-body > :last-child { margin-bottom: 0; }

/* A rack with nothing inside — no table rows, no list items — still needs to
   say so rather than show an empty header over a blank body. */
.rack-empty { padding: 1.5rem; text-align: center; color: var(--muted); font-size: .88rem; }

/* The shape of a populated page, faded — shown next to NoTenant so the page
   reads as "pick a tenant to see this" rather than as an empty install.
   Static, not shimmering: a placeholder that moves competes for attention
   with the actual notice above it, which is the one thing here asking to be
   read. */
.skeleton { opacity: .5; margin-top: 1.25rem; }
.skeleton-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .75rem; margin-bottom: 1.1rem; }
.skeleton-tile { height: 4.2rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.skeleton-rack { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; display: grid; gap: .65rem; }
.skeleton-row { height: 1rem; background: var(--border-soft); border-radius: 4px; }

.bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.bar .spacer { flex: 1; }

/* ─── Building blocks ─────────────────────────────────────────────────────── */

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .75rem; margin-bottom: 1.1rem; }

.tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1rem 1.1rem;
    display: grid;
    gap: .25rem;
}

.tile .label { color: var(--muted); font-size: .7rem; text-transform: uppercase; letter-spacing: .08em; order: -1; }
.tile .number { font-size: 1.7rem; font-weight: 800; line-height: 1; font-stretch: 110%; }
.tile .number.small { font-size: 1.05rem; }
.tile .hint { font-family: var(--mono); font-size: .7rem; color: var(--dim); }
.tile.warning { border-color: var(--warn); }
.tile.warning .number { color: var(--warn); }

.list, .table { width: 100%; border-collapse: collapse; }
.list th, .table th {
    text-align: left;
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    font-weight: 600;
    color: var(--muted);
    padding: .55rem .7rem;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}
/* `anywhere` here and `break-word` on the body, deliberately: a cell is exactly
   the place where the min-width contribution matters. A column holding an ISRC or
   a signed URL has to be allowed to claim it is one character wide, or the table
   is as wide as its longest identifier no matter what else gives way. */
.list td, .table td {
    padding: .65rem .7rem;
    border-top: 1px solid var(--border-soft);
    vertical-align: middle;
    font-size: .88rem;
    overflow-wrap: anywhere;
}
code { overflow-wrap: anywhere; }
.list tr:first-child td, .table tr:first-child td { border-top: 0; }
.list tbody tr:hover td, .table tbody tr:hover td { background: var(--surface-bright); }
.list .actions, .table .actions { text-align: right; white-space: nowrap; }

/* Everything in an actions cell sits on ONE line, centred against everything
   else on it. Both halves of that are load-bearing.

   The alignment, because the cell mixes an inline-flex .button with a plain
   inline element, and inline-flex aligns on its own baseline — which is not the
   text baseline beside it. Without this a button and a link in the same cell sit
   a couple of pixels apart vertically, which reads as sloppy rather than as a
   bug and so never gets fixed.

   The line, because .address is a BLOCK. It was drawn for the space under a
   heading, with air above and below; dropped into an actions cell it kept
   behaving that way, and the buttons either side of it were pushed onto their
   own lines. The catalogue's three controls came out as three stacked rows, each
   one right-aligned against nothing. Inline here, and the vertical margins go —
   the component is unchanged everywhere it is still a block. */
.list .actions > *, .table .actions > * { vertical-align: middle; }

/* And a declared gap between them, because the one the markup would otherwise
   supply is not reliably there: Razor drops whitespace-only nodes between
   elements unless a component asks it not to, so whether two buttons in a cell
   end up with a space between them depends on how the file happens to be
   indented. Stated here instead. */
.list .actions > * + *, .table .actions > * + * { margin-left: .4rem; }

.list .actions .address, .table .actions .address {
    display: inline-flex;
    margin: 0;
}

/* And it does not need to break mid-token here: the cell is `nowrap` anyway, and
   whatever an address column does with a long URL, a control strip should not. */
.list .actions .address a, .table .actions .address a { word-break: normal; }

/* ─── Tables in the space they actually have ──────────────────────────────────
   NO SIDEWAYS SCROLL, ANYWHERE. The old answer was `overflow-x: auto` on the
   rack, which is a scrollbar with no affordance hiding whatever sits in the last
   column — and on these pages the last column is the actions, so the button that
   opens a row was simply off-screen.

   CONTAINER QUERIES AND NOT THE VIEWPORT, because the viewport is the wrong
   number: at 1024 px the window is wide and the rack is 718, since the sidebar
   and the padding have taken the rest. A viewport rule missed exactly that case —
   measured, the catalogue table needed 798 px inside a 718 px rack and scrolled.

   Two steps, in the order that keeps the most:

     under 900  headers and action cells stop being `nowrap`. That alone took the
                catalogue from 798 to 718 — an exact fit — so a table that can be
                squeezed stays a table.
     under 520  a row stops being a row and becomes a block: one field per line,
                the header hidden, the actions at the end where a thumb reaches
                them. This is the phone case, where no amount of wrapping helps.

   Nothing here needs a change per stylesheet — every table in the application is
   .list or .table. It does need one thing per page, and that is the catch: hiding
   the header only works if the cells name their own column, so every <td> under a
   named <th> carries a data-label. Three files had them and twenty-six did not,
   which under 520 px meant twenty-six pages of unlabelled values — a column of
   bare numbers where the header used to say what they counted. Adding a table
   means adding the labels with it. Cells under a BLANK header — the actions
   column, an artwork thumbnail — deliberately have none: there is no column name
   to print, and "" above a button reads as a bug. */
.rack-body { container-type: inline-size; }

@container (max-width: 900px) {
    /* Wrapping beats scrolling: two lines of "Delivery" cost nothing, and the
       column stays where somebody can see it. */
    .list th, .table th,
    .list .actions, .table .actions { white-space: normal; }

    /* AND A LONG HEADER MAY BREAK MID-WORD. Measured on /gates at 600 px: every
       cell had already shrunk to nothing — the widest value in the table was nine
       pixels — and the table was still 27 px too wide, because six one-word
       HEADERS could not shrink past themselves. "DOWNLOADED" alone claimed 99 px.
       Letting a header break is not pretty; a column of values sitting under a
       scrollbar nobody can see is worse, and the break only happens in the width
       where the alternative is exactly that. */
    .list th, .table th { overflow-wrap: anywhere; }
}

@container (max-width: 520px) {
    .list, .table { display: block; }
    .list thead, .table thead { display: none; }
    .list tbody, .table tbody { display: block; }

    .list tr, .table tr {
        display: block;
        padding: .6rem .1rem;
        border-top: 1px solid var(--border-soft);
    }
    .list tr:first-child, .table tr:first-child { border-top: 0; }

    /* `overflow-wrap: anywhere` is not repeated here: cells carry it at every
       width, above, for the same reason it matters more in a stack. */
    .list td, .table td {
        display: block;
        border-top: 0;
        padding: .18rem .3rem;
    }

    /* The column's name, for the cells that carry one. Without it a stack of
       bare values is a puzzle: "4062248123456" says nothing on its own. */
    .list td[data-label]::before, .table td[data-label]::before {
        content: attr(data-label);
        display: block;
        font-size: .68rem;
        text-transform: uppercase;
        letter-spacing: .07em;
        color: var(--dim);
    }

    /* Left, not right: in a stack there is no column to line up with, and a
       right-aligned button under left-aligned text reads as a different
       element. Wrapping, because two or three actions in a row do not fit. */
    .list .actions, .table .actions, .list td.actions, .table td.actions {
        text-align: left;
        white-space: normal;
        display: flex;
        flex-wrap: wrap;
        gap: .35rem;
        padding-top: .45rem;
    }

    /* The cell is a flex container down here, so its own gap does the spacing —
       the inline margin from the wider layout would stack on top of it. */
    .list .actions > * + *, .table .actions > * + * { margin-left: 0; }

    .list tbody tr:hover td, .table tbody tr:hover td { background: none; }
}
tr.selected td { background: var(--surface-alt); }

/* A row that opens its own detail on click. The cursor has promised that on four
   pages since the class was written, and nothing ever listened for the click —
   the promise was kept by a button in the last column instead. Now the row keeps
   it, and those buttons are gone.

   Focusable on purpose. Dropping the button would otherwise have taken the
   keyboard path to the detail view with it; tab reaches the row and Enter opens
   it. The outline is inset because a row is flush against the rack's edge, and
   an outset ring on the first or last row is clipped by the rack's own
   overflow. */
tr.click { cursor: pointer; }
tr.click:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.badge {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .18rem .55rem;
    border-radius: 999px;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--muted);
    font-family: var(--mono);
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    white-space: nowrap;
}

.badge::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }

.badge.good, .badge.active, .badge.verified { color: var(--good); border-color: var(--good); }
.badge.bad, .badge.suspended { color: var(--bad); border-color: var(--bad); }
.badge.pending, .badge.warning { color: var(--warn); border-color: var(--warn); }
.badge.platform { color: var(--accent); border-color: var(--accent); }

/* An uploaded image shown back at the size it will actually appear at admin
   scale — release artwork, a link page's avatar — never the size it was
   uploaded at. */
.thumbnail { display: block; width: 96px; height: 96px; object-fit: cover;
             border-radius: var(--radius-sm); border: 1px solid var(--border); }

.button {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .5rem .9rem;
    background: var(--surface-bright);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font: inherit;
    font-weight: 500;
    text-decoration: none;
    transition: border-color .12s ease, background .12s ease;
}

.button:hover:not(:disabled) { border-color: var(--dim); background: var(--surface-alt); }
.button:disabled { opacity: .45; cursor: not-allowed; }
.button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 650; }
.button.primary:hover:not(:disabled) { filter: brightness(1.08); border-color: var(--accent); }
.button.danger { color: var(--bad); }
.button.danger:hover:not(:disabled) { border-color: var(--bad); }
.button.small { padding: .32rem .6rem; font-size: .82rem; }
.button.wide { width: 100%; text-align: left; display: grid; gap: .1rem; justify-content: unset; }

.buttons { display: flex; gap: .6rem; margin: .9rem 0; flex-wrap: wrap; }
.rack-body > .buttons:last-child { margin-bottom: 0; }

.field { display: grid; gap: .7rem; margin: .9rem 0; }
.field.horizontal { grid-auto-flow: column; grid-auto-columns: 1fr auto; align-items: center; }
.field label { display: grid; gap: .3rem; font-size: .82rem; color: var(--muted); }
.field label.checkbox { display: flex; flex-direction: row; align-items: center; gap: .5rem; font-size: .88rem; color: var(--text); }
.field label.checkbox input { width: auto; }

/* `field inline` was written in two places and defined NOWHERE — .field is a
   grid, so every child became its own full-width row: a status badge stretched
   the whole width of the panel and each button sat on a line of its own. It
   only ever meant "lay these out along one line", so that is what it says now.
   The badge needs its own note because a grid item stretches to its column by
   default, and inside a label it is the badge, not the input, that looks
   wrong for it. */
.field.inline { display: flex; flex-wrap: wrap; align-items: end; gap: .7rem; }
.field.inline > label { flex: 1 1 12rem; }
.field label > .badge { justify-self: start; }

input, select, textarea {
    padding: .5rem .65rem;
    background: var(--ink);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font: inherit;
    width: 100%;
}

input:focus, select:focus, textarea:focus, :focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/* Blazor's enhanced navigation focuses the page's own h1 after every route
   change, for screen readers. Useful, but plain heading text has no visible
   affordance for being a focus target, so the same ring that helps on a
   button just reads as a stray orange box here. */
h1:focus-visible { outline: none; }
input[type="checkbox"] { width: auto; accent-color: var(--accent); }

.notice {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
    padding: .85rem 1.1rem;
    margin: 1rem 0;
    font-size: .9rem;
}

.notice.warning { border-left-color: var(--warn); }
.notice h2 { margin-top: 0; text-transform: none; letter-spacing: normal; font-size: 1rem; color: var(--text); }

.two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start; }
@media (max-width: 950px) { .two-column { grid-template-columns: 1fr; } }

.block-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .6rem; }

.block-list li {
    background: var(--surface-bright);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .6rem;
    display: grid;
    gap: .4rem;
}

.block-list .row { display: flex; gap: .4rem; }
.block-list .id { font-family: var(--mono); font-size: .8rem; }

.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: .2rem; }
.steps li { display: grid; grid-template-columns: 1.2rem 1fr; gap: .5rem; font-size: .88rem; padding: .15rem 0; }
.steps li .kind { color: var(--dim); }
.steps li.done .kind { color: var(--good); }
.steps li.pending { color: var(--muted); }

.diff { list-style: none; padding: 0; margin: 0; display: grid; gap: .3rem; }

.diff li {
    display: grid;
    grid-template-columns: 1.2rem 10rem 1fr;
    gap: .5rem;
    padding: .45rem .6rem;
    border-radius: var(--radius-sm);
    background: var(--surface-bright);
    font-size: .85rem;
}

.diff .kind { font-weight: 700; text-align: center; }
.diff .id { font-family: var(--mono); color: var(--muted); font-size: .8rem; }
.diff li.add .kind { color: var(--good); }
.diff li.remove .kind { color: var(--bad); }
.diff li.modify .kind { color: var(--warn); }
.diff li.unchanged, .diff li.foreignkept { opacity: .55; }

.tenant-choice .list { list-style: none; padding: 0; display: grid; gap: .5rem; }

/* Rows of equal-weight items — a segment, a backlog entry, a queued job. Its
   own pattern because a <table> row assumes columns line up across many rows,
   and a lot of what these pages show is really a short flat list where only
   one row's numbers ever differ in shape from the next. */
.rows { display: flex; flex-direction: column; gap: .4rem; }

.row {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .6rem .75rem;
    background: var(--surface-bright);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-sm);
}

.row .row-main { min-width: 0; flex: 1; }
.row .row-title { font-weight: 600; font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .row-sub { font-family: var(--mono); font-size: .74rem; color: var(--dim); margin-top: .1rem; }

/* The bar that appears when the circuit is gone.
 *
 * display:none is not decoration — it IS the switch. The framework looks for
 * #blazor-error-ui and sets display:block on it when a circuit dies; with no rule
 * here the element would stand on every page from the start.
 *
 * Fixed to the bottom and above everything, because it has to be readable over a
 * page that is still fully drawn and no longer reacts to anything. */
#blazor-error-ui {
    display: none;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1000;
    padding: .85rem 3rem .85rem 1.25rem;
    background: var(--bad);
    color: #fff;
    box-shadow: 0 -2px 12px rgba(0, 0, 0, .35);
    font-size: .95rem;
}

#blazor-error-ui strong { margin-right: .5rem; }
#blazor-error-ui .reload { color: #fff; margin-left: .75rem; font-weight: 600; }

#blazor-error-ui .dismiss {
    position: absolute;
    top: .5rem;
    right: 1rem;
    color: #fff;
    text-decoration: none;
    font-size: 1.2rem;
    line-height: 1;
}

/* ─── Modal ────────────────────────────────────────────────────────────────── */
/*
 * A native <dialog>, shown with showModal() — the backdrop below is the
 * ::backdrop pseudo-element that comes with it, not a div this stylesheet
 * has to position and stack by hand.
 */
.modal {
    padding: 0;
    border: none;
    border-radius: var(--radius);
    background: transparent;

    /* WIDTH, NOT ONLY MAX-WIDTH, and that is the whole of it.
     *
     * A <dialog> is `width: fit-content` in every browser's own stylesheet, so a
     * max-width alone caps a size it never sets: each modal came out as wide as
     * its own longest line happened to be. Measured against this stylesheet at
     * 1280px, the four shapes in use were 147px, 227px, 321px and 624px — a
     * three-field "New release" form rendering at 227px, a confirmation at 147px,
     * and no two dialogs in the application agreeing on a width.
     *
     * One declared width fixes both halves of that at once: nothing is slim
     * because its text is short, and every modal of a given kind is the same size
     * as every other. The viewport term keeps it from overflowing a phone, and is
     * why this is not simply `width: 640px`.
     */
    width: min(640px, calc(100vw - 2rem));

    max-height: calc(100vh - 4rem);
    max-height: calc(100dvh - 4rem);
}

/* Detail modals wrapping a data table need more than 640px or the table
   forces a horizontal scrollbar inside the already vertically-scrolling
   modal body — technically reachable, but nothing hints it's there.
   Width for the same reason as above: a wide modal whose table happened to be
   narrow was not 920px, it was whatever the table measured. */
.modal-wide { width: min(920px, calc(100vw - 2rem)); }

/* A confirmation is one sentence and two buttons, and 640px of dialog around it
   reads as more consequence than there is. Narrower on purpose — but a DECLARED
   narrow, not the old shrink-to-fit one, so every confirmation in the
   application is the same size as every other rather than as long as its own
   sentence. Applied by hand and not by a rule about content: LinkPageAdmin's
   diff has no input either and is not a confirmation. */
.modal-narrow { width: min(420px, calc(100vw - 2rem)); }

.modal::backdrop { background: rgba(6, 8, 14, .55); }

.modal-box {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: flex;
    flex-direction: column;
    max-height: inherit;
}

.modal-head {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .8rem 1rem;
    border-bottom: 1px solid var(--border-soft);
    flex: 0 0 auto;
}

.modal-head h2 { flex: 1; margin: 0; }

.modal-close {
    background: none;
    border: none;
    color: var(--muted);
    font-size: 1.4rem;
    line-height: 1;
    padding: 0 .2rem;
    cursor: pointer;
}

.modal-close:hover { color: var(--text); }

.modal-body { padding: 1rem; overflow-y: auto; overflow-x: auto; }
.modal-body > :first-child { margin-top: 0; }
.modal-body > :last-child { margin-bottom: 0; }

@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
