/* =========================================
   THE PALETTE

   Slate #2c3e50 and Outfit come from heaphomestead.com, which is where this
   palette started - the marketing site and the app looking like one product
   rather than two that happen to link to each other.

   THE ACCENT NO LONGER MATCHES IT. The site's is burnt orange #e67e22; this is
   a plum, and the ground and the rules were cooled to sit under it - a warm
   cream under a cool purple reads as two schemes arguing. Whether the site
   follows is an open question, and until it does, the two halves agree about
   everything except the one colour a visitor sees on every button.

   What this block replaces was two blues nobody had chosen - Bootstrap's
   default and a brighter accent - sitting beside Bootstrap's green, amber and
   teal. The old comment said picking one was a real design decision rather
   than a refactor. It was, and this is it.

   HOW IT REACHES BOOTSTRAP. 5.3's utilities read --bs-<name>-rgb at use time,
   so retinting --bs-primary-rgb here moves every .text-primary, .bg-primary
   and .border-primary in the app at once. Its BUTTONS do not: .btn-primary
   carries literal hexes in its own --bs-btn-* variables, which is why each
   button variant we use is restated below. A variant not restated keeps
   Bootstrap's colour, so check here before reaching for a new one.

   SAGE IS "THAT WENT WELL", AND ONLY THAT. Bootstrap's success green was the
   last stock colour most pages showed - a saturated #198754 beside the plum,
   on every Saved toast and every RSVP yes. It is now a muted sage, the plum's
   complement, and it is spent on positive states alone: saved, coming, done.
   The rule that keeps two accents from fighting: plum marks what you can DO,
   sage marks what is TRUE. A sage button that starts something is the wrong
   colour. heaphomestead.com carries the same value (shared.css).

   WHAT IS DELIBERATELY NOT RETINTED. danger, warning and info keep
   Bootstrap's values. Red and amber mean what everyone already reads them as,
   and a scheme of our own for "this failed" buys nothing.

   PRIMARY IS INK, NOT ACTION. Slate is the colour of headings, icons and
   rules; the accent marks the one thing on a page you would click, and is
   spent through .btn-accent rather than by making .btn-primary orange. Both
   read as "primary" in English, and having the word mean two colours depending
   on whether it followed btn- was the trap worth avoiding.
========================================= */
:root {
    /* ---------------------------------------------------------------
       THE SCHEME. Every colour in the app resolves to one of these
       values, and nothing below this block contains a hex. Swapping the
       app's whole appearance is editing these lines and nothing else -
       which is the point of doing it this way rather than retinting each
       page, and the cheapest way to try a different scheme and put this
       one back if it looks worse in daylight.

       The -rgb pairs have to be raw triplets because Bootstrap composes
       them with an alpha at use time - rgba(var(--bs-primary-rgb), .5) -
       so a hex there silently produces no colour at all. Keep each one in
       step with the hex above it.
       --------------------------------------------------------------- */
    --app-slate: #2c3e50;
    --app-slate-rgb: 44, 62, 80;
    --app-slate-dark: #1f2d3a;
    --app-accent: #6d4c9a;
    --app-accent-rgb: 109, 76, 154;
    --app-accent-dark: #573c7d;
    --app-ground: #f8f7fa;
    --app-surface: #ffffff;
    --app-text: #4a4a4a;
    --app-muted: #7f8c8d;
    --app-rule: #e2e0e6;
    --app-sunken: #f1eff5;

    /* Sage: positive states only (see the note above). -dark is its text on a
       light ground and holds 4.5:1 on the tint; the plain value holds it for
       white text on a filled button. */
    --app-sage: #4f7a5a;
    --app-sage-rgb: 79, 122, 90;
    --app-sage-dark: #34523d;
    --app-sage-tint: #e5eee7;
    --app-sage-tint-strong: #c3d6c8;

    /* The app bar. Its own token rather than slate, so the bar can change without
    moving every heading and icon that is also slate. A dark sage: green enough to
    belong with the positive colour, dark enough that a sage Saved or Yes still
    stands out against it. -muted is its secondary link text, -faint its group labels. */
    --app-bar: #2e4637;
    --app-bar-muted: #bfd0c4;
    --app-bar-faint: #8fa596;

    /* Tints of the slate, for subtle fills and emphasised text. Derived by
       hand rather than by colour-mix() so they hold in older browsers. */
    --app-slate-tint: #e8ecef;
    --app-slate-tint-strong: #c3ccd4;

    /* The two page widths. Reading pages hold a column somebody reads; tool
       pages hold a table somebody scans. Every page picks one through
       PageFrame, and nothing hardcodes a max-width of its own. */
    --app-width-reading: 900px;
    --app-width-tool: 1320px;

    --app-font: 'Outfit', 'Segoe UI', system-ui, -apple-system, sans-serif;

    /* ---------------------------------------------------------------
       BOOTSTRAP, POINTED AT THE SCHEME ABOVE. Nothing new is decided
       here; each line only tells Bootstrap which of our tokens to use.
       --------------------------------------------------------------- */
    --bs-primary: var(--app-slate);
    --bs-primary-rgb: var(--app-slate-rgb);
    --bs-primary-text-emphasis: var(--app-slate-dark);
    --bs-primary-bg-subtle: var(--app-slate-tint);
    --bs-primary-border-subtle: var(--app-slate-tint-strong);

    --bs-body-bg: var(--app-ground);
    --bs-body-color: var(--app-text);
    --bs-body-font-family: var(--app-font);
    --bs-border-color: var(--app-rule);
    --bs-secondary-color: var(--app-muted);
    --bs-secondary-bg: var(--app-sunken);
    --bs-tertiary-bg: var(--app-sunken);

    --bs-link-color: var(--app-slate);
    --bs-link-color-rgb: var(--app-slate-rgb);
    --bs-link-hover-color: var(--app-accent);
    --bs-link-hover-color-rgb: var(--app-accent-rgb);

    --bs-heading-color: var(--app-slate);
    --bs-focus-ring-color: rgba(var(--app-accent-rgb), 0.55);

    --bs-success: var(--app-sage);
    --bs-success-rgb: var(--app-sage-rgb);
    --bs-success-text-emphasis: var(--app-sage-dark);
    --bs-success-bg-subtle: var(--app-sage-tint);
    --bs-success-border-subtle: var(--app-sage-tint-strong);
}

