/* ---- Design tokens -------------------------------------------------
   Palette: dawn breaking over dusk parchment. The page background is a
   soft, pastel gradient — cool night giving way to warm blush and gold —
   read top to bottom as darkness lifting into first light: hope, not
   dusk waiting. Candlelight amber is still the one warm accent used only
   where something is actually "lit" (the prayer count / the pray button),
   softened here so it sits comfortably inside the pastel gradient instead
   of competing with it.

   Dark mode (below) mirrors the same dawn narrative at night: a dusk
   gradient (deep indigo into a muted ember) instead of the light version's
   bright sky, same "candlelight is the one warm color" logic, all pairs
   re-verified for contrast rather than just inverted. Applied automatically
   via prefers-color-scheme, or forced by data-theme="light"/"dark" on <html>
   (js/nav.js sets this from the logged-in user's saved preference).
   The three --gradient-bg stops were originally near-identical in perceived
   lightness (all ~12-13%), which made the gradient read as almost a flat
   fill rather than a visible transition — widened the spread (~9%-17%
   equivalent) while keeping the same indigo→plum→ember hue progression, so
   the direction is now genuinely visible, not just present in theory.
------------------------------------------------------------------- */
:root {
    --bg: #F7F1E9;
    --bg-card: #FFFEFB;
    /* A shade lighter than --bg-card, for small "raised" elements sitting
       on top of a card (e.g. each verse in the Bible verse picker) that
       need to read as distinguishable from the card itself. */
    --bg-raised: #FFFFFF;
    --ink: #262B3D;
    --ink-soft: #5C6178;
    /* Slightly darker than --ink-soft, used only for the "time ago" half of
       a request card's meta line so the category name (still --ink-soft)
       reads as the more prominent of the two. */
    --ink-meta-time: #4C5166;
    --accent: #C08A2E;
    --accent-glow: #F0C878;
    --line: #E7E0D4;
    --danger: #A5432F;
    --on-danger: #FFFFFF;

    --gradient-bg: linear-gradient(160deg, #E8EDF8 0%, #F6E9E5 48%, #FBEEDB 100%);
    --shadow-soft: 0 2px 14px rgba(38, 43, 61, 0.3);
    --nav-glass: rgba(255, 254, 251, 0.85);
    --crisis-bg: #FBEFE9;
    --crisis-border: #E3B9A6;
    --answered-bg: #FBF3E3;
    --btn-primary-hover: #333A54;

    /* Password strength meter — a generic weak→strong scale, kept separate
       from --accent (candlelight amber is reserved for "lit" elements). */
    --strength-fair: #D9822B;
    --strength-good: #7BA23D;
    --strength-strong: #3E8E52;

    /* Form input/button surface — a lighter tint of --bg (the page's own
       background), not an unrelated color, so inputs and secondary buttons
       read as a distinct layer while still visibly belonging to the same
       palette as the rest of the page. (A prior attempt at "darker than
       --bg-card" landed on an over-saturated tan that clashed with the
       page's neutral cream/parchment tones — this is the correction.) Dark
       mode below uses the same "lighter tint of --bg" formula, which lands
       on a dark blue-gray rather than a light well, so it also flips
       --input-text/--input-text-soft to the light --ink/--ink-soft pair
       instead of keeping the dark navy pair light mode uses. */
    --input-bg: #FBF7EF;
    --input-text: var(--ink);
    --input-text-soft: var(--ink-soft);
    /* Genuine input/textarea/select fields only (not .chip/.btn-secondary,
       which keep using --line) — same as --line in light mode, but dark
       mode below overrides this separately, since --line there reads as
       nearly invisible against --input-bg (they're almost the same
       lightness) whereas a border actually needs to stand out against the
       specific surface it's drawn on. */
    --input-border: var(--line);

    --font-display: "Fraunces", Georgia, serif;
    --font-body: "Inter", -apple-system, sans-serif;
    --font-mono: "IBM Plex Mono", monospace;
    /* Nav-only: a geometric-humanist sans (not the serif --font-display used
       for page titles) — stays crisp at the small sizes the bottom tab bar
       uses on mobile, where a serif's thin strokes get lost. */
    --font-nav: "Manrope", -apple-system, sans-serif;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #1B1E2B;
        --bg-card: #242838;
        --bg-raised: #2F3446;
        --ink: #EDEAE2;
        --ink-soft: #A6ABC0;
        --ink-meta-time: #8D91A3;
        --accent: #D9A84E;
        --accent-glow: #E8B85A;
        --line: #363C52;
        --danger: #E8785A;
        --on-danger: #1A0E0A;

        --gradient-bg: linear-gradient(160deg, #12141F 0%, #221B28 48%, #2E2013 100%);
        --shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.4);
        --nav-glass: rgba(27, 30, 43, 0.85);
        --crisis-bg: #3A241E;
        --crisis-border: #5C3A2E;
        --answered-bg: #3A2E14;
        --btn-primary-hover: #DCD8CE;

        --strength-fair: #E0A050;
        --strength-good: #96C15C;
        --strength-strong: #5CC17E;

        /* Same "lighter tint of --bg" formula as light mode's --input-bg
           above, which here lands on a dark blue-gray rather than a light
           well — a few earlier attempts (reusing light mode's near-white
           bg-card verbatim, then two rounds of a beige/tan "darker" fix)
           all either clashed with the rest of dark mode's cool indigo
           palette or drifted into an unrelated tan hue entirely; staying in
           the same blue-gray family as --bg/--bg-card fixes both. Because
           this is now a dark surface, --input-text/--input-text-soft switch
           to the light --ink/--ink-soft pair instead of light mode's dark
           navy pair — dark text would be unreadable here. Nudged one shade
           lighter again (#2B2F42 → #343A52) per feedback that it needed a
           little more contrast against --bg-card — unlike light mode
           (input darker than its card), dark mode's input is already
           lighter than --bg-card, so lightening it further is what actually
           widens that gap here. */
        --input-bg: #343A52;
        --input-text: var(--ink);
        --input-text-soft: var(--ink-soft);
        /* --line (#363C52) is nearly the same lightness as --input-bg here,
           so it reads as barely-there against an actual input field — a
           dedicated, visibly lighter border just for inputs instead. */
        --input-border: #4C5372;
    }
}

:root[data-theme="dark"] {
    --bg: #1B1E2B;
    --bg-card: #242838;
    --bg-raised: #2F3446;
    --ink: #EDEAE2;
    --ink-soft: #A6ABC0;
    --ink-meta-time: #8D91A3;
    --accent: #D9A84E;
    --accent-glow: #E8B85A;
    --line: #363C52;
    --danger: #E8785A;
    --on-danger: #1A0E0A;

    --gradient-bg: linear-gradient(160deg, #12141F 0%, #221B28 48%, #2E2013 100%);
    --shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.4);
    --nav-glass: rgba(27, 30, 43, 0.85);
    --crisis-bg: #3A241E;
    --crisis-border: #5C3A2E;
    --answered-bg: #3A2E14;
    --btn-primary-hover: #DCD8CE;

    --strength-fair: #E0A050;
    --strength-good: #96C15C;
    --strength-strong: #5CC17E;

    --input-bg: #343A52;
    --input-text: var(--ink);
    --input-text-soft: var(--ink-soft);
    --input-border: #4C5372;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--gradient-bg);
    background-attachment: fixed;
    color: var(--ink);
    font-family: var(--font-body);
    line-height: 1.5;
}

/* 820px, not the original 640px — widened so the Dashboard sub-nav's six
   pills (Dashboard/My Requests/Saved/Groups/Subscriptions/Settings, ~735px
   of natural content width) fit without needing their own horizontal
   scroll. There was no documented reason for 640px specifically (it wasn't
   tied to a readability constraint anywhere in this file or README) — it
   was just the original single-column-feed width, so this applies
   site-wide via the one shared .wrap class rather than carving out a
   special case for just the Dashboard pages. */
.wrap {
    max-width: 820px;
    margin: 0 auto;
    padding: 32px 20px 80px;
}

/* ---- Shared icon set (js/icons.js) — replaces the emoji this app used to
   render for nav items and card actions. Sized in `em`s, not a fixed px
   value, so each icon scales with whatever font-size its own context
   already sets (a nav link, a pill, a heading) rather than needing a
   size override everywhere it's used — the same approach FLAME_SVG/
   .site-nav-flame-icon already take. currentColor means it always matches
   the surrounding text color unless a context overrides it (e.g. .flame
   inside .pray-btn, which still sets its own explicit stroke color). ---- */
