:root {
    --primary: #4a90d9;
    --primary-dark: #2563eb;
    /* Buttons need a darker surface than links: white on #4a90d9 is 3.34:1,
       below AA for the 12-13px labels these buttons carry. The link colour
       stays lighter because it is text on a dark background, not text on blue. */
    --primary-action: #2f6fb5;
    --primary-text: #60a5fa;
    --secondary: #6c757d;
    --secondary-text: #cbd5e1;
    --success: #28a745;
    --success-solid: #1f7a3f;
    --success-text: #4ade80;
    --danger: #dc3545;
    --danger-solid: #b02a37;
    --danger-text: #f87171;
    --warning: #ffc107;
    --warning-text: #fbbf24;
    --light: #f8f9fa;
    --dark: #343a40;
    --bg: #1a1a2e;
    --bg-card: #16213e;
    --bg-input: #0f3460;
    --text: #e4e4e4;
    --text-muted: #a0a0a0;
    --border: #2a3f5f;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.6;
    min-height: 100vh;
}

a {
    color: var(--primary);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

/* Layout */
.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1rem;
}

/* Header */
.header {
    background: var(--bg-card);
    border-bottom: 1px solid var(--border);
    padding: 1rem 0;
    position: sticky;
    top: 0;
    z-index: 100;
}

.header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: var(--text);
    flex: 0 0 auto;
    white-space: nowrap;
}

.nav {
    display: flex;
    gap: 1.5rem;
    align-items: center;
}

.nav a {
    color: var(--text-muted);
    transition: color 0.2s;
}

.nav a:hover, .nav a.active {
    color: var(--text);
    text-decoration: none;
}

/* Main content */
.main {
    padding: 2rem 0;
    min-height: calc(100vh - 140px);
}

/* Cards */
.card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
}

.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border);
}

.card-title {
    font-size: 1.25rem;
    font-weight: 600;
}

/* Grid */
.grid {
    display: grid;
    gap: 1.5rem;
}

.grid-2 {
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}

.grid-3 {
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

/* App card */
.app-card {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    height: 100%;
    transition: transform 0.2s, box-shadow 0.2s;
}

.app-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

.app-icon {
    width: 64px;
    height: 64px;
    background: var(--bg-input);
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    flex-shrink: 0;
}

/* A game that has a picture of its own fills the tile with it; one that does not
   keeps the letter on the plain background. `cover` rather than `contain`: an icon
   and a screenshot have nothing in common but the square they are put in. */
.app-icon img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 12px;
    display: block;
}

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

.app-name {
    font-weight: 600;
    margin-bottom: 0.25rem;
}

.app-desc {
    color: var(--text-muted);
    font-size: 0.875rem;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 6px;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.2;
    box-sizing: border-box;
    cursor: pointer;
    transition: background 0.2s, transform 0.1s;
    text-decoration: none;
}

.btn:hover {
    text-decoration: none;
    transform: translateY(-1px);
}

.btn:active {
    transform: translateY(0);
}

.btn-primary {
    background: var(--primary-action);
    color: white;
}

.btn-primary:hover {
    background: var(--primary-dark);
    color: white;
}

.btn-secondary {
    background: var(--secondary);
    color: white;
}

.btn-success {
    background: var(--success-solid);
    color: white;
}

.btn-danger {
    background: var(--danger-solid);
    color: white;
}

/* Warning is a light colour: dark text keeps the contrast well above AA,
   whereas white on #ffc107 was barely readable. Used for undoable or
   attention-worthy actions such as "revoke" and "stop". */
.btn-warning {
    background: var(--warning);
    color: var(--bg);
}

.btn-warning:hover {
    background: #d39e00;
    color: var(--bg);
}

.btn-sm {
    padding: 0.25rem 0.75rem;
    font-size: 0.8rem;
}

.btn-lg {
    padding: 0.75rem 1.5rem;
    font-size: 1rem;
}

/* Forms */
.form-group {
    margin-bottom: 1rem;
}

.form-label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: var(--text-muted);
}

.form-control {
    width: 100%;
    padding: 0.625rem 0.875rem;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 1rem;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.form-control:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.2);
}

.form-control::placeholder {
    color: var(--text-muted);
}

.password-wrap {
    position: relative;
}

.password-wrap .form-control {
    padding-right: 4rem;
}

.password-toggle {
    position: absolute;
    top: 50%;
    right: 0.35rem;
    transform: translateY(-50%);
    padding: 0.25rem 0.4rem;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.8rem;
    cursor: pointer;
}

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

/* A field that cannot be edited must not look like one that can. The slug and
   a version number are disabled on purpose; a read-only theme is next to
   editable colour pickers, so the difference has to be visible. */
.form-control:disabled,
.form-control[readonly] {
    opacity: 0.65;
    cursor: not-allowed;
    background: var(--bg-card);
}

.btn:disabled,
.btn.disabled {
    opacity: 0.55;
    cursor: not-allowed;
    transform: none;
}

select.form-control {
    cursor: pointer;
}

textarea.form-control {
    min-height: 100px;
    resize: vertical;
}

.form-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.form-check input[type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
    accent-color: var(--primary);
}

/* Tables */
.table {
    width: 100%;
    border-collapse: collapse;
}

.table th, .table td {
    padding: 0.75rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
}

.table th {
    color: var(--text-muted);
    font-weight: 500;
    font-size: 0.875rem;
}

.table tr:hover {
    background: rgba(255,255,255,0.02);
}

/* Badges */
.badge {
    display: inline-block;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: 500;
}

.badge-success {
    background: rgba(74, 222, 128, 0.14);
    color: var(--success-text);
}

.badge-warning {
    background: rgba(251, 191, 36, 0.16);
    color: var(--warning-text);
}

.badge-secondary {
    background: rgba(148, 163, 184, 0.16);
    color: var(--secondary-text);
}

.badge-primary {
    background: rgba(96, 165, 250, 0.16);
    color: var(--primary-text);
}

.badge-danger {
    background: rgba(248, 113, 113, 0.14);
    color: var(--danger-text);
}

/* Hide helpers */
.hide-desktop {
    display: none;
}

/* Alert */
.alert {
    padding: 1rem;
    border-radius: 6px;
    margin-bottom: 1rem;
}