html, body {
    font-family: var(--app-font);
    background-color: var(--app-ground);
    color: var(--app-text);
}

/* .text-muted is Bootstrap's most-used class in this app and its grey is a
   cold one. Warmed to the site's own muted, once, rather than in each page. */
.text-muted {
    color: var(--app-muted) !important;
}

.btn-primary {
    --bs-btn-color: var(--app-surface);
    --bs-btn-bg: var(--app-slate);
    --bs-btn-border-color: var(--app-slate);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-slate-dark);
    --bs-btn-hover-border-color: var(--app-slate-dark);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-slate-dark);
    --bs-btn-active-border-color: var(--app-slate-dark);
    --bs-btn-disabled-color: var(--app-surface);
    --bs-btn-disabled-bg: var(--app-slate);
    --bs-btn-disabled-border-color: var(--app-slate);
    --bs-btn-focus-shadow-rgb: var(--app-slate-rgb);
}

.btn-outline-primary {
    --bs-btn-color: var(--app-slate);
    --bs-btn-border-color: var(--app-slate);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-slate);
    --bs-btn-hover-border-color: var(--app-slate);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-slate);
    --bs-btn-active-border-color: var(--app-slate);
    --bs-btn-disabled-color: var(--app-slate);
    --bs-btn-disabled-border-color: var(--app-slate);
    --bs-btn-focus-shadow-rgb: var(--app-slate-rgb);
}

/* The one action on a page. If two buttons on the same screen are wearing
   this, one of them is the wrong colour. */
.btn-accent {
    --bs-btn-color: var(--app-surface);
    --bs-btn-bg: var(--app-accent);
    --bs-btn-border-color: var(--app-accent);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-accent-dark);
    --bs-btn-hover-border-color: var(--app-accent-dark);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-accent-dark);
    --bs-btn-active-border-color: var(--app-accent-dark);
    --bs-btn-disabled-color: var(--app-surface);
    --bs-btn-disabled-bg: var(--app-accent);
    --bs-btn-disabled-border-color: var(--app-accent);
    --bs-btn-focus-shadow-rgb: var(--app-accent-rgb);
}

