﻿/* =========================================
   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);
}

/* ROOM UNDER A FOCUSED FIELD FOR THE EDITORS' SAVE BAR.

   When the browser scrolls something into view - tabbing to the next input, a
   phone keyboard opening, a jump to an anchor - it scrolls just far enough to
   make the element visible, and it has no idea a bar is pinned across the
   bottom. A field at the foot of the viewport therefore lands behind it.
   scroll-padding is the one thing browsers honour for this, and it has to sit
   on the scroll container, which is the document - so this is site-wide
   although only the editors have the bar.

   Harmless elsewhere: it only shifts how far an automatic scroll travels, and
   stopping a little short of the very bottom reads better on every page. */
html {
    scroll-padding-bottom: 6rem;
}

/* .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;
}

/* The stage a full-screen CLIP sits on. It exists to catch the clicks meant for the player:
   tapping the picture to dismiss it is right for a picture, and on a video that same tap is how
   you pause. Sized to its video rather than filling the overlay, so the area that does NOT close
   is exactly the area somebody is aiming at. */
.image-modal-stage {
    display: flex;
    max-width: 90vw;
    max-height: 90vh;
    cursor: default;
}

    .image-modal-stage video {
        /* A video has no intrinsic size until it loads, so without this it can start as a sliver
           and jump once the metadata arrives. */
        min-width: min(60vw, 480px);
    }

.image-modal-close {
    position: absolute;
    /* Clears the notch on a phone. The app bar is no longer a problem here - the overlay paints
       over it now that it is rendered outside the view-change fade, which used to cap its
       stacking context and let the bar sit on top of this button. */
    top: calc(20px + env(safe-area-inset-top, 0px));
    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;
    }
}

/* ---------------------------------------------------------------------------
   THE RECONNECT PANEL

   Blazor Server holds this page's state on the server, tied to a websocket, so
   a broken socket is something the person has to be told about. The stock panel
   for that is an unstyled grey box with a reload button, and it reads like the
   app has crashed - which is wrong most of the time it appears. A backgrounded
   tab whose keepalives got throttled, a locked phone, a closed lid: all of them
   come back, with everything intact, in about a second.

   So this is built around which of those two it is. Reconnecting gets a quiet
   panel that says nothing is lost, because nothing is. Only once the client has
   stopped trying does it become a thing with buttons.

   It blocks the page in both states, deliberately. With the circuit down, every
   control on the page is inert - a dimmed page that ignores clicks is honest,
   where a page that still looks live and silently swallows them is not.
   --------------------------------------------------------------------------- */

#components-reconnect-modal {
    display: none;
}

/* These four are the whole vocabulary - the framework clears all of them and
   then adds exactly one, so each is listed on its own rather than relying on
   -show surviving alongside the others. */
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
    display: flex;
}

/* Last, so that coming back wins over whatever state got us here. */
#components-reconnect-modal.components-reconnect-hide {
    display: none;
}

.reconnect-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    /* Tinted with the page's own plum rather than neutral black: a grey scrim
       is the look of a browser dialog, and this is still our page. */
    background-color: rgba(var(--app-accent-rgb), .18);
    backdrop-filter: blur(2px);
}

/* Heavier once it needs a decision, so the card is unmistakably the thing to
   deal with rather than something to wait out. */
.components-reconnect-failed.reconnect-overlay {
    background-color: rgba(var(--app-slate-rgb), .45);
}

.reconnect-card {
    max-width: 24rem;
    width: 100%;
    padding: 1.5rem;
    border-radius: .9rem;
    background-color: var(--app-surface);
    box-shadow: 0 .75rem 2.5rem rgba(var(--app-slate-rgb), .22);
    text-align: center;
}

/* Which half shows. The retrying panel is the default, so the rejected state -
   where reconnect.js is already reloading - reads as "Reconnecting" for the
   fraction of a second it is up, rather than flashing an error on its way to a
   page that was going to come back anyway. */
.reconnect-lost {
    display: none;
}

.components-reconnect-failed .reconnect-trying {
    display: none;
}

.components-reconnect-failed .reconnect-lost {
    display: block;
}

.reconnect-title {
    margin: 0 0 .35rem;
    font-weight: 600;
    color: var(--app-slate);
}

.reconnect-note {
    margin: 0;
    font-size: .9rem;
    color: var(--app-muted);
}

/* Faint on purpose. The wait here can run to two minutes, which is far too long
   to sit under "this usually takes a moment" with nothing moving - but the
   numbers are for anyone who looks, not an invitation to watch them. Both spans
   are filled in by the framework, which finds them by id. */
