/* Dark by default: this panel is operated alongside c4ds, which is dark, and a white page at night
   in the field is its own kind of hostile. */

:root {
    --bg: #14171a;
    --surface: #1c2126;
    --border: #2b333b;
    --text: #e6edf3;
    --muted: #8b969f;
    --accent: #22c55e;
    --accent-text: #04140a;
    --error: #f87171;
    --radius: 8px;

    /* Used as a mask, so it takes its colour from whatever sits under it. */
    --placeholder-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3 3.5-4.5 4.5 6H5l3.5-4.5z'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

h1 { font-size: 20px; margin: 0; }
h2 { font-size: 17px; margin: 0 0 16px; font-weight: 600; }

.centred {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}

.topbar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 24px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

/* Nothing in the bar breaks across lines. Every item in it is either a name or a control, and a
   control whose label wraps mid-word ("My / Plugins") is unreadable at exactly the glance the bar
   exists to serve. The wordmark sits a size down from a page heading for the same reason: it is
   longer than the bar was originally built for, and a name split over two lines reads as two. */
.topbar h1 {
    font-size: 18px;
    white-space: nowrap;
}

.nav-item { white-space: nowrap; }

.topbar button { white-space: nowrap; }

/* The address is what gives way when the row runs short. It is the one thing here that is not a
   control and the only one already answerable elsewhere, so it truncates rather than the whole bar
   growing a second line around it. `min-width: 0` is what lets a flex item shrink past its text. */
.topbar .muted {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.spacer { flex: 1; }

/* Word on a laptop, door on a phone. Both are always in the DOM and the stylesheet chooses, so the
   control keeps one identity and one accessible name across the breakpoint — see `Main.kt`. */
.signin-glyph { display: none; }

/* The icon is a white plate with the mark on it, so it is not rounded here to taste — it is rounded
   to the same proportion the SVG uses, or the plate's own corners show through the crop. */
.topbar-logo {
    width: 26px;
    height: 26px;
    border-radius: 6px;
    flex-shrink: 0;
    display: block;
}

/* The mark and the wordmark, as one link home -- see `brand` in Main.kt.
   Its own flex row, because it is a single item of the bar now: without this the anchor would be
   a block, the two halves would stack, and it would stretch to the bar's full height so the hover
   state read as a band across it. The gap matches the bar's own, so wrapping the pair in a link
   moves nothing. */
.brand {
    display: flex;
    align-items: center;
    gap: 12px;
    text-decoration: none;
    color: inherit;
    border-radius: 6px;
}

/* The whole thing dims together. The wordmark alone would say the mark beside it is not part of
   the target, which is exactly the impression the link is here to correct. */
.brand:hover { opacity: 0.85; }

.brand:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
}

.nav {
    display: flex;
    gap: 4px;
    margin-left: 20px;
}

.nav-item {
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 14px;
    font-weight: 500;
    color: var(--muted);
    text-decoration: none;
    /* The active tab is marked by weight and contrast rather than an underline, so the row does not
       shift by a pixel when it changes. */
    border: 1px solid transparent;
}

.nav-item:hover {
    color: var(--text);
    background: var(--bg);
}

.nav-item-active {
    color: var(--text);
    background: var(--bg);
    border-color: var(--border);
}

/* Invitations waiting on My organisations. Orange, filled: the one thing in the bar that asks for
   an answer, and a colour nothing else in it uses. */
.nav-count {
    display: inline-block;
    min-width: 18px;
    margin-left: 6px;
    padding: 0 6px;
    border-radius: 9px;
    background: #f97316;
    color: #1a0b02;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    vertical-align: 1px;
}

/* ---- The tabs where the bar cannot hold them: a drawer --------------------------------------
   The row holds the wordmark, the tabs and the account at once only on some screens, and which
   is not a width anybody can write down: an admin has nine tabs and a member three, Ukrainian
   labels run longer than English ones, counts appear on them, and the account button carries an
   organisation's name. A breakpoint measured for one of those ran the account button off the
   edge of a small laptop for another. So `Main.kt` decides (`fitTopbar`): on a phone or a tablet
   always, and wherever else the tabs do not fit, the bar is `topbar-compact` — one button, and
   the tabs a list down the side that names every screen and what waits on each. See `navDrawer`
   there: the `nav` itself is moved into the dialog while it is open, so everything about a tab is
   styled once and only its layout is restated here. */
.nav-toggle { display: none; }

/* A bar too narrow even for what is left breaks rather than runs off the screen. */
.topbar-compact { flex-wrap: wrap; }

.topbar-compact .nav-toggle {
    display: inline-flex;
    position: relative;
}

/* In the bar it is not shown; in the drawer it is, by the rules below. */
.topbar-compact > .nav { display: none; }

/* Something waits on one of the tabs: the same orange, on the button that hides them. */
.topbar-compact:has(.nav-count:not(.hidden)) .nav-toggle::after {
    content: "";
    position: absolute;
    top: 7px;
    right: 7px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #f97316;
    border: 2px solid var(--surface);
}

.nav-drawer {
    /* A sheet against the left edge rather than the centred box a dialog is by default. */
    inset: 0 auto 0 0;
    margin: 0;
    width: min(320px, 86vw);
    height: 100%;
    max-width: none;
    max-height: none;
    padding: 0;
    border: none;
    border-right: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
}

.nav-drawer::backdrop { background: rgba(0, 0, 0, 0.6); }

.nav-drawer[open] { animation: nav-drawer-in 160ms ease-out; }

@keyframes nav-drawer-in {
    from { transform: translateX(-100%); }
}

@media (prefers-reduced-motion: reduce) {
    .nav-drawer[open] { animation: none; }
}

.nav-drawer-sheet {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.nav-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    /* The bar's own padding and height, so the wordmark does not move when the drawer covers it. */
    padding: 12px 12px 12px 16px;
    border-bottom: 1px solid var(--border);
}

.nav-drawer-title { font-size: 16px; font-weight: 700; }

.nav-drawer-close,
.nav-toggle {
    align-items: center;
    justify-content: center;
    /* The glyph is 22px; the padding is the target. */
    width: 44px;
    height: 44px;
    padding: 0;
    flex-shrink: 0;
}

.nav-drawer-close { display: inline-flex; }

.nav-drawer .nav {
    flex: 1;
    min-height: 0;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 12px;
    overflow-y: auto;
}

/* A row a thumb can hit, its count at the far end where a column of them can be read down. */
.nav-drawer .nav-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px;
    font-size: 15px;
    white-space: normal;
}

.nav-drawer .nav-count { margin-left: 0; flex-shrink: 0; }

.content {
    max-width: 900px;
    margin: 0 auto;
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 24px;
}

/* Two cards in a row were touching, their borders meeting as one line — which reads as a single
   card with a rule through it rather than two things. Only the second onward, so a lone card is
   unaffected. */
/* A column of cards, spaced like `.content` spaces its own children.
   Needed because a screen's cards are usually built inside a host element rather than appended
   straight to `.content`, and a flex gap only reaches direct children — which is why two cards used
   to sit with their borders touching. */
.stack {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.card-narrow { width: 100%; max-width: 420px; }

/* The sign-in card. Everything on one axis: heading, subtitle and the button all centred, rather
   than left-aligned text above a centred button, which reads as a mistake. */
.card-centred {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 32px 24px;
}

.card-centred h1 { margin-bottom: 8px; }

.card-centred .muted { margin: 0; }

/* Google's iframe is a fixed 320px wide, so the slot is sized to it rather than stretched — a
   full-width flex row would leave the button looking off-centre inside its own container. */
.card-centred .google-slot {
    width: 320px;
    max-width: 100%;
    margin: 24px 0 0;
}

/* Under the refusal it answers, as wide as Google's button so the two read as one column. */
.card-centred .login-request {
    width: 320px;
    max-width: 100%;
    margin: 16px 0 0;
}

.card-centred .login-request .button {
    display: block;
    text-align: center;
}

/* The error and the "loading sign-in" line both sit above the button; keeping them the same width
   as it stops the card jumping sideways when one appears. */
.card-centred .error,
.card-centred .hint,
.card-centred > p.muted:last-child {
    width: 100%;
}

.row-between {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
}

.row-between h2 { margin: 0; }

.muted { color: var(--muted); font-size: 13px; }

.hint {
    color: var(--muted);
    font-size: 12px;
    margin: 4px 0 0;
}

.field { margin-bottom: 16px; }

.field-inline {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.field-inline label { margin: 0; }
.field-inline .hint { flex-basis: 100%; margin: 0; }

label {
    display: block;
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 500;
}

/* The browser's own parts of a control — a date field's calendar icon and pop-up, a select's list —
   are drawn for a light page unless told otherwise, which put a black calendar icon on a black
   field. Set on the controls and not on the page: an embedded frame whose scheme differs from the
   page's gets an opaque white backdrop, which is what boxed the Google sign-in button in white. */
input, select, textarea { color-scheme: dark; }

input, select {
    width: 100%;
    padding: 9px 11px;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    font: inherit;
    font-size: 14px;
}

/* The chevron is drawn here rather than left to the browser. A native one cannot be positioned
   reliably — each engine insets it its own way and honours padding-right differently — so the gap
   at the end was never actually ours to set. As a background image it is a number.
   `appearance: none` is what removes the native one, and is why the image is not optional.
   A search picker that keeps its choice is a dropdown as well, and says so the same way. */
select,
.search-select input {
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%238b969f' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
    /* Clears the 12px glyph plus its 14px inset, so a long option cannot slide under the arrow. */
    padding-right: 34px;
}

input[type="checkbox"] { width: auto; }

/* The box is drawn here rather than left to the browser, for the reason the select chevron is: a
   native checkbox is painted by the platform in the platform's accent, so on this dark panel it
   arrived as a blue-and-white square owing nothing to the rest of the page. `appearance: none` is
   what removes it, and is why everything below is not optional. */
.checkbox-field input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    /* flex-basis too: this sits in a flex row, where a bare width is a suggestion. */
    width: 18px;
    height: 18px;
    flex: 0 0 18px;
    margin: 0;
    padding: 0;
    border: 1.5px solid var(--border);
    border-radius: 5px;
    background: var(--bg);
    cursor: pointer;
    display: grid;
    place-content: center;
    transition: background 120ms ease, border-color 120ms ease;
}

.checkbox-field input[type="checkbox"]:hover { border-color: #3d4852; }

.checkbox-field input[type="checkbox"]:checked {
    background: var(--accent);
    border-color: var(--accent);
}

/* The tick is a clipped block rather than a glyph or an image: a font may not have the character
   at the weight this needs, and a background-image cannot take its colour from a variable. */
.checkbox-field input[type="checkbox"]:checked::after {
    content: "";
    width: 11px;
    height: 11px;
    background: var(--accent-text);
    clip-path: polygon(14% 47%, 0 61%, 40% 100%, 100% 22%, 85% 9%, 39% 71%);
}

/* Keyboard focus only. `:focus` would ring the box on every mouse click as well, which reads as
   an error state on a control that has just been answered. */
.checkbox-field input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The label is the other half of the target, so it has to look like one. */
.checkbox-field label {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
}

/* Aligned under the label rather than under the box: the hint continues the sentence the label
   starts, and text that starts 26px to the left of it reads as a separate paragraph. */
.checkbox-field .hint {
    flex-basis: 100%;
    margin: 6px 0 0 26px;
}

/* A setting whose consequence lands on every device in the fleet. The row states its own answer
   from across the room, which one small square cannot. */
.checkbox-emphasis {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px 14px;
    background: var(--bg);
    transition: border-color 120ms ease, background 120ms ease;
}

/* `:has` rather than a class toggled in Kotlin: the state already lives in the DOM, and a second
   copy of it in a click handler is a second copy that can be wrong. */
.checkbox-emphasis:has(input[type="checkbox"]:checked) {
    border-color: var(--accent);
    background: rgba(34, 197, 94, 0.06);
}

.checkbox-emphasis input[type="checkbox"] { background: var(--surface); }
.checkbox-emphasis input[type="checkbox"]:checked { background: var(--accent); }

input:focus, select:focus {
    outline: none;
    border-color: var(--accent);
}

input[type="file"] {
    padding: 8px;
    color: var(--muted);
}

button {
    padding: 9px 18px;
    border: 1px solid transparent;
    border-radius: 6px;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

/* not-allowed rather than default: some of these are disabled permanently, not just mid-request,
   and the cursor is the only thing that says so before the tooltip arrives. */
button:disabled { opacity: 0.55; cursor: not-allowed; }

.primary {
    background: var(--accent);
    color: var(--accent-text);
    width: 100%;
}

.secondary {
    background: transparent;
    color: var(--text);
    border-color: var(--border);
}

/* Language switch. A segmented control rather than a <select>: with two languages a dropdown hides
   half the answer behind a click, and the point of this control is that somebody who cannot read
   the current language can still see the other one. */
/* ---- Description language tabs ----------------------------------------------------------
   The content's language, not the panel's. Deliberately not the pill-shaped `.lang-switch` below:
   two controls that look identical and change different things is the confusion this screen is
   most likely to produce, so these read as tabs sitting on top of the box they fill. */
.locale-tabs {
    display: flex;
    gap: 4px;
    margin-bottom: 6px;
}

.locale-tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    /* Overrides the base button padding, which is sized for a real button. */
    padding: 4px 12px;
    background: none;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--muted);
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
}

.locale-tab:hover { color: var(--text); }

.locale-tab-active {
    color: var(--text);
    background: var(--bg);
    border-color: var(--border);
}

/* "There are words in here", for the tab you are not looking at -- the only thing worth knowing
   about a language whose text is currently put away. Hidden rather than absent, so the tab does
   not change width when the dot appears and shift the one beside it. */
.locale-tab-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--accent);
    visibility: hidden;
}