.btn-outline-accent {
    --bs-btn-color: var(--app-accent-dark);
    --bs-btn-border-color: var(--app-accent);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-accent);
    --bs-btn-hover-border-color: var(--app-accent);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-accent);
    --bs-btn-active-border-color: var(--app-accent);
    --bs-btn-disabled-color: var(--app-accent-dark);
    --bs-btn-disabled-border-color: var(--app-accent);
    --bs-btn-focus-shadow-rgb: var(--app-accent-rgb);
}

/* Buttons carry literal hexes of their own, so the success pair is restated
   like the others. Used where the button records a yes - RSVP's "I'll be
   there" - never to start something; that is .btn-accent's job. */
.btn-success {
    --bs-btn-color: var(--app-surface);
    --bs-btn-bg: var(--app-sage);
    --bs-btn-border-color: var(--app-sage);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-sage-dark);
    --bs-btn-hover-border-color: var(--app-sage-dark);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-sage-dark);
    --bs-btn-active-border-color: var(--app-sage-dark);
    --bs-btn-disabled-color: var(--app-surface);
    --bs-btn-disabled-bg: var(--app-sage);
    --bs-btn-disabled-border-color: var(--app-sage);
    --bs-btn-focus-shadow-rgb: var(--app-sage-rgb);
}

.btn-outline-success {
    --bs-btn-color: var(--app-sage-dark);
    --bs-btn-border-color: var(--app-sage);
    --bs-btn-hover-color: var(--app-surface);
    --bs-btn-hover-bg: var(--app-sage);
    --bs-btn-hover-border-color: var(--app-sage);
    --bs-btn-active-color: var(--app-surface);
    --bs-btn-active-bg: var(--app-sage);
    --bs-btn-active-border-color: var(--app-sage);
    --bs-btn-disabled-color: var(--app-sage-dark);
    --bs-btn-disabled-border-color: var(--app-sage);
    --bs-btn-focus-shadow-rgb: var(--app-sage-rgb);
}

/* The QR code's fold: a <summary> styled as a button, its default marker removed so it reads as
   one. Closed by default at every width - a code is for someone else's camera, and on the phone
   in your hand it is a third of the screen between you and the rest of the invitation. The label
   fits both states on purpose: plain CSS cannot rewrite it on open, and a button that says "show"
   while the thing is shown is worse than one that just names it. */
.qr-disclosure > summary {
    list-style: none;
    cursor: pointer;
}

    .qr-disclosure > summary::-webkit-details-marker {
        display: none;
    }

/* A control that is a way of LOOKING at the page rather than a thing to do with it - the occasion's
   gallery switch, its party game, the host's own fold, the QR code under a link.

   The occasion page had eleven controls between the title and the invitation and ten of them were
   the same outlined pill, so the three a guest actually came for could not lead. This is the other
   weight: text and an icon, no border and no fill.

   QUIET IS THE FORM, NOT THE CONTRAST. The colour is the ordinary body text colour, not a pale
   grey - fading these out would buy the same calm by making them hard to read, which is a bad
   trade on a page half its readers open on a phone in a dim room. The 2.75rem floor is a thumb:
   these are the controls most likely to be pressed on a phone held one-handed. */
.link-quiet {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    min-height: 2.75rem;
    padding: .25rem .15rem;
    border: 0;
    background: none;
    color: var(--app-text);
    font-size: .9rem;
    font-weight: 500;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
}

    .link-quiet:hover,
    .link-quiet:focus-visible {
        color: var(--app-accent);
    }

/* The party game. A block like the RSVP's, because it is the other thing on the page a guest
   does rather than reads - and a statement people tap is a button, not a list item. */
.game-block {
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-left: 4px solid var(--app-sage);
    border-radius: 12px;
    padding: 1.5rem;
}