.lucide-icon {
    width: 1em;
    height: 1em;
    flex: 0 0 auto;
    vertical-align: -0.125em;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ---- Links — one consistent style site-wide. Deliberately does NOT need to
   exclude .site-nav-link/.notification-bell: both already set their own
   color and text-decoration explicitly, which wins on specificity alone.
   Uses --accent rather than the browser default blue, which is how this bug
   showed up in the first place — dashboard.html's plain <a> links had no
   color rule at all, so they fell back to default link-blue, which fails
   contrast against the dark gradient background. ---- */
a {
    color: var(--accent);
    text-decoration: underline;
}

a:hover {
    color: var(--accent-glow);
}

/* ---- Site navigation ---------------------------------------------------
   One shared component (js/nav.js) rendered on every page. Breakpoint at
   768px — the standard mobile/tablet boundary — splits it into two distinct
   styles: a fixed bottom tab bar on phones (the native-app pattern users
   already expect there) and a fixed top bar on tablet/desktop (the standard
   website pattern). Tablet and desktop share one treatment since this app's
   content is already capped at a fixed width (.wrap's max-width) — there's
   nothing left for a tablet-specific layout to do that the desktop one
   doesn't already do. ---- */
.site-nav {
    display: flex;
    align-items: center;
}

/* GetPrayer wordmark: the same flame mark used as the pray-button icon
   (js/common.js's FLAME_SVG) and the PWA/favicon, so the "lit candle" motif
   reads as one consistent brand identity everywhere it appears — not a
   second, unrelated icon design. Only shown on the tablet/desktop top bar —
   a left logo with nav links "to its right" is a website-header pattern,
   not a bottom-tab one. */
.site-logo {
    display: none;
    flex: 0 0 auto;
    align-items: center;
    gap: 7px;
    text-decoration: none;
    color: var(--ink);
}

.site-logo-mark {
    width: 22px;
    height: 22px;
    flex: 0 0 auto;
    /* Same fill+stroke recipe as .pray-btn.lit's flame (and the mobile bottom
       nav's Home icon, .site-nav-flame-icon) — a plain fill alone reads
       noticeably thinner than that. */
    fill: var(--accent-glow);
    stroke: var(--accent);
    stroke-width: 1.6;
}

.site-logo-text {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 1.05rem;
    letter-spacing: -0.01em;
    white-space: nowrap;
}

.site-nav-links {
    display: flex;
    align-items: center;
}

.site-nav-link {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 10px 14px;
    color: var(--ink-soft);
    text-decoration: none;
    font-family: var(--font-nav);
    font-weight: 500;
    font-size: 0.85rem;
    border-radius: 8px;
}

.site-nav-link:hover { background: var(--bg); }

.site-nav-link.active {
    color: var(--ink);
    font-weight: 600;
}

.site-nav-icon { font-size: 1rem; line-height: 1; }

/* Home's icon on the mobile bottom tab bar (js/nav.js's HOME_FLAME_ICON) —
   an inline SVG, not emoji text, so it needs its own explicit box size
   instead of inheriting .site-nav-icon's font-size. Same fill+stroke as
   .site-logo-mark so it reads as the same brand mark (this base rule is
   itself always overridden by the mobile media query below — kept in sync
   with it anyway so nothing looks inconsistent if that override ever
   changes without this one being noticed). */
.site-nav-flame-icon {
    width: 1.2rem;
    height: 1.2rem;
    fill: var(--accent-glow);
    stroke: var(--accent);
    stroke-width: 1.6;
}

/* Groups the notification bell with whichever of "Log In" (logged out) or
   "Dashboard" (logged in) is present — see nav.js's renderMainNav — so they
   sit right next to each other at the far right of the bar, instead of
   clustering with Home at the left while the bell sits alone at the true
   right edge with a big gap in between. Positioned at the far right via
   margin-left:auto on the wider top bar (see desktop media query below); on
   mobile it's simply the last item in the bar, matching where the bell
   alone used to sit. */
.site-nav-right {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
}

.notification-wrap {
    position: relative;
    flex: 0 0 auto;
    display: flex;
}

.notification-bell {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 10px;
    font-size: 1.15rem;
    color: var(--ink-soft);
    border-radius: 8px;
    background: none;
    border: none;
    font: inherit;
    cursor: pointer;
}

.notification-bell:hover { background: var(--bg); }

/* Anchored to .notification-wrap, not the button itself, so its position
   doesn't shift with the button's own padding/hover state. Opens downward
   from the top bar; the mobile media query below flips it to open upward
   from the bottom tab bar instead, since downward would run off-screen. */
.notification-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    width: 320px;
    max-width: calc(100vw - 24px);
    max-height: 70vh;
    overflow-y: auto;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: var(--shadow-soft);
    padding: 10px;
    z-index: 20;
}

.notification-section + .notification-section {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
}

.notification-section-title {
    margin: 0 0 6px;
    font-family: var(--font-nav);
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.notification-item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
    padding: 6px 4px;
    border-radius: 6px;
    color: var(--ink);
    text-decoration: none;
}

.notification-item:hover { background: var(--bg); }

.notification-item-text {
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.notification-item-time {
    flex: 0 0 auto;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    color: var(--ink-soft);
}

.notification-empty {
    margin: 4px;
    font-size: 0.82rem;
    color: var(--ink-soft);
}

.notification-view-all {
    display: block;
    margin-top: 6px;
    font-size: 0.78rem;
    text-align: right;
}

/* Dull/greyscale until there's something new, then switches to --ink (which
   is already near-black in light mode / near-white in dark mode) at full
   opacity — reuses the existing theme token instead of hardcoding colors. */
.notification-bell svg {
    width: 1.15rem;
    height: 1.15rem;
    color: var(--ink-soft);
    opacity: 0.45;
    filter: grayscale(1);
    transition: color 0.15s ease, opacity 0.15s ease;
}

.notification-bell.has-unread svg {
    color: var(--ink);
    opacity: 1;
    filter: none;
}

.notification-badge {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--danger);
    color: var(--on-danger);
    font-family: var(--font-mono);
    font-size: 0.62rem;
    font-weight: 700;
    border-radius: 999px;
    line-height: 1;
}

/* Tablet + desktop: fixed top bar. Logo left, nav links to its right with
   padding between them, bar height fixed at 56px so the logo's 36px height
   (bar height - 20px) is exact; width is auto since it's an icon+wordmark
   now, not a fixed-size placeholder box. */
@media (min-width: 768px) {
    body { padding-top: 56px; }
    .site-nav {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        height: 56px;
        justify-content: flex-start;
        background: var(--nav-glass);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        border-bottom: 1px solid var(--line);
        padding: 0 20px;
        z-index: 10;
    }
    .site-logo {
        display: flex;
        height: 36px;
    }
    .site-nav-links {
        gap: 12px;
        margin-left: 20px;
    }
    .site-nav-icon { display: none; }
    .site-nav-right { margin-left: auto; }
    /* Redundant with the logo (both go home) — hidden here, but still shown
       on the mobile bottom tab bar below, which has no logo at all. */
    .nav-home-link { display: none; }
}

/* Mobile: fixed bottom tab bar, icon over label, app-like. No logo here —
   Home (the only item .site-nav-links ever renders here, everything else
   lives in .site-nav-right — see renderMainNav) sits flush against the bar's
   left edge instead, balancing the bell + Log In/Dashboard group pinned to
   the right by .site-nav-links' own flex:1. */
@media (max-width: 767px) {
    body { padding-bottom: 64px; }
    .wrap { padding-top: 15px; padding-bottom: 30px; }
    .site-nav {
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        background: var(--nav-glass);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        border-top: 1px solid var(--line);
        padding: 6px 14px;
        z-index: 10;
    }
    .site-nav-links {
        flex: 1;
        justify-content: flex-start;
    }
    /* Every item in the bar — Home/Log In/Dashboard's two-line links, and
       the icon-only notification bell next to them — shares one explicit
       height and centers its own content within it, rather than sizing to
       content the way the desktop bar's links do. A label-less item (the
       bell) and two differently-sized icon types (Home's inline SVG vs.
       Log In/Dashboard's emoji glyph) would otherwise each size themselves
       slightly differently and throw the whole row's height off — an
       explicit shared height sidesteps that instead of chasing it via
       alignment tricks (aspect-ratio/stretch already proved fragile for a
       similar "match a sibling's height" case elsewhere in this file). */
    .site-nav-link {
        flex-direction: column;
        justify-content: center;
        gap: 2px;
        height: 50px;
        padding: 0 10px;
        border-radius: 0;
        font-size: 0.68rem;
    }
    .site-nav-icon {
        font-size: 1.25rem;
        height: 1.25rem;
        display: flex;
        align-items: center;
    }
    /* Home's flame is deliberately bigger than the emoji icon beside it (a
       brand mark, not just a functional glyph) — the shared height/centering
       on .site-nav-link above is what keeps the row aligned despite that,
       not equal icon sizes. margin-top nudges it to look vertically centered
       against its own label, same reasoning as the save-star nudge below.
       fill+stroke matches the same treatment .pray-btn.lit's flame uses
       (accent-glow fill, accent stroke) — a plain fill alone (this icon's
       original, .site-logo-mark-matching treatment) reads noticeably
       thinner than the request cards' flame at this size. */
    .site-nav-flame-icon {
        width: 1.9rem;
        height: 1.9rem;
        margin-top: 9px;
        fill: var(--accent-glow);
        stroke: var(--accent);
        stroke-width: 1.6;
    }

    .notification-wrap { height: 50px; }
    .notification-bell {
        height: 100%;
        padding: 0 10px;
    }
    .notification-bell svg { width: 1.25rem; height: 1.25rem; }

    /* The bell lives in a bottom-fixed bar here, so the dropdown opens
       upward instead of down (which would run off the bottom of the
       viewport), and is right-aligned to the screen edge rather than the
       narrow wrap element so it doesn't overflow off-screen on small phones. */
    .notification-dropdown {
        top: auto;
        bottom: calc(100% + 8px);
        right: -6px;
    }
}