.alert-success {
    background: rgba(40, 167, 69, 0.15);
    border: 1px solid var(--success);
    color: var(--success-text);
}

.alert-danger {
    background: rgba(220, 53, 69, 0.15);
    border: 1px solid var(--danger);
    color: var(--danger-text);
}

.alert-warning {
    background: rgba(255, 193, 7, 0.15);
    border: 1px solid var(--warning);
    color: var(--warning-text);
}

/* Page header */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 2rem;
}

.page-title {
    font-size: 1.75rem;
    font-weight: 600;
}

/* One game's page: media beside the facts on a desktop, one column on a
   phone. The inline 2fr/1fr rule this replaces never collapsed, which left
   the Info card about 110px wide at 390px and the screenshots one per row. */
.app-detail-layout {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: 2rem;
}

.app-detail-layout > * {
    min-width: 0;
}

@media (max-width: 860px) {
    .app-detail-layout {
        grid-template-columns: minmax(0, 1fr);
        gap: 1.25rem;
    }
}

/* The admin's answer to "what does an anonymous visitor get". One row per
   question, so the switches elsewhere do not have to be read as a combined
   answer. */
.visibility-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0.75rem 1.5rem;
}

.visibility-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.9rem;
}

.visibility-label {
    color: var(--text-muted);
}

.visibility-note {
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* Footer */
.footer {
    background: var(--bg-card);
    border-top: 1px solid var(--border);
    padding: 1.5rem 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.875rem;
}

/* Admin sidebar layout */
.admin-layout {
    display: flex;
    min-height: 100vh;
}

.sidebar {
    width: 250px;
    background: var(--bg-card);
    border-right: 1px solid var(--border);
    padding: 1.5rem 0;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    /* The navigation used to scroll away on a long page — a game's page can be
       several screens tall. It now owns the viewport height and scrolls its own
       list if a short screen cannot fit it. */
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.sidebar-nav {
    flex: 1;
}

.sidebar-footer {
    padding: 1rem 1.5rem;
    border-top: 1px solid var(--border);
}

/* The way to the public site is a utility, not a section: it belongs beside
   the language, below the list, and it should not compete with the panel's
   own doors. */
.sidebar-footer-link {
    display: block;
    margin-bottom: 0.75rem;
    color: var(--text-muted);
    font-size: 0.9rem;
}

.sidebar-footer-link:hover {
    color: var(--text);
    text-decoration: none;
}

.lang-switcher {
    display: flex;
    justify-content: center;
}

.lang-select {
    width: 100%;
    padding: 0.5rem 0.75rem;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.9rem;
    cursor: pointer;
    transition: border-color 0.2s;
}

.lang-select:hover {
    border-color: var(--primary);
}

.lang-select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.2);
}

.lang-select option {
    background: var(--bg-card);
    color: var(--text);
    padding: 0.5rem;
}

.sidebar-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem;
    padding: 0 1.5rem 1.5rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1rem;
}

/* The name and the way out sit at the far edge, not trailing the title. */
.sidebar-header .logo {
    margin-right: auto;
}

.sidebar-user {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.sidebar-nav a {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1.5rem;
    color: var(--text-muted);
    transition: all 0.2s;
}

.sidebar-nav a:hover, .sidebar-nav a.active {
    background: rgba(74, 144, 217, 0.1);
    color: var(--text);
    text-decoration: none;
    border-left: 3px solid var(--primary);
}

.admin-content {
    flex: 1;
    padding: 2rem;
    overflow-x: auto;
}

/* Version list */
.version-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    margin-bottom: 0.5rem;
}

.version-info {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.version-number {
    font-weight: 600;
    font-size: 1.1rem;
}

.version-meta {
    display: flex;
    gap: 0.5rem;
}

/* News item */
.news-item {
    border-bottom: 1px solid var(--border);
    padding-bottom: 1.5rem;
    margin-bottom: 1.5rem;
}

.news-item:last-child {
    border-bottom: none;
    margin-bottom: 0;
}

.news-title {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
}

.news-meta {
    color: var(--text-muted);
    font-size: 0.875rem;
    margin-bottom: 0.75rem;
}

.news-content {
    line-height: 1.7;
}

/* Modal */
.modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    z-index: 1000;
    align-items: center;
    justify-content: center;
}

.modal.active {
    display: flex;
}

.modal-content {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1.5rem;
    max-width: 500px;
    width: 90%;
    max-height: 90vh;
    overflow-y: auto;
}

.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
}

.modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 1.5rem;
    cursor: pointer;
}

/* Login page */
.login-container {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 2rem;
}

.login-box {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem;
    width: 100%;
    max-width: 400px;
}

.login-title {
    text-align: center;
    margin-bottom: 1.5rem;
    font-size: 1.5rem;
}

/* Actions */
.actions {
    display: flex;
    gap: 0.5rem;
}

/* Empty state */
.empty-state {
    text-align: center;
    padding: 3rem;
    color: var(--text-muted);
}

.empty-state-icon {
    font-size: 3rem;
    margin-bottom: 1rem;
}

/* Tabs */
.tabs {
    display: flex;
    gap: 0;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1.5rem;
    overflow-x: auto;
}