.game-statement {
    width: 100%;
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-radius: 10px;
    padding: .75rem 1rem;
    font: inherit;
    color: var(--app-text);
    transition: border-color .15s ease, background-color .15s ease;
}

    .game-statement:hover:not(:disabled) {
        border-color: var(--app-accent);
    }

    /* Your own answer, before anyone knows anything. */
    .game-statement.is-mine {
        border-color: var(--app-accent);
        background-color: var(--app-sunken);
    }

    /* The lie, once it is out. Red rather than sage: this is the one that was not true. */
    .game-statement.is-lie {
        border-color: var(--bs-danger-border-subtle);
        background-color: var(--bs-danger-bg-subtle);
    }

    .game-statement:disabled {
        opacity: 1;   /* a revealed round is still readable, not greyed out */
    }

/* THE ONE THING THE PAGE ASKS A GUEST TO DO.
   Every other section of an occasion is something to read, and an RSVP set as one more heading
   over one more list reads as more of the same. A filled, bordered block says "this part is
   yours"; the host's answers sit inside the same block, because it is one subject, not two. */
.rsvp-block {
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-left: 4px solid var(--app-accent);
    border-radius: 12px;
    padding: 1.5rem;
}

    .rsvp-block > section:last-child {
        margin-bottom: 0 !important;
    }

/* An answer already given - sage, because it is a thing that is true, not a thing to do. */
.rsvp-answer {
    background-color: var(--app-sage-tint);
    border: 1px solid var(--app-sage-tint-strong);
}

.rsvp-answer .rsvp-answer-line {
    color: var(--app-sage-dark);
}

/* The gap left above a newly revealed message when revealNewest scrolls to it.
   Here rather than in the script, so the breathing room is a design decision. */
#newest-segment,
#active-message-target {
    scroll-margin-top: 1rem;
}

/* The account section's own nav. Bootstrap's nav-pills fill the active item with
   --bs-primary, which is now slate - a heavy block of it down the side of a form.
   The accent marks where you are instead, the same way the top bar does. */
.account-nav .nav-link {
    color: var(--app-text);
    border-radius: 6px;
    padding: .5rem .75rem;
}

    .account-nav .nav-link:hover {
        background-color: var(--app-sunken);
        color: var(--app-slate);
    }

    .account-nav .nav-link.active {
        background-color: var(--app-sunken);
        color: var(--app-slate);
        font-weight: 600;
        border-left: 3px solid var(--app-accent);
        border-radius: 0 6px 6px 0;
    }

/* The library's kind filter. A row of these is the page saying what it can show
   you, so they are legible at rest rather than only on hover - an outline button
   that looks pressable and does nothing until clicked is worse than a label. */
.kind-pill {
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--app-rule);
    background-color: var(--app-surface);
    color: var(--app-text);
    border-radius: 999px;
    padding: .3rem .85rem;
    font-size: .86rem;
    text-decoration: none;
    transition: border-color .15s ease, color .15s ease;
}

    .kind-pill:hover {
        border-color: var(--app-accent);
        color: var(--app-slate);
    }

    .kind-pill.on {
        background-color: var(--app-slate);
        border-color: var(--app-slate);
        color: var(--app-surface);
    }

/* =========================================
   THE ACCOUNT CARD

   Signing in, registering, resetting a password: one short form, centred, on
   its own surface. Narrower than a reading column because the measure that
   matters here is the input field, not a line of prose - and a form that runs
   the width of a page reads as a chore before anybody has typed anything.
========================================= */
.account-card-wrap {
    max-width: 460px;
    width: 100%;
    margin: 3rem auto;
    padding-inline: 16px;
}

.account-card {
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-radius: 10px;
    padding: 2rem 1.75rem;
}

    .account-card h1 {
        color: var(--app-slate);
    }

    /* Bootstrap's floating labels sit on a white field by default; these sit on
       one too, so the only change needed is the resting border. */
    .account-card .form-control {
        border-color: var(--app-rule);
    }