.locale-tab-filled .locale-tab-dot { visibility: visible; }

.lang-switch {
    display: inline-flex;
    flex-shrink: 0;
    gap: 2px;
    padding: 2px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
}

.lang-option {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    /* Overrides the base button padding, which is sized for a real button and would make this
       control taller than the topbar it sits in. */
    padding: 4px 10px;
    background: none;
    border: none;
    border-radius: 999px;
    color: var(--muted);
    font-size: 12px;
    line-height: 1;
}

.lang-option:hover { color: var(--text); }

.lang-option-active {
    background: var(--surface);
    color: var(--text);
}

/* The segments are 20px tall by design — the padding above is deliberately sized so the control
   does not tower over the topbar. On a touch screen that is well under half a comfortable target,
   so the *hit area* grows rather than the pill: an invisible overlay centred on each segment, which
   costs nothing visually and leaves the desktop control exactly as drawn. Keyed on `pointer:
   coarse` rather than a width, because a small window on a laptop still has a mouse. */
@media (pointer: coarse) {
    .lang-option { position: relative; }

    .lang-option::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: 44px;
        transform: translateY(-50%);
    }
}

/* A fixed box whatever the flag's own drawing does, so the two segments align and neither icon can
   stretch the row. */
.lang-flag {
    display: block;
    width: 18px;
    height: 9px;
    flex-shrink: 0;
    overflow: hidden;
    /* The pale edge of a flag against a dark panel needs an outline or it bleeds into the row. */
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
}

.lang-flag svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* The sign-in card is centred and has no chrome to hang the switch off, so it gets its own row. */
.login-language {
    margin-top: 20px;
}

.error, .status {
    padding: 10px 12px;
    border-radius: 6px;
    font-size: 13px;
    margin-bottom: 14px;
    background: #232a31;
    border: 1px solid var(--border);
    /* Digests and storage paths are long and must stay readable rather than overflow the card. */
    word-break: break-word;
}

.error, .status-error {
    color: var(--error);
    border-color: #4a2226;
    background: #2a1c1e;
}

.status-ok {
    color: var(--accent);
    border-color: #1c3d29;
    background: #16251c;
}

.status-warn {
    color: #fbbf24;
    border-color: #4a3a16;
    background: #2a2213;
}

.badge {
    display: inline-block;
    margin-left: 8px;
    /* Horizontal padding has to clear the 10px corner radius, or the text runs into the curve at
       both ends; 7px did, and "unsigned key" was the worst of them. */
    padding: 2px 10px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    vertical-align: middle;
}

/* The file picker. A dashed box reads as "put something here" in a way a bare file input never
   does, and it doubles as the drop target. */
.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 28px 20px;
    margin-bottom: 20px;
    border: 1.5px dashed var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    cursor: pointer;
    text-align: center;
    transition: border-color 0.15s, background 0.15s;
}

.dropzone:hover {
    border-color: var(--accent);
    background: #171d21;
}

/* Dragging over: the same treatment as hover but solid, so the drop target is unambiguous while a
   file is in flight over it. */
.dropzone-over {
    border-style: solid;
    border-color: var(--accent);
    background: #16251c;
}

.dropzone-filled {
    border-style: solid;
    border-color: #2f4738;
    background: #161d19;
}

/* The glyph and the two lines are decoration: a drag crossing them must still reach the zone, or
   the zone sees a `dragleave` and the drop is lost on a child that handles none. Clicks still reach
   the zone, which is what opens the file dialog. */
.dropzone > * { pointer-events: none; }

.dropzone-glyph {
    font-size: 22px;
    line-height: 1;
    color: var(--muted);
    margin-bottom: 4px;
}

.dropzone-filled .dropzone-glyph { color: var(--accent); }

.dropzone-title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text);
    /* A long APK filename must not stretch the card. */
    word-break: break-all;
}

.dropzone-hint {
    font-size: 12px;
    color: var(--muted);
}

.artifact-name {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* Sized like a launcher icon, because that is what it is. */
.artifact-icon {
    width: 28px;
    height: 28px;
    border-radius: 6px;
    flex-shrink: 0;
    background: var(--bg);
    object-fit: contain;
}

/* Present but empty, so a row without an icon keeps its name on the same left edge as the rest. */
.artifact-icon-blank {
    border: 1px dashed var(--border);
    background: transparent;
}

/* ---- Listings ---- */

textarea {
    width: 100%;
    padding: 9px 11px;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    font: inherit;
    font-size: 14px;
    /* Vertical only: a textarea dragged wider than its card is a layout nobody chose. */
    resize: vertical;
    min-height: 96px;
}

textarea:focus {
    outline: none;
    border-color: var(--accent);
}

/* The character count turns red before the save is refused, not after. */
.hint-over { color: var(--error); }

.listing-subhead {
    font-size: 15px;
    font-weight: 600;
    margin: 24px 0 10px;
}

.listing-icon-row {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    margin-bottom: 20px;
}

/* Sized like a launcher icon rather than to fill the row: it is what the device will show. */
.listing-icon {
    width: 56px;
    height: 56px;
    border-radius: 12px;
    flex-shrink: 0;
    background: var(--bg);
}

.listing-icon-row .muted { margin: 0; }

/* A horizontal strip that scrolls rather than wrapping: the order is the point, and a grid that
   reflows makes "third" mean different things at different window widths. */
.shot-strip {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    padding-bottom: 8px;
    margin-bottom: 16px;
}

.shot-strip .muted { margin: 0; }

.shot {
    flex-shrink: 0;
    width: 150px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    padding: 8px;
}

/* Contained rather than cropped: a screenshot is being checked here, and cover would hide an edge
   of the very thing somebody is meant to be looking at. */
.shot-image {
    width: 100%;
    height: 190px;
    object-fit: contain;
    border-radius: 4px;
    display: block;
    background: #0f1214;
}

.shot-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    margin-top: 8px;
}

.shot-button {
    width: auto;
    padding: 3px 8px;
    font-size: 12px;
    line-height: 1.2;
}

.shot-actions .badge { margin-left: 0; }

/* Remove sits on the picture itself, top right, where a close control is looked for — rather than
   in the row of arrows below, where it was one button among three and easy to hit by accident. */
.shot { position: relative; }

.shot-remove {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 1;
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.25);
    line-height: 1;
}