.tab {
    padding: 0.75rem 1.5rem;
    color: var(--text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
    transition: all 0.2s;
}

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

.tab.active {
    color: var(--primary);
    border-bottom-color: var(--primary);
}

.tab-count {
    background: var(--bg-input);
    padding: 0.1rem 0.5rem;
    border-radius: 10px;
    font-size: 0.75rem;
    margin-left: 0.5rem;
}

/* Mobile hamburger */
.mobile-menu-toggle {
    display: none;
    background: none;
    border: none;
    color: var(--text);
    font-size: 1.5rem;
    cursor: pointer;
    padding: 0.5rem;
}

/* Responsive */
@media (max-width: 768px) {
    .admin-layout {
        flex-direction: column;
    }

    .sidebar {
        width: 100%;
        border-right: none;
        border-bottom: 1px solid var(--border);
        position: relative;
        /* The phone's sidebar is a header that opens, not a pinned column. */
        align-self: stretch;
        height: auto;
        overflow: visible;
    }

    .sidebar-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 0.75rem;
        margin-bottom: 0;
        border-bottom: none;
    }

    .mobile-menu-toggle {
        display: block;
    }

    .sidebar-nav {
        display: none;
        flex-direction: column;
        padding: 0;
    }

    .sidebar-nav.open {
        display: flex;
    }

    .sidebar-nav a {
        padding: 0.75rem 1.5rem;
        border-left: none !important;
        border-bottom: 1px solid var(--border);
    }

    .sidebar-nav a:hover, .sidebar-nav a.active {
        border-left: 3px solid var(--primary) !important;
        border-bottom: 1px solid var(--border);
    }

    .sidebar-footer {
        display: none;
        padding: 0.75rem 1.5rem;
    }

    .sidebar-footer.open {
        display: block;
    }

    /* Show mobile-only content */
    .hide-desktop {
        display: block;
    }

    .admin-content {
        padding: 1rem;
    }

    .page-header {
        flex-direction: column;
        gap: 1rem;
        align-items: stretch;
    }

    .page-header > div {
        flex-wrap: wrap;
    }

    .page-header .actions {
        flex-wrap: wrap;
    }

    /* Responsive tables */
    .table-responsive {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    .table {
        min-width: 0;
    }

    .table td:last-child {
        display: flex;
        flex-wrap: wrap;
        gap: 0.25rem;
    }

    .table td:last-child form {
        display: inline-flex !important;
    }

    /* Hide columns on mobile */
    .hide-mobile {
        display: none;
    }

    /* Stack actions */
    .actions,
    .action-buttons {
        display: flex;
        flex-wrap: wrap;
        gap: 0.25rem;
    }

    .action-buttons form {
        display: inline-flex !important;
    }

    /* Cards on mobile */
    .card {
        padding: 1rem;
    }

    .card-header {
        flex-direction: column;
        gap: 0.75rem;
        align-items: flex-start;
    }

    /* Version items */
    .version-item {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.75rem;
    }

    .version-info {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
    }

    /* Modal on mobile */
    .modal-content {
        width: 95%;
        max-height: 85vh;
        margin: 1rem;
    }

    /* Tabs on mobile */
    .tabs {
        gap: 0;
    }

    .tab {
        padding: 0.75rem 1rem;
        font-size: 0.9rem;
    }

    /* Grid on mobile */
    .grid-2, .grid-3 {
        grid-template-columns: 1fr;
    }

    /* Page title */
    .page-title {
        font-size: 1.4rem;
    }

    /* Buttons full width on mobile */
    .btn-mobile-full {
        width: 100%;
        justify-content: center;
    }
}

@media (max-width: 480px) {
    .admin-content {
        padding: 0.75rem;
    }

    .table th, .table td {
        padding: 0.5rem;
        font-size: 0.85rem;
    }

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

    .badge {
        font-size: 0.7rem;
        padding: 0.2rem 0.4rem;
    }
}

/* Version accordion */
.version-accordion {
    border-bottom: 1px solid var(--border);
}

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

.version-summary {
    display: flex;
    align-items: center;
    padding: 1rem;
    cursor: pointer;
    list-style: none;
    transition: background 0.2s;
}

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

.version-summary::before {
    content: '\25B6';
    font-size: 0.7rem;
    margin-right: 0.75rem;
    color: var(--text-muted);
    transition: transform 0.2s;
}

.version-accordion[open] .version-summary::before {
    transform: rotate(90deg);
}

.version-summary:hover {
    background: rgba(255, 255, 255, 0.03);
}

.version-content {
    padding: 0 1rem 1rem 2.25rem;
}

/* Loading overlay */
.loading-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.6);
    z-index: 2000;
    align-items: center;
    justify-content: center;
}

.loading-overlay.active {
    display: flex;
}

.loading-box {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem 3rem;
    text-align: center;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

.loading-spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--border);
    border-top-color: var(--primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 0 auto 1rem;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

.loading-text {
    color: var(--text);
    font-size: 1rem;
    font-weight: 500;
}

/* Asset library
   ---------------------------------------------------------------------------
   These tables carry many small columns *and* inline edit forms, so the usual
   horizontal-scroll treatment is wrong for them: it hides the forms off-screen.
   They stack into blocks instead. Fixed column widths are deliberately absent —
   they were what squeezed text into one-letter-per-line on a phone. */
.lib-table {
    width: 100%;
    font-size: 0.85rem;
}

.lib-table td {
    padding: 0.3rem 0.4rem;
    vertical-align: top;
    word-break: break-word;
}

.lib-form {
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
}

/* Inputs default to a content-based minimum, which stops flex from shrinking
   them and pushes the row wider than the screen. */
.lib-form input,
.lib-form select {
    min-width: 0;
    flex: 1 1 8rem;
}

@media (max-width: 768px) {
    .lib-table,
    .lib-table tbody,
    .lib-table tr,
    .lib-table td {
        display: block;
        width: auto;
    }

    .lib-table tr {
        border-bottom: 1px solid var(--border);
        padding: 0.4rem 0;
    }

    .lib-table td {
        padding: 0.15rem 0;
    }

    .lib-form {
        flex-direction: column;
        align-items: stretch;
    }
}

/* Preview thumbnails. Images are served from /api/files/{id}, which requires a
   session — that works in a browser, so a non-public library file still renders
   in an <img>. Non-image previews (animation clips) show their name instead of
   an inline player: a video tag per row would pull megabytes on page load. */
.lib-previews {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.lib-preview {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
}

.lib-preview img {
    height: 3.5rem;
    width: auto;
    max-width: 6rem;
    object-fit: cover;
    border: 1px solid var(--border);
    border-radius: 3px;
    display: block;
}


/* ---------------------------------------------------------------------------
   Media store gallery (/admin/media).

   Phone first: tiles are sized so two or three fit a narrow screen, and every
   image is a 320px lazily-fetched JPEG rather than an original — a grid of
   originals is exactly what made the owner say he could not look at screenshots
   from his phone. Nothing here depends on JavaScript.
   --------------------------------------------------------------------------- */
.media-intro {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.media-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: flex-end;
    margin-bottom: 1.25rem;
}

.media-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.media-field .form-control {
    min-width: 9rem;
}

.media-actions {
    flex-direction: row;
    gap: 0.5rem;
}

.media-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 0.75rem;
}

.media-cell {
    position: relative;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.4rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

/* Choosing pictures for a bulk tag: always visible, because it is not a caption —
   with the captions off the grid is still the place one picks a dozen files. */
.media-pick {
    position: absolute;
    top: 0.55rem;
    left: 0.55rem;
    z-index: 2;
    background: rgba(0, 0, 0, 0.55);
    border-radius: 3px;
    padding: 0 0.15rem;
    line-height: 1;
}

/* On a showcase, which is the one editorial state the grid answers at a glance. */
.media-published {
    position: absolute;
    top: 0.5rem;
    right: 0.55rem;
    z-index: 2;
    color: #ffd166;
    text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
    font-size: 0.9rem;
    pointer-events: none;
}

/* Storefront labels, next to the free tags but visibly a different kind of thing. */
.media-target {
    background: transparent;
    border: 1px solid var(--primary);
    color: var(--primary);
    border-radius: 3px;
    padding: 0 0.3rem;
    font-size: 0.7rem;
    text-decoration: none;
}

.media-tagbar .media-target-on {
    background: var(--primary);
    color: #fff;
}

.media-targetbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: center;
}

.media-targetbar .media-tag {
    font-size: 0.75rem;
    padding: 0.1rem 0.45rem;
    cursor: pointer;
}

.media-tagbar .media-target-off {
    border: 1px dashed var(--primary);
    background: transparent;
    color: var(--primary);
}

.media-bulk {
    margin-bottom: 0.75rem;
}

.media-bulk-drawer {
    margin-bottom: 0.75rem;
}

.media-bulk-drawer > summary {
    list-style: none;
    cursor: pointer;
    display: inline-block;
}

.media-bulk-drawer > summary::-webkit-details-marker {
    display: none;
}

.media-bulk-drawer form {
    margin-top: 0.75rem;
}

.media-filter-drawer {
    margin-bottom: 1rem;
}

.media-filter-drawer > summary {
    list-style: none;
    cursor: pointer;
    display: inline-block;
}

.media-filter-drawer > summary::-webkit-details-marker {
    display: none;
}

.media-filter-drawer form {
    margin-top: 0.75rem;
}

.media-upload {
    margin-bottom: 1rem;
}

.media-upload > summary {
    list-style: none;
    cursor: pointer;
    display: inline-block;
}

.media-upload > summary::-webkit-details-marker {
    display: none;
}

.media-upload form {
    margin-top: 0.75rem;
}

/* The two metadata forms in the viewer: where the picture ships, and whether it
   is on a showcase. */
.viewer-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin: 0.25rem 0 0.75rem;
}