/* ---- Header ---- */
.page-header-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}

/* The public wall's own header (index.html) — a 2-column grid instead of
   .page-header-row's plain flex row, so the verse (row 2, column 1) is
   constrained to the same width as the heading (row 1, column 1) rather
   than running the page's full width under the button. Column 2 is `auto`
   (the button's own content width) and only ever occupied in row 1 — the
   verse's column 1 track is sized against it automatically, so it stays
   correct however long "+ Submit request" ends up being, with no need to
   hardcode or measure the button's width. */
.wall-header {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    column-gap: 12px;
}

.wall-header .page-title { grid-column: 1; grid-row: 1; }
.wall-header #toggle-submit-btn { grid-column: 2; grid-row: 1; }
.wall-header .page-sub { grid-column: 1; grid-row: 2; }

/* Edit-profile pencil next to a group's name (js/group.js's groupHeroHtml)
   — faded until hovered/focused so it reads as a secondary affordance, not
   competing with the name itself for attention. */
.group-edit-icon {
    background: none;
    border: none;
    padding: 0;
    font-size: 0.375em;
    cursor: pointer;
    opacity: 0.35;
    vertical-align: middle;
}

.group-edit-icon:hover,
.group-edit-icon:focus-visible {
    opacity: 1;
}


.page-title {
    font-family: var(--font-display);
    font-size: 2.1rem;
    font-weight: 600;
    line-height: 2rem;
    padding-bottom: 0.5rem;
    margin: 0 0 4px;
    letter-spacing: -0.01em;
}

.page-sub {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--ink-soft);
    letter-spacing: 0.02em;
    margin: 0 0 28px;
}

/* ---- Category chips ---- */
.category-row {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 10px;
    margin-bottom: 24px;
    /* Themed scrollbar instead of the browser default — standard CSS, no
       library needed. scrollbar-width/scrollbar-color cover Firefox; the
       ::-webkit-scrollbar rules below cover Chrome/Safari/Edge. */
    scrollbar-width: thin;
    scrollbar-color: var(--line) transparent;
}

.category-row::-webkit-scrollbar {
    height: 6px;
}

.category-row::-webkit-scrollbar-track {
    background: transparent;
}

.category-row::-webkit-scrollbar-thumb {
    background-color: var(--line);
    border-radius: 999px;
}

.category-row::-webkit-scrollbar-thumb:hover {
    background-color: var(--ink-soft);
}

.chip {
    flex: 0 0 auto;
    font-family: var(--font-body);
    font-size: 0.85rem;
    padding: 7px 14px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--input-bg);
    color: var(--ink-soft);
    cursor: pointer;
    white-space: nowrap;
}

.chip.active {
    background: var(--ink);
    color: var(--bg);
    border-color: var(--ink);
}

/* Pending/Reported sub-nav count badge (js/group.js's renderGroupOwnerSubnav)
   — always computed from the same moderation.php snapshot the page under
   it renders from, so this number can never drift from that page's own
   displayed total. */
.subnav-count {
    display: inline-block;
    min-width: 16px;
    padding: 1px 5px;
    border-radius: 999px;
    background: var(--danger);
    color: var(--on-danger);
    font-size: 0.7rem;
    line-height: 1.4;
    text-align: center;
}

/* ---- Submit form ---- */
.submit-card {
    position: relative; /* anchors .submit-wisp/.submit-spark below */
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 18px;
    margin-bottom: 32px;
    box-shadow: var(--shadow-soft);
}

/* "Sent Upward — Fades Out" — plays once on the public wall's submit form
   right after a successful submission (js/app.js's playSubmitSentUpward()):
   the card the writer was just looking at lifts, glows, and dissolves away
   before the form actually resets/hides, rather than just vanishing. Same
   rise/wisp/sparks language as .pray-lift/.answered-celebrate elsewhere on
   a request card, just framed as an exit instead of an in-place reaction —
   the form starts fully visible (opacity:1) and fades OUT, the reverse of
   how .pray-lift/.answered-celebrate animate an already-visible card. */
.submit-card.sent-upward-fade-out {
    animation: sent-upward-fade-out 2s cubic-bezier(0.3, 0.1, 0.3, 1) forwards;
}

@keyframes sent-upward-fade-out {
    0%   { opacity: 1; transform: translateY(0) scale(1); box-shadow: var(--shadow-soft); }
    30%  { opacity: 1; box-shadow: 0 22px 40px -6px var(--accent-glow), var(--shadow-soft); transform: translateY(-8px) scale(1.01); }
    100% { opacity: 0; transform: translateY(-54px) scale(0.95); box-shadow: 0 30px 46px -10px transparent; }
}

.submit-wisp {
    position: absolute;
    left: 50%;
    bottom: 100%;
    width: 46%;
    height: 90px;
    transform: translateX(-50%);
    background: linear-gradient(to top, var(--accent-glow), transparent 85%);
    opacity: 0;
    filter: blur(2px);
    pointer-events: none;
    animation: wisp-rise 2s ease-out forwards;
}

@keyframes wisp-rise {
    0%   { opacity: 0; transform: translateX(-50%) translateY(10px) scaleY(0.6); }
    35%  { opacity: 0.55; }
    100% { opacity: 0; transform: translateX(-50%) translateY(-46px) scaleY(1.3); }
}

.submit-spark {
    position: absolute;
    left: var(--sx);
    top: var(--sy);
    width: var(--ssize);
    height: var(--ssize);
    border-radius: 50%;
    background: var(--accent-glow);
    box-shadow: 0 0 6px 1px var(--accent-glow);
    opacity: 0;
    pointer-events: none;
    animation: submit-spark-drift var(--sdur) ease-out var(--sdelay) forwards;
}

@keyframes submit-spark-drift {
    0%   { opacity: 0; transform: translate(0, 0) scale(0.4); }
    18%  { opacity: 1; transform: translate(calc(var(--dx) * 0.3), calc(var(--dy) * 0.3)) scale(1); }
    100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.5); }
}

@media (prefers-reduced-motion: reduce) {
    .submit-card.sent-upward-fade-out { animation: none; }
    .submit-wisp, .submit-spark { display: none; }
}

.submit-card textarea,
.submit-card input[type=text],
.submit-card input[type=email],
.submit-card input[type=password],
.submit-card select {
    width: 100%;
    font-family: var(--font-body);
    font-size: 0.95rem;
    padding: 10px 12px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
    margin-bottom: 10px;
}

.submit-card textarea::placeholder,
.submit-card input::placeholder {
    color: var(--input-text-soft);
    opacity: 1;
}

.submit-card textarea { min-height: 80px; resize: vertical; }

/* Breathing room above the category select on a group's collapsible
   "Post to group" form (js/group.js's #post-content, toggled display:
   none/block by the heading click) — the select otherwise sits flush
   against the heading right above it. Scoped to #post-content rather than
   .submit-card select generally so it only ever shows up while the section
   is actually expanded; display:none already hides it (and everything
   else inside) while collapsed, so no separate visibility rule is needed. */
#post-content {
    padding-top: 12px;
}

/* "Subscribe" — a checkbox against the visitor's own account email, used on
   every submit form site-wide (js/app.js's public wall, js/group.js's Post
   to group, js/my-requests.js) now that posting a request always requires
   an account (README §100). Rendered inline next to the poster's badge
   avatar in `.posting-as-row`, not on its own line. */
.notify-email-checkbox-label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.9rem;
    color: var(--ink-soft);
    margin-bottom: 10px;
    cursor: pointer;
}

.notify-email-checkbox-label input[type=checkbox] {
    flex: 0 0 auto;
    width: auto;
    margin: 0;
}

/* ---- Password field with a show/hide toggle ---- */
.password-field-wrap {
    position: relative;
}

.password-field-wrap input[type=password],
.password-field-wrap input[type=text] {
    padding-right: 48px;
}

.password-toggle-btn {
    position: absolute;
    right: 6px;
    top: 8px;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--input-text-soft);
    font-size: 0.72rem;
    font-family: var(--font-mono);
    padding: 6px 8px;
}

/* ---- Password strength meter (js/password-strength.js) ---- */
.password-strength-meter {
    margin: -6px 0 12px;
}

.password-strength-track {
    height: 4px;
    border-radius: 2px;
    background: var(--line);
    overflow: hidden;
}

.password-strength-fill {
    height: 100%;
    width: 0;
    border-radius: 2px;
    transition: width 0.2s ease, background-color 0.2s ease;
}