.shot-remove:hover { background: var(--error); color: #1a0d0d; }

/* Shorter than the publish dropzone: this one sits under a strip of thumbnails rather than being
   the only thing on the card. */
.listing-dropzone {
    padding: 18px 20px;
    margin-bottom: 16px;
}

.listing-save { width: auto; }

/* Google renders its button inside an iframe it controls, so this only positions it — the button's
   own appearance is not ours to style. */
.google-slot {
    display: flex;
    justify-content: center;
    min-height: 44px;
    margin: 20px 0 4px;
}

/* The download surface. One card per app, the action on the right so the column of buttons is
   scannable rather than hidden at the end of a row of metadata. */
.app-card {
    display: flex;
    align-items: center;
    gap: 20px;
    margin-bottom: 12px;
}

.app-main { flex: 1; min-width: 0; }

.app-name {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 4px;
}

.app-meta {
    font-size: 13px;
    color: var(--muted);
    /* Package names are long and must wrap rather than widen the card. */
    word-break: break-word;
}

/* Reads as attribution, so it is quieter than the name and warmer than the technical line. */
.app-publisher {
    margin-top: 2px;
    font-size: 13px;
    color: var(--text);
    opacity: 0.75;
}

.app-contact {
    margin-top: 1px;
    font-size: 12px;
    color: var(--muted);
}

.app-action {
    flex-shrink: 0;
    /* A column, right-aligned, so a licence line under Download sits under it rather than
       pushing it to the left edge of a column as wide as the line. */
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}
.app-action button { width: auto; }
.app-card .status { margin: 8px 0 0; }

/* Icon beside the name and publisher, with the description and the technical line below the pair.
   Not `align-items: center`: with a one-line name the icon would sit level with the name and leave
   the contact line hanging past its bottom edge, which reads as two blocks rather than one. */
.app-head {
    display: flex;
    align-items: flex-start;
    gap: 14px;
}

/* min-width:0 so a long package name wraps inside the column instead of widening the flex row and
   pushing the Download button off the card. */
.app-head-text { min-width: 0; }

.app-icon {
    width: 56px;
    height: 56px;
    border-radius: 12px;
    flex-shrink: 0;
    object-fit: cover;
    /* An icon with a transparent background would otherwise float on the card with no edge. */
    background: var(--bg);
}

/* Clamped to three lines until asked otherwise. `-webkit-line-clamp` is the only clamp with the
   ellipsis, and it is honoured by every engine this panel runs in; `.expanded` removes it rather
   than raising it, so an opened description has no second limit to run into. */
.app-description {
    margin: 12px 0 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
    opacity: 0.8;
    /* Descriptions arrive as written, newlines and all. */
    white-space: pre-wrap;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.app-description.expanded {
    display: block;
    overflow: visible;
}

/* Quiet: it sits inside a paragraph of body text, and a filled button there would outrank the
   Download control that is the point of the card. */
.app-description-toggle {
    margin-top: 4px;
    padding: 0;
    background: none;
    border: none;
    color: var(--accent);
    font-size: 13px;
}

.app-description-toggle:hover { text-decoration: underline; }

/* Compact enough to sit in a table cell without stretching the row. */
.role-select {
    width: auto;
    /* Same drawn chevron, pulled in to match the smaller cell. The shorthand has to restate the
       right padding or it would reset what the `select` rule reserves for the arrow. */
    padding: 4px 26px 4px 8px;
    background-position: right 9px center;
    font-size: 12px;
}

.badge-admin {
    color: var(--accent);
    background: #16251c;
    border: 1px solid #1c3d29;
}

/* Destructive, so it reads as such only on approach — a row of permanently red buttons trains
   people to ignore red. Not when disabled: a button that lights up under the cursor and then does
   nothing reads as broken rather than as refused. */
.danger:hover:not(:disabled) {
    color: var(--error);
    border-color: #4a2226;
}

/* The add-account form sits above its own table, so it needs to be compact rather than a page of
   stacked fields. */
.inline-form {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

.inline-form .field {
    flex: 1 1 220px;
    margin-bottom: 0;
}

.inline-form button {
    /* Aligns with the inputs rather than the labels above them. */
    margin-top: 25px;
    width: auto;
}

/* A segmented switch in the row: its segments are buttons too, and the rule above would push each
   of them down inside the pill. The pill itself sits level with the input beside it. */
.inline-form .role-switch { margin: 32px 0 0 0; flex: 0 0 auto; }
.inline-form .role-option { margin-top: 0; }

/* A checkbox in the row sits level with the input beside it and shares the row with it; its hint
   wraps under the box rather than stretching the box to the hint's length. */
.inline-form .checkbox-field { flex: 1 1 200px; margin-top: 30px; }
.inline-form .checkbox-field .hint { flex-basis: 100%; }

/* Attribution under an artifact name in the all-artifacts table. Quieter than the name, and it
   sits above the badge so the row reads name / who / warning. */
.artifact-publisher {
    margin-top: 2px;
    font-size: 12px;
    color: var(--muted);
}

.modal-crop { max-width: 540px; }

/* Title on the left, dismiss on the right, like the picker this was modelled on. */
.crop-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.crop-head .modal-title { margin: 0; }

/* Buttons here are full-width by default, which is right for the one that commits and absurd for
   an X. */
.crop-close {
    width: auto;
    display: flex;
    padding: 6px;
    background: transparent;
    color: var(--muted);
    border-radius: 8px;
}

.crop-close:hover { color: var(--text); background: var(--bg); }

/* Grab cursor because the whole picture is draggable, and no text selection to fight the drag. */
.crop-stage {
    display: block;
    margin: 0 auto 10px;
    border-radius: 6px;
    cursor: grab;
    user-select: none;
    max-width: 100%;
    /* Without this the width is capped while the height keeps its attribute, and the picture is
       squashed — the crop would then not be the square it appears to be. */
    height: auto;
    /* The drag is pointer events with capture; without this the browser claims the gesture for
       panning first and the selection never moves on a touch screen. */
    touch-action: none;
}

/* Carries the whole gap down to the button now that the zoom slider is gone from between them. */
.modal-crop .hint { margin: 0 0 18px; text-align: center; }

/* Full width and last, so the dialog ends in the one thing it is asking for. */
.crop-apply { margin: 0; }

/* An empty slot says what belongs in it. A dashed outline alone reads as a rendering fault as
   easily as an invitation, and it is the one state a first-time publisher always sees. */
.artifact-icon-blank::after,
.listing-icon-blank::after,
.organisation-logo-blank::after {
    content: "";
    position: absolute;
    inset: 22%;
    border-radius: 2px;
    /* A mountain-and-sun glyph, the conventional "picture goes here". Inline so it needs no asset
       and inherits nothing that could fail to load. */
    background: var(--muted);
    opacity: 0.35;
    transition: opacity 0.15s;
    -webkit-mask: var(--placeholder-mask) center / contain no-repeat;
    mask: var(--placeholder-mask) center / contain no-repeat;
}

.artifact-icon-blank,
.listing-icon-blank,
.organisation-logo-blank { position: relative; }

/* An image that is its own upload button. */
.picker-slot-wrap { display: inline-block; }

.picker-slot {
    position: relative;
    overflow: hidden;
    cursor: pointer;
}

.picker-slot-veil {
    position: absolute;
    inset: 0;
    display: flex;
    /* Camera above label, both centred — the shape the reference uses, and the reason the
       interactive slots below are larger than the ones that only display an icon. */
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 6px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    opacity: 0;
    transition: opacity 0.15s;
    /* Above the preview. Both are absolutely positioned and the picture is appended after the veil,
       so without this the picture paints over it — and a slot that already had a logo in it was the
       one slot with no hover state at all, which is precisely the slot you hover to replace one. */
    z-index: 1;
}

.picker-slot-label {
    font-size: 11px;
    font-weight: 600;
    line-height: 1.15;
    text-align: center;
}

/* Only where the slot *is* the control. The 28px icon in a table row is not clickable and has no
   room for any of this. */
.organisation-logo.picker-slot,
.listing-icon.picker-slot {
    width: 112px;
    height: 112px;
}

.picker-slot:hover .picker-slot-veil,
.picker-slot:focus-visible .picker-slot-veil { opacity: 1; }

/* The empty-slot placeholder sits behind the veil and showed through it, so the camera landed on
   top of a second, larger picture glyph. It has done its job once the veil is up. */
.picker-slot:hover::after,
.picker-slot:focus-visible::after { opacity: 0; }

.picker-slot:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.picker-slot-glyph {
    display: flex;
    color: #fff;
}

/* The slot holds a preview image absolutely, so the veil stays on top of it. */
.picker-slot img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The development sign-in. Deliberately plain: it is not a feature, it is a hole held open on
   purpose for a local checkout, and the warning above it does the talking.

   One block rather than two children, because the slot it goes in is the centring flex row Google's
   fixed-width iframe needs — two children there sit side by side. */
.dev-signin { width: 100%; }

.dev-signin .status { margin: 0 0 12px; }

.dev-signin-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.dev-signin-input { width: 100%; }

.search { margin-bottom: 14px; }

/* Cancels WebKit's native decorations so it matches every other input on the page. */
.search-input {
    width: 100%;
    appearance: none;
    -webkit-appearance: none;
}

.search-input::-webkit-search-decoration,
.search-input::-webkit-search-results-button { display: none; }

/* The organisation logo: a fixed square whatever was uploaded, so the form does not jump when a
   preview appears and the review queue's rows stay aligned. */
.organisation-logo {
    width: 64px;
    height: 64px;
    border-radius: 12px;
    overflow: hidden;
    flex-shrink: 0;
    background: var(--bg);
}

.organisation-logo-blank {
    border: 1px dashed var(--border);
}

.organisation-logo-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.organisation-detail {
    display: flex;
    gap: 16px;
    /* Top, not centre: the name is the first line of the text block, so it should sit level with
       the top of the logo rather than floating against its middle. */
    align-items: flex-start;
    margin-top: 20px;
}

/* Pushed to the far end of the row, so the button that edits the organisation sits beside it rather
   than stacked underneath the logo. */
.organisation-detail-actions { margin-left: auto; }

/* The text is what gives up room to the button beside it, rather than the row widening. */
.organisation-detail-text { min-width: 0; }

/* Cancels the block's own leading so the first line's cap-height lines up with the logo's edge. */
.organisation-detail-text { margin-top: -2px; }

.organisation-detail-name {
    margin: 0 0 4px;
    font-size: 15px;
    font-weight: 600;
}

.organisation-detail-text p { margin: 0 0 4px; }

/* The organisation form: the logo, its button and the hint were touching, and the submit button sat
   directly against the hint above it. */
.organisation-form-logo {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 8px 0 6px;
}

.organisation-form .primary { margin-top: 20px; }

/* A settled, unremarkable state. The base .badge sets no colours, so without this a rejected
   organisation renders as bare text in a column where every other value is a pill. */
.badge-muted {
    color: var(--muted);
    background: var(--bg);
    border: 1px solid var(--border);
}

.badge-warn {
    color: #fbbf24;
    background: #2a2213;
    border: 1px solid #4a3a16;
}

/* Sold rather than free. Blue so it cannot be read as either neighbour: green is the edition
   badge, amber is "Restricted" — and paid is neither a build nor a warning. */
.badge-paid {
    color: #7dd3fc;
    background: #0f2530;
    border: 1px solid #164e63;
}

/* A badge in a catalog cell wants the cell's left edge, under the column's heading — not the 8px it
   carries for sitting inline after a run of text. Two shapes need the reset and one selector cannot
   reach both: stacked under a name it is the cell's own child, and alone as the whole cell it sits
   inside the flex line that centres it. */
.catalog td > .badge,
.catalog .cell-line > .badge {
    margin-left: 0;
}

/* Stacked under a name, the space the 8px used to give it horizontally is wanted above instead. */
.catalog td > .badge { margin-top: 7px; }

/* Toasts. Anchored to the viewport, because the control that triggers one is often at the bottom of
   a long card while that card's status line is at the top. */
.toast-host {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 50;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    /* The host is a full-width flex column; only the toasts inside it should catch a click. */
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    max-width: min(90vw, 460px);
    padding: 12px 16px;
    border-radius: var(--radius);
    font-size: 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    /* Lifted off the page, because it is not part of it. */
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    word-break: break-word;
    animation: toast-in 0.15s ease-out;
}

.toast-ok {
    color: var(--accent);
    border-color: #1c3d29;
    background: #16251c;
}

.toast-error {
    color: var(--error);
    border-color: #4a2226;
    background: #2a1c1e;
}

.toast-leaving {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 0.2s, transform 0.2s;
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

/* Confirmation dialogs. Native <dialog>, so the backdrop, the top layer and Escape are the
   browser's job and only the surface is styled here. */
.modal {
    width: calc(100vw - 48px);
    max-width: 420px;
    padding: 22px 24px 18px;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* A dialog is only as tall as the screen it opens on. The cropper is the case that forced this:
   its canvas is sized in pixels and scaled down by `max-width`, which bounds the width and does
   nothing for the height — so a portrait picture pushed the head, the hint and the commit button
   past the bottom of a phone, with no way to scroll to them. `dvh` because the mobile URL bar
   changes the viewport as it hides; `vh` first so a browser without `dvh` still gets a bound. */
.modal {
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
}

.modal::backdrop { background: rgba(0, 0, 0, 0.6); }

.modal-title { font-size: 17px; margin: 0 0 8px; }

.modal-message {
    margin: 0;
    font-size: 14px;
    color: var(--muted);
    /* Email addresses and APK filenames are long and must wrap rather than widen the dialog. */
    word-break: break-word;
}

/* A dialog that explains says it in a few paragraphs, and may name somebody to write to. */
.modal-message + .modal-message { margin-top: 10px; }
.modal-message a { color: var(--accent); }

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 22px;
}

/* .primary is full-width for the forms it was written for; in a button row it is not. */
.modal-actions button { width: auto; }

/* Only ever the confirm button of a destructive dialog — the one place a permanently red button is
   earned, because by then the question has already been asked. */
.destructive {
    background: var(--error);
    color: #2a0d0f;
}

.hidden { display: none; }

.progress {
    height: 6px;
    background: var(--bg);
    border-radius: 3px;
    overflow: hidden;
    margin-bottom: 14px;
}

.progress-bar {
    height: 100%;
    width: 0;
    background: var(--accent);
    transition: width 0.15s linear;
}

.catalog {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.catalog th, .catalog td {
    text-align: left;
    padding: 9px 10px;
    border-bottom: 1px solid var(--border);
    /* Centred on the row rather than sitting on its top edge: with a 40px icon in the first cell
       and one line of text in the rest, `top` alignment leaves every other column floating above
       the name it belongs to. */
    vertical-align: middle;
}

.catalog th {
    color: var(--muted);
    font-weight: 500;
}

.catalog tr:last-child td { border-bottom: none; }

.mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
}

.catalog-body { overflow-x: auto; }

/* A row's controls, hard against the right edge — the column has no heading, so its left edge is
   not a place anything lines up to. Shared by every `.catalog` table: plugins, accounts, organisations.

   `width: 1%` with `nowrap` is the shrink-to-fit idiom: a percentage the content overrides, which
   leaves the table's auto layout handing every spare pixel to the columns on the left instead of
   padding this one. Without it the cell takes a share of the slack and `text-align: right` only
   reaches the right edge of an over-wide cell, which is not the right edge of the table.
   `nowrap` because these tables already scroll horizontally inside `.catalog-body`; wrapping two
   short buttons onto two lines would make every row taller to save a few pixels of width. */
.catalog td.actions-end {
    width: 1%;
    text-align: right;
    white-space: nowrap;
}

/* `text-align` does not reach the children of a flex container, so a cell holding a `.row-actions`
   row needs telling separately — otherwise the cell moves right and the buttons inside it do not. */
.catalog td.actions-end .row-actions { justify-content: flex-end; }

/* A withheld row is still a row: legible, and not styled as an error — hiding is a decision
   somebody made, not a fault. Dimming says "not currently offered" without a second badge, which
   the name cell already carries. */
.row-hidden td { opacity: 0.55; }
.row-hidden td .badge { opacity: 1; }

/* A lone badge in a table cell, centred against the rest of the row.
   `vertical-align: middle` on the cell is not enough: on an inline-block it means "middle of the
   parent's x-height", not the middle of the row, which left the pill 3.5px low beside the name. A
   flex line centres it on the cell it actually occupies. */
.catalog .cell-line {
    display: flex;
    align-items: center;
    min-height: 40px;
}

/* Bigger than a plugin's icon in the same table: an organisation's logo is its identity, and at
   28px a photographic one is unreadable. The row is a line of text tall either way, so this sets
   the row height rather than fitting inside it. */
.catalog .organisation-icon {
    width: 40px;
    height: 40px;
    border-radius: 8px;
}

/* An organisation's name in the queue. Underlined only on hover, because a table of underlined
   names reads as a list of warnings. */
.org-name-link {
    color: inherit;
    text-decoration: none;
}

.org-name-link:hover { text-decoration: underline; }

/* Logo on the left, the name and its facts beside it. */
.org-identity {
    display: flex;
    gap: 16px;
    align-items: flex-start;
}

/* The name and its verdict on one line, centred on each other.
   `baseline` was the typographically correct answer and the wrong-looking one: it aligns the badge's
   text with the name's, which leaves the pill drawn around that text sitting low — measured 2.5px
   below the name's centre. A pill is a box, so it centres like one. */
.org-title {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.org-title h2 { margin: 0; }

/* Full width under the header rather than in the column beside the logo: it is prose, and prose in
   what is left of a row next to a 72px logo wraps every few words. */
.org-about {
    margin-top: 16px;
    max-width: 70ch;
}

/* Sized to the three lines beside it — name, contact and creator come to 76px together.
   A fixed square rather than `align-self: stretch` with `aspect-ratio`, which looks like the
   derived answer and is not one: stretch gives the height, but the ratio will not resolve a width
   from it on a block element, so the logo collapses to its border. Squareness matters more here
   than tracking a block whose height only varies when a contact address is missing. */
.org-icon {
    width: 76px;
    height: 76px;
    border-radius: 12px;
    object-fit: cover;
    flex: none;
}

/* Takes the slack, which is what pushes Edit to the right edge — without it the button sits
   against the facts and reads as part of the identity rather than an action on it. */
.org-facts {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    flex: 1;
}

/* An organisation's lists, under its identity: links, one per screen, drawn like the top bar's tabs
   so the two read as the same kind of thing. One scrolling row on a phone rather than two wrapped
   ones, for the reason the top bar gives. */
.org-tabs {
    display: flex;
    gap: 4px;
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    overflow-x: auto;
    scrollbar-width: none;
}

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

.org-tabs .nav-item { flex-shrink: 0; }

/* The way back from one group or licence to its list: quiet, above what it leaves. */
.back-link {
    display: inline-block;
    margin-bottom: 12px;
    font-size: 13px;
    color: var(--muted);
    text-decoration: none;
}

.back-link:hover { color: var(--text); }

/* The members list: the address as wide as it is, the rights beside it taking what is left, and the
   buttons at the end — rather than the three sharing the width and Rights starting mid-row. Not on a
   phone, where the rows are cards. */
@media (min-width: 641px) {
    .members-table th:first-child, .members-table td:first-child,
    .members-table th:last-child, .members-table td:last-child { width: 1%; white-space: nowrap; }
    /* A column as narrow as its content wraps whatever may wrap: the badge beside an address. */
    .members-table .org-member-who { flex-wrap: nowrap; }
}

/* One member's page: who they are, then what they may do here and the groups they are in. */
.member-head { flex-wrap: wrap; gap: 12px; }
.member-who { display: flex; align-items: center; flex-wrap: wrap; min-width: 0; }
.member-who h2 { margin: 0; overflow-wrap: anywhere; }
.member-section { margin-top: 24px; }
.member-section h3 { margin: 0 0 8px; font-size: 14px; font-weight: 600; color: var(--muted); }
.member-section .hint { margin-top: 8px; }
.member-groups { margin: 0; padding-left: 18px; }
.member-groups li { padding: 2px 0; }

/* Two controls in a card header, which `row-between` alone would push apart. */
.row-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* The icon sits beside its explanation and the controls that change it, so the row is a column of
   text rather than one line squeezed next to a picture. */
.listing-icon-detail {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

/* Present but empty, so the row keeps its shape before an icon exists. */
.listing-icon-blank {
    border: 1.5px dashed var(--border);
    background: transparent;
}

/* A name in a table that is also the way into its own page. Underlined only on hover, for the
   reason `.org-name-link` gives: a column of underlined names reads as a column of warnings. */
.row-name-link {
    color: inherit;
    text-decoration: none;
}

.row-name-link:hover { text-decoration: underline; }

/* The plugin page's header, laid out like the organisation page's so the two do not have to be
   read differently: the mark, then the lines that name the thing, then the controls opposite. */
/* Wraps at every width, not only on phones. The actions are sized by their labels, and Ukrainian
   ones run far longer than English — without this they ran off the card's edge on a desktop. */
.plugin-head {
    display: flex;
    gap: 16px;
    align-items: flex-start;
    flex-wrap: wrap;
}

.plugin-icon {
    width: 76px;
    height: 76px;
    flex-shrink: 0;
    border-radius: 14px;
    object-fit: cover;
    background: var(--bg);
}

/* Keeps the name readable before the actions are allowed to share its line; below that they wrap. */
.plugin-facts { flex: 1 1 280px; min-width: 0; }

.app-contact { overflow-wrap: anywhere; }

/* Says which kind of page this is, above the name that says which one. Without it a plugin's page
   and an organisation's open identically and the only difference is what the words happen to be. */
.plugin-kicker {
    display: block;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: 2px;
}

/* Centred rather than baseline-aligned, for the reason `.org-title` gives: a badge is a box. */
.plugin-title {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 4px;
}

.plugin-title h2 { margin: 0; }
.plugin-title .badge { margin-left: 0; }

/* Top right, opposite the name — where the organisation page puts its Edit. */
.plugin-actions { flex-shrink: 0; }

/* Why a plugin of another of the caller's organisations has no controls: a line of its own under
   the header. */
.work-as {
    flex-basis: 100%;
    margin: 0;
}

/* Measured rather than full-width: prose set the width of a card wraps at a line length nobody
   wants to read down. */
.plugin-about {
    margin: 0;
    max-width: 70ch;
    white-space: pre-wrap;
}

/* The build facts. Two columns rather than a table: there is one row per fact and nothing to
   compare down a column, so a table's header row would be a heading over a single value. */
.plugin-fact-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 10px 24px;
    font-size: 13px;
}

.plugin-fact { display: contents; }

.plugin-fact-label { color: var(--muted); }

/* Wraps rather than stretching the column: a long list of device classes should not push the
   labels off to the left of a wide, mostly empty grid. */
.plugin-fact-value { overflow-wrap: anywhere; }

/* Read-only screenshots: the same strip the listings editor uses, without the controls it needs.
   The frame is the link, so the whole thumbnail is the target rather than the picture inside it. */
a.shot {
    display: block;
    text-decoration: none;
}

a.shot:hover { border-color: var(--accent); }

/* ---- Access requests ---------------------------------------------------------------------
   Cards rather than table rows: a request carries an address, a name, a unit, a status, two dates,
   a free-form note and three controls, which is more than a table can fit on a laptop without
   scrolling sideways and clipping the two things that matter most. */

/* The scroller. Bounded so the queue cannot push the page's own footer -- and the search field
   above it -- off the bottom of the screen: a backlog of forty requests made the one control for
   narrowing it unreachable without scrolling past all forty. `dvh` for the mobile URL bar, `vh`
   first so a browser without it still gets a bound. */
/* Under whatever it pages, centred, and the same on a table as on a list of cards — the two
   screens differ in what a row is, not in how you get to the next page. */
.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin-top: 16px;
    flex-wrap: wrap;
}

/* A single page draws no controls at all, so the element is left empty rather than absent — see
   `paged`. Without this it would still contribute its margin and push the page around. */
.pager:empty { display: none; }

/* Square, because the content is a number and a number has no natural width — sized off the text
   these would be as wide as their digits and "1" would be half the size of "10". */
.pager-page,
.pager-arrow {
    width: 38px;
    height: 38px;
    padding: 0;
    display: grid;
    place-content: center;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
    /* Tabular figures so the buttons do not resize as the digits change under them. */
    font-variant-numeric: tabular-nums;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}

.pager-page:hover:not(.pager-page-current),
.pager-arrow:hover:not(:disabled) {
    border-color: var(--accent);
    color: var(--accent);
}

/* Where you are, not somewhere to go. Filled rather than outlined: at a glance the eye is looking
   for one solid mark in a row of identical outlines. */
.pager-page-current {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
    cursor: default;
}

/* The arrows carry a glyph rather than a word, so they need to be a size that is comfortable to
   hit rather than a size that fits "‹". */
.pager-arrow { font-size: 18px; line-height: 1; color: var(--muted); }
.pager-arrow:disabled { opacity: 0.4; }

/* Not a button: there is nothing behind it to press. Sized like one so the row stays even. */
.pager-gap {
    width: 38px;
    height: 38px;
    display: grid;
    place-content: center;
    color: var(--muted);
    font-size: 14px;
    user-select: none;
}

.requests-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 16px;
}

.request-card {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 16px 18px;
    /* Cards do not shrink to fit the scroller; a long note scrolls the list, not the card. */
    flex-shrink: 0;
}

.request-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}

/* min-width:0 so a long address wraps inside its column instead of widening the flex row and
   pushing the badge off the card. */
.request-who { min-width: 0; }

.request-name {
    font-size: 15px;
    font-weight: 600;
    color: var(--text);
}

.request-email {
    font-size: 13px;
    color: var(--muted);
    margin-top: 2px;
    word-break: break-all;
}

.request-head .badge { margin-left: 0; flex-shrink: 0; }

/* Wraps to as many lines as it needs. A fixed grid would leave a hole on rows with no decider. */
.request-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 20px;
    margin-top: 12px;
}

.request-fact { font-size: 13px; }

.request-fact-label {
    color: var(--muted);
    margin-right: 6px;
}

.request-fact-value { color: var(--text); }

/* Inset, so the person's own words are visibly theirs and not the panel's description of them. */
.request-note {
    margin-top: 12px;
    padding: 10px 12px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

.request-note-label {
    font-size: 12px;
    color: var(--muted);
    margin-bottom: 4px;
}

.request-note-body {
    margin: 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
    /* They wrote the line breaks; collapsing them turns a structured explanation into a paragraph. */
    white-space: pre-wrap;
    word-break: break-word;
}

/* The role choice, as segments rather than a dropdown -- the same control the topbar's language
   switch uses. With three short options a <select> hides two thirds of the answer behind a click,
   and this one has to say what Approve is about to do. */
.role-switch {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    /* Pushed left so the segments group with the label idea and the two verbs stay together on the
       right. `auto` rather than a gap, so it collapses to nothing once the row wraps. */
    margin-right: auto;
}

.role-option {
    /* Overrides the base button padding, which is sized for a real button and would make this
       control taller than the Approve beside it. */
    padding: 6px 12px;
    background: none;
    border: none;
    border-radius: 999px;
    color: var(--muted);
    font-size: 13px;
    line-height: 1;
    white-space: nowrap;
}

.role-option:hover { color: var(--text); }

/* Selected is the accent, matching Approve. The two do read as one control at a glance, which is
   the trade taken deliberately: the segment says what Approve will grant, so tying them by colour
   says they belong together. The dark accent text is what keeps it legible on the green. */
.role-option-active {
    background: var(--accent);
    color: var(--accent-text);
    font-weight: 600;
}

/* Or the hover rule above would grey the selected one back out on the way to clicking it. */
.role-option-active:hover { color: var(--accent-text); }

/* Right-aligned and wrapping: on a narrow card the three controls stack into rows rather than
   being squeezed, which is what the table could not do. */
.request-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 8px;
    margin-top: 14px;
}

.request-actions button { width: auto; }

@media (max-width: 640px) {
    /* Full width each, one per line: at 375px three controls on one row leaves each too narrow to
       read, and Approve/Reject are the two presses that must not be mis-hit. */
    .request-actions > * {
        flex: 1 1 100%;
        width: 100%;
    }

    /* ...except the segments, which are one control and must not become a full-width bar with
       three stretched pills in it. `margin-right: auto` would also fight the centring here. */
    .request-actions .role-switch {
        justify-content: center;
        margin-right: 0;
    }

    .role-option { flex: 1 1 0; justify-content: center; }

    .request-head { flex-direction: column; }
}

/* ---- Screenshot viewer -----------------------------------------------------------------
   A horizontal snapping track rather than a single swapped <img>: scrolling it *is* the
   navigation, so a trackpad swipe and a touch drag need no handler of their own. See Lightbox.kt. */
.lightbox {
    position: fixed;
    inset: 0;
    /* Above the topbar, which is sticky and would otherwise sit over the overlay. */
    z-index: 100;
    background: rgba(6, 8, 10, 0.94);
    display: flex;
}

/* Focus lives on the overlay so the keys reach it; the ring around the whole screen is noise. */
.lightbox:focus { outline: none; }

.lightbox-track {
    display: flex;
    width: 100%;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    /* The buttons and the counter are positioned over this; a scrollbar under them would be one
       more thing between a click and the picture. */
    scrollbar-width: none;
}

.lightbox-track::-webkit-scrollbar { display: none; }

/* `flex: 0 0 100%` is what makes one slide one page, which is what `scrollLeft / clientWidth`
   in Lightbox.kt counts in. Changing this to fit two across would silently break the counter. */
.lightbox-slide {
    flex: 0 0 100%;
    scroll-snap-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 64px 24px;
    /* Every part of the slide except the picture closes the viewer. */
    cursor: zoom-out;
}

.lightbox-image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: var(--radius);
    cursor: default;
}