.viewer-targets,
.viewer-publish {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
}

.viewer-actions-label {
    color: var(--text-muted);
    font-size: 0.85rem;
}

.viewer-target {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.85rem;
}

.media-thumb {
    display: block;
    position: relative;
    aspect-ratio: 16 / 10;
    background: var(--bg-input);
    border-radius: 4px;
    overflow: hidden;
}

/* Pixel size in the corner of the tile. On the picture rather than under it
   because the label under it is switchable and on a phone it is switched off —
   and "which of these is 1080×1920" is the question the grid is browsed to
   answer. */
.media-dims {
    position: absolute;
    right: 0.25rem;
    bottom: 0.25rem;
    padding: 0 0.25rem;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: 0.65rem;
    line-height: 1.4;
    letter-spacing: 0.01em;
    pointer-events: none;
}

/* The tag filter as taps: what is on, what is off, one row each. */
.media-tagbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin: -0.75rem 0 1.25rem;
}

.media-tagbar .media-tag {
    font-size: 0.75rem;
    padding: 0.1rem 0.45rem;
}

.media-tag-on {
    background: var(--primary);
    color: #fff;
}

.media-tag-off {
    border: 1px dashed var(--border);
    background: transparent;
}

/* Zoom controls under the picture: - / Fit / +, and one line saying that the
   wheel does the same. */
.viewer-zoombar {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.6rem;
}

.viewer-zoombar-hint {
    color: var(--text-muted);
    font-size: 0.8rem;
    margin-left: 0.25rem;
}

.media-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.media-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    height: 100%;
    padding: 0.25rem;
    color: var(--text-muted);
}

.media-placeholder-mark {
    font-size: 1.4rem;
}

.media-placeholder-name {
    font-size: 0.7rem;
    text-align: center;
    word-break: break-all;
}

.media-meta {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 0.75rem;
}

.media-owner,
.media-note {
    color: var(--text-muted);
    font-size: 0.7rem;
}

.media-taglist {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem;
}

.media-tag {
    background: var(--bg-input);
    border-radius: 3px;
    padding: 0 0.3rem;
    font-size: 0.7rem;
    color: var(--text-muted);
    text-decoration: none;
}

.media-tagform {
    display: flex;
    gap: 0.25rem;
}

.media-tagform .form-control {
    font-size: 0.75rem;
    padding: 0.15rem 0.3rem;
    min-width: 0;
}

/* Cards on the public applications list, which now carry Play and Media. */
.app-card-link {
    display: flex;
    flex: 1 1 auto;
    gap: 1rem;
    align-items: center;
    text-decoration: none;
    color: inherit;
}

/* The actions are a footer of every card, pinned to the bottom: a Play button
   on one game and no button on the next used to leave the cards in a column at
   different heights. A card with nothing else to offer gets a Details button,
   so the footer, and the way in, are always in the same place. */
.app-card-actions {
    display: flex;
    flex: 0 0 auto;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 0.35rem;
    margin-top: auto;
    padding-top: 0.6rem;
}

/* The viewer (/admin/media/{id}). One picture, arrows either side of it and a
   way to keep it — sized for a thumb, since this is the page the phone opens
   after tapping a tile. */
.viewer {
    max-width: 900px;
}

.viewer-stage {
    cursor: grab;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40vh;
    overflow: hidden;
    /* The picture is zoomed inside this box by the script in media-item.html,
       never by zooming the page — see the note there. `pan-y` takes pinch and
       double-tap zoom away from the browser while leaving ordinary scrolling
       alone, and the script switches this to `none` once the picture is zoomed
       so a drag pans the picture instead of the page. */
    touch-action: pan-y;
}

.viewer-stage:active {
    cursor: grabbing;
}

.viewer-media {
    max-width: 100%;
    max-height: 70vh;
    display: block;
    /* Panning with a mouse is a pair with the wheel; a text selection or a
       native image drag on the way is the gesture being taken away. */
    user-select: none;
    -webkit-user-drag: none;
    /* The zoom is applied here: the element's own box never changes size, so
       nothing outside it can be affected by the gesture. */
    transform-origin: 50% 50%;
    will-change: transform;
}