.password-strength-fill.password-strength-weak { background: var(--danger); }
.password-strength-fill.password-strength-fair { background: var(--strength-fair); }
.password-strength-fill.password-strength-good { background: var(--strength-good); }
.password-strength-fill.password-strength-strong { background: var(--strength-strong); }

.password-strength-label {
    display: block;
    font-size: 0.75rem;
    margin-top: 4px;
    color: var(--ink-soft);
}

.password-strength-label.password-strength-weak { color: var(--danger); }
.password-strength-label.password-strength-fair { color: var(--strength-fair); }
.password-strength-label.password-strength-good { color: var(--strength-good); }
.password-strength-label.password-strength-strong { color: var(--strength-strong); }

/* ---- Initials box (dashboard registration form layout only) ---- */
.initials-box {
    display: inline-block;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 14px;
    margin-bottom: 10px;
}

/* ---- Poster identity in the wall's submit form: notify-email + optional
   inline account creation when logged out, or just the avatar when already
   logged in. Reuses .password-field-wrap/.initials-box from the dashboard's
   register form and .avatar-initials below. ---- */
.create-account-row {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.85rem;
    color: var(--ink-soft);
    margin: 2px 0 10px;
    cursor: pointer;
}

.create-account-row input[type=checkbox] {
    margin: 0;
}

.posting-as-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
}

/* ---- Reusable initials avatar — same look everywhere it's rendered.
   Generated via avatarInitialsHtml() / sanitizeInitials() in js/avatar.js. ---- */
.avatar-initials {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--bg-card);
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
    flex: 0 0 auto;
}

/* Only an avatar with an earned badge has anything to hover/tap for — see
   the tooltip listeners in js/avatar.js, which target this same attribute. */
.avatar-initials[data-badge-name] { cursor: pointer; }

/* Badge shapes — a "circle family" of regular polygons (see
   badge_definitions.shape / api/badges.php's best_badge). Tier 1 is a plain
   circle; each tier after that loses a side rather than gaining one, so the
   shape reads as progressively more sharply defined at higher tiers (§46 —
   originally the reverse direction; flipped on request). Exact polygon
   coordinates are precomputed regular-polygon vertices (first vertex
   pointing straight up), not hand-eyeballed. .avatar-initials--circle is an
   explicit no-op (the base .avatar-initials rule is already a circle) kept
   only so every value in the shape ENUM has a matching rule, for the same
   reason the other five do. */
.avatar-initials--circle { border-radius: 50%; clip-path: none; }
.avatar-initials--pentagon { border-radius: 0; clip-path: polygon(50% 0%, 97.6% 34.5%, 79.4% 90.5%, 20.6% 90.5%, 2.4% 34.5%); }
.avatar-initials--hexagon { border-radius: 0; clip-path: polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%); }
.avatar-initials--heptagon { border-radius: 0; clip-path: polygon(50% 0%, 89.1% 18.8%, 98.7% 61.1%, 71.7% 95%, 28.3% 95%, 1.3% 61.1%, 10.9% 18.8%); }
.avatar-initials--octagon { border-radius: 0; clip-path: polygon(50% 0%, 85.4% 14.6%, 100% 50%, 85.4% 85.4%, 50% 100%, 14.6% 85.4%, 0% 50%, 14.6% 14.6%); }
.avatar-initials--decagon { border-radius: 0; clip-path: polygon(50% 0%, 79.4% 9.5%, 97.6% 34.5%, 97.6% 65.5%, 79.4% 90.5%, 50% 100%, 20.6% 90.5%, 2.4% 65.5%, 2.4% 34.5%, 20.6% 9.5%); }

/* A badge-holder's name is never shown as a permanent label — only via this
   shared hover/tap tooltip (see avatarInitialsHtml() + the tooltip listeners
   in js/avatar.js). Positioned with inline left/top (computed per-target in
   JS, since the same avatar renders at very different sizes/places — the
   "posting as you" widget vs. a request card's compact poster badge). */
.avatar-tooltip {
    position: absolute;
    z-index: 50;
    background: var(--ink);
    color: var(--bg);
    font-family: var(--font-body);
    font-size: 0.72rem;
    font-weight: 600;
    padding: 4px 9px;
    border-radius: 6px;
    white-space: nowrap;
    pointer-events: none;
    box-shadow: var(--shadow-soft);
}

.btn {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.9rem;
    padding: 9px 18px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    white-space: nowrap;
    flex-shrink: 0;
    /* .btn is also used on <a> elements (e.g. the Users page's Edit link) —
       a button should never render with the browser's default link
       underline regardless of which element it's applied to. */
    text-decoration: none;
}

/* .btn sets cursor:pointer unconditionally above, which otherwise masks a
   disabled button's default not-allowed affordance (used by the Prev/Next
   pagination controls, js/subscriptions.js) — opacity is the primary signal
   here, cursor is just a secondary confirmation. */
.btn:disabled {
    opacity: 0.45;
    cursor: default;
}

.btn-primary { background: var(--ink); color: var(--bg); }
.btn-primary:hover { background: var(--btn-primary-hover); }

/* The site-wide "secondary action" button (Cancel, Edit, Dismiss, Remove,
   Log out, etc.) — same surface as form inputs and .chip (--input-bg: a
   lighter tint of the page background so it reads as its own distinct
   control, not just a bare outline), with a legible soft-ink text color, rather than
   each call site inline-styling `border:1px solid var(--line);background:var(--bg)`
   with NO explicit color at all. That was the actual bug behind two
   separate "button text is dark/hard to read in dark mode" reports: with no
   `color` set, the browser falls back to its own default form-control text
   color, which isn't dark-mode-aware the way every *other* color in this
   page is — it doesn't automatically become light just because --bg went
   dark. One shared class with an explicit, theme-aware color fixes every
   button using it at once, everywhere in the site, instead of one at a time
   as each is separately noticed. */
.btn-secondary {
    background: var(--input-bg);
    color: var(--ink-soft);
    border: 1px solid var(--line);
}

/* A button that should read as plain text, not a control — no fill, border,
   or padding, just the label (currently just "Log out" — see js/nav.js's
   renderLogoutButton()). Explicit font-family/size (matching .btn) rather
   than relying on inheritance, since a bare <button> otherwise falls back to
   the browser's own default form-control font, not the page's. */
.btn-text {
    background: none;
    border: none;
    padding: 0;
    font-family: var(--font-body);
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--ink-soft);
    cursor: pointer;
}

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

.crisis-note {
    margin-top: 10px;
    padding: 10px 12px;
    background: var(--crisis-bg);
    border: 1px solid var(--crisis-border);
    border-radius: 8px;
    font-size: 0.85rem;
    color: var(--danger);
}

/* ---- Prayer cards ---- */
.request-card {
    position: relative;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 18px;
    margin-bottom: 14px;
    box-shadow: var(--shadow-soft);
}

/* "Lifted in prayer" — plays once on the card the first time a visitor
   prays for it (js/common.js's handlePrayClick; already_praying is what
   guards "first time," not just the button's own lit/unlit class). No
   particles, no separate overlay element — just the card itself rising
   slightly and glowing warmly before settling back, distinct from
   .answered-celebrate's rays-burst below (that one's for an actual answer,
   this one's for every fresh act of praying). */
.request-card.pray-lift {
    animation: pray-lift 1.6s cubic-bezier(0.3, 0.6, 0.3, 1);
}

@keyframes pray-lift {
    0% { transform: translateY(0); box-shadow: var(--shadow-soft); }
    35% { transform: translateY(-12px); box-shadow: 0 18px 34px -6px var(--accent-glow), var(--shadow-soft); }
    100% { transform: translateY(0); box-shadow: var(--shadow-soft); }
}

@media (prefers-reduced-motion: reduce) {
    .request-card.pray-lift { animation: none; }
}

/* Unread indicator — a request not yet scrolled into view this browser's
   viewport (tracked in js/unread.js via localStorage). Disappears the
   moment IntersectionObserver marks it viewed (see js/app.js). */
.unread-dot {
    position: absolute;
    top: 12px;
    left: 12px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #4CAF50;
    box-shadow: 0 0 0 3px var(--bg-card);
}

.request-meta {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.request-meta-time { color: var(--ink-meta-time); }

/* A real link to /?category=<slug> (same destination the category filter
   chips already use) but deliberately not styled like one — resets the
   site-wide `a` color/underline so it stays visually identical to the
   plain text it used to be, per request. */
.request-meta-category {
    color: inherit;
    text-decoration: none;
}

.request-meta-category:hover {
    text-decoration: underline;
}

/* Save + "more options" menu, inline with the category/timestamp on the
   card's top-right — see js/request-card.js's requestCardHtml(). Not
   uppercase/letter-spaced like the rest of .request-meta despite living
   inside it (these render icons/symbols, not the tracked meta text). */
.request-meta-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    text-transform: none;
    letter-spacing: normal;
    flex: 0 0 auto;
}

.request-meta-actions .save-btn {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 1rem;
    border-radius: 50%;
}

.request-menu-wrap { position: relative; display: flex; }

.request-menu-btn {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
    color: var(--ink-soft);
    background: none;
    border: 1px solid var(--line);
    border-radius: 50%;
    cursor: pointer;
}