.lightbox-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 18px;
    /* Above the track, or the slide's own click handler would take these presses. */
    z-index: 1;
}

.lightbox-counter {
    font-size: 13px;
    color: var(--muted);
    /* Digits change width as the number changes; without this the counter shifts as you scroll. */
    font-variant-numeric: tabular-nums;
}

.lightbox-button {
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(28, 33, 38, 0.85);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: 999px;
    /* The glyphs are SVG — see `Lightbox.kt` — because the text characters sat off-centre. */
    line-height: 0;
}

.lightbox-button svg { display: block; }

.lightbox-button:hover { border-color: var(--accent); }

.lightbox-close {
    width: 38px;
    height: 38px;
    padding: 0;
}

.lightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    padding: 0;
    z-index: 1;
}

.lightbox-prev { left: 16px; }
.lightbox-next { right: 16px; }

/* Set on <body> while the viewer is open. Without it the page behind scrolls under the overlay —
   most visibly on a phone, where the drag that pages the track carries it along. */
.no-scroll { overflow: hidden; }

@media (max-width: 640px) {
    /* The arrows are for a mouse. On a phone the gesture is the drag, and two 44px targets over a
       screenshot cover the parts of it somebody opened the viewer to read. */
    .lightbox-nav { display: none; }

    .lightbox-slide { padding: 56px 12px; }
}