.viewer-fallback {
    padding: 2rem 1rem;
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
}

.viewer-bar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.6rem 0;
}

/* Arrows: a thumb target first, a glyph second. They were 3.5rem and read as
   small; the page cannot be pinched to make them bigger, because that gesture
   zooms the picture, so they have to be right at their own size. */
.viewer-arrow {
    flex: 0 0 auto;
    min-width: 4.5rem;
    min-height: 3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 1.6rem;
    line-height: 1;
}

.viewer-arrow.disabled {
    opacity: 0.35;
    pointer-events: none;
}

.viewer-save {
    flex: 1 1 auto;
    text-align: center;
}

.viewer-meta {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: 1rem;
}

.viewer-name {
    font-weight: 500;
    font-size: 1.05rem;
    word-break: break-all;
}

.viewer-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    color: var(--text-muted);
    font-size: 0.9rem;
}

.viewer-tags {
    margin-top: 0.75rem;
}

/* The two controls a thumb uses most, in the viewer and in the bar. */
.viewer-save {
    font-size: 1rem;
    padding-top: 0.6rem;
    padding-bottom: 0.6rem;
}

.viewer-zoom {
    font-size: 0.95rem;
}

.viewer-tags .form-control {
    font-size: 0.95rem;
}

/* Full screen in the viewer — "almost full screen" on purpose.

   Deliberately not the Fullscreen API: iOS Safari has no
   Element.requestFullscreen, so a control built on it would do nothing on the
   phone this page exists for. What is dropped instead is every piece of chrome
   we own — the sidebar, the padding — and the picture takes the viewport, which
   lands within one URL bar of the same result on every browser and keeps the
   arrows working across pictures. */
.viewer-fs .sidebar {
    display: none;
}

/* The two bars sit at the top and bottom of the window.
   They are pinned, and this time it holds, because the page they are pinned to
   can no longer be zoomed: `touch-action: pan-y` on the full-screen content
   refuses pinch and double-tap zoom, and the gesture goes to the picture instead
   (media-item.html). Two earlier attempts tried to *survive* page zoom —
   `position: fixed` alone, then a correction driven by `window.visualViewport` —
   and both came back from a real phone wrong: on that browser the bars were
   magnified with the picture and the right arrow ended up off the edge. Page zoom
   is semantics each engine implements its own way; a layout that never changes
   has no such question to answer. Translucent, because the picture behind them is
   the reason you are here. */
.viewer-fs .admin-content {
    padding: 0;
    display: block;
    height: auto;
    /* Self-contained and scrolled inside itself: the viewer's page never scrolls,
       so the bars pinned to the top and bottom of it cannot be scrolled away
       either. Pinch and double-tap zoom are refused here as well — the gesture
       belongs to the picture (media-item.html), and a page that never zooms is a
       page whose chrome cannot move. */
    position: fixed;
    inset: 0;
    overflow-y: auto;
    touch-action: pan-y;
}

.viewer-fs .page-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    margin: 0;
    padding: 0.4rem 0.5rem;
    background: rgba(0, 0, 0, 0.55);
}

.viewer-fs .viewer {
    max-width: none;
    display: block;
}

.viewer-fs .viewer-stage {
    /* The whole window, less the pinned bar at the bottom of it. */
    height: calc(100vh - 3.4rem);
    height: calc(100dvh - 3.4rem);
    min-height: 0;
    border: none;
    border-radius: 0;
    background: #000;
}

.viewer-fs .viewer-media {
    max-height: 100%;
    max-width: 100%;
    object-fit: contain;
}

.viewer-fs .viewer-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 20;
    margin: 0;
    padding: 0.4rem 0.5rem;
    background: rgba(0, 0, 0, 0.55);
}

/* The zoom buttons float over the picture's corner in full screen: the stage
   already owns the whole window, so a row underneath it would be off the bottom
   of the page. The wheel and the pinch work here either way; these are for a
   mouse that wants one tap. */
.viewer-fs .viewer-zoombar {
    position: fixed;
    right: 0.5rem;
    bottom: 4rem;
    z-index: 21;
    margin: 0;
    padding: 0.2rem;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.55);
}

.viewer-fs .viewer-zoombar-hint {
    display: none;
}

/* The public viewer in full screen: the same rules above, with the site's own
   header in place of the admin sidebar. */
.viewer-fs .header {
    display: none;
}

.viewer-fs .main {
    padding: 0;
}

.viewer-fs .container {
    max-width: none;
    padding: 0;
}

/* In full screen the page header keeps only what is about this picture — where
   it sits in the run, and the way back out. The gallery link and the caption
   switch are one tap away by leaving, and on a phone every extra button in that
   row is a row of picture. */
.viewer-fs .viewer-back,
.viewer-fs .viewer-labels {
    display: none;
}

.viewer-fs .page-header {
    justify-content: space-between;
}

/* The details block, when it is on, scrolls under the pinned bar — keep its last
   line reachable rather than behind the buttons. */
.viewer-fs .viewer-meta,
.viewer-fs .viewer-tags {
    margin: 0.5rem 0.5rem 0;
}

.viewer-fs .viewer-tags {
    margin-bottom: 3.4rem;
}

/* ---------------------------------------------------------------------------
   Navigation: sections in the sidebar, and a bar at the bottom on a phone.

   The list had grown to fourteen identical entries, which is a list nobody
   reads — grouping it puts what is opened daily at the top. The `.active` class
   was styled from the start and never set by anything; AdminModelAdvice now
   passes the current path so the section you are in is lit up.
   --------------------------------------------------------------------------- */
.nav-group {
    padding: 1.1rem 1.5rem 0.3rem;
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    opacity: 0.8;
}

.admin-bottom {
    display: none;
}