.reconnect-progress {
    margin: .6rem 0 0;
    font-size: .8rem;
    color: var(--app-muted);
    opacity: .75;
    font-variant-numeric: tabular-nums;
}

.reconnect-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    justify-content: center;
    margin-top: 1.1rem;
}

.reconnect-spinner {
    display: block;
    width: 1.75rem;
    height: 1.75rem;
    margin: 0 auto .85rem;
    border: 2px solid var(--app-sunken);
    border-top-color: var(--app-accent);
    border-radius: 50%;
    animation: reconnect-spin 900ms linear infinite;
}

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

/* A still ring rather than no ring: it is the only thing on the card that says
   this panel is waiting on something, so it keeps its shape and loses only the
   motion. */
@media (prefers-reduced-motion: reduce) {
    .reconnect-spinner {
        animation: none;
        border-top-color: var(--app-accent);
    }

    .reconnect-overlay {
        backdrop-filter: none;
    }
}

/* ---------------------------------------------------------------------------
   THE HOST'S NOTES ON AN OCCASION

   Folded until asked, because this page was decluttered on purpose and three
   paragraphs of parking instructions should not push the RSVP off the screen
   for somebody who only wanted the date.
   --------------------------------------------------------------------------- */

.occasion-notes-short {
    max-height: 7.5rem;
    overflow: hidden;
    /* Faded rather than cut, so it reads as "there is more" instead of "this
       is broken". */
    -webkit-mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
    mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
}

/* Markdown brings its own tags, and they arrive carrying the browser's default
   spacing - which is looser than anything else on this page. */
.occasion-notes > :first-child { margin-top: 0; }
.occasion-notes > :last-child { margin-bottom: 0; }

.occasion-notes p { margin-bottom: .5rem; }

/* Headings are allowed but kept close to body size. A host writing "## Parking"
   wants a label, not a banner competing with the occasion's own name. */
.occasion-notes h1,
.occasion-notes h2,
.occasion-notes h3 {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--app-slate);
    margin: 1rem 0 .35rem;
}

.occasion-notes ul,
.occasion-notes ol {
    padding-left: 1.25rem;
    margin-bottom: .5rem;
}

/* A registry url is long and has no spaces in it, which is how a line ends up
   wider than a phone. */
.occasion-notes a { word-break: break-word; }

@media (prefers-reduced-motion: reduce) {
    .occasion-notes-short { transition: none; }
}

/* =========================================
   THE LONG EDITORS - FOLDING SECTIONS AND A SAVE THAT STAYS PUT

   The occasion editor is seven parts and the thing editor is six. Both were
   one continuous column, which on a phone put the save button an entire form
   away from the field somebody came to change.

   Two separate fixes, and the second is the cheaper of the two: sections fold,
   and the save bar sticks to the bottom of the viewport. Either alone would
   help; the bar is what removes the long scroll back.

   These deliberately do NOT reuse .thing-row, which the editors borrowed
   before. That class is for a row in a list - it takes the accent on hover
   because it is about to be opened. A panel in a form is not going anywhere,
   and a card that lights up when the pointer crosses it on the way to a text
   box is noise.
   ========================================= */
.editor-section {
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-radius: 6px;
    margin-bottom: 1rem;
}

.editor-section-heading {
    margin: 0;
    font-size: 1rem;
}

/* The whole header is the control, not a chevron somebody has to hit. On a
   phone that is the difference between a target the width of the screen and a
   target the width of a thumbnail. */
.editor-section-toggle {
    width: 100%;
    display: flex;
    align-items: baseline;
    gap: .6rem;
    background: none;
    border: 0;
    text-align: left;
    padding: 1rem 1.25rem;
    color: var(--app-muted);
    font: inherit;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    font-size: .9rem;
    border-radius: 6px;
}

    .editor-section-toggle:hover {
        color: var(--app-accent);
    }

    .editor-section-toggle:focus-visible {
        outline: 2px solid var(--app-accent);
        outline-offset: -2px;
    }

.editor-section-chevron {
    flex-shrink: 0;
    transition: transform .15s ease;
    /* Baseline alignment is right for the words beside it and wrong for an
       icon, which has no descenders to sit on. */
    align-self: center;
}

.editor-section-open > .editor-section-heading .editor-section-chevron {
    transform: rotate(90deg);
}

.editor-section-title {
    flex-shrink: 0;
}

/* Sentence case, normal weight, no tracking: this is the one part of the
   header that is CONTENT rather than a label, and it should not be shouting in
   the same voice as the section's name. Pushed right so a column of folded
   sections lines its summaries up. */