.request-menu-btn:hover { background: var(--bg); }

/* Anchored to .request-menu-wrap (not the button), same pattern as the
   nav's .notification-dropdown — see js/nav.js. */
.request-menu-dropdown {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 150px;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: var(--shadow-soft);
    padding: 6px;
    z-index: 15;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.request-menu-dropdown .remind-btn,
.request-menu-dropdown .share-btn,
.request-menu-dropdown .report-link,
.request-menu-dropdown .edit-request-btn,
.request-menu-dropdown .delete-request-btn,
.request-menu-dropdown .admin-edit-request-btn,
.request-menu-dropdown .admin-delete-request-btn {
    width: 100%;
    text-align: left;
    border: none;
    border-radius: 6px;
    padding: 8px 10px;
    font-family: var(--font-body);
    font-size: 0.85rem;
    color: var(--ink);
    background: none;
}

.request-menu-dropdown .remind-btn:hover,
.request-menu-dropdown .share-btn:hover,
.request-menu-dropdown .report-link:hover,
.request-menu-dropdown .edit-request-btn:hover,
.request-menu-dropdown .delete-request-btn:hover,
.request-menu-dropdown .admin-edit-request-btn:hover,
.request-menu-dropdown .admin-delete-request-btn:hover {
    background: var(--bg);
}

.request-menu-dropdown .report-link,
.request-menu-dropdown .delete-request-btn,
.request-menu-dropdown .admin-delete-request-btn { color: var(--danger); }

/* Poster badge, pray, and comments/updates grouped together on the
   footer's left, so they read as one inline stats row — .request-actions
   (just answer now, is_mine only — edit/delete moved into the "⋮" menu)
   stays a separate group on the right
   via .request-footer's own space-between. */
.request-stats {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}

.request-title {
    font-family: var(--font-display);
    font-size: 1.15rem;
    font-weight: 600;
    margin: 0 0 6px;
}

.request-body {
    font-size: 0.95rem;
    margin: 0 0 14px;
    white-space: pre-wrap;
}

.request-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
}

.request-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}

/* The request's poster's own badge avatar — bottom-left of the footer,
   first in .request-stats (before the pray button), inline with
   pray/comments/updates (see requestCardHtml() in js/request-card.js).
   Sized down from the "posting as you" widget's own
   40px default — this sits next to small icon buttons in a tight row, not
   as a standalone identity element, so it's rendered "compact" (no name
   label underneath — see avatarInitialsHtml()'s compact option; the name
   is still there as a hover title instead). Only ever rendered at all when
   the request was posted while its author was logged in — never shown for
   an anonymous public post. */
/* Deliberately an explicit fixed square (2rem ~= .pray-btn's own ~32px
   rendered height), NOT align-self:stretch + aspect-ratio — two real,
   mutually-exclusive engine bugs were hit trying that: aspect-ratio on the
   nested .avatar-initials circle (sized via height:100%) gets recomputed
   AFTER cross-axis stretch resolves, so the circle ends up wider than the
   outer item's own width, which was already frozen from an earlier,
   pre-stretch pass — the circle then visually overflows into the pray
   button's gap. Moving aspect-ratio onto the outer flex item instead just
   never gets consulted in Chromium when flex-basis is content-auto and the
   cross size comes from stretch — the item falls back to shrink-wrapping
   its content's un-stretched width, leaving it far skinnier than its
   stretched height. A plain fixed size sidesteps both. */
.request-poster-badge {
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
}

.request-poster-badge .avatar-initials {
    width: 100%;
    height: 100%;
    font-size: 0.65rem;
}

/* ---- Pills (status badges) ---- */
.pill {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--bg);
    border: 1px solid var(--line);
}

.pill-pending {
    display: inline-block;
    margin: 6px 0 0;
    color: var(--danger);
    border-color: var(--crisis-border);
    background: var(--crisis-bg);
}

/* ---- "Marked answered" celebration — a ~4s rotating sunburst behind the
   card plus a matching warm glow on the card itself, triggered once by
   celebrateAnswered() in js/request-card.js whenever a request actually
   transitions to answered (never replayed on a plain page load of an
   already-answered request). .rays-burst is created and removed by JS —
   it isn't part of every card's normal markup, since most cards never play
   this. Sized with min(440px, 88vw) rather than a bare 440px so it can't
   force a horizontal scrollbar on a narrow phone screen; centered on the
   card via translate(-50%,-50%) baked into every keyframe step (a second,
   separate rotate-only animation can't be layered on top of this one — per
   the CSS animations spec, when two animations on one element both touch
   `transform`, only the last-listed one applies, not a blend of both — so
   opacity/scale/rotate all live in this one keyframe instead). */
/* .rays-burst is NOT a child of .request-card — it's a preceding SIBLING,
   both inside a wrapper .celebrate-wrap that celebrateAnswered()
   (js/request-card.js) builds around the card the first time it's needed.
   That's deliberate, and replaces two earlier attempts (negative z-index;
   negative z-index + isolation:isolate on the card) that both relied on
   subtle stacking-context rules and, per real user testing, still didn't
   reliably paint the rays behind the card. This way needs no z-index
   trickery at all: neither element sets a z-index, so both sit in the same
   "positioned, z-index:auto" painting layer, where plain DOM order breaks
   the tie — and since the card element comes AFTER .rays-burst in the
   wrapper, the card's own opaque background is *guaranteed* to paint over
   the rays wherever they visually overlap, regardless of the mask/size
   below. Correctness here comes from element order, not from carefully
   tuning the mask to avoid the card's footprint. */
.celebrate-wrap {
    position: relative;
}

.rays-burst {
    position: absolute;
    top: 50%;
    left: 50%;
    width: min(440px, 88vw);
    height: min(440px, 88vw);
    pointer-events: none;
    opacity: 0;
    background: repeating-conic-gradient(
        from 0deg,
        var(--accent-glow) 0deg 3.2deg,
        transparent 3.2deg 18deg
    );
    -webkit-mask-image: radial-gradient(circle, rgba(0, 0, 0, 0.85) 30%, transparent 62%);
    mask-image: radial-gradient(circle, rgba(0, 0, 0, 0.85) 30%, transparent 62%);
    animation: rays-celebrate 4s cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}

.request-card.answered-celebrate {
    animation: rays-card-glow 4s ease-out forwards;
}

@keyframes rays-celebrate {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.7) rotate(0deg); }
    12%  { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(18deg); }
    70%  { opacity: 0.9; transform: translate(-50%, -50%) scale(1.1) rotate(105deg); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.15) rotate(150deg); }
}

@keyframes rays-card-glow {
    0%   { box-shadow: var(--shadow-soft); }
    12%  { box-shadow: 0 0 0 1px var(--accent-glow), 0 0 40px 6px var(--accent-glow); }
    70%  { box-shadow: 0 0 0 1px var(--accent-glow), 0 0 34px 4px var(--accent-glow); }
    100% { box-shadow: var(--shadow-soft); }
}

@media (prefers-reduced-motion: reduce) {
    .rays-burst { display: none; }
    .request-card.answered-celebrate { animation: none; }
}

/* ---- Save / answer / comment toggle / share buttons ---- */
.save-btn,
.answer-btn,
.activity-toggle,
.share-btn,
.unsave-btn,
.update-delete-btn {
    font-size: 0.8rem;
    color: var(--ink-soft);
    background: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 5px 12px;
    cursor: pointer;
}

.update-delete-btn {
    color: var(--danger);
    border-color: var(--danger);
}

.save-btn.saved {
    color: var(--accent);
    border-color: var(--accent);
}

/* The star (js/icons.js's ICON_STAR) is the same outline icon either way —
   .saved switches it to a filled star via CSS rather than swapping in a
   different icon/glyph, so there's only ever one save-star markup to keep
   in sync (previously two code paths wrote a raw ★/☆ character; now
   neither does — see handleSaveClick/openSaveModal in request-card.js). */
.save-btn.saved .lucide-icon {
    fill: currentColor;
}

/* Marks the comments/updates thread as currently open — same accent
   treatment .save-btn.saved already uses for its own "on" state, so an open
   .activity-toggle reads as the thing to click again to close it, not just
   a plain unclicked button sitting next to an already-expanded section. */
.activity-toggle.open {
    color: var(--accent);
    border-color: var(--accent);
}

/* A per-request notify-preference picker (comments/updates/everything/none)
   — used both on the Saved page's card footer (js/saved.js, not wrapped in
   .owner-panel so it needs its own complete theming, not just a borrowed
   one) and the Dashboard Subscriptions page's request row (js/subscriptions.js,
   inside .owner-panel). Deliberately not width:100% like a normal
   .owner-panel form field — this sits inline next to a Remove button or a
   card's other action buttons, not stretched to the panel's full width. */