@media (max-width: 768px) {
    /* Reaching the menu meant aiming at a hamburger at the top of the page —
         which on a phone is the far end of the screen from your thumb. */
    .admin-bottom {
        display: flex;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 30;
        background: var(--bg-card);
        border-top: 1px solid var(--border);
        padding: 0.3rem 0.25rem;
        padding-bottom: calc(0.3rem + env(safe-area-inset-bottom, 0px));
    }

    .admin-bottom a,
    .admin-bottom button {
        flex: 1 1 0;
        text-align: center;
        font-size: 0.75rem;
        padding: 0.45rem 0.15rem;
        background: none;
        border: none;
        color: var(--text-muted);
        text-decoration: none;
        font-family: inherit;
    }

    .admin-bottom a.active {
        color: var(--primary);
        font-weight: 500;
    }

    /* Room for the bar, so the last row of a page is not behind it. */
    .admin-content {
        padding-bottom: 4.5rem;
    }

    /* Never in the viewer's full screen: there the picture is the point, and the
         viewer has a bar of its own. */
    .viewer-fs .admin-bottom {
        display: none;
    }
}

/* The magnification, while it changes and for a moment after.
   The pinch has no other feedback — the picture grows, but "how far in" is the
   question the gesture raises, and the answer costs a pill the size of a word.
   Fixed and centred above the bar; it never takes a tap (pointer-events: none),
   because a badge that swallows a thumb tap on the picture would be worse than
   no badge. */
.viewer-zoom {
    position: fixed;
    left: 50%;
    bottom: 3.9rem;
    transform: translateX(-50%);
    z-index: 25;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 0.8rem;
    opacity: 0;
    transition: opacity 0.25s;
    pointer-events: none;
}

.viewer-zoom.shown {
    opacity: 1;
}

/* ---------------------------------------------------------------------------
   Tasks: what a tester reported, and the conversation about it.

   One shape used by both sides — the admin's page and the tester's — because it
   is the same thing seen from two chairs: a statement, then replies, either of
   which may be mostly a picture.
   --------------------------------------------------------------------------- */
.thread {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-top: 0.75rem;
}

.thread-entry {
    border-left: 3px solid var(--border);
    padding-left: 0.75rem;
}

.thread-who {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    font-size: 0.75rem;
    color: var(--text-muted);
}

.thread-body {
    white-space: pre-wrap;
    margin-top: 0.25rem;
}

.thread-when {
    color: var(--text-muted);
    font-size: 0.75rem;
}

.thread-images {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.4rem;
}

/* Big enough to see what a screenshot is of, small enough that four of them fit
   a phone. The viewer is one tap away for a proper look. */
.thread-images img {
    height: 7rem;
    width: auto;
    max-width: 12rem;
    object-fit: cover;
    border: 1px solid var(--border);
    border-radius: 4px;
    display: block;
}

.task-card {
    margin-bottom: 0.75rem;
}

.task-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.6rem;
}

.task-game {
    font-weight: 500;
}

.task-who {
    color: var(--text-muted);
    font-size: 0.8rem;
}

.task-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.75rem;
}

/* The tester's own form: room to write, and the labels above rather than beside,
   because on a phone a label in a column is a column of nothing. */
.remark-form {
    display: flex;
    flex-direction: column;
}

.remark-label {
    margin-top: 0.75rem;
    margin-bottom: 0.25rem;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.remark-mine {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border);
    color: inherit;
    text-decoration: none;
}

/* A picture in a list of media, at the top of its card. Sized like a contact
   sheet rather than a gallery: this is a list somebody scans for the right file,
   and the viewer is one tap away for a proper look. */
.media-tile {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    object-fit: cover;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    margin-bottom: 0.5rem;
}

/* Nothing to show — a model, an archive — so the card says what it is instead of
   leaving a hole where a picture would be. */
.media-tile-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* The same idea in the asset library's rows, which are a table rather than a
   grid: small enough to keep the row scannable. */
.lib-thumb {
    display: block;
    height: 4rem;
    width: auto;
    max-width: 8rem;
    object-fit: cover;
    border: 1px solid var(--border);
    border-radius: 4px;
}

/* One task in the tester's list: a whole row is a link, because on a phone the
   whole row is what a thumb aims at. */
.remark-task {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border);
    color: inherit;
    text-decoration: none;
}

/* The task list and a task's thread, read on a phone: the same reason as the
   viewer — nothing here can be pinched bigger, so the sizes have to be right. */
.thread-body {
    font-size: 1rem;
}

.thread-who,
.thread-when {
    font-size: 0.85rem;
}

.task-who,
.media-owner,
.media-note {
    font-size: 0.85rem;
}

.remark-label {
    font-size: 0.95rem;
}

.remark-mine,
.remark-task {
    font-size: 1rem;
}

/* A task's name in a list: the thing somebody scans for, above the paragraph. */
.task-title {
    font-weight: 500;
    font-size: 1.05rem;
    margin-top: 0.35rem;
}

.remark-task-title {
    font-weight: 500;
}

/* The task list: one row per task, and a row is a name and where it stands.
   Everything else about a task — the words, the pictures, the buttons — is on
   its page, one tap away; a list that carries all of it is not a list. */
.task-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.task-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.7rem 0.8rem;
    border-bottom: 1px solid var(--border);
    color: inherit;
    text-decoration: none;
}

.task-row:last-child {
    border-bottom: none;
}

/* The name takes the width, and the rest sits after it — on a phone the metadata
   wraps under the title rather than squeezing it. */
.task-row-title {
    flex: 1 1 12rem;
    font-weight: 500;
}

.task-row-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* One picture and what it is. The role sits under the thumbnail rather than
   beside it: a badge on every tile is noise, and the word is short. */
.thread-image {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    align-items: center;
}

.thread-image-role {
    font-size: 0.7rem;
    color: var(--text-muted);
}

/* "Take it off the task": as small as the caption it sits in, and quiet, because
   the tap people come for is the picture itself. */
.thread-image-off {
    background: none;
    border: 0;
    padding: 0 0 0 0.35rem;
    font: inherit;
    font-size: 0.7rem;
    color: var(--text-muted);
    text-decoration: underline;
    cursor: pointer;
}

/*
 * A flash message on a phone.
 *
 * The links table is long, and the Save button of the row somebody just edited
 * is nowhere near the top of the page — a confirmation that appears above the
 * fold is a confirmation nobody sees. Sticky keeps it in view while the page
 * stays where it was.
 */