/* =========================================
   THE TWO SURFACES A LIST IS MADE OF

   The survey counted eight card treatments across the app, from
   "card h-100 border-0 shadow-sm" to "card p-4 mb-4 shadow-sm border-primary
   border-top border-4". Border, fill, radius and shadow each say "separate
   object", and spending all four on every box flattens the hierarchy until
   nothing on the page is lifted because everything is.

   So there are two, and the difference between them is the shape of the
   content, not its importance:

   .thing-tile  a photograph with a name under it, in a grid. For things you
                recognise by their picture - a memorial, a volume.
   .thing-row   a line with a small image at the left and facts beside it. For
                things you recognise by their words - an occasion's date and
                venue, an experience's opening sentence.

   Both are a white surface on the warm ground with a hairline rule, and no
   shadow at rest: one flat plane, the way a printed page is. The hover state
   is where the accent earns its keep - it marks the one row under the cursor,
   which is the only moment a list needs to lift anything.
========================================= */
.thing-tile,
.thing-row {
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-radius: 6px;
    transition: border-color .15s ease, box-shadow .15s ease;
}

    .thing-tile:hover,
    .thing-row:hover {
        border-color: var(--app-accent);
        box-shadow: 0 1px 6px rgba(var(--app-slate-rgb), 0.10);
    }

    /* Keyboard users get the same signal, since the hover state is carrying
       real information about which row is about to be opened. */
    .thing-tile:focus-visible,
    .thing-row:focus-visible {
        border-color: var(--app-accent);
        outline: 2px solid var(--app-accent);
        outline-offset: 2px;
    }

/* The letterbox a tile's photograph sits in. contain rather than cover, for
   the reason the memorials grid documents: these are whatever people uploaded,
   nothing stores their dimensions, and cover is a promise to mangle anything
   that is not roughly tile-shaped. */
.thing-tile-photo {
    width: 100%;
    aspect-ratio: 1 / 1;
    background-color: var(--app-sunken);
    border-bottom: 1px solid var(--app-rule);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: .5rem;
}

    .thing-tile-photo img {
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
    }

/* The small square at the left of a row, and the placeholder that stands in
   for it. Sized by the page, since an occasion's is 72px and an experience's
   is 84px and both were chosen against their own content. */
.thing-row-photo {
    border-radius: 4px;
    object-fit: cover;
    flex-shrink: 0;
}

.thing-row-placeholder {
    border-radius: 4px;
    background-color: var(--app-sunken);
    border: 1px solid var(--app-rule);
    color: var(--app-muted);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.text-accent {
    color: var(--app-accent-dark) !important;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-select:focus, .form-check-input:focus {
    box-shadow: 0 0 0 0.1rem var(--app-surface), 0 0 0 0.25rem rgba(var(--app-accent-rgb), 0.55);
}

/* The fallback for anything that brings no focus style of its own. Without it
   the browser draws its own ring, and each draws a different one - Android
   Chrome's is amber, which on a plum page looks like a warning. */
:focus-visible {
    outline-color: var(--app-accent);
}

/* Bootstrap gives a focused field a literal blue border (#86b7fe) under the ring
   above, and .form-select a blue ring of its own - so a selected box was plum
   around blue, and a dropdown beside it was blue alone. One colour for "you are
   here", the same one the nav uses. */
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
    border-color: var(--app-accent);
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--app-sage);
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

.app-layout {
    display: flex;
    height: 100vh;
    width: 100vw;
    overflow: hidden;
}

.main-content {
    flex-grow: 1; /* Takes up the remaining horizontal space */
    overflow-y: auto; /* Adds a scrollbar to the content if it gets too long */
    background-color: var(--app-ground);
}

.content-wrapper {
    padding: 2rem;
}

/* The semi-transparent screen that covers the old page */
.global-loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(255, 255, 255, 0.7); /* A slight white frost effect */
    z-index: 9999; /* Keeps it on top of everything else */
    /* Flexbox to center the spinner */
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    font-size: 1.2rem;
    color: var(--app-slate);
    font-weight: bold;
}

/* The animated spinning circle */
.spinner {
    border: 4px solid rgba(var(--app-slate-rgb), 0.18);
    border-top: 4px solid var(--app-accent);
    border-radius: 50%;
    width: 50px;
    height: 50px;
    animation: spin 0.8s linear infinite;
    margin-bottom: 15px;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

/* =========================================
   SHARED FULLSCREEN IMAGE MODAL
   Used by WeekRemember, WeekTestPlay, and Memorial/Event's image zoom.
   Was previously copy-pasted with minor variations in each page's own
   .razor.css - one definition here now.
========================================= */
.image-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.85);
    backdrop-filter: blur(5px);
    z-index: 9999;
    display: flex;
    justify-content: center;
    align-items: center;
    animation: fadeInModal 0.2s ease-out forwards;
}