.notify-type-select {
    font-family: var(--font-body);
    font-size: 0.8rem;
    padding: 5px 8px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

.owner-panel .notify-type-select {
    width: auto;
}

/* "New comments/updates since you last opened this thread" — tracked in
   localStorage by js/activity-unread.js, same small-green-dot language as
   .unread-dot (card-level "new request"), just anchored to the 💬 button
   itself instead of the card's corner since this is a per-thread signal. */
.activity-toggle {
    position: relative;
}

.activity-unread-dot {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #4CAF50;
    box-shadow: 0 0 0 2px var(--bg-card);
}

/* ---- Combined comments + updates thread (js/request-card.js's
   loadActivity()) — one chronological list mixing both, with a filter row
   to narrow to just one type. Comments render plainly; an update (always
   from the request's owner) gets its own card-like treatment via
   .activity-item--update below so it visibly stands out from comments. ---- */
.activity-thread {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
}

.activity-filter {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}

.activity-filter-btn {
    font-size: 0.78rem;
    padding: 5px 12px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: none;
    color: var(--ink-soft);
    cursor: pointer;
}

.activity-filter-btn.active {
    color: var(--accent);
    border-color: var(--accent);
    font-weight: 600;
}

/* Scoped narrower than the shared .empty-state (40px top/bottom — sized
   for a full page's loading/empty state) since this sits inside an
   already-small inline thread panel, not a whole page. */
.activity-thread .empty-state { padding: 12px 0; }

.comment-item {
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
}

.comment-item:last-of-type { border-bottom: none; }

/* Makes an update visibly stand out from a plain comment: its own
   card-like block (background tint + accent left border + a label) rather
   than just another row in the list. */
.activity-item--update {
    border-bottom: none;
    margin: 8px 0;
    padding: 10px 12px;
    border-radius: 8px;
    border-left: 3px solid var(--accent);
    background: var(--answered-bg);
}

.activity-item-label {
    font-family: var(--font-mono);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--accent);
    font-weight: 600;
    margin-bottom: 3px;
}

.comment-meta {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--ink-soft);
    margin-bottom: 3px;
}

.comment-body { font-size: 0.88rem; margin: 0; }

/* Per-comment report/delete — same minimal text-link treatment as
   .report-link (the request-level equivalent), sized for a comfortable
   mobile tap target despite the small font. */
.comment-actions {
    display: flex;
    gap: 14px;
    margin-top: 4px;
}

.comment-report-btn,
.comment-delete-btn {
    font-size: 0.72rem;
    color: var(--ink-soft);
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px 0;
}

.report-link-text { text-decoration: none; }

.comment-delete-btn { color: var(--danger); }

.comment-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 10px;
    background: var(--bg);
    border-radius: 10px;
    padding: 12px;
}

/* Auto-expanding — js/request-card.js grows the height on 'input' to fit
   the content (including line breaks) instead of scrolling internally, so
   resize/overflow are both turned off here rather than left to the
   browser's own textarea defaults. */
.comment-input {
    width: 100%;
    resize: none;
    overflow: hidden;
    min-height: 38px;
    font-family: var(--font-body);
    font-size: 0.85rem;
    line-height: 1.4;
    padding: 8px 10px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

.comment-input::placeholder {
    color: var(--input-text-soft);
    opacity: 1;
}

.comment-form-actions { display: flex; justify-content: space-between; gap: 8px; }

.comment-form input {
    font-family: var(--font-body);
    font-size: 0.85rem;
    padding: 8px 10px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

.comment-form input::placeholder {
    color: var(--input-text-soft);
    opacity: 1;
}

.verse-picker-toggle-btn {
    flex: 0 0 auto;
    font-size: 1rem;
    line-height: 1;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg);
    cursor: pointer;
}

/* ---- Bible verse picker (comment box) ---- */
.verse-picker {
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 8px;
    background: var(--bg);
}

.verse-picker-input { width: 100%; }

.verse-picker-results {
    max-height: 220px;
    overflow-y: auto;
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.verse-picker-results:empty { margin-top: 0; }

.verse-book-btn {
    align-self: flex-start;
    font-size: 0.82rem;
    padding: 6px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-raised);
    /* Explicit, not inherited — a bare <button> gets the browser's own
       default text color (often a fixed dark tone that ignores the page's
       dark palette) unless something overrides it, which made book-name
       suggestions unreadable in dark mode. */
    color: var(--ink);
    cursor: pointer;
}

.verse-book-btn:hover { color: var(--accent); border-color: var(--accent); }

.verse-insert-btn {
    display: block;
    width: 100%;
    text-align: left;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg-raised);
    cursor: pointer;
    /* Was var(--input-text) — a token meant for text sitting on the
       deliberately-always-light form-input surface (see its definition
       above), not on this button's var(--bg-raised) background, which is
       dark in dark mode. That mismatch made the verse text (the child
       <span> below, which has no color of its own) render as near-black
       text on a dark background — readable in light mode by accident, but
       invisible in dark mode. Only the reference line stayed visible since
       it explicitly overrides to --accent. */
    color: var(--ink);
}

.verse-insert-btn:hover { border-color: var(--accent); }

.verse-insert-btn strong {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--accent);
    margin-bottom: 2px;
}

.verse-insert-btn span {
    display: block;
    font-size: 0.82rem;
    line-height: 1.35;
}

/* ---- Request updates (posted by the original poster) — rendered inside
   the same .activity-thread as comments (see above); .activity-item--update
   is what actually makes one stand out visually. ---- */
.update-item {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
}

.update-item:last-of-type { border-bottom: none; }

.update-meta {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--ink-soft);
    margin-bottom: 3px;
}

.update-body { font-size: 0.88rem; margin: 0; white-space: pre-wrap; }

.update-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 10px;
    background: var(--bg);
    border-radius: 10px;
    padding: 12px;
}

.update-form-actions { display: flex; justify-content: flex-start; }