.alert-flash {
    position: sticky;
    top: 0.5rem;
    z-index: 50;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/*
 * The links table as cards on a phone.
 *
 * Seven columns do not fit: the games column squeezed everything else into a
 * strip of letters, and the headers overlapped. Below 768px each row becomes a
 * card, each cell a "label: value" line — the same data, readable at thumb
 * width.
 */
@media (max-width: 768px) {
    .links-table thead {
        display: none;
    }

    .links-table tr {
        display: block;
        border: 1px solid var(--border);
        border-radius: 6px;
        margin-bottom: 0.75rem;
        padding: 0.6rem 0.75rem;
    }

    .links-table td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 0.75rem;
        border: none;
        padding: 0.3rem 0;
    }

    .links-table td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-weight: 600;
        color: var(--text-muted);
        font-size: 0.85rem;
    }

    .links-table td:last-child {
        justify-content: flex-end;
    }

    .links-table td:last-child::before {
        content: none;
    }
}

/* Navigation and the person, as one block on the right. */
.header-right {
    display: flex;
    align-items: center;
    gap: 2rem;
    margin-left: auto;
}

/* Who is looking, and the way out. */
.header-user {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.9rem;
    color: var(--text-muted);
}

.public-lang .lang-select {
    width: auto;
    padding: 0.35rem 0.5rem;
    font-size: 0.85rem;
}

/* ---------------------------------------------------------------------------
   The task page: three steps, three cards.

   What was reported, what was said about it, and where it stands. The controls
   that belong to a step live inside it — editing behind "Edit the task", adding
   a comment in the composer under the last reply, and closing next to the
   release that fixed it. A card that mixes them turns one question ("what do I
   press?") into several.
   --------------------------------------------------------------------------- */
.task-title-lg {
    font-size: 1.3rem;
    font-weight: 600;
    line-height: 1.3;
    margin: 0.4rem 0 0.2rem;
}

.task-edit,
.task-store {
    margin-top: 0.9rem;
    border-top: 1px solid var(--border);
    padding-top: 0.6rem;
}

.task-store {
    border-top: 0;
    margin-top: 0.75rem;
}

.task-edit > summary,
.task-store > summary {
    cursor: pointer;
    color: var(--text-muted);
    font-size: 0.95rem;
}

/* The composer is the only thing on the page that adds to the conversation, so
   it is separated from the last reply by a line rather than hidden below the
   rest of the page. */
.task-composer {
    margin-top: 1rem;
    border-top: 1px solid var(--border);
    padding-top: 0.75rem;
}

.task-composer h3 {
    margin: 0 0 0.35rem;
    font-size: 1rem;
}

.task-solution {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}

.task-solution .remark-label {
    margin: 0;
}

/* Deleting a task is not part of working it: quiet, last, and away from the
   buttons somebody presses every day. */
.task-danger {
    margin-top: 1rem;
    border-top: 1px solid var(--border);
    padding-top: 0.6rem;
}

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

/* Picking a picture that is already in the store: the radio and the thumbnail
   are one tap target, sized like the tiles around them. */
.store-pick {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    cursor: pointer;
}

.store-pick input {
    accent-color: var(--primary);
}

/* The application's operations, wherever the application is in context. */
.app-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}

.app-actions .btn {
    font-size: 0.8rem;
    padding: 0.3rem 0.6rem;
}

/* The secondary operations of one application: a native disclosure, so it
   works without JavaScript and does not add a framework to the page. */
.actions-more {
    position: relative;
}

.actions-more > summary {
    list-style: none;
    cursor: pointer;
}

.actions-more > summary::-webkit-details-marker,
.actions-more > summary::marker {
    display: none;
    content: "";
}

.actions-menu {
    position: absolute;
    top: calc(100% + 0.35rem);
    right: 0;
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 12rem;
    max-width: calc(100vw - 2rem);
    padding: 0.35rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}

/* Menu rows, not a stack of grey buttons. */
.actions-menu .btn {
    width: 100%;
    justify-content: flex-start;
    background: transparent;
    color: var(--text);
    border-radius: 4px;
}

.actions-menu .btn:hover {
    background: rgba(74, 144, 217, 0.16);
    color: var(--text);
    transform: none;
}

@media (max-width: 768px) {
    .actions-menu {
        left: 0;
        right: auto;
    }
}

.app-actions-bar {
    margin-bottom: 1rem;
}

/* ---------------------------------------------------------------------------
   A task's words, and the conversation under them.

   The statement is the thing being discussed; replies are set in from it, so
   the eye can tell a report from a conversation before reading a word. The
   same shape is used on the admin's page and the tester's.
   --------------------------------------------------------------------------- */
.task-statement {
    margin-bottom: 1rem;
}

.task-body {
    background: var(--bg-input);
    border-left: 3px solid var(--primary);
    border-radius: 4px;
    padding: 0.6rem 0.8rem;
    margin-top: 0.5rem;
}

/* Replies sit in from the statement. On the tester's page the statement is the
   first entry of the same thread, so it is the one that keeps the margin at 0. */
.thread-entry {
    margin-left: 1rem;
}

.thread-entry.statement {
    margin-left: 0;
}

/* Rendered Markdown: the body is HTML now, so pre-wrap would add whitespace
   around blocks that already have their own spacing. */
.thread-body.markdown {
    white-space: normal;
}

.markdown > :first-child { margin-top: 0; }
.markdown > :last-child { margin-bottom: 0; }
.markdown p { margin: 0 0 0.5rem; }
.markdown ul, .markdown ol { margin: 0.3rem 0 0.5rem; padding-left: 1.25rem; }
.markdown li { margin: 0.15rem 0; }
.markdown code {
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 0 0.25rem;
    font-size: 0.9em;
}
.markdown pre {
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 0.6rem;
    overflow-x: auto;
}
.markdown pre code { border: 0; padding: 0; }
.markdown blockquote {
    border-left: 3px solid var(--border);
    margin: 0.4rem 0;
    padding-left: 0.75rem;
    color: var(--text-muted);
}
.markdown a { color: var(--primary); }
.markdown h1, .markdown h2, .markdown h3 {
    font-size: 1.05rem;
    margin: 0.6rem 0 0.3rem;
}
.markdown table { border-collapse: collapse; margin: 0.4rem 0; }
.markdown th, .markdown td { border: 1px solid var(--border); padding: 0.25rem 0.5rem; }
.markdown img { max-width: 100%; height: auto; }

/* A minimal Markdown toolbar: the buttons are the part people cannot remember,
   and they sit over the field they act on. */