/* An anchor that acts as a button.
   Navigation, not submission — publishing the next build of a plugin has an address worth copying,
   and middle-click should open it in a tab. `button` styles a tag rather than a class, so a link
   playing that role has to be told the whole thing separately. */
a.button {
    display: inline-block;
    padding: 9px 18px;
    border: 1px solid transparent;
    border-radius: 6px;
    font: inherit;
    font-weight: 600;
    /* Inherited, not 1.2. A real `button` takes the body's line-height through `font: inherit`, so
       a hard 1.2 here made every anchor-button 4.5px shorter than the button beside it -- visible
       wherever the two share a row, which is most places one is used. */
    line-height: inherit;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
}

/* `.primary` carries `width: 100%` for a form's submit control, which is not what a link in a row
   of actions wants. Two classes out-specify the one that set it. */
a.button.primary,
a.button.secondary {
    width: auto;
}

a.button.primary {
    background: var(--accent);
    color: var(--accent-text);
}

a.button.secondary {
    background: transparent;
    color: var(--text);
    border-color: var(--border);
}

/* One line per published build: what it is, how big, and when it landed. A table would give three
   headings to three self-evident values and force the widths to agree with the tables above. */
.plugin-versions {
    display: flex;
    flex-direction: column;
}

.plugin-version {
    display: grid;
    grid-template-columns: 1fr max-content max-content;
    gap: 16px;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    font-size: 13px;
}

