/* NOTE: this used to @import styles.css to avoid redeclaring the shared
   --color-primary/etc values below. Dropped that — @import pulls in the
   *entire* kiosk stylesheet, not just :root, and styles.css's kiosk-only
   `html, body { overflow: hidden; height: 100vh; }` (meant to lock the
   unattended kiosk display to the screen) silently applied to this page
   too, breaking scrolling here. Its `.panel-section` rule also collided
   with this file's own `.panel-section` card styling. Self-contained is
   safer than DRY across two independently-purposed pages — if the
   palette defaults below ever change, update both files' :root blocks. */
:root {
  /* Theme-driven — overwritten by theme.js applyTheme() (reads the
     active palette from Postgres). These are fallback defaults (the
     Awqōt palette) so the page looks right even before that async
     fetch resolves, and everything below derives from these three —
     no raw hex colors past this point. */
  --color-primary: #0B4A44;
  --color-secondary: #FFFFFF;
  --color-accent: #C89B3C;
  --theme-transition-ms: 400ms;

  /* Role convention: primary = text/ink, secondary = surface/background
     (see theme.js). Card/divider shades blend toward each palette's own
     primary rather than a fixed white/black, so this works correctly
     whether secondary happens to be light (12 of 13 palettes) or dark
     (Kaabah). */
  --bg: var(--color-secondary);
  --panel-bg: color-mix(in srgb, var(--color-secondary) 92%, var(--color-primary) 8%);
  --text-primary: var(--color-primary);
  --text-muted: color-mix(in srgb, var(--color-primary) 55%, var(--color-secondary) 45%);
  --accent: var(--color-accent);
  /* Fixed dark text for anything with an accent-colored background —
     verified >=4.9:1 contrast against all 13 palettes' accent colors,
     whereas white-on-accent fails badly for most of them. Not itself
     theme-driven on purpose: it's a safety choice, not a brand color. */
  --accent-text: #111111;
  --divider: color-mix(in srgb, var(--color-secondary) 80%, var(--color-primary) 20%);

  /* Semantic, not theme-driven — destructive actions and warnings stay
     recognizable regardless of which palette is active. */
  --danger: #c9524b;
  --warning: #d9974a;

  /* Source Sans 3 loaded as a webfont (see admin/index.html and
     signup/index.html) so it renders identically on every OS — the
     system stack after it is a last-resort fallback only, for if the
     font fails to load. */
  --font-body: "Source Sans 3", -apple-system, "Segoe UI", sans-serif;
  --font-arabic: "Amiri Quran", "Scheherazade New", serif;
}

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    transition: background-color var(--theme-transition-ms) ease,
                color var(--theme-transition-ms) ease,
                border-color var(--theme-transition-ms) ease;
  }

  html, body {
    min-height: 100vh;
    background: var(--bg);
    font-family: var(--font-body);
    color: var(--text-primary);
  }

  /* Same marketing-site background as styles.css's .landing (see that
     rule's own comment) — signup is a front-facing page too, but this
     stylesheet is shared with admin/index.html (the actual signed-in
     admin panel), which must NOT get it. :has(#signupForm) scopes this
     to the one page that actually has that form, without needing a new
     body class on either page — #signupForm only exists in
     signup/index.html (confirmed: zero matches in admin/index.html).
     No background-attachment:fixed here (unlike .landing's own natural
     "doesn't scroll away" effect, which comes for free from its
     independently-scrolling .landing-scroll-col, not from this
     property) — fixed has real, well-documented mobile Safari rendering
     issues, and this body scrolls normally with no equivalent structural
     workaround, so it just scrolls with the page like any other
     background-image would. */
  body:has(#signupForm) {
    background-image: url("../assets/awqotbg.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  }

  .wrap {
    max-width: 1180px;
    margin: 0 auto;
    padding: 3rem 1.5rem 5rem;
  }

  h1 {
    font-size: 1.6rem;
    margin-bottom: 0.3rem;
  }

  .subtitle {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin-bottom: 2.2rem;
  }

  .hidden { display: none !important; }

  /* Login form */
  #loginForm {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    max-width: 340px;
  }

  label {
    font-size: 0.85rem;
    color: var(--text-muted);
    display: block;
    margin-bottom: 0.3rem;
  }

  input[type="email"],
  input[type="password"],
  input[type="text"] {
    width: 100%;
    padding: 0.6rem 0.7rem;
    background: var(--panel-bg);
    border: 1px solid var(--divider);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 0.95rem;
  }

  .password-field {
    position: relative;
  }

  .password-field input {
    padding-right: 3.5rem; /* room for the toggle button below */
  }

  .password-toggle {
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    padding: 0.3rem 0.6rem;
    font-size: 0.78rem;
    color: var(--text-muted);
    background: none;
    border: none;
  }

  .password-toggle:hover {
    border: none;
    color: var(--accent);
  }

  button {
    font-family: inherit;
    font-size: 0.9rem;
    cursor: pointer;
    border-radius: 6px;
    border: 1px solid var(--divider);
    background: var(--panel-bg);
    color: var(--text-primary);
    padding: 0.6rem 1rem;
  }

  button:hover { border-color: var(--accent); }
  button:disabled { opacity: 0.5; cursor: not-allowed; }

  button.primary {
    background: var(--accent);
    color: #ffffff;
    border-color: var(--accent);
    font-weight: 600;
  }

  button.danger {
    background: transparent;
    color: var(--danger);
    border-color: var(--danger);
  }

  hr.divider {
    border: none;
    border-top: 1px solid var(--divider);
  }

  .error-message {
    color: var(--danger);
    font-size: 0.85rem;
    min-height: 1.2em;
  }

  /* "One more step," not a failure — accent-tinted, not the red
     .error-message treatment (see handleFileSelection()'s profile-
     completeness gate in admin.js). */
  .upload-gate-notice {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
    border-radius: 6px;
    padding: 0.6rem 0.8rem;
    font-size: 0.85rem;
    color: var(--text-primary);
  }

  .upload-gate-notice.hidden {
    display: none;
  }

  .upload-gate-link {
    color: var(--accent);
    text-decoration: underline;
    cursor: pointer;
    font-weight: 600;
    white-space: nowrap;
  }

  /* Same accent-tinted treatment as .upload-gate-notice — a confirmation,
     not a warning or error, so it gets the same calm styling rather than
     .error-message's red. Sits above the login form (see admin/index.html)
     when landing back here from /update-password with ?passwordUpdated=1. */
  .toast-success {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
    border-radius: 6px;
    padding: 0.7rem 0.9rem;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 1.4rem;
    max-width: 340px;
  }

  .toast-success.hidden {
    display: none;
  }

  /* Ongoing condition, not a one-time confirmation — warning-tinted
     (amber, same token as .header-text-counter.over-word-guideline)
     rather than .toast-success's accent treatment, so it reads as
     "worth noticing" without the alarm of .error-message's red (there's
     nothing the admin can fix themselves here, see admin-time-sync-
     banner.js — panic isn't useful, awareness is). */
  /* Platform-wide Maintenance Notice — full-width top strip, outside
     .wrap (see admin/index.html's own comment), normal document flow so
     it pushes the page down rather than covering anything. Same muted
     warning tint as .time-sync-banner below (an "ongoing condition,"
     not an error), but shaped as a thin banner instead of that one's
     inline rounded box, since this needs to read as page-level, not
     scoped to one section. */
  .platform-notice-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40px;
    max-height: 50px;
    padding: 0.4rem 1rem;
    background: color-mix(in srgb, var(--warning) 15%, transparent);
    border-bottom: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    font-size: clamp(0.75rem, 1.5vw, 0.95rem);
    font-weight: 600;
    color: var(--text-primary);
    text-align: center;
  }

  .platform-notice-banner.hidden {
    display: none;
  }

  .time-sync-banner {
    background: color-mix(in srgb, var(--warning) 15%, transparent);
    border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    border-radius: 6px;
    padding: 0.7rem 0.9rem;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-top: 1.4rem;
  }

  .time-sync-banner.hidden {
    display: none;
  }

  /* Same shell as .time-sync-banner, but --danger instead of --warning —
     a pending deletion is categorically more urgent than "worth
     noticing" (see admin.js's requestMosqueDeletion()/renderDeletionUI()).
     --danger is the same fixed (not per-theme) token panel-section-alert
     already uses, for the same reason: this must stay visibly red
     regardless of which theme the mosque has picked. */
  .deletion-pending-banner {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border: 1px solid var(--danger);
    border-radius: 6px;
    padding: 0.7rem 0.9rem;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-top: 1.4rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
  }

  .deletion-pending-banner.hidden {
    display: none;
  }

  /* Admin view */
  #adminView header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 2rem;
  }

  /* managingMosque + the kiosk link, inline together (2026-09-05,
     relocated from .header-actions below) — one flex item on header's
     left, so header's own justify-content:space-between still splits
     into exactly two things instead of spacing three apart with a large
     gap in the middle. Shared with /present's own .present-header (see
     present/index.html), which links this file directly. */
  .managing-mosque-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
  }

  /* Sign out alone now (2026-09-05) — the kiosk link moved into
     .managing-mosque-row above, on header's own left side. Kept as its
     own flex item, not folded into a bare <button>, so header's
     space-between still has exactly two top-level children regardless
     of whatever else might join this side of the header later. */
  .header-actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
  }

  /* Shared by /admin (this file) and /present (links this file directly)
     — plain underlined text link to {mosque-slug}.awqot.app, the live
     kiosk page for whichever mosque the page is currently
     managing/presenting. Boxed-button look dropped (2026-09-05) now that
     this sits inline right after the managingMosque text in
     .managing-mosque-row, not floating on its own in the header's
     top-right — a plain link reads more naturally there than a button
     next to plain text. */
  .kiosk-link-btn {
    display: inline-block;
    font-family: inherit;
    font-size: 0.9rem;
    color: var(--text-primary);
    text-decoration: underline;
  }

  .kiosk-link-btn:hover { color: var(--accent); }
  /* .hidden (global, !important, defined above) already covers the
     hidden-until-resolved state — no scoped override needed here. */

  /* Which mosque this session is editing — worth surfacing now that
     accounts are genuinely multi-tenant (see mosque_admins), so staff
     can't lose track of which mosque's slides/theme/config they're
     actually changing. */
  .managing-mosque {
    color: var(--text-primary);
    font-size: 1.1rem;
    font-weight: 600;
    margin-top: 0.1rem;
  }

  /* Rotating admin greeting (see greeting.js) — stands in for the
     h1/subtitle pair once signed in (those get hidden via admin.js),
     spanning full width above the 2-column dashboard grid rather than
     living inside either column. Fresh random pick every time the
     dashboard is shown, not tied to any "seen" state. */
  .admin-greeting {
    margin-bottom: 2rem;
  }

  .greeting-main {
    font-size: 2rem;
    font-weight: 700;
    margin-bottom: 0.3rem;
  }

  .greeting-question {
    color: var(--text-muted);
    font-size: 1.05rem;
  }

  .panel-section {
    background: var(--panel-bg);
    border: 1px solid var(--divider);
    border-radius: 10px;
    padding: 1.3rem;
    margin-bottom: 1.5rem;
  }

  .panel-section h2 {
    font-size: 1rem;
    margin-bottom: 1rem;
  }

  /* Unverified-trial call-out (Mosque Verification panel, admin/index.html)
     — --danger is a fixed token, not redefined per mosque theme (unlike
     --accent), so this stays red regardless of which theme is active. */
  .panel-section-alert {
    background: color-mix(in srgb, var(--danger) 8%, var(--panel-bg));
    border-color: var(--danger);
  }

  .panel-section-alert h2 {
    color: var(--danger);
  }

  #uploadStatus {
    margin-top: 0.8rem;
    font-size: 0.85rem;
    color: var(--text-muted);
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
  }

  /* Slide list — one row per slide. */
  #slideList {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }

  .selection-summary {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin-bottom: 0.8rem;
  }

  .slide-row {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.7rem 0.8rem;
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 8px;
  }

  .slide-row.dragging {
    opacity: 0.4;
  }

  .slide-row .drag-handle {
    cursor: grab;
    color: var(--text-muted);
    font-size: 1rem;
    line-height: 1;
    flex-shrink: 0;
    user-select: none;
  }

  .slide-row .select-checkbox {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    cursor: pointer;
    accent-color: var(--accent);
  }

  .slide-row .order-badge {
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    font-size: 0.8rem;
    width: 1.5rem;
    text-align: right;
    flex-shrink: 0;
  }

  .slide-row .reorder-arrows {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex-shrink: 0;
  }

  .slide-row .reorder-arrows button {
    padding: 0.05rem 0.4rem;
    font-size: 0.65rem;
    line-height: 1.3;
  }

  /* De-scoped from .slide-row so the verification-doc status row (see
     .verification-doc-status below) can reuse these verbatim for its own
     filename display — same truncating-flex-child pattern, not a second
     implementation of it. Confirmed unused outside .slide-row before
     this change (grepped every className/class= use of "filename" and
     "slide-info" across js/ and every *.html). */
  .slide-info {
    flex: 1;
    min-width: 0;
  }

  .filename {
    font-size: 0.95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .slide-row .meta {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin-top: 0.15rem;
  }

  .badge {
    display: inline-block;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.1rem 0.4rem;
    border-radius: 4px;
    border: 1px solid var(--divider);
    color: var(--text-muted);
  }

  .badge.pending {
    color: var(--accent);
    border-color: var(--accent);
  }

  .empty-state {
    color: var(--text-muted);
    font-size: 0.9rem;
    padding: 1rem 0;
  }

  /* Storage usage indicator */
  .usage-bar-track {
    width: 100%;
    height: 10px;
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 999px;
    overflow: hidden;
  }

  .usage-bar-fill {
    height: 100%;
    width: 0%;
    border-radius: 999px;
    background: var(--accent);
    transition: width 0.3s ease, background-color 0.3s ease;
  }

  .usage-bar-fill.warning { background: var(--warning); }
  .usage-bar-fill.critical { background: var(--danger); }

  .usage-text {
    margin-top: 0.6rem;
    font-size: 0.85rem;
    color: var(--text-muted);
  }

  .usage-text.critical { color: var(--danger); }

  /* Theme picker — card style intentionally mirrors awqot-palettes.html
     (the design reference), adapted to this panel's spacing/typography. */
  .theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 0.9rem;
  }

  .theme-card {
    display: block;
    width: 100%;
    padding: 0;
    text-align: left;
    font-family: inherit;
    color: inherit;
    background: none;
    border: 2px solid var(--divider);
    border-radius: 10px;
    overflow: hidden;
    cursor: pointer;
  }

  .theme-card:hover { border-color: var(--accent); }
  .theme-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

  .theme-swatches {
    display: flex;
    height: 46px;
  }

  .theme-swatch { flex: 1; }

  .theme-info {
    padding: 0.65rem 0.75rem 0.75rem;
  }

  .theme-name {
    font-size: 0.9rem;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.15rem;
  }

  .theme-active-badge {
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    background: var(--accent);
    color: var(--accent-text);
    padding: 0.1rem 0.35rem;
    border-radius: 4px;
  }

  .theme-desc {
    font-size: 0.75rem;
    color: var(--text-muted);
    margin-bottom: 0.6rem;
  }

  .theme-mock {
    display: flex;
    height: 36px;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid var(--divider);
  }

  .theme-mock-slide {
    flex: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 8px;
    letter-spacing: 0.4px;
  }

  .theme-mock-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: 7px;
    gap: 1px;
  }

  /* Slide panel header preview — mirrors the kiosk's actual .slide-header
     (see styles.css) so staff see a true preview, not an approximation.
     --bg here (not --panel-bg, which is what the real strip uses) purely
     to stay visible against this card's own --panel-bg background — the
     two tokens are barely distinguishable in practice (see theme.js). */
  .header-preview {
    display: flex;
    align-items: center;
    gap: 1rem;
    min-height: 70px;
    padding: 0.8rem 1.2rem;
    margin-bottom: 1rem;
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 8px;
  }

  .header-preview-logo {
    max-height: 50px;
    width: auto;
    flex-shrink: 0;
    object-fit: contain;
  }

  .header-preview-logo.hidden {
    display: none;
  }

  .header-preview-text {
    flex: 1;
    min-width: 0;
    color: var(--text-primary);
    font-size: 1.1rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .header-preview-text.is-placeholder {
    color: var(--text-muted);
    font-size: 0.85rem;
  }

  .field-hint {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin-top: 0.3rem;
  }

  .field-hint.is-checking { color: var(--text-muted); }
  .field-hint.is-available { color: var(--accent); }

  /* /signup */
  #signupForm { max-width: 400px; }
  #signupForm > div { margin-bottom: 1rem; }

  /* PDPA consent notice, just above the submit button — slightly larger
     than .field-hint (0.78rem) since this is a legal notice being agreed
     to by clicking submit, not a passive form hint. */
  /* Wraps the checkbox itself, not just the text — clicking anywhere in
     the notice toggles it (standard label behavior), except the Privacy
     Policy/Terms links nested inside it, which still just navigate. */
  .consent-notice {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    font-size: 0.82rem;
    line-height: 1.5;
    color: var(--text-muted);
    margin-bottom: 1rem;
    cursor: pointer;
  }

  .consent-notice input[type="checkbox"] {
    flex-shrink: 0;
    margin-top: 0.2rem;
    width: 16px;
    height: 16px;
    accent-color: var(--accent);
    cursor: pointer;
  }

  .signup-footer-link {
    margin-top: 2rem;
  }

  /* Small-print Privacy Policy/Terms of Usage links at the bottom of
     signup/reset-password/update-password — these pages have no full
     site <footer> (see landing-footer for that, marketing pages only),
     just this same .subtitle treatment reused for a second, later line. */
  .legal-links {
    margin-top: 0.75rem;
  }

  .legal-links a + a {
    margin-left: 0.75rem;
  }

  #signupSuccess {
    max-width: 400px;
    padding: 1rem 1.2rem;
    background: var(--panel-bg);
    border: 1px solid var(--divider);
    border-radius: 8px;
    color: var(--text-primary);
  }

  #headerTextForm > div,
  #mosqueProfileForm > div,
  #emailChangeForm > div,
  #passwordChangeForm > div { margin-bottom: 0.9rem; }
  #headerTextForm .form-actions,
  #mosqueProfileForm .form-actions,
  #emailChangeForm .form-actions,
  #passwordChangeForm .form-actions { display: flex; align-items: center; gap: 0.6rem; }

  /* Success indicator for the Save header text button — errors already
     have their own .error-message slot above this, so this only ever
     shows the positive case. Cleared as soon as the field is edited again
     (see admin.js) so it can't linger as a stale "saved" claim. */
  .save-status {
    font-size: 0.85rem;
    color: var(--accent);
  }

  .header-text-counter {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin-top: 0.3rem;
  }

  /* Past the 7-word soft guideline — still allowed (only the 70-char cap
     is hard-enforced), just flagged so staff notice before saving. */
  .header-text-counter.over-word-guideline {
    color: var(--warning);
  }

  /* Quote source: three mutually-exclusive options (auto / always mine /
     always default) — a binary switch can't represent a third state, so
     this is a radio group styled as selectable rows rather than the
     toggle-switch used elsewhere on this page for genuinely 2-value
     settings. */
  .radio-option-group {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }

  .radio-option {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 0.7rem;
    row-gap: 0.15rem;
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--divider);
    border-radius: 8px;
    cursor: pointer;
    transition: border-color var(--theme-transition-ms) ease, background-color var(--theme-transition-ms) ease;
  }

  .radio-option:has(input:checked) {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 8%, transparent);
  }

  .radio-option input[type="radio"] {
    grid-row: 1 / 3;
    align-self: center;
    accent-color: var(--accent);
    width: 16px;
    height: 16px;
  }

  .radio-option-title {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-primary);
  }

  .radio-option-badge {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--accent);
    border: 1px solid var(--accent);
    border-radius: 999px;
    padding: 0.05rem 0.45rem;
    margin-left: 0.3rem;
  }

  .radio-option-desc {
    font-size: 0.82rem;
    color: var(--text-muted);
  }

  /* Display mode's own radio group (/present) — .radio-option-group's
     default flex-column stack above is still exactly right for /admin's
     Quote source group (3 options, reads fine stacked); Display mode's
     own locked spec wants these mutually-exclusive cards in a single row
     instead. A distinct class layered on top, not a change to
     .radio-option-group itself, so Quote source stays untouched. Also
     distinct from .control-grid's own 2-column cap (built for a single-
     field toggle row, or two same-shape fields side by side) — sharing it
     would tangle this group's own sizing with every other .control-grid
     usage on the page. Later in source order than .radio-option-group
     above, so display:grid wins over that class's own display:flex at
     equal specificity.
     BUG FIXED: this stayed a 3-column grid (repeat(3, 1fr)) from before
     Stage 2 removed the Auto option, leaving only 2 cards — a phantom
     empty third column was skewing the row (each real card ~2/3 width,
     blank space on the right) ever since. 2 columns now, matching the
     2 options that actually exist. */
  .display-mode-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
  }

  /* Same grid-track-bubbling fix .control-grid > * uses below — a card's
     own badge/description text has a min-content width that would
     otherwise force each column wider than 1fr actually allows. */
  .display-mode-grid > * {
    min-width: 0;
  }

  @media (max-width: 700px) {
    .display-mode-grid {
      grid-template-columns: 1fr;
    }
  }

  /* Genuinely 2-value settings (clock panel shown/hidden, prayer override
     side panel/fullscreen) keep the switch — an actual switch, not two
     buttons, so the current state reads unambiguously at a glance. */
  /* inline-flex, not flex — a toggle-switch-row's actual content (label +
     switch + label) is only ever a few rem wide; a block-level flex
     container still stretches to its own parent's full width regardless
     of how little of that width its children use, leaving dead space
     next to a standalone toggle. This is the "single-control row taking
     a full row width" fix for the 3 places a toggle-switch-row sits
     alone with nothing else to grid-pair it with (present.js's
     panelBActiveCheckbox row; would also apply to any future one) — see
     .control-grid below for the OTHER shape this same problem takes
     (whole small sections, or several same-form fields) elsewhere. */
  .toggle-switch-row {
    display: inline-flex;
    align-items: center;
    gap: 0.8rem;
  }

  .toggle-label {
    font-size: 0.9rem;
    color: var(--text-muted);
  }

  .toggle-label.active-label {
    color: var(--text-primary);
    font-weight: 600;
  }

  .toggle-switch {
    position: relative;
    display: inline-block;
    width: 46px;
    height: 26px;
    flex-shrink: 0;
  }

  .toggle-switch input {
    opacity: 0;
    width: 0;
    height: 0;
  }

  .toggle-slider {
    position: absolute;
    inset: 0;
    cursor: pointer;
    background: var(--divider);
    border-radius: 999px;
    transition: background-color var(--theme-transition-ms) ease;
  }

  .toggle-slider::before {
    content: "";
    position: absolute;
    height: 20px;
    width: 20px;
    left: 3px;
    bottom: 3px;
    background: var(--panel-bg);
    border-radius: 50%;
    transition: transform 0.25s ease;
  }

  .toggle-switch input:checked + .toggle-slider {
    background: var(--accent);
  }

  .toggle-switch input:checked + .toggle-slider::before {
    transform: translateX(20px);
  }

  /* Quote add/edit form */
  select,
  textarea {
    width: 100%;
    padding: 0.6rem 0.7rem;
    background: var(--panel-bg);
    border: 1px solid var(--divider);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 0.9rem;
    font-family: inherit;
  }

  textarea {
    resize: vertical;
    min-height: 4rem;
  }

  .arabic-input {
    font-family: var(--font-arabic);
    font-size: 1.15rem;
    direction: rtl;
  }

  /* khutbahSlidesTextarea's own font-size zoom (see admin.js) — sits
     directly above that textarea, right-aligned. Deliberately does not
     touch font-family/direction anywhere, just the textarea's own
     font-size, so Arabic text pasted into it keeps whatever glyph
     fallback the browser already gives it at any size. */
  .khutbah-textarea-zoom {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-bottom: 0.4rem;
  }

  .khutbah-textarea-zoom-label {
    font-size: 0.8rem;
    color: var(--text-muted);
    min-width: 3.2em; /* holds "32px" without the buttons shifting as the label's own text width changes */
    text-align: right;
  }

  /* Icon buttons (magnifying glass, see admin/index.html) — same shared
     SVG for both, distinguished by rendered size alone: the enlarge icon
     is bigger than the reduce icon, so which does which is legible
     directly off the icon rather than needing separate glyphs. Fixed
     width/height (not just the icon's own size) keeps both buttons the
     same overall footprint despite the smaller icon otherwise leaving a
     visibly emptier button. */
  .font-zoom-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
  }

  .font-zoom-btn-reduce svg { width: 12px; height: 12px; }
  .font-zoom-btn-enlarge svg { width: 18px; height: 18px; }

  #quoteForm > div { margin-bottom: 0.9rem; }
  #quoteForm .form-actions { display: flex; gap: 0.6rem; }

  /* CSV bulk upload */
  .csv-actions {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    flex-wrap: wrap;
  }

  #quoteCsvSummary {
    margin-top: 0.8rem;
    font-size: 0.85rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
  }

  .csv-summary-line.rejected { color: var(--danger); }

  /* Custom quote list */
  #quoteList {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }

  .quote-row {
    display: flex;
    align-items: flex-start;
    gap: 0.8rem;
    padding: 0.7rem 0.8rem;
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 8px;
  }

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

  .quote-row .quote-ref-line {
    font-size: 0.85rem;
    font-weight: 600;
    margin-bottom: 0.25rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  .quote-row .quote-preview {
    font-size: 0.8rem;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .quote-row .quote-actions {
    display: flex;
    gap: 0.4rem;
    flex-shrink: 0;
  }

  .quote-row .quote-actions button {
    padding: 0.35rem 0.6rem;
    font-size: 0.78rem;
  }

  /* Live Title Overlays gallery (renamed from "Khutbah Subtitle" — display
     text only, see js/admin.js's own renderKhutbahOverlayGallery() comment)
     — one row per image, ordered.
     Deliberately plain up/down buttons for reordering, not drag-and-
     drop — no drag interaction exists anywhere else in this codebase,
     and a small gallery doesn't need one. */
  .khutbah-overlay-gallery-row {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--divider);
  }

  .khutbah-overlay-gallery-row:last-child {
    border-bottom: none;
  }

  .khutbah-overlay-gallery-thumb {
    width: 80px;
    height: 45px; /* 16:9 — a small orientation preview only, the kiosk's own real render has no such cap (see this section's own note in admin/index.html) */
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--divider);
    flex-shrink: 0;
  }

  .khutbah-overlay-gallery-position {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-muted);
    flex-shrink: 0;
  }

  .khutbah-overlay-gallery-controls {
    display: flex;
    gap: 0.4rem;
    margin-left: auto;
  }

  .khutbah-overlay-gallery-controls button {
    padding: 0.4rem 0.7rem;
    font-size: 0.85rem;
  }

  /* Camera/mic device picker row (see js/present-camera-capture.js) —
     plain two-column layout, same minimal unstyled-select convention the
     rest of this file's form elements already use (no bespoke select
     styling exists anywhere in this file; this doesn't add any either,
     just spacing). Class name is a holdover from when this lived on
     admin/index.html (moved to /present, 2026-09-04, see that module's
     own header comment) — not renamed, admin.css is reused as-is by
     present/index.html already (see that file's own <head> comment), so
     this rule already applies there with zero changes needed; a rename
     would only cost a diff, not fix anything. Stacks on narrow viewports
     rather than a hard two-column grid, matching both pages' own mobile-
     friendly intent. */
  .admin-camera-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin: 0.75rem 0;
  }

  .admin-camera-controls > div {
    flex: 1 1 220px;
  }

  .admin-camera-controls select {
    width: 100%;
    padding: 0.5rem 0.6rem;
    background: var(--panel-bg);
    border: 1px solid var(--divider);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 0.9rem;
  }

  /* Live Arabic citation preview (see refreshCitationPreviews() in
     admin.js) — bordered-row shell, same idiom as .quote-row, with a
     second line for the fetched Arabic (or its status). */
  .khutbah-citation-row {
    padding: 0.6rem 0.8rem;
    margin-bottom: 0.5rem;
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 8px;
    font-size: 0.85rem;
    white-space: pre-wrap;
  }

  /* Same font/direction convention as .arabic-input above, for consistency
     between the one Arabic *input* field this admin panel has and this
     read-only Arabic *display*. */
  .khutbah-citation-arabic {
    margin-top: 0.4rem;
    font-family: var(--font-arabic);
    font-size: 1.15rem;
    direction: rtl;
  }

  .khutbah-citation-status {
    margin-top: 0.4rem;
    color: var(--text-muted);
    font-style: italic;
  }

  .khutbah-citation-status-error {
    color: var(--danger);
    font-style: normal;
  }

  /* Over-cap line warning (see renderLineLengthWarnings() in admin.js) —
     same amber "worth noticing, not an error" treatment as
     .header-text-counter.over-word-guideline: nothing here is blocked,
     the 175-char cap just never gets a chance to run on a manually-edited
     or merged line (only packSentencesIntoBodySlides, at extraction time,
     enforces it), so this is the only thing that would ever catch one. */
  .khutbah-line-length-warning-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0.7rem;
    margin-bottom: 0.4rem;
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    border-radius: 6px;
    font-size: 0.8rem;
  }

  .khutbah-line-length-warning-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-primary);
  }

  .khutbah-line-length-warning-count {
    flex-shrink: 0;
    font-weight: 700;
    color: var(--warning);
  }

  /* Same row layout as .khutbah-line-length-warning-row just above, but
     --danger instead of --warning — this one flags slides that block
     Save entirely (a placeholder still needing real Arabic pasted in,
     see handleKhutbahSave() in admin.js), not a soft "worth noticing"
     issue, and needs to read as visually more urgent than the amber
     length warning at a glance. */
  .khutbah-pending-arabic-warning-row {
    padding: 0.5rem 0.7rem;
    margin-bottom: 0.4rem;
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
    border-radius: 6px;
    font-size: 0.8rem;
  }

  .khutbah-pending-arabic-warning-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--danger);
    font-weight: 600;
  }

  /* ===== Admin dashboard tabs — the whole dashboard (was a mix of a
     4-field kiosk-assets sub-tab-group plus a separate 2-column grid for
     storage/deck/quotes/theme) now lives in one tab set: Profile /
     Announcements / Display / Themes / Quotes. Announcements is
     first/default since it's the highest-frequency workflow. */
  #assetsSection {
    margin-bottom: 1.5rem;
  }

  .asset-tabs-nav {
    display: flex;
    gap: 0.4rem;
    margin: -0.3rem 0 1.3rem;
    border-bottom: 1px solid var(--divider);
  }

  /* No prior mobile handling at all here (admin.css had zero @media rules
     before this pass) — same audit finding as the marketing pages: 5 tab
     labels in one non-wrapping flex row overflow past a 390px viewport
     (measured: pushes the layout viewport out to 489px). A horizontally-
     scrollable strip, not vertical stacking, is the right pattern for a
     tab bar specifically — stacking 5 tabs into a column would work but
     reads oddly for tab navigation and eats a lot of vertical space. */
  @media (max-width: 860px) {
    .asset-tabs-nav {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
    }

    .asset-tab-label {
      flex-shrink: 0;
      white-space: nowrap;
    }
  }

  input[name="assetTab"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  .asset-tab-label {
    padding: 0.55rem 0.9rem;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-muted);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    user-select: none;
  }

  .asset-tab-label:hover { color: var(--text-primary); }

  .asset-tab-panel { display: none; }

  #tabProfile:checked ~ .asset-tabs-nav label[for="tabProfile"],
  #tabAnnouncements:checked ~ .asset-tabs-nav label[for="tabAnnouncements"],
  #tabDisplay:checked ~ .asset-tabs-nav label[for="tabDisplay"],
  #tabThemes:checked ~ .asset-tabs-nav label[for="tabThemes"],
  #tabQuotes:checked ~ .asset-tabs-nav label[for="tabQuotes"],
  #tabKhutbah:checked ~ .asset-tabs-nav label[for="tabKhutbah"],
  #tabLive:checked ~ .asset-tabs-nav label[for="tabLive"],
  /* /present's own two tabs (Stage 1 of its redesign) — deliberately
     NOT named #tabKhutbah/data-tab="khutbah" even though /admin already
     has both: no functional collision (separate documents, this
     stylesheet is evaluated per-document), but reusing those exact
     names for a completely different concept (a live khutbah SESSION
     tab here vs. /admin's khutbah CONTENT tab) would be a readability
     trap for anyone grepping either name later. Prefixed instead. */
  #presentTabKhutbah:checked ~ .asset-tabs-nav label[for="presentTabKhutbah"],
  #presentTabLecture:checked ~ .asset-tabs-nav label[for="presentTabLecture"] {
    color: var(--accent);
    border-bottom-color: var(--accent);
  }

  #tabProfile:checked ~ .asset-tab-panel[data-tab="profile"],
  #tabAnnouncements:checked ~ .asset-tab-panel[data-tab="announcements"],
  #tabDisplay:checked ~ .asset-tab-panel[data-tab="display"],
  #tabThemes:checked ~ .asset-tab-panel[data-tab="themes"],
  #tabQuotes:checked ~ .asset-tab-panel[data-tab="quotes"],
  #tabKhutbah:checked ~ .asset-tab-panel[data-tab="khutbah"],
  #tabLive:checked ~ .asset-tab-panel[data-tab="live"],
  #presentTabKhutbah:checked ~ .asset-tab-panel[data-tab="present-khutbah"],
  #presentTabLecture:checked ~ .asset-tab-panel[data-tab="present-lecture"] {
    display: block;
  }

  /* Reusable upload-field component — every asset field (existing or new)
     uses this same shell: title + dimension/size spec pill + optional
     "coming soon" badge, a live preview, the file input, and its error
     slot. Consistent regardless of what the field is actually for. */
  .upload-field {
    padding-bottom: 1.4rem;
    margin-bottom: 1.4rem;
    border-bottom: 1px dashed var(--divider);
  }

  .upload-field:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
  }

  .upload-field-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.7rem;
    flex-wrap: wrap;
  }

  .upload-field-title {
    font-size: 0.95rem;
    font-weight: 600;
  }

  /* BUG FIXED: white-space:nowrap here was fine for every original spec
     (all short, e.g. "PNG · max 1440×100px · 2 MB") but the Panel images
     fields' much longer spec text overflowed straight past this pill's
     own edges instead of wrapping. max-width:100% resolves against
     .upload-field-head's own content width (that flex row's real
     available space — i.e. the container this is meant to match),
     capping the pill there instead of letting it grow arbitrarily wide
     before ever needing to wrap. */
  .upload-field-spec {
    font-size: 0.72rem;
    color: var(--text-muted);
    background: var(--bg);
    border: 1px solid var(--divider);
    border-radius: 999px;
    padding: 0.18rem 0.65rem;
    white-space: normal;
    overflow-wrap: break-word;
    max-width: 100%;
  }

  .upload-field-body {
    display: flex;
    gap: 1.2rem;
    align-items: flex-start;
  }

  .upload-field-preview {
    flex-shrink: 0;
  }

  .upload-field-controls {
    flex: 1;
    min-width: 0;
  }

  /* min-width:0 above lets this column shrink correctly, but a bare
     <input type="file"> still renders at its own native intrinsic width
     (Chrome: ~250px) rather than respecting that — found on the Display
     tab at 390px, where Kiosk background's file input overflowed past
     the .mini-preview-wide box beside it (measured: pushed the page to
     488px wide). Not really viewport-specific — the mismatch exists at
     any width narrow enough to matter — so this isn't gated behind the
     @media block above like .asset-tabs-nav was. */
  .upload-field-controls input[type="file"] {
    max-width: 100%;
  }

  .upload-field-note {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin-top: 0.4rem;
  }

  /* "Uploaded" state for a verification doc slot (see renderVerificationUI()
     in admin.js) — layout only. The checkmark reuses .save-status's
     existing accent-colored confirmation convention (profileFormStatus
     etc.) and the filename reuses .filename/.slide-info's own truncating-
     flex-child pattern above, the same one the slide deck list already
     uses for its (also variable-length, already-solved) filenames. No
     color/sizing declared here beyond the row layout itself — both
     children already carry their own from the classes being reused. */
  .verification-doc-status {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.4rem;
  }

  /* Small square preview used by the kiosk background field — same
     visual weight as the other upload-field previews in this section. */
  .mini-preview {
    width: 96px;
    height: 96px;
    border-radius: 6px;
    border: 1px dashed var(--divider);
    background: var(--bg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.72rem;
    padding: 0.4rem;
  }

  .mini-preview img {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  /* Wide 16:9 preview for the kiosk background field. */
  .mini-preview-wide {
    width: 170px;
    height: 96px;
  }

  /* Kiosk-layout orientation diagram (Panel images section) — plain
     labeled SVG rectangles, no live data/real kiosk markup behind them
     (see the diagram's own HTML comment for why this stays simple).
     Theme-aware via the same custom properties the rest of this page
     already keys off, so it repaints correctly on a theme switch with no
     JS of its own needed. */
  .panel-diagram {
    max-width: 360px;
    margin: 0 auto 0.75rem;
  }

  .panel-diagram svg {
    width: 100%;
    height: auto;
    display: block;
  }

  .panel-diagram-region {
    fill: var(--bg);
    stroke: var(--divider);
    stroke-width: 1.5;
  }

  /* B/C share one accent-tinted treatment — both are upload targets on
     this page; A (no control here, orientation only) stays neutral. */
  .panel-diagram-region-bc {
    fill: color-mix(in srgb, var(--accent) 12%, var(--bg));
    stroke: var(--accent);
  }

  /* Second, visually distinct highlight for /present's live B-panel status
     diagram (present.js's renderPanelBStatusDiagram()) — accent (above)
     marks "live camera" wherever it appears (Main or B), this primary-
     tinted variant marks "uploaded image" in B specifically, so the two
     genuinely different B states never look identical at a glance. Same
     --color-primary token theme.js already drives everywhere else, not a
     new hardcoded color. */
  .panel-diagram-region-primary {
    fill: color-mix(in srgb, var(--color-primary) 12%, var(--bg));
    stroke: var(--color-primary);
  }

  .panel-diagram-label {
    font-family: var(--font-body);
    font-size: 22px;
    font-weight: 700;
    fill: var(--text-primary);
  }

  .panel-diagram-sublabel {
    font-family: var(--font-body);
    font-size: 11px;
    font-weight: 600;
    fill: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }

  /* General-purpose repack for content that was each claiming a full row
     on its own despite being small enough to share one — a handful of
     single-field form rows (Password change, Iqamat offsets, Day-
     specific override), two small standalone sections that were each a
     lone toggle (Timing panel + Prayer time override, admin/index.html's
     Display tab), or, in Panel images above, two upload-fields. max 2
     columns per the actual instruction, not an auto-fit grid that could
     pack more on a wide screen — an odd child just falls to its own row.
     Side-by-side .upload-field children lose the dashed
     stacking-separator that class normally draws between vertically-
     stacked fields — nothing to separate when they're already visually
     split into their own columns. */
  .control-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem;
  }

  /* BUG FIXED (found via real-browser testing at a narrow viewport, not
     assumed): a grid item's default min-width is auto, which resolves to
     its content's min-content size — for the Panel images fields, that
     content includes a spec pill's own long wrapping text, whose
     min-content is still fairly wide even *with* wrapping allowed (the
     longest unbreakable word). Without this, that min-content width
     bubbles up through the 1fr track and the grid itself just grows
     wider than its own container to accommodate it — collapsing to 1
     column at 860px (see the @media rule below) never actually helped,
     since the single remaining column still wouldn't shrink past that
     same floor. This is what actually lets 1fr mean "the container's
     real available width," not "at least as wide as my widest child's
     own content." */
  .control-grid > * {
    min-width: 0;
  }

  .control-grid > .upload-field {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
  }

  /* Grid items don't need form's own <div> rows to keep any bottom
     margin they carried for vertical stacking either. */
  .control-grid > div {
    margin-bottom: 0;
  }

  @media (max-width: 860px) {
    .control-grid {
      grid-template-columns: 1fr;
    }
  }