.update-form input {
    font-family: var(--font-body);
    font-size: 0.85rem;
    padding: 8px 10px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

.update-form input::placeholder {
    color: var(--input-text-soft);
    opacity: 1;
}

/* ---- Reminder modal (js/request-card.js's showReminderModal) — same
   .modal-box shell as Share/Group profile, so its datetime-local input
   needs the same theming .modal-box's text/textarea rule already gives
   those, just for a different input type. ---- */
.reminder-set-form {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.reminder-set-form input[type=datetime-local] {
    flex: 1;
    min-width: 180px;
    font-family: var(--font-body);
    font-size: 0.85rem;
    padding: 8px 10px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

/* The native calendar-icon glyph inside a datetime-local input is a flat
   black SVG in most browsers, baked in — it doesn't automatically follow
   --input-text the way real text does, so left alone it's a barely-visible
   near-black icon against dark mode's own light input text. invert(1)
   flips it to white without needing a custom icon asset. */
:root[data-theme="dark"] .reminder-set-form input[type=datetime-local]::-webkit-calendar-picker-indicator {
    filter: invert(1);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .reminder-set-form input[type=datetime-local]::-webkit-calendar-picker-indicator {
        filter: invert(1);
    }
}

/* ---- Inline edit form for a request (My Requests page) ---- */
.edit-request-form {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.edit-request-form textarea,
.edit-request-form input[type=text],
.edit-request-form select {
    font-family: var(--font-body);
    font-size: 0.9rem;
    padding: 8px 10px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

.edit-request-form textarea { min-height: 80px; resize: vertical; }

.edit-request-form-actions {
    display: flex;
    gap: 8px;
}

/* ---- Owner analytics ---- */
.stat-tiles {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    margin-bottom: 16px;
}

.stat-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 12px 6px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: var(--shadow-soft);
}

.stat-num {
    font-family: var(--font-display);
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--ink);
}

.stat-label {
    font-family: var(--font-mono);
    font-size: 0.68rem;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.analytics-bar-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.82rem;
    margin-bottom: 8px;
}

.analytics-bar-label {
    flex: 0 0 110px;
    color: var(--ink-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.analytics-bar-track {
    flex: 1;
    height: 8px;
    background: var(--bg);
    border-radius: 999px;
    overflow: hidden;
}

.analytics-bar-fill {
    display: block;
    height: 100%;
    background: var(--accent);
    border-radius: 999px;
}

.analytics-bar-count {
    flex: 0 0 24px;
    text-align: right;
    color: var(--ink-soft);
    font-family: var(--font-mono);
    font-size: 0.75rem;
}

/* ---- Owner/admin review panels — shared by group.html's owner panel and
   admin.html's site-wide queue, so both look and behave identically. ---- */
.owner-panel {
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 18px;
    margin-bottom: 24px;
    box-shadow: var(--shadow-soft);
}

.owner-panel h3 {
    font-family: var(--font-display);
    margin: 0 0 10px;
    font-size: 1.05rem;
}

/* Ajax paging controls (js/subscriptions.js's Categories/Groups/Requests
   sections) — hidden entirely (empty innerHTML) rather than shown-but-inert
   when a section has nothing to page through at all. */
.pagination-controls {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 12px;
}

.pagination-controls:empty { margin-top: 0; }

.pagination-page-label {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--ink-soft);
}

/* ---- Dashboard overview cards (/overview/) — one per sub-nav section ---- */
.overview-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
}

.overview-card {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 18px;
    box-shadow: var(--shadow-soft);
    text-decoration: none;
    color: var(--ink);
    transition: border-color 0.15s ease;
}

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

.overview-card-icon { font-size: 1.8rem; line-height: 1; flex: 0 0 auto; }

.overview-card-body h3 {
    font-family: var(--font-display);
    margin: 0 0 4px;
    font-size: 1.05rem;
}

.overview-card-body p {
    margin: 0;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.overview-card-stat {
    margin-top: 8px;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--accent);
}

/* The invite-code gate (js/group.js's init(), shown to a visitor with no
   group access) has a lone plain <input> with no wrapping .submit-card or
   .owner-panel — was raw unstyled browser default (always white, regardless
   of theme) until this was added; same CSS-gap pattern as the comment below. */
.invite-code-input {
    padding: 8px 10px;
    margin-right: 8px;
    font-family: var(--font-mono);
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
}

/* Same treatment as .submit-card's inputs — several owner-panel forms (group
   profile, invite custom-code, admin's group-add form) use plain inputs
   directly inside .owner-panel rather than nesting a .submit-card, and were
   otherwise left as raw unstyled browser defaults (the same CSS-gap pattern
   flagged elsewhere in this project — see CLAUDE.md). */
.owner-panel input[type=text],
.owner-panel input[type=email],
.owner-panel textarea,
.owner-panel select {
    width: 100%;
    font-family: var(--font-body);
    font-size: 0.9rem;
    padding: 10px 12px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
    margin-bottom: 10px;
}

.owner-panel textarea { min-height: 70px; resize: vertical; }

/* Invite members section (js/group.js's renderInviteSection, Members page)
   and Post to group (js/group.js's renderMemberView, Overview page) — flex
   row so the 📨 emoji lines up on the vertical center of "Invite members"
   instead of following its own glyph's natural baseline, which sat a little
   low next to the text. Specificity bumped to h3.invite-toggle-heading (and
   placed after .owner-panel h3 in source order) so this always wins over
   .owner-panel h3's own `margin: 0 0 10px` on the Members page — both
   headings should have identical spacing above and below, which means no
   margin of their own at all (the surrounding .submit-card/.owner-panel's
   own padding is already the spacing on every other side). */
h3.invite-toggle-heading {
    font-family: var(--font-display);
    font-size: 1.05rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
}

/* The 📨 emoji glyph sits low within its own character cell in most emoji
   fonts, so even a vertically-centered flex item (see above) still reads as
   slightly below the middle of the adjacent text — nudged up to compensate. */
.invite-toggle-icon {
    display: inline-block;
    transform: translateY(-2px);
}

/* The QR canvas (js/group.js's renderInviteSection) sits directly under the
   "Invite members" heading with no margin of its own, so it looked flush
   against it — unlike the invite-URL text right below the QR, which gets
   breathing room from its own default <p> top margin. Matches that same
   spacing above the QR instead of leaving it flush. */
#qr {
    margin-top: 12px;
}

/* Regenerate-random vs. set-a-custom-code presented as two equally-weighted
   options side by side with "or" between them, rather than one stacked
   above the other reading like a two-step sequence. */
.invite-code-options {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
}

.invite-code-or {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--ink-soft);
    flex: 0 0 auto;
}

.invite-code-option {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1;
    min-width: 220px;
}

/* .owner-panel input[type=text] (further up this file) sets its own
   margin-bottom: 10px at higher specificity (.owner-panel + attribute
   selector) than a plain ".invite-code-option input" override can beat —
   that leftover margin was inflating this input's box just enough to throw
   off .invite-code-options' align-items:center, so the custom-code input
   sat visibly higher than the buttons beside it. Matching .owner-panel's
   own specificity (class + attribute selector) here is what actually wins. */
.owner-panel .invite-code-option input[type=text] {
    flex: 1;
    margin-bottom: 0;
}

/* Group hero banner (js/group.js's groupHeroHtml) — the name/edit-icon/
   subscribe-button header row renders as an overlay on top of an
   admin-approved, owner-uploaded image (see api/group_hero.php) rather than
   the image and heading stacking as two separate blocks. Fixed min-height
   so a very short/wide image still gives the overlaid header room to
   breathe, cover+center so any aspect ratio fills it without distortion. */
.group-hero-banner {
    background-size: cover;
    background-position: center;
    background-color: var(--bg-card); /* shows briefly while the image itself loads */
    border-radius: 14px;
    margin-bottom: 14px;
    overflow: hidden;
    /* Raised from 140px now that the description also renders inside the
       scrim (see js/group.js's groupHeroHtml) — enough extra room that the
       taller scrim still only covers part of the image, not most of it. */
    min-height: 190px;
    display: flex;
    align-items: flex-end;
}

/* The actual readability guarantee: a fixed dark gradient plus forced white
   heading text, independent of whatever colors the uploaded image itself
   contains — deliberately not image-color-analysis-based (fragile against
   a genuinely arbitrary owner-uploaded image; a flat, generous scrim is the
   part that's actually guaranteed to work every time regardless of what
   was uploaded). */
.group-hero-scrim {
    width: 100%;
    padding: 16px 20px;
    background: linear-gradient(180deg, rgba(10, 10, 15, 0.15) 0%, rgba(10, 10, 15, 0.75) 100%);
    /* Explicit rather than assumed — a non-member without access to this
       group renders this whole block inside .gate, which sets
       text-align:center. The heading only ever looked correctly left-
       aligned there by accident (a flex item sized to its own content, so
       centering it within its own box is a no-op) — the description,
       a plain full-width block, visibly centered instead once it moved in
       here from its own separate paragraph below the image. */
    text-align: left;
}

.group-hero-scrim .page-header-row {
    margin: 0;
}

/* Description + Subscribe/Unsubscribe button share one row (js/group.js's
   groupHeroHtml) — align-items:flex-end bottom-aligns the button with the
   description's own bottom edge, instead of the button sitting on its own
   separate line underneath with extra top margin. */
.group-hero-desc-row {
    display: flex;
    align-items: flex-end;
    gap: 12px;
}

.group-hero-action-row {
    display: flex;
    justify-content: flex-end;
    flex: 0 0 auto;
}

.group-hero-scrim .page-title {
    color: #FFFFFF;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}

.group-hero-scrim .group-edit-icon {
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
}

/* Same readability treatment as .page-title above — the description now
   renders inside the scrim too (js/group.js's groupHeroHtml), so it needs
   the same forced-light-color/shadow guarantee against an arbitrary
   uploaded image, and its own default 28px bottom margin (meant for plain
   page flow) replaced with just a little breathing room under the heading. */
.group-hero-scrim .page-sub {
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    margin: 4px 0 0;
}

/* flex:1 (not the plain block .page-sub normally is) so the description
   fills .group-hero-desc-row's available width — pushes the button to the
   right edge even when there's no description text at all, without needing
   a conditional justify-content. margin reset to 0 since the row's own gap
   supplies the spacing .page-sub's margin-top handled before. Declared
   after the plain .group-hero-scrim .page-sub rule above (same
   specificity, source order breaks the tie) so this margin reset actually
   wins over it. */
.group-hero-desc-row .page-sub {
    flex: 1;
    margin: 0;
}

.member-row, .pending-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
    font-size: 0.85rem;
}

/* No dangling divider under the last row of a list — e.g. the group Members
   page's member list (js/group.js) renders a flat list of these with
   nothing else after the last one. (The Groups page's own "my groups" list
   used to share this class too, but now uses .group-row below instead —
   see its comment.) */
.member-row:last-child {
    border-bottom: none;
}

/* Groups page's "my groups" list (dashboard.html) — a full border per row
   instead of .member-row's plain bottom-divider style, so the group name
   and its "Open" button read as one combined unit rather than a bare list. */
.group-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 10px;
    margin-bottom: 8px;
    font-size: 0.85rem;
}

.group-row:last-child {
    margin-bottom: 0;
}

.gate { text-align: center; padding: 60px 20px; }

/* ---- The signature element: the candle ---- */
.pray-btn {
    display: flex;
    align-items: center;
    gap: 4px;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 6px 14px 6px 10px;
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--ink-soft);
    transition: border-color 0.2s ease;
}

.pray-btn .flame { width: 18px; height: 18px; transition: transform 0.2s ease; }
.pray-btn .flame path { fill: none; stroke: var(--ink-soft); stroke-width: 1.6; }

.pray-btn.lit {
    border-color: var(--accent);
    color: var(--accent);
}
.pray-btn.lit .flame path {
    fill: var(--accent-glow);
    stroke: var(--accent);
}
.pray-btn.lit .flame {
    filter: drop-shadow(0 0 4px var(--accent-glow));
}

/* Answered state (js/request-card.js's prayBtnHtml()/saved.js's cardHtml())
   — replaces the old separate "Answered 🙏" pill above the title, which is
   gone now that the button itself carries the answered state. Same
   gold/accent look .pill-answered used to have, not the browser's default
   disabled-button dimming — this is a settled, celebrated state, not a
   temporarily-unavailable control. */