.plugin-version:last-child { border-bottom: none; }

.plugin-version-name {
    display: flex;
    align-items: center;
    gap: 10px;
}

.plugin-version-name .badge { margin-left: 0; }

/* Widths of their own, the same in every row: each row is a grid of its own, so a longer date
   pushed its row's size out of line with the rest. Wide enough for "999.9 MB" and "28 Sept 2026" —
   "28 серп. 2026 р." in Ukrainian — and the size kept further from the date than the gap. */
.plugin-version-size {
    min-width: 5.5em;
    margin-right: 16px;
    text-align: right;
    white-space: nowrap;
}

.plugin-version-date {
    min-width: 6.75em;
    text-align: right;
    white-space: nowrap;
}

:lang(uk) .plugin-version-date { min-width: 8.25em; }

/* A version that came with a release note. The border moves from the row to the wrapper, so it
   sits under the note rather than between the row and its own body. */
.plugin-version-entry { border-bottom: 1px solid var(--border); }

.plugin-version-entry:last-child { border-bottom: none; }

.plugin-version-entry > .plugin-version { border-bottom: none; }

/* The row itself is the control. `list-style` removes the default marker everywhere but WebKit,
   which needs telling separately. */
.plugin-version-entry > summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.plugin-version-entry > summary::-webkit-details-marker { display: none; }

.plugin-version-entry > summary:hover .plugin-version-caret { color: var(--text); }

/* Its own column rather than a marker on the version number: every row reserves the width, so the
   versions line up whether or not their build came with a note. */
.plugin-version-caret {
    flex: none;
    width: 10px;
    color: var(--muted);
}

.plugin-version-entry > summary .plugin-version-caret::before { content: "▸"; }

.plugin-version-entry[open] > summary .plugin-version-caret::before { content: "▾"; }

/* Indented past the caret, and wrapped as typed — the line breaks in a release note are the note. */
.plugin-changelog {
    padding: 0 0 12px 20px;
    color: var(--muted);
    white-space: pre-wrap;
}

/* ---- Public storefront ------------------------------------------------------------------
   The one surface people who have never signed in ever see, so it is wider and less dense than
   the panel: a grid of cards rather than the panel's tables, and room for a description. */

.store { max-width: 1040px; }

.store-hero {
    padding: 16px 0 4px;
    border-bottom: 1px solid var(--border);
}

/* Larger than .muted, which is sized for the panel's dense captions. This is the first sentence
   somebody reads about the product and should not be the smallest text on the page. */
.store-hero-text {
    font-size: 15px;
    max-width: 60ch;
}

.store-section { display: flex; flex-direction: column; gap: 4px; }

.store-section-title {
    font-size: 18px;
    margin: 0;
}

.store-section-intro { margin: 0 0 12px; }

/* auto-fill rather than auto-fit: with one card auto-fit stretches it to the full width, which
   makes a single published plugin look like a banner. */
.store-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 16px;
}

/* A column so the action sits at the bottom edge regardless of how long the description is —
   otherwise buttons in a row of cards land at different heights. */
.store-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 20px;
}

.store-card-head {
    display: flex;
    align-items: center;
    gap: 12px;
}

.store-card-title { min-width: 0; }

/* The badge takes its own line under the name, the way the detail page's does. Without this it
   keeps the base 8px left margin and reads as a stray indent rather than a label. */
.store-card-title .badge { margin-left: 0; }

.store-icon {
    width: 48px;
    height: 48px;
    border-radius: 10px;
    flex-shrink: 0;
    object-fit: cover;
}

/* Clamped rather than scrolled: a card is a summary, and a long description that pushes the
   download button off the bottom of the grid cell defeats the point of the grid. */
.store-card-description {
    margin: 0;
    font-size: 13px;
    color: var(--muted);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* margin-top:auto is what pins the button to the bottom of the flex column. */
.store-card-action { margin-top: auto; padding-top: 4px; }

/* Dimmed, not disabled. The placeholder's button works — it leads to the same form — so the card
   must not look inert; it only needs to read as less solid than a real listing. */
.store-card-placeholder { border-style: dashed; }

/* The challenge sits between the last field and the submit, which is where a visitor's eye already
   is. Empty until the widget arrives, and it reserves no height when a deployment has no CAPTCHA. */
.register-challenge:not(:empty) { margin: 4px 0 18px; }

/* `overflow: hidden` is load-bearing on a narrow phone, not tidiness. Turnstile will not lay out
   below 300px, so on a screen that cannot give it that the widget is scaled by `fitNarrow` — a
   transform leaves the layout box at its full 300px, and without this the box still pushes the
   page sideways. At that scale the painted widget is narrower than the clip, so nothing is cut. */
.register-challenge { overflow: hidden; }

/* Centred horizontally and no more. Vertical centring would need a viewport height, and there is a
   topbar above this — see RegisterView. */
.register-screen {
    display: flex;
    justify-content: center;
    padding: 48px 24px;
}

.register-form { margin-top: 16px; }

/* A stack rather than a row: on a phone two buttons side by side wrap into something that looks
   like a mistake, and there is no reason for them to be horizontal on a desktop either. */
.register-alternatives {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.register-alternatives p { margin: 0 0 4px; }

/* The quietest button there is — for "back", which is always available and never the thing the
   screen is asking you to do. */
.ghost {
    background: transparent;
    color: var(--muted);
    border-color: transparent;
}

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

/* ---- Storefront detail page ------------------------------------------------------------- */

/* The clickable region of a card. A pointer on the whole card would promise the button is a link
   too; this stops above it. */
.store-card-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    cursor: pointer;
}

.store-card:hover { border-color: var(--accent); }

.store-hero-card { display: flex; flex-direction: column; gap: 14px; }

/* Wraps so the action, top right on a wide screen, drops under the name on a narrow one instead of
   squeezing it. */
.store-detail-head {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    flex-wrap: wrap;
}

.store-detail-icon {
    width: 72px;
    height: 72px;
    border-radius: 14px;
    flex-shrink: 0;
    object-fit: cover;
}

.store-detail-title { flex: 1 1 240px; min-width: 0; }

.store-detail-title h1 {
    font-size: 22px;
    margin: 0 0 6px;
}

/* A name and its tags. One wrapping row where there is room — the tags sit beside the name — and
   a stack on a phone, where a tag beside a long name wrapped into the publisher line and read as
   part of the name. Shared by the store's cards and page and the panel's app page. */
.title-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 10px;
}

.store-detail-title .title-row { margin-bottom: 6px; }
.store-detail-title .title-row h1 { margin: 0; }

.title-tags {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
}

.title-tags .badge { margin: 0; }
.title-tags:empty { display: none; }

@media (max-width: 640px) {
    .title-row {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
    }
}

.store-detail-action {
    flex: 0 0 220px;
    margin-left: auto;
}

.store-detail-action .primary { width: 100%; }

/* On a phone it has a line of its own anyway, so it takes the whole of it — a thumb-sized target
   rather than a button stranded at the right edge under the name. */
@media (max-width: 640px) {
    .store-detail-action {
        flex-basis: 100%;
        margin-left: 0;
    }
}

/* Left-aligned and quiet — it is a way out, not the thing the page is offering. */
.store-back {
    align-self: flex-start;
    padding-left: 0;
}

.plugin-changelog-text {
    margin: 0;
    color: var(--muted);
    white-space: pre-wrap;
}

/* The app's name on a Host apps card, which is also the way to its page. Styled as the heading it
   already was rather than as a link — the whole card reads as one thing, and an underlined blue
   name inside a card of plain text is louder than the Download button beside it. */
.app-name-link {
    color: inherit;
    text-decoration: none;
}

.app-name-link:hover { text-decoration: underline; }

/* ---- Phone ------------------------------------------------------------------------------
   The 1100px breakpoint above is about the topbar running out of room. This one is about the
   screens themselves: at 375px a card header holding a heading and two buttons has roughly 310px
   of usable width, and flexbox spends it on the buttons — they carry `white-space: nowrap`, so the
   heading is what shrinks. "Host apps" was measured at 39px wide with the controls at 271px,
   which is the overlap it looks like. */