.image-modal-content {
    max-width: 90vw;
    max-height: 90vh;
    border-radius: 12px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.5);
    object-fit: contain;
    animation: zoomInImage 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}

.image-modal-close {
    position: absolute;
    top: 20px;
    right: 30px;
    background: rgba(255, 255, 255, 0.2);
    border: none;
    color: white;
    font-size: 1.5rem;
    width: 45px;
    height: 45px;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    transition: background 0.2s;
}

    .image-modal-close:hover {
        background: rgba(255, 255, 255, 0.4);
    }

@keyframes fadeInModal {
    0% { opacity: 0; }
    100% { opacity: 1; }
}

@keyframes zoomInImage {
    0% { transform: scale(0.8); opacity: 0; }
    100% { transform: scale(1); opacity: 1; }
}

/* =========================================
   SIMPLE CONFIRMATION MODAL
   Used by WeekTest.razor's "Start Early?" prompt. This was missing
   entirely before - the .simple-modal-* classes were referenced in markup
   with no matching CSS anywhere, so the dialog rendered unstyled.
========================================= */
.simple-modal-backdrop {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 9999;
}

.simple-modal-content {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #ffffff;
    border-radius: 16px;
    padding: 2rem;
    max-width: 90vw;
    width: 420px;
    text-align: center;
    z-index: 10000;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

/* =========================================
   "TAP TO RESPOND" HINT
   Used by Memorial/Event's timeline continue button, and now WeekRemember.
========================================= */
.bounce-hint {
    animation: bounceDown 2s infinite;
    opacity: 0.8;
}

@keyframes bounceDown {
    0%, 20%, 50%, 80%, 100% {
        transform: translateY(0);
    }

    40% {
        transform: translateY(-5px);
    }

    60% {
        transform: translateY(-3px);
    }
}

/* ---------------------------------------------------------------------------
   QR codes (event links, memorial and challenge share panels)

   Generated server-side by QrCode.ForUrl as inline SVG carrying a viewBox, so
   the code scales to whatever box it's given. That matters because a longer URL
   needs more modules - the svg's own dimensions grow with the link, and only
   the container decides how big it actually renders.

   Sizing lives on the wrapper rather than the svg so the white quiet zone a
   scanner needs stays inside the border.
   --------------------------------------------------------------------------- */
.qr-frame {
    /* These sit inside d-flex centering wrappers, and a flex item shrinks below its width by
       default - which collapsed the code to the width of its own padding while the height held.
       Never shrink; on a narrow phone fall back to the available width instead. */
    flex: 0 0 auto;
    max-width: 100%;
    aspect-ratio: 1 / 1;
}

.qr-frame svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* Beside an event link - small enough that several sit in a row. */
.qr-frame-sm { width: 152px; height: 152px; }

/* The share panel's single code, meant to be scanned across a table. */
.qr-frame-lg { width: 200px; height: 200px; }

/* --- Occasion photo strip -------------------------------------------------
   A horizontal scroller rather than a grid: the day's photos arrive over time,
   from the host and from guests, so the count is unknown and a grid would keep
   reflowing the page underneath the header as people post.

   Snap points make it feel deliberate on a phone, which is where most of these
   are looked at - and where the guests posting them are standing. */
.event-gallery {
    display: flex;
    gap: .5rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    padding-bottom: .35rem;
    /* Bleed to the container edge so a half-visible photo says "there is more",
       instead of the strip looking like it simply ends. */
    scrollbar-width: thin;
}

.event-gallery-item {
    flex: 0 0 auto;
    scroll-snap-align: start;
    padding: 0;
    border: 0;
    background: none;
    border-radius: .5rem;
    overflow: hidden;
    cursor: zoom-in;
    line-height: 0;
}

    .event-gallery-item img {
        height: 190px;
        width: auto;
        max-width: 78vw;
        object-fit: cover;
        border-radius: .5rem;
        box-shadow: 0 .125rem .35rem rgba(0, 0, 0, .12);
    }

    .event-gallery-item:focus-visible {
        outline: 2px solid var(--app-accent);
        outline-offset: 2px;
    }

@media (max-width: 575.98px) {
    .event-gallery-item img {
        height: 150px;
    }
}

/* ============================================================================
   Upload zone (MediaDropZone)

   Replaces the bare <InputFile class="form-control">, which on a phone was a
   small grey "Choose Files / No file chosen" - a tap target barely bigger than
   the text in it, with nothing to say what it wanted or whether anything was
   happening.
   ============================================================================ */

.upload-zone {
    border: 2px dashed #ced4da;
    border-radius: .75rem;
    padding: 1.5rem 1rem;
    text-align: center;
    background-color: #fbfbfc;
    transition: border-color .15s ease, background-color .15s ease;
}

    /* The zone is focusable so paste works, but a dashed box with a default focus
       ring on it looks broken. Tint instead. */
    .upload-zone:focus,
    .upload-zone:focus-visible {
        outline: none;
        border-color: var(--app-accent);
        background-color: var(--app-sunken);
    }

.upload-zone-compact {
    padding: 1rem .75rem;
}

.upload-zone-over {
    border-color: var(--app-accent);
    background-color: var(--app-sunken);
}

.upload-zone-icon {
    display: block;
    font-size: 1.75rem;
    color: #6c757d;
    margin-bottom: .35rem;
}

/* ---- the strip of what is being uploaded ---- */

.upload-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    justify-content: center;
    margin-top: 1rem;
}