.pray-btn:disabled {
    background: var(--answered-bg);
    border-color: var(--accent);
    color: var(--accent);
    cursor: default;
    opacity: 1;
}

.pray-btn.just-lit .flame {
    animation: flicker 0.5s ease;
}

@keyframes flicker {
    0% { transform: scale(1); }
    30% { transform: scale(1.25) rotate(-3deg); }
    60% { transform: scale(0.95) rotate(2deg); }
    100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .pray-btn.just-lit .flame { animation: none; }
}

.report-link {
    font-size: 0.75rem;
    color: var(--ink-soft);
    background: none;
    border: none;
    cursor: pointer;
    text-decoration: none;
    padding: 0;
}

/* Invisible infinite-scroll trigger (see common.js's setupInfiniteScroll())
   — needs real height to reliably intersect the viewport; a true 0-height
   div can be missed by the IntersectionObserver depending on layout timing. */
.scroll-sentinel {
    height: 1px;
}

.empty-state {
    text-align: center;
    color: var(--ink-soft);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    padding: 40px 0;
}

/* Generic dialog overlay — currently only the "Share this request" modal
   (js/share.js), written generically enough to reuse for a future modal. */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    padding: 16px;
}

.modal-box {
    background: var(--bg-card);
    border-radius: 14px;
    box-shadow: var(--shadow-soft);
    padding: 22px;
    width: 100%;
    max-width: 420px;
}

.modal-title {
    font-family: var(--font-display);
    margin: 0 0 14px;
    font-size: 1.1rem;
    color: var(--ink);
}

/* Same treatment as .submit-card/.owner-panel's inputs — the group profile
   modal (js/group.js's showGroupProfileModal) has plain inputs directly
   inside .modal-box, which otherwise leaves them fully unstyled browser
   defaults (no width, no theme colors) the same CSS-gap pattern flagged
   elsewhere in this project. */
.modal-box input[type=text],
.modal-box input[type=password],
.modal-box textarea,
.modal-box select {
    width: 100%;
    font-family: var(--font-body);
    font-size: 0.9rem;
    padding: 10px 12px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--input-text);
    margin-bottom: 10px;
    box-sizing: border-box;
}

.modal-box textarea { min-height: 70px; resize: vertical; }

/* Hero image crop preview (js/group.js's showGroupProfileModal, Cropper.js
   via CDN) — a fixed height so Cropper.js has a stable box to lay out its
   crop handles in regardless of the source image's own dimensions; the
   image itself is constrained to fit within it before cropping starts. */
.hero-crop-container {
    max-height: 280px;
    margin: 10px 0;
    overflow: hidden;
}

.hero-crop-container img {
    max-width: 100%;
    display: block;
}

/* Current-hero and pending-hero thumbnails (js/common.js's heroStatusHtml)
   — same fixed max-height treatment on both, one shared class rather than
   repeating the same inline style on each <img>. */
.hero-thumb {
    max-width: 100%;
    max-height: 120px;
    border-radius: 8px;
    display: block;
    margin-bottom: 10px;
}

/* The hero image upload's native "Choose File" control (js/group.js's and
   js/admin-groups.js's file input) otherwise renders as the browser's own
   unstyled default button — styled here to match every other button in the
   app (.btn-secondary's look), via the two vendor pseudo-elements browsers
   expose for it. Only the button portion is restyled; the "no file
   chosen"/filename text next to it stays the browser's own plain text. */
.group-hero-file-input {
    font-family: var(--font-body);
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.group-hero-file-input::file-selector-button,
.group-hero-file-input::-webkit-file-upload-button {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.9rem;
    padding: 9px 18px;
    border-radius: 8px;
    border: 1px solid var(--line);
    background: var(--input-bg);
    color: var(--ink-soft);
    cursor: pointer;
    margin-right: 10px;
}

.modal-share-url {
    width: 100%;
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--input-text);
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: 8px;
    padding: 10px 12px;
    box-sizing: border-box;
}

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

/* ---- Site Admin pages (Overview + Requests/Reported Comments/Badges/
   Groups/Hero Images, each its own page under admin-*.html) ---- Moved out
   of a single <style> block in admin.html (back when everything lived on
   one page) now that five separate pages share these same classes. */
.admin-toolbar { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.admin-toolbar input[type=text] { flex: 1; min-width: 160px; margin-bottom: 0; }
.admin-toolbar select { margin-bottom: 0; width: auto; }
.admin-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--line); font-size: 0.85rem; }
.admin-row:last-child { border-bottom: none; }
.admin-row-main { flex: 1; min-width: 0; }
/* display:flex (rather than the plain text-flow block this used to be) so
   a request row's poster badge avatar (js/admin-requests.js) can sit inline
   with the rest of the meta line instead of forcing a line break — the
   avatar itself is a flex container (.avatar-initials), which only lays out
   inline correctly inside another flex parent. */
.admin-row-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-family: var(--font-mono); font-size: 0.68rem; color: var(--ink-soft); margin-bottom: 4px; }
.admin-row-actions { display: flex; gap: 6px; flex-wrap: wrap; flex: 0 0 auto; }

/* Drag-and-drop reorder handle (js/admin-badges.js) — only this element is
   draggable, not the whole row, so grabbing it never fights with clicking
   Edit/Delete or selecting text elsewhere in the row. align-self:center
   rather than the row's own default flex-start so it sits level with the
   row's content regardless of how tall that content is. */
.drag-handle {
    align-self: center;
    flex: 0 0 auto;
    cursor: grab;
    color: var(--ink-soft);
    font-size: 1rem;
    line-height: 1;
    padding: 0 4px;
    user-select: none;
}

.admin-row.dragging { opacity: 0.4; }
.admin-row-actions .btn { font-size: 0.75rem; padding: 6px 10px; }
.admin-hero-preview { display: block; max-width: 320px; max-height: 160px; border-radius: 8px; margin-top: 6px; }
.admin-edit-form { width: 100%; }
.admin-edit-form textarea, .admin-edit-form input[type=text], .admin-edit-form input[type=number], .admin-edit-form input[type=password], .admin-edit-form select { width: 100%; margin-bottom: 8px; }
#badge-add-form input, #badge-add-form select { width: 100%; margin-bottom: 8px; }

/* Every admin-page text input/select/textarea, whatever form it lives in —
   none of these had real visual styling before (just width/spacing), so
   they rendered as raw browser-default fields instead of matching the
   app's design system. Confirmed by browser testing (computed style showed
   the literal UA default: 2px inset grey border, 0 radius) — same class of
   CSS gap that's bitten this project before (see CLAUDE.md). input[type=
   password] added alongside input[type=text] here after the same gap turned
   up a third time, on the Edit User page's password-reset field. */
.admin-toolbar input[type=text],
.admin-toolbar select,
.admin-edit-form textarea,
.admin-edit-form input[type=text],
.admin-edit-form input[type=number],
.admin-edit-form input[type=password],
.admin-edit-form select,
#group-add-form input,
#group-add-form textarea,
#badge-add-form input,
#badge-add-form select {
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 8px 10px;
    background: var(--input-bg);
    color: var(--input-text);
    font-family: var(--font-body);
    font-size: 0.85rem;
}

.admin-pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 12px; font-size: 0.8rem; }

/* ---- First-ever-prayer guided tour (js/first-pray-tour.js) ---- */
/* A fixed box whose own oversized box-shadow dims the whole viewport except
   for this box's own rect — no SVG masking or a separate full-page overlay
   element needed, just one box positioned/sized (in JS) to match whichever
   element the current step points at. z-index is well above every other
   layer in the app (dropdowns/modals top out at 100) so it dims genuinely
   everything, including an opened "⋮" dropdown mid-tour. */
/* Dims the rest of the PAGE (everything outside the current request card)
   at normal strength — sized/positioned (in JS) to the card's own rect, not
   the step's target, so the card itself always sits fully undimmed
   relative to the page around it. */
.tour-page-dim {
    position: fixed;
    z-index: 1000;
    border-radius: 14px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
    pointer-events: none;
    transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
}

/* Dims the rest of THIS card (but not the step's own target) much more
   lightly than .tour-page-dim — sized to the card's rect, clipping its
   child's oversized box-shadow at the card's own edge so that dimming
   never escapes onto the (already-dimmed) page outside it. */
.tour-card-clip {
    position: fixed;
    z-index: 1000;
    overflow: hidden;
    border-radius: 14px;
    pointer-events: none;
    transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
}

.tour-card-dim {
    position: absolute;
    border-radius: 10px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.22);
    transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
}

.tour-tooltip {
    position: fixed;
    z-index: 1001;
    width: 260px;
    max-width: calc(100vw - 16px);
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: var(--shadow-soft);
    padding: 12px 14px;
    transition: top 0.25s ease, left 0.25s ease;
}

.tour-tooltip-body {
    margin: 0 0 10px;
    font-size: 0.85rem;
    color: var(--ink);
}

.tour-tooltip-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.tour-step-count {
    font-family: var(--font-mono);
    font-size: 0.68rem;
    color: var(--ink-soft);
    flex: 0 0 auto;
}