@media (max-width: 640px) {
    /* Side padding is 48px of a 375px screen. Still generous at 32px, and it buys back a word
       per line in every description on every screen. */
    .content { padding: 16px; }

    .card { padding: 18px 16px; }

    .topbar { padding: 12px 16px; row-gap: 10px; }
    .topbar h1 { font-size: 16px; }

    /* ---- One row ------------------------------------------------------------------------
       [menu] wordmark ............ [account]

       The tabs are behind the menu button, in a drawer, as on anything narrower than the full
       bar — see "The tabs where the bar cannot hold them" above. The address, the organisation,
       the language and signing out live behind the account button.

       `order`, rather than a grid: the bar's children are a flat list that varies, and named
       grid areas would leave an unplaced item to auto-place itself somewhere surprising. */
    .account {
        order: 1;
        /* Pushes it to the end of the first row, whatever the wordmark's width turns out to be. */
        margin-left: auto;
    }

    /* The glyphs alone: the organisation is the first thing the panel says. The padding is the
       hit area, not decoration — a 20px glyph is well under a comfortable target. */
    .account-name { display: none; }
    .account-button { min-width: 44px; min-height: 44px; justify-content: center; }

    /* `flex: 1` on a wrapped bar would hold a whole line open on its own. */
    .topbar .spacer { display: none; }

    .topbar .muted,
    .topbar .badge { order: 3; }

    /* The address already truncates; this stops it claiming so much of the row that the toggle
       beside it is pushed onto a line of its own. */
    .topbar .muted { max-width: 55vw; }

    /* Aligned to the end of the address row, mirroring the door above it. */
    .lang-switch {
        order: 4;
        margin-left: auto;
    }

    /* Flags alone in the bar. The two letters were the half of each segment that did not fit,
       and a flag is what the switch exists to show somebody who cannot read the current language.
       The segments grow instead, to a thumb's width. The sign-in card keeps the letters. */
    .topbar .lang-name { display: none; }
    .topbar .lang-option { padding: 8px 9px; }
    .topbar .lang-flag { width: 20px; height: 10px; }

    /* ---- The storefront's bar: one row -----------------------------------------------------
       wordmark ............ [EN|UA] [enter]

       Nothing on this surface needs a second row: there is no address and no tabs, and the
       break the panel forces put the language switch alone on a line under a sign-in button
       that was itself too wide. The wordmark truncates rather than the row growing. */
    .store-topbar::after { display: none; }
    .store-topbar .lang-switch { order: 0; }
    .store-topbar .brand { min-width: 0; }
    .store-topbar .brand h1 { overflow: hidden; text-overflow: ellipsis; }

    .signin {
        min-width: 44px;
        min-height: 44px;
        padding: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
    }

    .signin-glyph { display: inline-flex; }
    .signin-label { display: none; }

    /* The header stacks instead of competing. The heading takes its own line at full width, the
       controls take the next, and they divide it between them rather than one being squeezed to
       nothing. */
    .row-between {
        flex-wrap: wrap;
        gap: 12px;
    }

    .row-between > h2 { flex: 1 1 100%; }

    .row-between .row-actions { width: 100%; }

    /* `flex: 1 1 0` and not `auto`: with `auto` the basis is the label, so "Publish a host app"
       still takes three times the width of "Refresh". Equal columns read as a pair of controls. */
    .row-between .row-actions > * {
        flex: 1 1 0;
        min-width: 0;
        justify-content: center;
        text-align: center;
    }

    /* Both are full-width columns here, so the label has to fit rather than sit on one line. */
    .row-between .row-actions button,
    .row-between .row-actions .button { white-space: normal; }

    /* `stretch`, not the base rule's `center`. Two buttons whose labels wrap to different numbers
       of lines were different heights, and the shorter one floated in the middle of the taller
       one's space -- which reads as a smaller button rather than as a shorter label. */
    .row-actions { align-items: stretch; }



    /* Bound the cropper's picture by height too. `width: auto` keeps the aspect ratio, and the
       pointer maths already reads the rendered box rather than the attribute — see ImageCropper. */
    .crop-stage {
        max-height: 45vh;
        max-height: 45dvh;
        width: auto;
    }

    /* Two buttons at the end of a dialog have room; three, or long labels, do not. */
    .modal-actions button { flex: 1 1 0; }

    /* The card is icon + text + Download on one row, which had ~310px to divide before an icon was
       in it. Stacking gives the description the full width and the button its own line, rather
       than flexbox taking the width out of the text column because the button will not shrink. */
    .app-card {
        flex-direction: column;
        align-items: stretch;
        gap: 14px;
    }

    .app-action button { width: 100%; }
    .app-action { align-items: stretch; }

    /* The reorder gutter has no column to be a gutter of once the card is a stack: it became a
       row of its own above the app, ninety pixels of arrows with nothing beside them. It sits in
       the card's top-right corner instead, one horizontal pair with thumb-sized targets — where
       the panel puts actions elsewhere — and the head leaves it room, so a long name wraps short
       of the arrows rather than under them. Admins alone ever see it. */
    .app-card { position: relative; }

    /* Qualified with the card: the gutter's own rules are declared after this block and would
       otherwise win the tie and keep the arrows in a column. */
    .app-card .app-order {
        position: absolute;
        top: 14px;
        right: 16px;
        flex-direction: row;
        gap: 6px;
    }

    /* 40 wide rather than 44: the pair ends flush with the card's content, so every pixel it
       takes comes off the name beside it, and at 44 "ComBat 4 (DJI)" no longer fit on a line. */
    .app-card .app-order-button {
        width: 40px;
        min-width: 40px;
        min-height: 44px;
        padding: 0;
    }

    /* Only the lines level with the arrows leave them room. Padding the whole head narrowed the
       contact line as well, and it broke an email address in two for no reason — the arrows end
       two lines above it. */
    .app-card:has(.app-order) :is(.title-row, .app-publisher) { padding-right: 94px; }

    .app-icon {
        width: 44px;
        height: 44px;
        border-radius: 10px;
    }

    /* ---- Tables become cards ----------------------------------------------------------
       Every panel table is "an identity, some facts, and an action", which is a row on a laptop
       and a card on a phone. Left as a table it either overflows the card and clips its own
       controls, or scrolls sideways and hides the column the controls are in -- both of which this
       panel did until now, on all four of them.

       The headings move onto the cells as `data-label` (see `catalogTable`), because a stacked
       cell has nothing above it to say what it is. */
    .catalog thead { display: none; }

    .catalog,
    .catalog tbody,
    .catalog tr,
    .catalog td {
        display: block;
        width: auto;
    }

    .catalog tr {
        padding: 14px 16px;
        margin-bottom: 12px;
        background: var(--bg);
        border: 1px solid var(--border);
        border-radius: var(--radius);
    }

    /* Label left, value right, on one line each. `gap` rather than `justify-content` alone so a
       long value wraps against the label instead of underneath it. */
    .catalog td {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 16px;
        padding: 3px 0;
        border: none;
        text-align: right;
    }

    .catalog td::before {
        content: attr(data-label);
        flex-shrink: 0;
        color: var(--muted);
        font-size: 13px;
        text-align: left;
    }

    /* The first cell is the row's identity -- a name, an address, an artifact. It reads as the
       card's title rather than as another labelled fact. */
    .catalog td:first-child {
        justify-content: flex-start;
        text-align: left;
        font-weight: 600;
        padding-bottom: 8px;
    }

    .catalog td:first-child::before { display: none; }

    /* The identity cell is a row here, so the attribution that sits under the name on a laptop
       landed beside it — "gallery release · від «82 Десантно-штурмова»" on one line, wrapping the
       publisher's name in two. It takes the next line, aligned under the title rather than under
       the icon, and the badges stay on the name's line. */
    .catalog td:first-child {
        flex-wrap: wrap;
        align-items: center;
        gap: 4px 10px;
    }

    .catalog td:first-child .artifact-publisher {
        order: 2;
        flex-basis: 100%;
        margin-top: 0;
        /* The icon's 28px and the 10px gap after it. */
        padding-left: 38px;
        font-weight: 400;
    }

    /* The list's rows keep the pill on the name's line even on a phone: the name is one word or
       two beside a small icon, and a second line for one pill would double every row. Centred
       on the icon's box, which the name is centred in too. */
    .catalog .title-row {
        flex-direction: row;
        align-items: center;
        gap: 10px;
    }

    /* A package name is one long unbroken token and would otherwise widen the card past the
       screen -- which is the overflow this whole block exists to remove. */
    .catalog td.mono { overflow-wrap: anywhere; }

    /* The actions cell has no heading, so it gets the full width and its controls spread across
       it rather than huddling against the right edge under an empty label. */
    .catalog td.actions-end {
        /* `width: auto` explicitly: the desktop rule sets `width: 1%` to shrink this column to its
           buttons, and `.catalog td.actions-end` outranks the generic `.catalog td` above -- so
           without this the stacked cell collapsed to 3px and its buttons overhung it.
           `nowrap` goes for the same reason: two full-width buttons stack here, they do not share
           a line, so keeping them on one would only force the card wider than the screen. */
        width: auto;
        white-space: normal;
        padding-top: 10px;
    }

    .catalog td.actions-end > * { flex: 1 1 0; }

    .catalog .row-actions { width: 100%; }

    /* An artifact's page heads with an icon, its identity and two buttons on one row. The buttons
       carry `flex-shrink: 0` -- correct on a laptop, where the identity should give up the width --
       so at 375px the row simply ran off the screen and took the page's horizontal scroll with it.
       Measured at 630px wide inside a 375px viewport.

       Wrapping the actions onto their own line is the same answer the request cards use: the
       identity keeps the first line, the two verbs share the second. */
    .plugin-head { flex-wrap: wrap; }

    /* Or the facts column refuses to go below its longest unbroken word and pushes the icon out. */
    .plugin-facts { min-width: 0; }

    .plugin-actions {
        flex-basis: 100%;
        margin-top: 4px;
        /* The licence line under Download takes a line of its own here. Beside the button it
           shared the row's height with it, and a Download stretched to the height of three lines
           of small print read as a panel, not a button. */
        flex-wrap: wrap;
        align-items: flex-start;
    }

    .plugin-actions > * { flex: 1 1 0; }
    .plugin-actions .licence-notice { flex-basis: 100%; }

    /* A version's size and date go under its number and tag, still in line with the other rows':
       beside them, the two widths squeezed "Waiting for review" onto three lines. Indented past
       the caret. */
    .plugin-version { grid-template-columns: max-content 1fr; row-gap: 4px; }
    .plugin-version-name { grid-column: 1 / -1; }
    .plugin-version-size { margin: 0 0 0 20px; text-align: left; }
    .plugin-version-date { text-align: left; }

    /* 16px, and not for legibility. Safari on iOS zooms the page in whenever a focused field is
       under 16px and never zooms back out, so the first tap on the registration form leaves
       somebody magnified and scrolling sideways through the rest of it. The desktop rules stay at
       14px — the panel's density is built on them. */
    input, select, textarea { font-size: 16px; }
}

/* "Request access" where Download would be, with the line that says what happens next. */
.paid-access {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}

.paid-access .hint {
    margin: 0;
    max-width: 280px;
    text-align: right;
}

/* Checkout names the organisation it buys for rather than offering a choice. */
.checkout-for { display: flex; gap: 8px; align-items: baseline; margin: 4px 0 16px; }
.checkout-for-name { font-weight: 600; }
.checkout-ask { font-weight: 600; margin: 12px 0 4px; }