.editor-section-summary {
    margin-left: auto;
    text-align: right;
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    color: var(--app-text);
    opacity: .75;
    font-size: .85rem;
    /* Long ones wrap rather than push the title off a narrow screen. */
    min-width: 0;
}

.editor-section-body {
    padding: 0 1.25rem 1.25rem;
}

/* The rule only exists to separate the header from content below it, so it
   belongs to the open state and not to the header. */
.editor-section-open > .editor-section-heading {
    border-bottom: 1px solid var(--app-rule);
    margin-bottom: 1.25rem;
}

@media (max-width: 575.98px) {
    .editor-section-toggle {
        /* The summary gets its own line rather than competing with the title
           for a 375px row. */
        flex-wrap: wrap;
    }

    .editor-section-summary {
        margin-left: 1.6rem;
        text-align: left;
        flex-basis: 100%;
    }

    .editor-section-body {
        padding: 0 .85rem 1rem;
    }

    .editor-section-toggle {
        padding: .85rem;
    }
}

/* THE SAVE BAR.

   sticky rather than fixed, so it sits in the flow and the page can still end:
   fixed would cover the last of the form forever and need padding underneath
   to compensate. This works because the DOCUMENT scrolls - see the note at the
   top of MainLayout.razor.css, where a 100vh container with its own overflow
   was removed. If that ever comes back, this stops sticking and nothing warns.

   Below Bootstrap's modal (1055) and the editors' toast (1050), above ordinary
   page content. */
.editor-save-bar {
    position: sticky;
    bottom: 0;
    z-index: 1020;
    background-color: var(--app-surface);
    border: 1px solid var(--app-rule);
    border-radius: 6px 6px 0 0;
    padding: .85rem 1.25rem;
    /* Clears the home indicator on a phone, and is zero everywhere else. */
    padding-bottom: calc(.85rem + env(safe-area-inset-bottom, 0px));
    box-shadow: 0 -2px 10px rgba(var(--app-slate-rgb), .10);
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}

/* btn-lg and px-5 are right for a button sitting in a page and too big for one
   in a bar across the bottom of a phone, where the pair wrapped to two rows and
   took 140px of a 812px screen. Shrunk and allowed to share a row instead: the
   bar is only worth having if it costs less than the scroll it saves. */
@media (max-width: 575.98px) {
    .editor-save-bar {
        gap: .5rem;
        padding: .6rem .75rem;
        padding-bottom: calc(.6rem + env(safe-area-inset-bottom, 0px));
    }

        .editor-save-bar .btn {
            flex: 1 1 0;
            padding-left: .5rem;
            padding-right: .5rem;
            font-size: 1rem;
            white-space: nowrap;
        }
}

@media (prefers-reduced-motion: reduce) {
    .editor-section-chevron { transition: none; }
}

/* =========================================
   CONTROLS ON A CLIP THAT IS PRETENDING TO BE A GIF

   Global rather than scoped to PhotoGallery, because the memorial's timeline shows the same kind
   of clip and needs the same three buttons; scoped CSS is per component, so a second copy there
   was the alternative. js/clip.js drives these and attaches to any .gallery-clip.

   Only ever drawn on a video with no controls of its own - a clip shown with controls already has
   play, volume and the browser's own fullscreen.
   ========================================= */
.gallery-clip-bar {
    position: absolute;
    bottom: 8px;
    right: 8px;
    display: flex;
    gap: 6px;
    z-index: 2;
}

.gallery-clip-btn {
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: 50%;
    background-color: rgba(0,0,0,0.6);
    color: #fff;
    font-size: 0.8rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}

    .gallery-clip-btn:hover {
        background-color: rgba(0,0,0,0.85);
    }

    .gallery-clip-btn:focus-visible {
        outline: 2px solid #fff;
        outline-offset: 2px;
    }

/* WHICH ICON SHOWS IS CSS, driven by data attributes clip.js writes on the wrapper. Both icons of
   each pair are always in the markup; the script never touches their classes, because Blazor owns
   those elements and would diff any change back on its next render - leaving a control that
   quietly disagrees with the video it belongs to.

   The defaults here are the autoplay gallery's starting state: playing and silent. */
.clip-paused,
.clip-unmuted {
    display: none;
}

.gallery-clip[data-paused="true"] .clip-playing { display: none; }
.gallery-clip[data-paused="true"] .clip-paused { display: block; }

.gallery-clip[data-muted="false"] .clip-muted { display: none; }
.gallery-clip[data-muted="false"] .clip-unmuted { display: block; }