.upload-tile {
    position: relative;
    width: 72px;
    height: 72px;
    border-radius: .5rem;
    overflow: hidden;
    background-color: #e9ecef;
    flex: 0 0 auto;
}

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

.upload-tile-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: #6c757d;
    font-size: 1.5rem;
}

/* Sits along the bottom edge rather than covering the picture: the thumbnail is
   there so someone can see they picked the right photo, and a full-tile scrim
   would hide the one thing it is for. */
.upload-tile-overlay {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 5px;
    background-color: rgba(0, 0, 0, .35);
}

.upload-tile-bar {
    height: 100%;
    background-color: var(--app-accent);
    transition: width .15s linear;
}

.upload-tile-failed {
    opacity: .55;
}

.upload-tile-error {
    height: auto;
    top: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(220, 53, 69, .75);
    color: #fff;
    font-size: 1.25rem;
}

/* ============================================================================
   Photo strip arrows (occasion page)

   The strip itself has always scrolled. On a phone that is the right and only
   affordance - a row of photos is a thing your thumb swipes. On a desktop it
   left a trackpad gesture and the native scrollbar, so five photos looked like
   three and a bit with no way to reach the rest.
   ============================================================================ */

.event-gallery-wrap {
    position: relative;
}

.event-gallery-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;

    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;

    border: 1px solid rgba(0, 0, 0, .08);
    border-radius: 50%;
    background-color: rgba(255, 255, 255, .92);
    color: #212529;
    box-shadow: 0 2px 8px rgba(0, 0, 0, .18);

    cursor: pointer;
    transition: opacity .15s ease, background-color .15s ease;
}

    .event-gallery-nav:hover {
        background-color: #fff;
    }

    .event-gallery-nav.prev {
        left: 6px;
    }

    .event-gallery-nav.next {
        right: 6px;
    }

/* Hidden rather than disabled at each end: a chevron pointing at nothing is
   worse than no chevron, and the strip's own edge already says where it stops.
   visibility keeps the layout still so the photos do not shift as you scroll. */
.event-gallery-wrap.at-start .event-gallery-nav.prev,
.event-gallery-wrap.at-end .event-gallery-nav.next,
.event-gallery-wrap.no-overflow .event-gallery-nav {
    opacity: 0;
    visibility: hidden;
}

/* The actual question is "does this device have a mouse", not "is the window
   wide" - a phone held in landscape is wider than a small laptop window, and a
   touch laptop is both. Where the swipe is natural, two chevrons on top of the
   photos are just two photos you cannot see. */
@media (hover: none), (pointer: coarse) {
    .event-gallery-nav {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .event-gallery-nav {
        transition: none;
    }
}