/* A licence's end, under Download: muted while far off, amber in its last month, red once over. */
.licence-state { margin: 0; font-size: 13px; text-align: right; }
.licence-ending { color: #fbbf24; }
.licence-ended { color: var(--error); }

.badge-ended {
    color: #fca5a5;
    background: #2a1515;
    border: 1px solid #5b2121;
}

/* Choosing a licence's last day: presets first, a date of one's own after, the result under both. */
.licence-picker { grid-column: 1 / -1; }
.licence-picker > label { display: block; margin-bottom: 6px; }
.licence-presets { display: flex; flex-wrap: wrap; gap: 6px; }
/* One box model for every chip, whether it is a <button> or the <label> around the date input:
   a fixed height, and the weight a button gets from the global rule, so the two cannot drift. */
.licence-preset {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: auto;
    height: 34px;
    padding: 0 14px;
    border-radius: 17px;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
}
.licence-preset-active {
    color: #7dd3fc;
    background: #0f2530;
    border-color: #164e63;
}
.licence-preset:hover { border-color: #3d4852; }

/* "Pick a date", as a chip. The real date input lies over it, transparent, so the whole chip is
   the target and keyboard focus lands on something real. */
.licence-date {
    position: relative;
    cursor: pointer;
}
.licence-date-icon { display: inline-flex; color: #7dd3fc; }
.licence-date input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    padding: 0;
    border: 0;
}
.licence-date input::-webkit-calendar-picker-indicator {
    position: absolute;
    inset: 0;
    width: auto;
    height: auto;
    cursor: pointer;
}
.licence-date:focus-within { outline: 2px solid #7dd3fc; outline-offset: 2px; }
.licence-picker .hint { margin-top: 8px; }

.licence-editor { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.licence-editor-actions { display: flex; gap: 8px; margin-top: 12px; }
.licence-editor-actions button { width: auto; }

/* An admin's own grant, above the list it adds to. */
.paid-grant-form {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 12px;
    align-items: end;
    margin-bottom: 20px;
}

.paid-grant-form h3,
.paid-grant-form .status { grid-column: 1 / -1; margin: 0; }
.paid-grant-form .field { margin: 0; }

@media (max-width: 640px) {
    .paid-grant-form { grid-template-columns: 1fr; }
    .paid-access { align-items: stretch; }
    .paid-access .hint { text-align: left; max-width: none; }
    .licence-state { text-align: left; }
}

/* The reorder gutter on a host card: two small buttons, the width of neither the icon nor the
   name, so the row still reads as an app rather than as a control panel. */
.app-order {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex-shrink: 0;
}

.app-order-button {
    width: 32px;
    min-width: 32px;
    padding: 4px 0;
    line-height: 1;
    font-size: 15px;
}

/* Dimmed rather than hidden at the ends of the list: a control that disappears moves everything
   under it, and the row would jump as somebody works down the column. */
.app-order-button:disabled { opacity: 0.35; }

/* Who runs the Market and its support address, under every page. Quiet: it is looked for, not
   read. */
.site-footer {
    display: block;
    padding: 24px 16px 28px;
    color: var(--muted);
    font-size: 12px;
    text-align: center;
}

.site-footer-line {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 4px;
}

.site-footer-line > * + *::before { content: "·"; margin: 0 8px; }

/* A line each on a phone: wrapped, the separators started the lines they fell onto. */
@media (max-width: 640px) {
    .site-footer-line { flex-direction: column; }
    .site-footer-line > * + *::before { content: none; }
}

.site-footer a { color: inherit; text-decoration: underline; }

.site-footer a:hover { color: var(--text); }

/* The build this deployment is running, under the footer line on a dev deployment only. */
.build-footer {
    margin-top: 6px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}


/* ---- Seats: organisations, licences, orders, checkout ---------------------------------------- */

/* A price under an app on the storefront: quiet, but not muted — it is the one number a visitor
   came for. */
.store-price { margin: 8px 0 0 0; font-size: 14px; color: var(--text); font-weight: 600; }

/* A member row's identity: the address and, beside it, "no account yet" or the group a seat came
   from. Wraps under the address on a phone rather than pushing the button off the row. */
.org-member-who { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.org-member-who .badge { margin-left: 0; }

/* A member's rights: chips their owner presses, lit while held; plain badges for anybody else. */
.rights { display: flex; flex-wrap: wrap; gap: 6px; }
.rights .badge { margin-left: 0; }
.right-chip {
    width: auto;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--muted);
    font-size: 12px;
    font-weight: 600;
    line-height: 1.4;
}
.right-chip:hover:not(:disabled) { color: var(--text); }
.right-chip-on,
.right-chip-on:hover:not(:disabled) { color: var(--accent); background: #16251c; border-color: #1c3d29; }

/* Who can take a licence's seats: everyone, or the groups ticked under the switch. */
.audience-groups { margin: 12px 0 0 0; }
.audience-add { margin-bottom: 8px; }
.audience-add .field { margin-bottom: 0; }
/* The way to the Groups list while there are none to choose. */
.audience-groups > a.button { margin: 8px 0; }

/* A field that finds one of a list by name: the list opens under it on focus and narrows as
   somebody types, over whatever is below. */
.search-picker { position: relative; }
/* A long name ends in an ellipsis before the chevron instead of running under it. */
.search-select input { text-overflow: ellipsis; }
.search-picker-list {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    margin-top: 4px;
    padding: 4px 0;
    max-height: 240px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.search-picker-option { padding: 8px 12px; cursor: pointer; }
.search-picker-option:hover,
.search-picker-option.active { background: rgba(34, 197, 94, 0.12); }
.search-picker-empty { margin: 0; padding: 8px 12px; }
.audience-chosen { margin-bottom: 8px; }
.audience-chosen-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 6px 0;
    border-top: 1px solid var(--border);
}
.audience-chosen-row button { width: auto; margin: 0; padding: 4px 12px; font-size: 12px; }
.audience-save { width: auto; margin-top: 4px; }

/* The two prices on a listing, side by side on a laptop. */
.pricing-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.pricing-fields .field { margin: 0; }

/* Checkout: the quote reads as the answer to the choices above it. */
.checkout-form .field { margin-bottom: 16px; }
.checkout-form .licence-picker { margin-bottom: 16px; }
.checkout-quote { margin: 4px 0 12px 0; font-size: 15px; font-weight: 600; }
.checkout-form .primary { width: auto; }
.checkout-form > .hint { margin-top: 8px; }

/* The issue form's hint spans the grid like its heading. */
.paid-grant-form .issue-hint { grid-column: 1 / -1; margin: 0; }
.paid-grant-form .checkbox-field { margin: 0; }

@media (max-width: 640px) {
    .pricing-fields { grid-template-columns: 1fr; }
}

/* Issuing seats: two columns, labels level at the top so a hint under one field does not push
   its neighbour's label down, and the button on its own row. */
.issue-form { grid-template-columns: 1fr 1fr; align-items: start; }
.issue-form .licence-picker { grid-column: 1 / -1; }
.issue-form .checkbox-field { grid-column: 1 / -1; }
.issue-form .primary { grid-column: 1 / -1; width: auto; justify-self: start; }

@media (max-width: 640px) {
    .issue-form { grid-template-columns: 1fr; }
}

/* My organisations: a card per organisation with the seats it gives the caller under its name, the
   main one (where their paid apps come from first) ringed and on top. */
.org-cards { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }
.org-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; }
.org-card-main { border-color: var(--accent); }
.org-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.org-card-name { font-weight: 600; }
.org-card-head button { width: auto; padding: 4px 12px; font-size: 12px; }
.badge-main { color: var(--accent-text); background: var(--accent); border: 1px solid var(--accent); }

/* The device holding a seat on a licence's roster: its make and model, and under them the ID the
   phone shows. The ID alone keeps the single-line cell it had. The badge sits 8px after the name, or
   starts the next line where the cell is too narrow for both — spaced by the line, since a catalog
   cell resets the badge's own margin (see above). */
.seat-device .cell-line { flex-wrap: wrap; gap: 4px 8px; }
.seat-device:has(.seat-device-id) { padding: 6px 0; }
.seat-device:has(.seat-device-id) .cell-line { min-height: 0; }
.seat-device-id { display: block; font-size: 12px; }

/* An organisation asking to be joined: its logo and name, who asked, and the two answers. */
.invite-card { border-color: #7c3d12; }
.invite-who { display: flex; align-items: center; gap: 12px; min-width: 0; }
.invite-icon { width: 40px; height: 40px; border-radius: 8px; object-fit: cover; flex: none; }
.invite-facts { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.invite-from { font-size: 13px; overflow-wrap: anywhere; }

/* On an organisation's Members, between the invite form and the roster, ruled off from the roster
   so the two tables do not read as one. */
.invites-pending { margin: 20px 0 20px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.invites-pending h3 { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--muted); }

/* The person's corner of the top bar: one button carrying the organisation they work as, and the
   panel under it with the address, the organisation, the language and signing out. */
.account { position: relative; flex-shrink: 0; }
.account-button {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    width: auto;
    max-width: 300px;
    padding: 6px 10px;
    font-size: 13px;
}
.account-glyph { display: inline-flex; flex-shrink: 0; }
.account-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 20;
    width: 300px;
    max-width: calc(100vw - 32px);
    padding: 4px 16px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
.account-section { padding: 12px 0; border-top: 1px solid var(--border); }
.account-section:first-child { border-top: none; }
.account-label { margin: 0 0 4px 0; font-size: 12px; color: var(--muted); }
.account-email { margin: 0; font-size: 14px; overflow-wrap: anywhere; }
.account-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.account-org { min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.account-link { color: var(--accent); text-decoration: none; white-space: nowrap; font-size: 13px; }
.account-link:hover { text-decoration: underline; }
.account-signout {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
}

/* Reviews: one card per build waiting for an admin — who sent it, what it replaces, and the three
   answers. The reason for a no opens under them, so it is written before it is sent. */
.review-head { display: flex; align-items: center; gap: 12px; }
.review-icon { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; flex: none; }
.review-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.review-name { font-weight: 600; font-size: 16px; }
.review-version { margin: 12px 0 4px; font-weight: 500; }
/* What the publisher says the build changes, labelled and flush with the rest of the card; wrapped as
   typed, since the line breaks in a release note are the note. */
.review-notes { margin: 8px 0 12px; }
.review-notes-label { display: block; margin-bottom: 2px; font-size: 12px; font-weight: 600; color: var(--muted); }
.review-notes-text { margin: 0; white-space: pre-wrap; }
.review-actions { margin-top: 12px; justify-content: flex-end; }
.review-actions button { width: auto; }
/* `:not(.hidden)`, or this rule's display beats `.hidden` and the box shows before Reject is pressed. */
.review-reject:not(.hidden) { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.review-reject textarea { width: 100%; }
.review-reject button { align-self: flex-end; width: auto; }

/* The order's number is the one way into it, so it reads as a link. */
.order-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.order-link:hover { text-decoration: underline; }
.order-facts { margin-top: 12px; }

.order-actions { margin-top: 16px; flex-wrap: wrap; }

/* `.primary` is a form's full-width submit; here it is one of two answers, side by side. */
.order-actions button { width: auto; white-space: nowrap; }

/* The admins' comment on the answer, between the order and the buttons that give it. */
.order-comment { margin-top: 16px; display: flex; flex-direction: column; gap: 6px; }
.order-comment textarea { width: 100%; }
.order-comment .hint { margin: 0; }

/* A comment keeps the lines it was written in. */
.order-note { white-space: pre-line; }

/* The order's files: a name that downloads, what it is, and Delete for an admin. */
.order-files { margin-top: 20px; }
.order-files h3 { margin: 0 0 8px; font-size: 15px; }
.order-file-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.order-file-list p { margin: 0; }
.order-file { display: flex; align-items: center; flex-wrap: wrap; column-gap: 12px; row-gap: 2px; }
.order-file-name {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font-weight: 600;
    text-align: left;
    overflow-wrap: anywhere;
}
.order-file-name:hover { text-decoration: underline; }
.order-file-meta { font-size: 13px; }
.order-file-delete { margin-left: auto; padding: 4px 12px; }
.order-attach { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; }
.order-attach .hint { margin: 0; }