.md-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: 0.4rem;
}

.md-toolbar .btn {
    padding: 0.15rem 0.45rem;
    font-size: 0.8rem;
    line-height: 1.2;
}

/* A pasted or chosen picture, shown before it is sent: the file input alone
   gives no sign that the paste worked. */
.paste-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.4rem;
}

.paste-hint {
    display: inline;
    color: var(--text-muted);
}

.paste-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.4rem;
}

.paste-chip {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 0.15rem 0.3rem;
    font-size: 0.75rem;
}

.paste-chip img {
    height: 2rem;
    width: auto;
    border-radius: 3px;
}

.paste-off {
    background: none;
    border: 0;
    padding: 0 0.15rem;
    color: var(--text-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

/* The store picker opens on a button, and its thumbnails are fetched then. */
.store-picker > summary {
    list-style: none;
    display: inline-block;
    cursor: pointer;
}

.store-picker > summary::-webkit-details-marker {
    display: none;
}

.store-picker[open] > summary {
    margin-bottom: 0.5rem;
}

/* ---------------------------------------------------------------------------
   The public header on a narrow screen.

   At 360px the logo and the menu used to touch — "bukazoidHome" — because the
   row had no gap and no wrapping. The menu now takes its own row under the
   logo, which keeps every link reachable without a second menu implementation.
   --------------------------------------------------------------------------- */
@media (max-width: 600px) {
    .header {
        padding: 0.75rem 0;
    }

    .header-content {
        flex-wrap: wrap;
        row-gap: 0.5rem;
    }

    .header-right {
        width: 100%;
        margin-left: 0;
        gap: 0.5rem 1rem;
        flex-wrap: wrap;
    }

    .header-user {
        margin-left: auto;
    }

    .nav {
        gap: 1rem;
        flex-wrap: wrap;
    }

    .nav a {
        font-size: 0.9rem;
    }
}

/* The latest release: the version, what changed, and one named button per
   platform. */
.release-meta {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin-bottom: 0.75rem;
}

.release-notes {
    line-height: 1.6;
    margin-bottom: 1rem;
    white-space: pre-wrap;
}

.release-downloads {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.privacy-langs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

.privacy-lang {
    text-decoration: none;
}

/* Container and deployment logs. Without this the <pre> had neither a height
   nor a scrollbar: one long line stretched the whole page sideways. */
.logs-container,
.logs-output,
.status-output {
    max-height: 70vh;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.75rem;
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.45;
}

/* ---------------------------------------------------------------------------
   Interaction and accessibility polish.
   --------------------------------------------------------------------------- */

/* Keyboard users need to see where they are: the form fields had a focus ring,
   links and buttons did not. */
a:focus-visible,
.btn:focus-visible,
summary:focus-visible,
.tab:focus-visible,
.media-tag:focus-visible {
    outline: 2px solid var(--primary-text);
    outline-offset: 2px;
}

/* Native disclosure markers are replaced by the button styling. */
summary.btn {
    list-style: none;
}

summary.btn::-webkit-details-marker {
    display: none;
}

/* A status neutral enough for the deployment pages. */
.badge-info {
    background: rgba(96, 165, 250, 0.16);
    color: var(--primary-text);
}

.alert-info {
    background: rgba(96, 165, 250, 0.15);
    border: 1px solid var(--primary-text);
    color: var(--primary-text);
}

/* The fallback letter in an icon: a lowercase package name should not look
   like a lowercase initial. */
.app-icon span {
    text-transform: uppercase;
}

@media (max-width: 768px) {
    /* Thumbs, not cursors. */
    .btn,
    .btn-sm {
        min-height: 2.75rem;
    }

    /* iOS zooms the page when a focused field is under 16px; several compact
       forms set 0.75-0.9rem inline. */
    input,
    select,
    textarea,
    .form-control {
        font-size: 16px !important;
    }
}

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

/* A back link above a page's content, the same in every admin page. */
.page-back {
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.page-back a {
    color: var(--text-muted);
}

.page-back a:hover {
    color: var(--text);
    text-decoration: none;
}

/* A trail, not a back link: root / parent / where you are. */
.breadcrumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 1rem;
    font-size: 0.9rem;
    color: var(--text-muted);
}

.breadcrumbs a {
    color: var(--text-muted);
}

.breadcrumbs a:hover {
    color: var(--text);
    text-decoration: none;
}

.crumb-sep {
    opacity: 0.5;
}

.crumb-current {
    color: var(--text);
}

/* A caption for screen readers, without a second visible title. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* One edit panel per row: the name and the rights are one question, so one
   disclosure and one Save. */
.row-edit {
    margin-top: 0.35rem;
}

.row-edit > summary {
    list-style: none;
    cursor: pointer;
    display: inline-block;
}

.row-edit > summary::-webkit-details-marker {
    display: none;
}

.row-edit-form {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.5rem;
    min-width: 14rem;
}

.row-edit-rights {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 0.75rem;
    font-size: 0.85rem;
}

/* ---------------------------------------------------------------
   Utilities, for the values that were repeated inline hundreds of
   times: muted captions, small sizes, the margins between cards and
   form rows, and inline forms in table cells.

   Defined last on purpose. A utility and a component class have the
   same specificity, so being later is what lets the utility win —
   which is what the inline style it replaced always did. Anything
   that needed more than the component's own look is here; one-off
   layout values stay inline.
   --------------------------------------------------------------- */
.text-muted { color: var(--text-muted); }
.text-danger { color: var(--danger-text); }
.text-warning { color: var(--warning); }
.text-small { font-size: 0.85rem; }
.text-tiny { font-size: 0.8rem; }
.text-micro { font-size: 0.78rem; }
.text-body { font-size: 0.9rem; }
.mb-sm { margin-bottom: 0.5rem; }
.mb-md { margin-bottom: 1rem; }
.mb-lg { margin-bottom: 1.5rem; }
.mt-sm { margin-top: 0.5rem; }
.mt-md { margin-top: 1rem; }
.mt-lg { margin-top: 1.5rem; }
.mt-075 { margin-top: 0.75rem; }
.d-inline { display: inline; }
.d-block { display: block; }
.pad-075 { padding: 0.75rem; }
.break-all { word-break: break-all; }
.choice-row { display: block; padding: 0.2rem 0; }
