[hidden] {
    display: none !important;
}

.soft-nav-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0%;
    background: var(--accent, #2b7a78);
    z-index: var(--z-toast);
    opacity: 0;
    transition: width 0.2s ease, opacity 0.15s ease;
    pointer-events: none;
}

.gesture-toast {
    position: fixed;
    bottom: 1.5rem;
    left: 50%;
    transform: translateX(-50%) translateY(1rem);
    max-width: 90vw;
    background: var(--text);
    color: var(--surface);
    padding: 0.75rem 1.25rem;
    border-radius: 999px;
    font-size: 0.9rem;
    text-align: center;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
    z-index: var(--z-toast);
    opacity: 0;
    transition: opacity 0.3s ease, transform 0.3s ease;
    pointer-events: none;
}

.gesture-toast.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

.soft-nav-progress.active {
    width: 70%;
    opacity: 1;
    transition: width 4s cubic-bezier(0.1, 0.7, 0.3, 1), opacity 0.15s ease;
}

:root {
    --bg: #f7f5f0;
    --surface: #ffffff;
    --text: #23303a;
    --muted: #6b7a85;
    --border: #e2ddd2;
    --accent: #1d7a8c;
    --accent-dark: #145a68;
    --accent-soft: #dff0f0;
    --danger: #b3413a;
    --radius: 10px;

    /* ---------------------------------------------------------------
       CANONICAL OVERLAY STACKING TOKENS — read DEV_LESSONS.md before
       changing or bypassing these.

       Standing rule for this site: EVERY modal / dialog / popup /
       lightbox / picker panel uses `z-index: var(--z-overlay)` and is a
       direct child of <body> (public/js/overlay-top.js hoists them
       automatically). Nothing else in this stylesheet may use a z-index
       at or above --z-overlay unless it is deliberately meant to sit on
       top of an open modal — today the only such things are transient
       toasts and the soft-nav progress bar (--z-toast).

       Why a token and not "just a big number": z-index is only ever
       compared inside the nearest ancestor stacking context, so a large
       number on a modal nested inside `main.page` (position:relative;
       z-index:1) still loses to the sticky `.site-header`. The number
       and the direct-child-of-<body> placement are one rule, not two.

       Decorative full-screen layers that must stay BEHIND modals
       (.easter-egg-layer, 9997) keep their own lower values on purpose.
       --------------------------------------------------------------- */
    --z-overlay: 12000;
    --z-toast: 12100;

    /* Default colors for a per-entry title row ("banner") whose color has
       NOT been manually set — Emergency Contacts, Itinerary, Attractions &
       Food, Favorite Places. Deliberately defined in terms of the theme's
       own accent tokens rather than as literal hexes: every [data-theme]
       block below already redefines --accent-soft/--accent-dark, so an
       unset banner re-colors itself the moment the theme changes (including
       any theme added later) with no per-theme duplication here. A banner
       whose color WAS manually picked gets a concrete inline background
       instead and is therefore untouched by theme switches — see
       banner_attrs() in app/lib/format.php. */
    --banner-default-bg: var(--accent-soft);
    --banner-default-fg: var(--accent-dark);

    font-size: 16px;
}

/* Sunset: warm coral/orange accents over a warm sand background. */
[data-theme="sunset"] {
    --bg: #fbf1e6;
    --accent: #e07a4f;
    --accent-dark: #b8532c;
    --accent-soft: #fbe3d3;
    --border: #ecd9c4;
}

/* Seafoam: cool mint/teal-green, lighter and airier than the ocean default. */
[data-theme="seafoam"] {
    --bg: #f2f7f3;
    --accent: #3a9188;
    --accent-dark: #276b64;
    --accent-soft: #dcefe9;
    --border: #dbe8e1;
}

* { box-sizing: border-box; }

/* min-height, NOT height. `height: 100%` capped <body> — a column flex
   container — at exactly the viewport height, so on any page taller than
   the viewport the real content *overflowed* body's box instead of growing
   it. Two bugs fell out of that: the scrollable area ended short of the
   footer (the footer's own margin-top is not counted as scrollable
   overflow), leaving the last ~100px of it unreachable; and body always
   had NEGATIVE flex free space, so any item that lost its automatic
   minimum size got crushed — which is what the shimmer effect's
   `overflow: hidden` used to do to the footer. With min-height, body grows
   to its content, free space is never negative, and the footer is fully
   scrollable into view. */
html, body { min-height: 100%; }

body {
    margin: 0;
    background: var(--bg);
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    line-height: 1.5;
    position: relative;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* Sticky-bottom footer: .page grows to fill any leftover vertical space
   so the footer sits at the bottom of the viewport on a short page, and
   falls in naturally after content (not stuck to the viewport) on a
   long one — the classic flex sticky-footer pattern. */
main.page {
    flex: 1 0 auto;
    /* The Expand/Collapse-All button is absolutely positioned as the
       first child of this element — without an explicit position here,
       it anchors to <body> instead (the next positioned ancestor), which
       spans the full browser width. On a wide window that put it far to
       the right of the actual (narrower, centered) content column,
       overlapping whatever card heading happened to be near that
       viewport-relative spot rather than sitting inside .page itself. */
    position: relative;
}

/* When a custom background photo is set, keep content legible over it by
   scrimming the page in a translucent version of the current theme's
   background color, rather than showing the photo at full strength behind
   text and form fields. */
.bg-overlay {
    position: fixed;
    inset: 0;
    background: var(--bg);
    opacity: 0.88;
    z-index: 0;
    pointer-events: none;
}

.site-header, .wave-divider, .page, .login-wrap, .site-footer { position: relative; z-index: 1; }

a { color: var(--accent-dark); }

.wave-divider {
    line-height: 0;
    margin-top: -1px;
}

.wave-divider svg {
    width: 100%;
    height: 16px;
    display: block;
}

.wave-divider path { fill: var(--accent-soft); }

.page {
    max-width: 720px;
    margin: 0 auto;
    padding: 1rem;
    padding-bottom: 1.5rem;
}

.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0.75rem 1rem;
    position: sticky;
    top: 0;
    z-index: 10;
}

.brand {
    font-weight: 700;
    text-decoration: none;
    color: var(--text);
    font-size: 1.1rem;
}

.brand-logo {
    display: block;
    max-width: 100%;
}

.nav-toggle {
    display: block;
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--text);
}

.main-nav {
    display: none;
    flex-direction: column;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0.5rem 0;
}

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

.main-nav a {
    padding: 0.75rem 1.25rem;
    text-decoration: none;
    color: var(--text);
}

.main-nav a:hover { background: var(--bg); }

@media (min-width: 640px) {
    .nav-toggle { display: none; }
    .main-nav {
        display: flex;
        flex-direction: row;
        align-items: center;
        position: static;
        border: none;
        padding: 0;
        gap: 0.25rem;
    }
    .main-nav a { padding: 0.5rem 0.75rem; }
}

/* Universal "insert an emoji into whatever field you last focused"
   button — see public/js/icon-picker.js. Lives inside #main-nav so it
   reads as part of the menu rather than a bolted-on floating widget:
   on mobile the dropdown is position:absolute, so this floats in its
   top-right corner and is hidden/shown by the SAME .open class as the
   rest of the menu (no separate JS needed for that); on desktop the nav
   itself becomes a normal static flex row, so this just becomes its
   last item, sitting after "Me" — the site header is sticky, so it
   never scrolls out of view either way. */
.emoji-fab {
    position: absolute;
    top: 0.5rem;
    right: 0.75rem;
    width: 2.2rem;
    height: 2.2rem;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    border: none;
    font-size: 1.15rem;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
    padding: 0;
    margin: 0;
    flex-shrink: 0;
}

.emoji-fab:hover { background: var(--accent-dark); }

.emoji-fab.emoji-fab-nudge { animation: emoji-fab-nudge 0.4s ease; }

@keyframes emoji-fab-nudge {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-3px); }
    75% { transform: translateX(3px); }
}

@media (min-width: 640px) {
    .emoji-fab {
        position: static;
        box-shadow: none;
        margin-left: 0.25rem;
    }
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1rem;
    margin-bottom: 1rem;
}

h1, h2, h3 { line-height: 1.25; }

/* The page title card ("<h1>Overview</h1>" and friends). Height and the
   gap below it are per-page admin settings — see app/lib/page_header.php
   and Site Styling -> Page Header Cards — emitted as inline style on the
   card itself, because soft-nav.js swaps only <main class="page"> and a
   rule up in <head> would keep the previous page's numbers.

   The compact treatment only applies once a page has an explicit height
   (.page-header-card-sized). Without one, a header card is byte-for-byte
   what it always was, so nothing changes on a page nobody has tuned. */
.page-header-card-sized {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-block: 0.5rem;
}

/* An <h1> alone in a card is mostly browser default margin (0.67em top
   AND bottom at 2em font size — ~43px of pure whitespace), which is what
   actually made the Overview header tall enough to push the Headcount
   widget below the fold on a 375px screen. Centring handles the spacing
   instead, so the margins go. */
.page-header-card-sized > h1 {
    margin: 0;
}

.page-header-card-sized > :last-child {
    margin-bottom: 0;
}

.field-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border);
}

/* A .field-row that's an <a> (e.g. Overview's "What's Planned" rows,
   linking to a pre-filtered Itinerary) should read as a row you can tap,
   not as a normal blue underlined link. */
a.field-row {
    text-decoration: none;
    color: inherit;
    cursor: pointer;
}

a.field-row:hover .field-label { text-decoration: underline; }

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

.payment-due-entry {
    padding: 0.6rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 75%, black 10%);
}

.payment-due-entry:last-child { border-bottom: none; }

/* ---- The standard location/entry title row ("banner") ----------------
   THE site-wide model for every per-entry title line: Emergency Contacts
   (where the pattern started), Itinerary items, Attractions & Food, and
   Favorite Places. Anything new with a per-entry title row should use
   .entry-banner + .entry-banner-actions rather than rolling its own.

   Two rules make it work:
   - flex-wrap on the row itself, so the action icons drop to a second
     row intact instead of squashing when the title is long;
   - a theme-following default background (--banner-default-bg), which an
     explicitly-picked per-entry color overrides inline. */
.entry-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    flex-wrap: wrap;
    padding: 0.5rem 0.75rem;
    border-radius: 8px;
    background: var(--banner-default-bg);
    color: var(--banner-default-fg);
}

/* flex-wrap here too so a long icon group wraps within itself rather
   than overflowing the banner; flex-shrink:0 keeps the icons from being
   squeezed by a greedy title (they wrap to the next row instead). */
.entry-banner-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.3rem;
    flex-shrink: 0;
}

.entry-banner-name {
    font-weight: 700;
    font-size: 1.1rem;
    min-width: 0;
}

.contact-banner {
    margin: -0.6rem 0 0.6rem;
}

.contact-banner-name {
    font-weight: 700;
    font-size: 1.1rem;
}

.contact-banner-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.3rem;
    flex-shrink: 0;
}

.contact-entry {
    padding: 0.6rem 0 1rem;
    margin-bottom: 0.5rem;
    border-bottom: 2px solid color-mix(in srgb, var(--border) 75%, black 10%);
}

.contact-entry:last-child { border-bottom: none; margin-bottom: 0; }

.field-label {
    color: var(--muted);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
}

.field-value {
    font-weight: 600;
    text-align: right;
    word-break: break-word;
}

form.stacked label {
    display: block;
    font-size: 0.9rem;
    color: var(--muted);
    margin: 0.75rem 0 0.25rem;
}

input[type=text], input[type=email], input[type=password], textarea, select {
    width: 100%;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    font-size: 1rem;
    background: var(--surface);
    color: var(--text);
}

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

/* "Hours" fields are multi-line (one line per day-range, filled that way
   by Smart Search — see places_format_weekday_text()) but are usually
   only 3-4 lines, so they start shorter than a notes box and the user
   drags them taller from the corner grip if they need more. Line height
   is tightened a touch so a 7-day worst case still reads as a block. */
textarea.hours-input {
    min-height: 3.2rem;
    line-height: 1.35;
}

button, .btn {
    display: inline-block;
    background: var(--accent);
    color: #fff;
    border: none;
    padding: 0.6rem 1rem;
    border-radius: 8px;
    font-size: 1rem;
    cursor: pointer;
    text-decoration: none;
    margin-top: 0.75rem;
}

button:hover, .btn:hover { background: var(--accent-dark); }
.btn.secondary.active { background: var(--accent); color: #fff; }

button.secondary, .btn.secondary {
    background: transparent;
    color: var(--accent-dark);
    border: 1px solid var(--accent-dark);
}

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

.flash {
    padding: 0.7rem 1rem;
    border-radius: 8px;
    margin-bottom: 1rem;
    font-size: 0.95rem;
}

.flash-success { background: #dff3e3; color: #1e5e2f; }
.flash-error { background: #fbe2e0; color: #8a2c24; }

.note {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
}

.note:last-child { border-bottom: none; }

.avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    background: var(--border);
}

.note-body-wrap { flex: 1; min-width: 0; }

.note-meta {
    font-size: 0.8rem;
    color: var(--muted);
    margin-bottom: 0.15rem;
}

.note-text { white-space: pre-wrap; word-break: break-word; }

.list-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border);
}

.list-item:last-child { border-bottom: none; }

.list-item.done .item-label {
    text-decoration: line-through;
    color: var(--muted);
}

.item-label { flex: 1; }

.item-meta { font-size: 0.75rem; color: var(--muted); }

.role-badge {
    display: inline-block;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    background: var(--bg);
    color: var(--muted);
    border: 1px solid var(--border);
}

.login-wrap {
    max-width: 380px;
    margin: 3rem auto;
    padding: 0 1rem;
}

.muted { color: var(--muted); }
.text-right { text-align: right; }

table.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

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

@media (max-width: 480px) {
    table.admin-table thead { display: none; }
    table.admin-table, table.admin-table tbody, table.admin-table tr, table.admin-table td {
        display: block;
        width: 100%;
    }
    table.admin-table tr {
        border-bottom: 2px solid var(--border);
        padding: 0.5rem 0;
    }
    table.admin-table td {
        border-bottom: none;
        padding: 0.2rem 0;
    }
    table.admin-table td::before {
        content: attr(data-label);
        font-weight: 600;
        display: inline-block;
        min-width: 100px;
        color: var(--muted);
    }
}

.mini-player {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: var(--surface);
    border-top: 1px solid var(--border);
    padding: 0.5rem 1rem;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    z-index: 20;
    box-shadow: 0 -2px 8px rgba(0,0,0,0.05);
}

.mini-player-info {
    display: flex;
    flex-direction: column;
    font-size: 0.8rem;
    min-width: 0;
}

.mini-player-label { color: var(--muted); }
.mini-player-title { font-weight: 600; }

.mini-player audio {
    flex: 1;
    min-width: 180px;
    height: 32px;
}

.song-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
}

.song-row:last-child { border-bottom: none; }
.song-row.current { background: rgba(29,122,140,0.06); border-radius: 8px; padding: 0.75rem; }

.song-info {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    flex: 1;
    min-width: 200px;
}

.song-info audio { width: 100%; height: 34px; }

.song-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.song-actions form button { margin: 0; }

.itin-date {
    margin: 1.25rem 0 0.4rem;
    color: var(--accent-dark);
}
/* No longer zeroing the first date's top margin — with the Filter by Tag
   pill (or other controls) now commonly sitting right above it, a zeroed
   margin made the first date look glued to whatever's above instead of
   grouped with its own items below. */

.itin-item {
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border);
}

.itin-item:last-child { border-bottom: none; }

/* Time + title sit together as one row. Carries .entry-banner (the shared
   title-row model above) for its layout, padding, radius and default
   theme-following background; only the itinerary-specific gap and bottom
   margin live here. An explicitly-picked color arrives as .tinted-banner
   plus an inline background. */
.itin-banner {
    gap: 0.75rem;
    margin-bottom: 0.4rem;
    border-radius: var(--radius);
}

.itin-time {
    width: 4.5rem;
    flex-shrink: 0;
    font-size: 0.85rem;
    color: var(--muted);
}

.itin-body { min-width: 0; }

/* flex-basis 8rem (not the old `flex: 1`, i.e. 1 1 0%) is what makes the
   action icons wrap to a second row the way Emergency Contacts' do: with
   a 0% basis the title always shrank to make room and the icons were
   squeezed instead of wrapping. Now once title + icons can't both fit,
   the icon group drops below. */
.itin-title { font-weight: 600; flex: 1 1 8rem; min-width: 0; }

/* Edit/delete/contact/favorite icons on a location's title row — same
   pattern as Emergency Contacts' .contact-banner-actions, kept
   consistent across every "location title row" on the site (Itinerary,
   Emergency Contacts, and anywhere similar gets added later). */
.itin-banner-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.3rem;
    flex-shrink: 0;
}

.itin-link {
    display: block;
    font-size: 0.9rem;
    text-decoration: none;
    margin-top: 0.15rem;
}

.itin-link:hover { text-decoration: underline; }

.itin-notes {
    font-size: 0.85rem;
    color: var(--muted);
    margin-top: 0.3rem;
    white-space: pre-wrap;
}

.section-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0 0 0.5rem;
    padding-bottom: 0.5rem;
    border-bottom: 2px solid var(--accent);
    font-size: 1rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--accent-dark);
}

.autocomplete-wrap { position: relative; }

/* address-autocomplete.js wraps the <input> in this div the first time it's
   focused, so it's no longer a direct flex child of .input-icon/field-pair
   containers — without this, the wrapper (unstyled) shrinks to the
   input's default intrinsic size instead of filling the space the input
   had a moment before, which looked like the field suddenly shrinking. */
.input-icon .autocomplete-wrap {
    flex: 1;
    min-width: 0;
}

.autocomplete-wrap input {
    width: 100%;
}

.autocomplete-list {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 8px 8px;
    max-height: 220px;
    overflow-y: auto;
    z-index: 30;
    box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}

.autocomplete-list div {
    padding: 0.5rem 0.7rem;
    cursor: pointer;
    font-size: 0.9rem;
}

.autocomplete-list div:hover,
.autocomplete-list div.active {
    background: var(--bg);
}

.role-help summary {
    cursor: pointer;
    color: var(--accent-dark);
    font-weight: 600;
    display: inline-block;
    margin: 0.25rem 0 0.75rem;
    list-style: none;
}

.role-help summary::-webkit-details-marker { display: none; }
.role-help summary::before { content: "❓ "; }

.role-help-body {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.75rem 1rem;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.role-help-body dt { font-weight: 700; margin-top: 0.5rem; }
.role-help-body dt:first-child { margin-top: 0; }
.role-help-body dd { margin: 0.1rem 0 0; color: var(--muted); }

.field-pair {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1rem;
}

.field-half {
    flex: 1 1 200px;
    min-width: 180px;
}

/* A read-only field-row inside a side-by-side pair (e.g. WiFi Network /
   WiFi Password): label as a column header, value on its own line below
   — avoids the label+value trying to share one narrow row and wrapping
   unpredictably. Consistent on mobile and desktop since it no longer
   depends on how much horizontal room the half-width column has. */
.field-half .field-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
}

.field-half .field-row .field-value {
    text-align: left;
}

.field-icon {
    margin-right: 0.4rem;
    display: inline-block;
}

/* In a side-by-side pair, the second field's icon is redundant — it only
   reappears once field-pairs.js detects the pair has wrapped to its own row. */
.field-pair .field-half:nth-child(2) .second-icon {
    display: none;
}

.field-pair.wrapped .field-half:nth-child(2) .second-icon {
    display: inline-block;
}

.input-icon {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.input-icon input { flex: 1; min-width: 0; }

.nav-icon-btn {
    flex-shrink: 0;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    width: 2.4rem;
    height: 2.4rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    text-decoration: none;
    cursor: pointer;
    margin-top: 0;
    padding: 0;
    color: var(--text);
}

.nav-icon-btn:hover { background: var(--border); }

.favorite-star-btn {
    color: var(--muted);
    cursor: pointer;
}

.favorite-star-btn.is-favorited {
    color: #d4a017;
    border-color: #d4a017;
}

.field-row .nav-icon-btn {
    width: 2rem;
    height: 2rem;
    font-size: 1rem;
    vertical-align: middle;
    margin-left: 0.3rem;
}

/* Standardized "Add to Contacts" icon placement, immediately right of a
   location entity's title, wherever that title lives outside a .field-row. */
.itin-title .nav-icon-btn {
    width: 1.8rem;
    height: 1.8rem;
    font-size: 0.9rem;
    vertical-align: middle;
    margin-left: 0.4rem;
}

.picker-icon {
    cursor: pointer;
    user-select: none;
}

.copy-btn { font-size: 1rem; }

.copy-toast {
    position: fixed;
    bottom: 5.5rem;
    left: 50%;
    transform: translateX(-50%) translateY(10px);
    background: var(--text);
    color: #fff;
    padding: 0.5rem 1rem;
    border-radius: 999px;
    font-size: 0.85rem;
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s ease;
    z-index: 40;
    pointer-events: none;
}

.copy-toast.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

.import-warning {
    background: #fbe2e0;
    color: #6b1f18;
    border: 1px solid var(--danger);
    border-radius: 8px;
    padding: 0.85rem 1rem;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.import-warning ul {
    margin: 0.5rem 0 0.25rem;
    padding-left: 1.25rem;
}

.import-warning li { margin-bottom: 0.3rem; }

.house-header { text-align: center; }

.house-photo-frame {
    position: relative;
    display: inline-block;
    max-width: 100%;
    margin: 0 auto;
}

.house-photo {
    max-width: 100%;
    max-height: 320px;
    border-radius: var(--radius);
    display: block;
    cursor: pointer;
}

.photo-controls {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    display: flex;
    gap: 0.4rem;
}

.photo-controls form { margin: 0; }

.photo-control-btn {
    width: calc(1.9rem * var(--overlay-icon-size, 1));
    height: calc(1.9rem * var(--overlay-icon-size, 1));
    border-radius: 50%;
    border: none;
    background: rgba(0,0,0,0.35);
    color: #fff;
    font-size: calc(0.9rem * var(--overlay-icon-size, 1));
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    margin: 0;
    padding: 0;
    opacity: var(--fab-idle-opacity, 0.2);
    transition: opacity 0.15s ease, background 0.15s ease;
}

.photo-control-btn:hover, .photo-control-btn:focus-visible, .photo-control-btn:active {
    background: rgba(0,0,0,0.65);
    opacity: 1;
}

.photo-upload-widget {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 2rem 1rem;
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    background: var(--bg);
}

.photo-upload-icon { font-size: 2rem; }

.photo-modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.85);
    z-index: var(--z-overlay);
    align-items: center;
    justify-content: center;
    padding: 2rem;
}

.photo-modal.open { display: flex; }

.photo-modal img {
    max-width: 100%;
    max-height: 100%;
    border-radius: 8px;
}

.photo-modal-close {
    position: fixed;
    top: 1rem;
    right: 1.25rem;
    background: rgba(255,255,255,0.15);
    color: #fff;
    border: none;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    font-size: 1.3rem;
    cursor: pointer;
}

/* Reusable alternating-row tint for any settings section with more than
   one line item (table rows, or a container of row-shaped elements) — see
   DEV_LESSONS.md's compact-settings-sections rule. `.zebra-rows` is the
   general-purpose form: put this class on the row elements' container and
   every other direct child of a given tag gets tinted. Deliberately
   `:nth-of-type` rather than `:nth-child` — a container can have other
   non-row children interleaved among the rows (e.g. a `<form>` with hidden
   `<input>`s before its `<label>` rows, or a "Select All" toolbar a JS
   helper injects at runtime) that would throw off a plain child-index
   count without actually being a row themselves; `:nth-of-type` only
   counts same-tag siblings, so it stays correct regardless. #page-header-table
   predates that generalization and targets <tr> directly at the same
   tint values; its pinned header row gets a distinctly stronger tint to
   set it apart as behaving differently from the rest of the table. */
.zebra-rows > *:nth-of-type(even),
#page-header-table tbody tr:nth-of-type(even) { background: rgba(0,0,0,0.06); }
#page-header-table tbody tr.page-header-default-row { background: rgba(0,0,0,0.12); }

.ph-apply-btn {
    padding: 0.36rem 0.27rem;
    font-size: 0.6rem;
    border-radius: 5px;
    margin-top: 0;
    line-height: 1;
}

.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.6);
    z-index: var(--z-overlay);
    display: flex;
    /* flex-start (not center) deliberately: centering a flex item taller
       than its container clips the top with no reliable cross-browser way
       to scroll back to it (align-items:center + overflow is an old,
       inconsistently-supported combination). Anchoring to the top instead
       guarantees the modal's top is visible the instant it opens and the
       rest is reachable by scrolling, on any content length or viewport
       size — the small cosmetic loss of true vertical centering on short
       modals is worth the reliability. */
    align-items: flex-start;
    justify-content: center;
    padding: 1.5rem;
    overflow-y: auto;
}

.modal-overlay[hidden] { display: none; }

.modal-box {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    max-width: 24rem;
    width: 100%;
    text-align: center;
    position: relative;
}

.modal-close {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    background: none;
    border: none;
    font-size: 1.1rem;
    cursor: pointer;
    color: var(--muted);
}

.avatar-card-trigger {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
}

.switch-entry-form {
    display: flex;
    align-items: flex-end;
    gap: 0.6rem;
    flex-wrap: wrap;
}

.switch-entry-form select { width: auto; flex: 1; min-width: 160px; }
.switch-entry-form button { margin-top: 0; }

.poi-tag-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 1rem;
}

.poi-tag-btn {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0.35rem 0.8rem;
    font-size: 0.85rem;
    cursor: pointer;
    color: var(--text);
    margin: 0;
}

.poi-tag-btn.active {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

.icon-picker-btn {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    width: 2.4rem;
    height: 2.4rem;
    padding: 0;
    margin: 0;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

/* Fixed (not absolute) and JS-positioned right next to the button that
   opened it (see positionPanel() in icon-picker.js) — escapes any
   ancestor's stacking context by construction, and keeps mouse travel to
   the button short instead of anchoring to the edge of whatever wide
   flex row the button happens to sit in. */
.icon-picker-panel {
    position: fixed;
    z-index: var(--z-overlay);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.5rem;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    width: 18rem;
    max-height: 20rem;
    overflow-y: auto;
}

.icon-picker-assign-banner {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--accent-dark);
    background: var(--bg);
    border: 1px dashed var(--accent);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
    margin-bottom: 0.5rem;
}

.icon-picker-settings {
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.icon-picker-settings label {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.75rem;
    color: var(--muted);
    font-weight: normal;
}

.icon-picker-settings input[type=number] {
    width: 3rem;
    padding: 0.2rem;
    border: 1px solid var(--border);
    border-radius: 4px;
}

.icon-picker-category {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    opacity: 0.6;
    margin: 0.4rem 0 0.15rem;
}

.icon-picker-category:first-child {
    margin-top: 0;
}

.icon-picker-grid {
    display: grid;
    grid-template-columns: repeat(8, 1.8rem);
    gap: 0.2rem;
}

.icon-picker-emoji {
    background: none;
    border: none;
    font-size: 1.2rem;
    cursor: pointer;
    border-radius: 4px;
    padding: 0.15rem;
    line-height: 1.4;
}

.icon-picker-emoji:hover {
    background: var(--border);
}

/* Always-visible strip of recently-used emoji next to the picker button —
   a shortcut for "same icon as last time" without opening the full panel.
   Hidden entirely (via [hidden], set in JS) until there's at least one
   recently-used emoji to show. */
.icon-picker-quickbar {
    display: inline-flex;
    align-items: center;
    gap: 0.1rem;
    flex-shrink: 0;
}

.icon-picker-quickbar .icon-picker-emoji {
    font-size: 1.1rem;
}

/* The same fixed slots, mirrored at the top of the panel — a "shelf" so
   they read as a distinct, always-there row rather than just another
   category grid. */
.icon-picker-quickbar-shelf {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem;
    background: var(--border);
    border-radius: 6px;
    padding: 0.3rem;
    margin-bottom: 0.4rem;
}

/* Attractions & Food title row — the shared .entry-banner model does the
   work; this only adds the spacing that separates it from the tags/links
   below it. */
.poi-banner {
    margin-bottom: 0.4rem;
}

/* Favorite Places reuses .field-row for its title line, which carries a
   divider of its own — redundant once the row is a filled banner. */
.field-row.entry-banner {
    border-bottom: none;
}

/* Leasing Company (tools.php) and Grocery Shopper Service (list.php).
   Unlike the other sections these are single-current-entry cards rather
   than lists, so the banner is the first thing inside the card body and
   the field rows follow it directly — hence a little breathing room on
   both sides rather than just below. Everything else (icon sizing,
   padding, radius, the theme-following unset color) comes from the shared
   .entry-banner model; do not re-specify it here. */
.leasing-banner,
.grocery-service-banner {
    margin: 0.25rem 0 0.6rem;
}

.poi-card {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
}

.poi-card:last-child { border-bottom: none; }

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

.poi-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin: 0.3rem 0;
}

.house-name {
    margin: 0 0 1rem;
}

.edit-toggle {
    margin-top: 1rem;
    border-top: 1px solid var(--border);
    padding-top: 0.75rem;
}

.edit-toggle summary {
    cursor: pointer;
    color: var(--accent-dark);
    font-weight: 600;
    list-style: none;
    display: inline-block;
}

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

.edit-toggle-body {
    margin-top: 0.75rem;
}

/* A tag/filter toggle — visually distinct from .edit-toggle's plain text
   link (used for "add new" affordances): a small bordered button so it
   reads as its own control, separating it from whatever list/section it
   precedes rather than blending into body text. */
.filter-toggle {
    margin: 0;
    border: none;
    padding: 0;
    /* Anchors the expanded panel below, as a floating dropdown, instead of
       normal in-flow expansion — the summary/pill itself never moves when
       toggled, only the panel appears/disappears beneath it. */
    position: relative;
}

.filter-toggle summary {
    cursor: pointer;
    color: var(--accent-dark);
    font-size: 0.85rem;
    font-weight: 600;
    list-style: none;
    display: inline-block;
    border: 1px solid var(--accent-dark);
    border-radius: 999px;
    padding: 0.25rem 0.75rem;
}

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

.filter-toggle .edit-toggle-body {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0;
    z-index: 5;
    min-width: 100%;
    width: max-content;
    max-width: min(28rem, 90vw);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.75rem;
    box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}

/* A section's display-only controls (sort, filter) share one row when
   there's room, instead of each stacking on its own line. */
.section-controls-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: center;
    margin-bottom: 0.75rem;
}

.house-edit-toggle {
    margin-top: 0;
    border-top: none;
    padding-top: 0;
}

.house-edit-toggle .floating-edit-btn {
    position: absolute;
    bottom: 0.75rem;
    right: 0.75rem;
    width: 2.4rem;
    height: 2.4rem;
    border-radius: 50%;
    background: var(--bg);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
}

.house-edit-toggle .floating-edit-btn:hover { background: var(--border); }

/* Deliberately stays position:absolute even when open, so the button
   never jumps to a different spot — it just tracks the (now taller)
   card's corner, letting the user click the same spot again to close. */

.item-edit-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 0.95rem;
    padding: 0.2rem 0.4rem;
    margin: 0;
    color: var(--muted);
}

.item-edit-btn:hover { color: var(--accent-dark); }

.item-actions {
    display: flex;
    gap: 0.25rem;
    align-items: flex-start;
}

.inline-edit-form {
    display: none;
    margin-top: 0.6rem;
    padding-top: 0.6rem;
    border-top: 1px dashed var(--border);
    width: 100%;
    flex-basis: 100%;
}

.inline-edit-form.open { display: block; }

.payment-amount {
    display: inline-block;
    padding: 0.3rem 0.75rem;
    border-radius: 8px;
    border: 2px solid;
    font-weight: 700;
}

.payment-amount.unpaid {
    border-color: var(--danger);
    color: var(--danger);
    background: rgba(179,65,58,0.08);
}

.payment-amount.paid {
    border-color: #2f9e44;
    color: #2f9e44;
    background: rgba(47,158,68,0.08);
}

.default-highlight {
    background: var(--accent-soft);
    border-color: var(--accent);
}

.card-collapsible > summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.card-collapsible > summary::-webkit-details-marker { display: none; }

.card-collapsible > summary::after {
    content: "▾";
    float: right;
    color: var(--muted);
    transition: transform 0.15s ease;
}

.card-collapsible[open] > summary::after {
    transform: rotate(180deg);
}

.card-collapsible-body {
    margin-top: 0.5rem;
}

.section-sort-toolbar {
    margin-bottom: 0.5rem;
    font-size: 0.9rem;
}

.drag-handle {
    cursor: grab;
    color: var(--muted);
    margin-right: 0.5rem;
    touch-action: none;
    user-select: none;
}

.sortable-sections > [data-sort-id].dragging {
    opacity: 0.5;
}

.sortable-grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-columns, 2), 1fr);
    gap: 1rem;
}

.sortable-grid .drag-handle {
    display: block;
    margin-bottom: 0.3rem;
}

/* Easter Eggs admin: Sound library list. .sound-row is the sortable-
   sections drag-item — section-sort.js inserts its .drag-handle as this
   element's literal first child, so the row itself is the flex container
   (handle + everything else) and .sound-row-body holds the actual two
   display rows (name line, then player+delete line) so the handle sits
   beside both rather than only the first. */
.sound-library-divider {
    border-top: 2px solid var(--border);
    margin: 1rem 0 0.75rem;
}

.sound-row {
    display: flex;
    align-items: flex-start;
    padding: 0.5rem 0.4rem;
}

.sound-row-body { flex: 1; min-width: 0; }

.sound-row-title {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.sound-row-num {
    font-weight: 700;
    color: var(--muted);
    flex-shrink: 0;
}

.sound-row-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.3rem;
}

/* A fixed width (rather than the browser default ~300px) is what makes
   every row's player line up as a visual column, and is also just
   "slightly smaller" per the ask. */
.sound-row-controls audio {
    width: 13rem;
    height: 2rem;
}

.sound-row-controls form { margin-left: auto; }

.sound-delete-btn {
    flex-shrink: 0;
    width: 1.8rem;
    height: 1.8rem;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: none;
    background: var(--danger);
    color: #fff;
    font-size: 0.85rem;
    line-height: 1;
    cursor: pointer;
    margin: 0;
}

.sound-delete-btn:hover { opacity: 0.85; }

.sprite-row-thumb {
    width: 1.6rem;
    height: 1.6rem;
    object-fit: contain;
    border-radius: 4px;
    background: var(--bg);
    flex-shrink: 0;
}

/* Theme-tag filter chips (above the Fire-a-sprite/Sound selects) and the
   matching checkbox row on the upload forms — same visual language,
   different context (filtering vs. assigning). */
.egg-tag-filter, .egg-tag-checks {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.3rem 0 0.6rem;
}

.egg-tag-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-weight: normal;
    font-size: 0.82rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0.25rem 0.6rem;
    cursor: pointer;
}

.egg-tag-chip input { width: auto; margin: 0; }

.egg-tag-badge {
    font-size: 0.72rem;
    background: var(--accent-soft);
    color: var(--accent-dark);
    border-radius: 999px;
    padding: 0.1rem 0.5rem;
}

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

.member-status { font-weight: 600; }
.member-status.status-active { color: #2e7d32; }
.member-status.status-invited { color: #b8722c; }

.member-action-btn {
    padding: 0.35rem 0.6rem;
    font-size: 0.8rem;
    margin-top: 0.3rem;
}

.admin-tool-tile { border-left-width: 8px; }
.admin-tool-tile.tile-orange { border-left-color: #d99a5b; }
.admin-tool-tile.tile-yellow { border-left-color: #cbb454; }
.admin-tool-tile.tile-blue { border-left-color: #6f9bb8; }
.admin-tool-tile.tile-purple { border-left-color: #a488b8; }
.admin-tool-tile.tile-green { border-left-color: #7fab7f; }

.weather-strip {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.weather-day {
    text-align: center;
    flex: 1;
    min-width: 3.5rem;
    display: block;
    color: inherit;
    text-decoration: none;
    border-radius: 8px;
    padding: 0.2rem;
}

a.weather-day:hover {
    background: var(--border);
}

.weather-date {
    font-size: 0.75rem;
    color: var(--muted);
    text-transform: uppercase;
}

.weather-icon { font-size: 1.6rem; margin: 0.2rem 0; }
.weather-temps { font-size: 0.85rem; }

.avatar-cloud {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1.5rem;
    padding: 1.5rem 0;
}

.avatar-cloud-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    animation-name: avatar-float;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
}

.avatar-cloud-item .avatar {
    width: 4rem;
    height: 4rem;
}

.avatar-cloud-name {
    font-size: 0.75rem;
    color: var(--muted);
    text-align: center;
}

@keyframes avatar-float {
    0%, 100% { transform: translateY(0) scale(1); }
    50% { transform: translateY(-10px) scale(1.05); }
}

@media (prefers-reduced-motion: reduce) {
    .avatar-cloud-item { animation: none; }
}

/* Physics-style avatar cloud (opt-in alternative to the CSS keyframe
   float above): JS switches the container into free positioning and
   drives each item's transform every frame. Respects
   prefers-reduced-motion itself (avatar-cloud-physics.js no-ops and
   this container never gets .physics-active, leaving the static classic
   layout above — already covered by the reduced-motion block above). */
.avatar-cloud.physics-active {
    position: relative;
    display: block;
    overflow: hidden;
}

.avatar-cloud.physics-active .avatar-cloud-item {
    position: absolute;
    top: 0;
    left: 0;
    margin: 0;
    animation: none;
    width: 5.75rem;
    text-align: center;
}

/* Static: same flex layout as Classic/Bounce, just with the float
   keyframe turned off. */
.avatar-cloud.static-active .avatar-cloud-item {
    animation: none;
}

/* A brief label confirming which cloud-animation style was just picked —
   appears above the toggle button, holds, then slides up and fades out. */
.avatar-cloud-toast {
    position: absolute;
    transform: translate(-50%, -100%);
    margin-top: -0.5rem;
    background: var(--text);
    color: var(--surface);
    padding: 0.4rem 0.9rem;
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    pointer-events: none;
    z-index: 60;
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.avatar-cloud-toast.visible {
    opacity: 1;
    transform: translate(-50%, -100%);
}

.avatar-cloud-toast.leaving {
    opacity: 0;
    transform: translate(-50%, -160%);
    transition: opacity 0.4s ease, transform 0.4s ease;
}

.countdown-banner {
    background: var(--accent);
    color: #fff;
    text-align: center;
    font-weight: 700;
    font-size: 1.1rem;
    /* Extra vertical padding so the text (one line on desktop, two on
       mobile) isn't flush against the container edges. */
    padding: 0.85rem 1rem;
    /* Admin-configurable leading — see "Countdown Text Spacing" in
       admin-styling.php. Tightens the gap between the two mobile
       fragments; the fallback matches the shipped default. */
    line-height: var(--countdown-line-height, 0.72);
    border-radius: var(--radius);
    margin-bottom: var(--home-banner-gap, 5px);
    white-space: pre-wrap;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
}

/* On mobile the two countdown sentence fragments (see index.php) are
   forced onto exactly two clean lines instead of relying on natural text
   wrap, which produced an orphan word / misaligned parenthetical. */
@media (max-width: 480px) {
    .countdown-frag {
        display: block;
    }
}

.countdown-banner-party {
    min-height: 220px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background: linear-gradient(135deg, var(--accent), var(--accent-dark));
    font-size: 1.5rem;
}

.countdown-banner-text {
    position: relative;
    z-index: 2;
    text-shadow: 0 1px 6px rgba(0,0,0,0.35);
}

.fireworks-container {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 1;
}

.firework-particle {
    position: absolute;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    left: var(--fx, 50%);
    top: var(--fy, 50%);
    background: var(--fcolor, #fff);
    animation: firework-burst var(--fdur, 1.1s) ease-out forwards;
    box-shadow: 0 0 6px 1px var(--fcolor, #fff);
}

@keyframes firework-burst {
    0% {
        transform: translate(0, 0) scale(1);
        opacity: 1;
    }
    70% {
        opacity: 1;
    }
    100% {
        transform: translate(var(--fdx, 0), var(--fdy, 0)) scale(0.3);
        opacity: 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .firework-particle { animation: none; opacity: 0; }
}

.inline-label-edit {
    display: inline-block;
    width: auto;
    max-width: 8rem;
    border: none;
    border-bottom: 1px dashed var(--border);
    background: transparent;
    padding: 0 0.15rem;
    font: inherit;
    font-weight: 600;
    color: inherit;
}

.inline-label-edit:focus { border-bottom-color: var(--accent); outline: none; }

.impersonation-banner {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    background: #3d2a5c;
    color: #fff;
    padding: 0.5rem 1rem;
    font-size: 0.9rem;
}

.impersonation-banner .secondary {
    background: transparent;
    color: #fff;
    border: 1px solid #fff;
}

.dial-confirm-label {
    font-weight: 600;
    font-size: 1.05rem;
}

.dial-confirm-slider {
    width: 100%;
    margin-top: 1rem;
    accent-color: var(--danger, #c0392b);
    height: 2.25rem;
}

.emergency-contact-row {
    padding: 0.9rem 0;
    margin-bottom: 0.5rem;
    border-bottom: 1px solid var(--border);
}

.emergency-contact-row:last-of-type { border-bottom: none; margin-bottom: 0; }

.ec-line1 {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-weight: 600;
    flex-wrap: wrap;
}

.ec-line2 {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.5rem;
    margin-top: 0.3rem;
}

.ec-col-phone { min-width: 11rem; }
.ec-col-website { min-width: 8rem; }

.ec-line3 {
    margin-top: 0.3rem;
    font-size: 0.85rem;
}

.gallery-remove-form {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    z-index: 2;
    margin: 0;
}

.gallery-remove-x {
    width: calc(1.35rem * var(--overlay-icon-size, 1));
    height: calc(1.35rem * var(--overlay-icon-size, 1));
    padding: 0;
    margin: 0;
    border-radius: 50%;
    background: rgba(0,0,0,0.55);
    color: #fff;
    font-size: calc(0.7rem * var(--overlay-icon-size, 1));
    line-height: 1;
    opacity: var(--fab-idle-opacity, 0.2);
    transition: opacity 0.15s ease;
}

.gallery-remove-x:hover, .gallery-remove-x:focus-visible, .gallery-remove-x:active {
    opacity: 1;
}

.upload-progress-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

.upload-progress-box {
    background: rgba(0,0,0,0.75);
    color: #fff;
    padding: 1rem 1.5rem;
    border-radius: 12px;
    text-align: center;
    font-size: 0.9rem;
}

.upload-spinner {
    width: 28px;
    height: 28px;
    margin: 0 auto 0.5rem;
    border: 3px solid rgba(255,255,255,0.3);
    border-top-color: #fff;
    border-radius: 50%;
    animation: upload-spin 0.8s linear infinite;
}

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

/* Deliberately NOT var(--accent-dark) — that varies per site theme/
   per-user override and was landing too close to the page background in
   some combinations, making the footer link hard to read. A fixed, always-
   dark background paired with fixed white text guarantees contrast
   regardless of theme. */
.site-footer {
    width: 100%;
    max-width: 720px;
    /* NEVER let flex compress the footer. This used to be load-bearing:
       `html, body { height: 100% }` (line ~96, now min-height) gave <body>
       — a column flex container — a *definite* height of exactly 100vh, so
       every page taller than the viewport had NEGATIVE free space and flex
       shrank items to absorb it. main.page
       is `flex: 1 0 auto` (shrink 0) and so is immune; the footer's only
       protection was its automatic minimum size (`min-height: auto` =
       content height), which any `overflow` other than visible silently
       resolves to 0. That is exactly how the shimmer effect broke the
       footer — see .footer-shimmer below. flex-shrink: 0 makes the floor
       explicit instead of incidental. */
    flex-shrink: 0;
    margin: 2rem auto 0;
    padding: 0.6rem 1rem;
    box-sizing: border-box;
    text-align: center;
    font-size: 0.85rem;
    /* background/color come from an inline style (app/includes/footer.php)
       driven by the admin-configurable Footer Colors setting — never
       hardcode a color here, since the whole point is it must stay
       readable against whatever background the admin picks. */
}

/* Optional "shimmering water" footer effect (admin toggle:
   footer_shimmer_enabled, plus Speed / Zoom / Contrast / Erraticism in
   Site Styling -> Footer Colors).

   DESIGN DECISION (2026-09-15, see HANDOFF -28): this is a **luminance
   overlay**, not a pair of independently-coloured bands. Every stop is
   either rgba(255,255,255,a) or rgba(0,0,0,a) on top of whatever
   background colour the admin picked, so the shimmer lightens and darkens
   the real footer colour instead of painting colours of its own. That is
   why there are no light-colour/dark-colour settings: a single "contrast"
   amount is enough, and the effect cannot clash with a future footer
   colour or theme.

   GEOMETRY WARNING -- read before changing `inset`. The old version used
   an oversized layer (`inset: -50% -20%` plus a translateY in the
   keyframe) that hung ~20px below the footer. That overhang is
   *scrollable overflow* of <body>/<html>, and `clip-path` clips PAINT
   ONLY -- it does not suppress scrollable overflow the way the
   `overflow: hidden` it replaced did. Result: the document became ~20-34px
   taller than its content and the footer "floated" that far above the
   bottom of the screen at full scroll. Both layers are now `inset: 0`
   with NO transform, and all motion is background-position on oversized
   repeating gradients, so the effect contributes exactly zero overflow in
   either direction. Do not give these pseudo-elements a negative inset, a
   transform, or `overflow: hidden` on the footer.

   Two pseudo-elements rather than one because a single element can only
   carry one animation *period*; ::before and ::after run at different
   durations derived from --fs-erratic, and their beat against each other
   is what makes the motion read as organic rather than mechanical. */
.site-footer.footer-shimmer {
    position: relative;
    isolation: isolate;
    /* Fallbacks only -- app/includes/footer.php overrides all five inline
       from get_setting(). --fs-phase is re-randomised per page load so two
       visitors are never in the same place in the cycle. */
    --fs-speed: 18s;
    --fs-zoom: 90px;
    --fs-contrast: 0.24;
    --fs-erratic: 0.5;
    --fs-phase: 0;
    /* Paint-only safety net: costs nothing, creates no scroll container,
       and keeps the gradients inside the footer box if it ever gets a
       border-radius. NEVER use overflow: hidden here -- it resolves the
       footer's flex automatic minimum size to 0 and lets a column-flex
       <body> crush it (the original shimmer bug, HANDOFF -27). */
    clip-path: inset(0);
}

.site-footer.footer-shimmer::before,
.site-footer.footer-shimmer::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-repeat: repeat;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    animation-direction: alternate;
}

/* Coarse layer. Each gradient runs white -> nothing -> black -> nothing,
   so one pass gives both a highlight and a shadow; two gradients at
   different angles cross into patches instead of stripes. */
.site-footer.footer-shimmer::before {
    background-image:
        repeating-linear-gradient(
            calc(115deg + var(--fs-erratic) * 25deg),
            rgba(255, 255, 255, var(--fs-contrast)) 0%,
            rgba(255, 255, 255, 0) 26%,
            rgba(0, 0, 0, 0) 30%,
            rgba(0, 0, 0, var(--fs-contrast)) 50%,
            rgba(0, 0, 0, 0) 70%,
            rgba(255, 255, 255, 0) 74%,
            rgba(255, 255, 255, var(--fs-contrast)) 100%),
        repeating-linear-gradient(
            calc(-38deg - var(--fs-erratic) * 30deg),
            rgba(0, 0, 0, var(--fs-contrast)) 0%,
            rgba(0, 0, 0, 0) 24%,
            rgba(255, 255, 255, 0) 28%,
            rgba(255, 255, 255, var(--fs-contrast)) 50%,
            rgba(255, 255, 255, 0) 72%,
            rgba(0, 0, 0, 0) 76%,
            rgba(0, 0, 0, var(--fs-contrast)) 100%);
    /* The vertical tile is deliberately SHORTER than the horizontal one.
       The footer is ~40px tall and ~720px wide, so a tall tile shows only a
       thin horizontal slice of itself and the pattern degenerates into
       flat vertical bands. Keeping the y-extent near the footer's own
       height is what makes it read as mottled water. */
    background-size:
        var(--fs-zoom) calc(var(--fs-zoom) * 0.85),
        calc(var(--fs-zoom) * 1.37) calc(var(--fs-zoom) * 0.62);
    animation-name: footer-shimmer-a;
    animation-duration: calc(var(--fs-speed) * (1 + var(--fs-erratic) * 0.35));
    animation-delay: calc(var(--fs-speed) * var(--fs-phase) * -1);
}

/* Fine layer -- ~0.6x the patch size and a different period, so the two
   drift in and out of phase. */
.site-footer.footer-shimmer::after {
    background-image:
        repeating-linear-gradient(
            calc(72deg - var(--fs-erratic) * 34deg),
            rgba(255, 255, 255, var(--fs-contrast)) 0%,
            rgba(255, 255, 255, 0) 22%,
            rgba(0, 0, 0, 0) 32%,
            rgba(0, 0, 0, var(--fs-contrast)) 50%,
            rgba(0, 0, 0, 0) 68%,
            rgba(255, 255, 255, 0) 78%,
            rgba(255, 255, 255, var(--fs-contrast)) 100%),
        repeating-linear-gradient(
            calc(160deg + var(--fs-erratic) * 20deg),
            rgba(0, 0, 0, var(--fs-contrast)) 0%,
            rgba(0, 0, 0, 0) 30%,
            rgba(255, 255, 255, 0) 38%,
            rgba(255, 255, 255, var(--fs-contrast)) 50%,
            rgba(255, 255, 255, 0) 62%,
            rgba(0, 0, 0, 0) 70%,
            rgba(0, 0, 0, var(--fs-contrast)) 100%);
    background-size:
        calc(var(--fs-zoom) * 0.61) calc(var(--fs-zoom) * 0.47),
        calc(var(--fs-zoom) * 0.88) calc(var(--fs-zoom) * 0.33);
    animation-name: footer-shimmer-b;
    animation-duration: calc(var(--fs-speed) * (1 - var(--fs-erratic) * 0.42));
    animation-delay: calc(var(--fs-speed) * var(--fs-phase) * -1.7);
}

/* Motion is background-position ONLY -- never transform, never a change of
   box geometry (see the geometry warning above). */
@keyframes footer-shimmer-a {
    0%   { background-position: 0 0, 0 0; }
    100% { background-position: 340px -120px, -260px 90px; }
}

@keyframes footer-shimmer-b {
    0%   { background-position: 0 0, 0 0; }
    100% { background-position: -300px 140px, 220px -80px; }
}

/* Same treatment as the avatar-float keyframe: no motion for viewers who
   ask for reduced motion -- the shimmer layers stay as a flat, static
   pattern rather than disappearing entirely. */
@media (prefers-reduced-motion: reduce) {
    .site-footer.footer-shimmer::before {
        animation: none;
        background-position: 120px -40px, -90px 30px;
    }
    .site-footer.footer-shimmer::after {
        animation: none;
        background-position: -110px 50px, 80px -30px;
    }
}

.site-footer a { color: inherit; text-decoration: none; font-weight: 700; }

.site-footer-sep { color: inherit; opacity: 0.6; }

.gallery-quick-actions {
    position: absolute;
    bottom: 1rem;
    right: 1rem;
    display: flex;
    gap: 0.5rem;
}

.gallery-fab, .floating-icon-btn {
    width: calc(2.75rem * var(--overlay-icon-size, 1));
    height: calc(2.75rem * var(--overlay-icon-size, 1));
    border-radius: 50%;
    padding: 0;
    font-size: calc(1.3rem * var(--overlay-icon-size, 1));
    line-height: 1;
    box-shadow: 0 2px 8px rgba(0,0,0,0.25);
    /* Faint by default (admin-configurable) so it doesn't cover the text
       it overlaps — full opacity on hover/focus/touch-active still makes
       clear a button is there and where to tap. */
    opacity: var(--fab-idle-opacity, 0.2);
    transition: opacity 0.15s ease;
}

.gallery-fab:hover, .gallery-fab:focus-visible, .gallery-fab:active,
.floating-icon-btn:hover, .floating-icon-btn:focus-visible, .floating-icon-btn:active {
    opacity: 1;
}

/* A small icon-only control pinned to the bottom-right corner of whatever
   positioned ancestor it lives in (that ancestor needs position:relative),
   for a section-scoped control that shouldn't compete with the section's
   main content for visual weight. */
.floating-icon-btn {
    position: absolute;
    bottom: 0.75rem;
    right: 0.75rem;
    z-index: 2;
}

/* Weather widgets: one markup, two views.
   Every widget (the Home page's primary one, the Weather tool's primary
   one, and every clone on either page) renders the SAME full-detail
   markup via weather_strip_html(); "compact" is purely this class hiding
   parts of it. That is what lets public/js/widget-view-toggle.js flip an
   instance instantly, with no reload and no refetch -- and it is why the
   PHP must never render the extra rows conditionally. */
.weather-widget {
    /* The floating toggle is position:absolute; this is the ancestor it
       anchors to. */
    position: relative;
}

.weather-widget .weather-date-short { display: none; }

.weather-widget.weather-compact .weather-date-full { display: none; }
.weather-widget.weather-compact .weather-date-short { display: inline; }
.weather-widget.weather-compact .weather-full-only { display: none; }

/* The toggle sits over the bottom-right of the widget. Bumped up a little
   from .floating-icon-btn's default so it clears the Remove/Unpin button
   that sits under a cloned widget. */
.weather-view-toggle {
    bottom: 0.4rem;
    right: 0.4rem;
}

:root {
    --gallery-thumb-size: 9rem;
}

.gallery-view-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.5rem;
    align-items: center;
    font-size: 0.85rem;
    padding: 0.9rem 1rem;
}

.gallery-view-controls label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-weight: normal;
    margin: 0;
}

.gallery-view-controls input[type="range"] {
    width: 7rem;
}

.gallery-sort-toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
    margin-bottom: 0.6rem;
}

.gallery-sort-toolbar select {
    width: auto;
    display: inline-block;
}

.gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--gallery-thumb-size), 1fr));
    gap: 1rem;
}

/* Masonry: CSS columns keep it dependency-free — tiles flow into
   balanced columns instead of a fixed grid, natural for mixed-aspect
   photos once "true aspect ratio" is also on. */
.gallery-grid.masonry {
    display: block;
    column-width: var(--gallery-thumb-size);
    column-gap: 1rem;
}

.gallery-grid.masonry .gallery-tile {
    display: inline-block;
    width: 100%;
    break-inside: avoid;
    margin-bottom: 1rem;
}

.gallery-tile {
    text-align: center;
}


.gallery-tile img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--border);
}

.gallery-grid.true-aspect .gallery-tile img {
    aspect-ratio: auto;
    object-fit: contain;
    background: var(--surface);
}

.gallery-caption {
    font-size: 0.85rem;
    margin-top: 0.25rem;
    line-height: 1.25;
}

.gallery-meta {
    font-size: 0.7rem;
    margin-top: 0.05rem;
}

.gallery-reactions {
    display: flex;
    gap: 0.25rem;
    justify-content: center;
    align-items: center;
    margin-top: 0.25rem;
    flex-wrap: nowrap;
}

.gallery-reactions form { margin: 0; }

.reaction-btn {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.1rem 0.35rem;
    margin-top: 0;
    font-size: 0.75rem;
    cursor: pointer;
    opacity: 0.6;
    white-space: nowrap;
}

.reaction-btn.active {
    opacity: 1;
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 15%, var(--surface));
}

.gallery-tile[data-is-my-fave="0"].fave-filter-hidden {
    display: none;
}

.gallery-tile img { cursor: zoom-in; }

body.lightbox-open { overflow: hidden; }

.lightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    background: rgba(0,0,0,0.92);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.lightbox-stage {
    flex: 1;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    padding: 3.5rem 1rem;
    box-sizing: border-box;
}

.lightbox-img {
    max-width: 92vw;
    max-height: 78vh;
    object-fit: contain;
    cursor: zoom-in;
    transition: transform 0.15s ease;
    user-select: none;
}

.lightbox-zoomed .lightbox-stage {
    overflow: auto;
    align-items: flex-start;
    justify-content: flex-start;
    padding: 0;
}

.lightbox-img.zoomed {
    max-width: none;
    max-height: none;
    width: auto;
    height: 200vh;
    cursor: zoom-out;
}

.lightbox-close {
    position: absolute;
    top: 0.75rem;
    right: 0.9rem;
    z-index: 9002;
    background: rgba(255,255,255,0.12);
    color: #fff;
    border: none;
    border-radius: 50%;
    width: 2.5rem;
    height: 2.5rem;
    font-size: 1.2rem;
    cursor: pointer;
}

.lightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 9001;
    background: rgba(255,255,255,0.12);
    color: #fff;
    border: none;
    border-radius: 50%;
    width: 2.75rem;
    height: 2.75rem;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
}

.lightbox-prev { left: 0.6rem; }
.lightbox-next { right: 0.6rem; }

.lightbox-bar {
    position: absolute;
    bottom: 0.75rem;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 0.75rem;
    background: rgba(0,0,0,0.5);
    padding: 0.4rem 0.9rem;
    border-radius: 999px;
    color: #fff;
    z-index: 9001;
}

.lightbox-react {
    background: transparent;
    border: 1px solid rgba(255,255,255,0.4);
    color: #fff;
    border-radius: 999px;
    padding: 0.15rem 0.6rem;
    font-size: 0.85rem;
    cursor: pointer;
    opacity: 0.75;
}

.lightbox-react.active {
    opacity: 1;
    border-color: #fff;
    background: rgba(255,255,255,0.18);
}

.lightbox-index {
    font-size: 0.8rem;
    opacity: 0.85;
}

@media (max-width: 600px) {
    .lightbox-nav { width: 2.25rem; height: 2.25rem; font-size: 1.3rem; }
    .lightbox-stage { padding: 3rem 0.25rem; }
}

.gallery-slider {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 6;
    overflow: hidden;
    border-radius: var(--radius);
}

.gallery-slide {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 0.6s ease;
}

.gallery-slide.active {
    opacity: 1;
}

.gallery-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.gallery-slide-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.5);
    color: #fff;
    padding: 0.4rem 0.75rem;
    font-size: 0.85rem;
}

#favoriters-list .favoriter-row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.3rem 0;
    border-bottom: 1px solid var(--border);
    font-size: 0.9rem;
}

.checkbox-select-all-toolbar {
    margin-bottom: 0.5rem;
    font-size: 0.85rem;
}

.link-btn {
    background: none;
    border: none;
    padding: 0;
    color: var(--accent, #0a6);
    text-decoration: underline;
    cursor: pointer;
    font-size: inherit;
}

details.card-collapsible {
    position: relative;
}

/* Laid out as a flex sibling of the heading text inside .section-heading
   (see .section-heading above) rather than absolutely positioned, so it
   can never overlap the heading regardless of container width or text
   length. */
.expand-collapse-all-btn {
    margin: 0 0 0 auto;
    flex-shrink: 0;
    white-space: nowrap;
    font-size: 0.85rem;
    padding: 0.35rem 0.75rem;
    text-transform: none;
    letter-spacing: normal;
}

#back-to-top {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 500;
    padding: 0.55rem 0.9rem;
    border-radius: 999px;
    border: none;
    box-shadow: 0 2px 8px rgba(0,0,0,0.25);
    cursor: pointer;
}

/* Digital business card (Overview's avatar-click popup) — styled as a
   vertical business card rather than a generic modal form. */
.member-card {
    background: var(--accent-dark);
    color: #fff;
    max-width: 20rem;
    padding: 2rem 1.5rem 1.5rem;
}

.member-card .modal-close {
    color: #fff;
}

.member-card-avatar {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    border: 3px solid rgba(255,255,255,0.6);
    object-fit: cover;
    margin: 0 auto 0.75rem;
    display: block;
}

.member-card #member-card-name {
    color: #fff;
    margin: 0 0 1rem;
}

.member-card-cta {
    background: rgba(255,255,255,0.18);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.4);
    border-radius: 999px;
    padding: 0.6rem 1.5rem;
    font-weight: 700;
    margin-bottom: 1.25rem;
}

.member-card-cta:hover {
    background: rgba(255,255,255,0.3);
}

.member-card-fields, .member-card-fun {
    text-align: left;
}

/* This card's own .muted usages (the "hasn't shared any contact info"
   line, the fun-facts intro line) sit on a dark teal background — the
   site-wide .muted gray was tuned for a light surface and reads as very
   low contrast here. Always check/override contrast explicitly whenever
   a non-default background is introduced, rather than as a follow-up fix. */
#member-card-none, .member-card-fun p.muted {
    color: rgba(255,255,255,0.8) !important;
}

.member-card-row {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.55rem 0;
    border-top: 1px solid rgba(255,255,255,0.15);
}

.member-card-icon {
    width: 2rem;
    height: 2rem;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0,0,0,0.25);
    border-radius: 8px;
    font-size: 1rem;
    align-self: center;
}

/* No dedicated icon for fun-fact rows (see fieldRow() in
   overview-member-card.js) — a generic speech-bubble was tried and dropped:
   with the label column also fighting for room, an icon on every row left
   too little horizontal space for the (often long) value text. */

/* Favor the value column — usually the longer, more variable content —
   over the label column, letting the label wrap instead if it must. */
.member-card-label {
    color: rgba(255,255,255,0.75);
    font-size: 0.85rem;
    flex: 0 0 34%;
}

.member-card-value {
    flex: 1;
    min-width: 0;
    color: #fff;
    text-decoration: none;
    word-break: break-word;
    /* Preserve line breaks the member actually typed (e.g. a multi-line
       "anything else" answer) — textContent keeps literal newlines in the
       string, but default white-space collapses them visually unless told
       not to. */
    white-space: pre-wrap;
}

a.member-card-value:hover {
    text-decoration: underline;
}

.member-card-checkbox {
    width: auto;
    flex: none;
    margin: 0;
    align-self: center;
}

.member-card-fun {
    margin-top: 0.5rem;
}

/* Notes-length counter on the Add to Contacts card. Appears only once
   the selected fun facts have pushed the contact's Notes field past
   1000 characters, and then stays for the rest of the dialog session —
   see public/js/overview-member-card.js. Colors are explicit rather
   than inherited: this sits on the member card's dark surface, where
   the usual muted gray is too low-contrast to read. */
.member-card-notes-counter {
    margin: 0.5rem 0 0;
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
    font-weight: 600;
}

.member-card-notes-counter.over {
    color: #ffb4ab;
}

.member-card-notes-counter.ok {
    color: #9ee6b4;
}

.member-card-limit-warning {
    margin-top: 0.75rem;
    padding: 0.85rem;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.25);
    text-align: left;
}

.member-card-limit-warning p {
    margin: 0 0 0.5rem;
    font-size: 0.9rem;
}

.member-card-limit-warning p.muted {
    color: rgba(255, 255, 255, 0.82);
}

.member-card-limit-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.member-card-limit-actions button {
    flex: 1 1 8rem;
    margin: 0;
}

/* The shared .secondary button is a dark outline meant for the light page
   surface; on the member card's dark accent background it renders nearly
   invisible (dark text on dark). Restated here in the card's own terms —
   see the "design for contrast at implementation time" note in
   DEV_LESSONS.md, which this button walked straight into. */
.member-card-limit-actions .secondary {
    background: transparent;
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.6);
}

.member-card-limit-actions .secondary:hover {
    background: rgba(255, 255, 255, 0.14);
    color: #fff;
}

/* Gallery multi-select share */
.gallery-selecting .gallery-tile {
    cursor: pointer;
}

.gallery-tile.selected {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    border-radius: 8px;
}

.gallery-tile.selected::after {
    content: "✓";
    position: absolute;
    top: 0.4rem;
    left: 0.4rem;
    width: 1.5rem;
    height: 1.5rem;
    background: var(--accent);
    color: #fff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.85rem;
    z-index: 3;
}

.gallery-share-bar {
    position: sticky;
    top: 0;
    z-index: 50;
    background: var(--accent-dark);
    color: #fff;
    padding: 0.6rem 1rem;
    border-radius: var(--radius);
    margin-bottom: 1rem;
}

.gallery-share-instructions {
    margin: 0 0 0.5rem;
    font-size: 0.85rem;
    text-align: center;
}

.gallery-share-bar-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    justify-content: center;
}

.gallery-share-bar .muted {
    color: rgba(255,255,255,0.85) !important;
}

.home-banner {
    margin: 0 auto 1.5rem;
    max-width: 720px;
    border-radius: var(--radius);
    /* No overflow:hidden here — an inline box-shadow (admin-configurable,
       see css_shadow_value()) lives on this same element, and
       overflow:hidden would clip it off entirely since box-shadow paints
       just outside the border box. Rounding is handled by the image's
       own border-radius below instead. */
}

.home-banner img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius);
}

.how-to-video-list {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin: 0.75rem 0;
}

/* A compact ROW now, not a gallery tile — small thumbnail on the left,
   caption beside it on the same line (§-48, per the user's own explicit
   layout request: "easy to read list", not "huge rectangles"). Only
   .how-to-video-edit still gets its own full-width row below (see its
   flex-basis:100% further down) — the thumb+caption pair share one row via
   `align-items: center` so the caption is vertically centered next to a
   short, wide thumbnail. flex-wrap stays on: at very narrow widths (or a
   long caption) the caption still drops below the thumb rather than being
   crushed, which is the §-46 lesson this file already documents. */
.how-to-video-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.how-to-video-item.dragging {
    opacity: 0.5;
}

.how-to-video-item video {
    flex: 1;
    min-width: 0;
    max-width: 100%;
    border-radius: 8px;
    background: #000;
}

.how-to-video-item .drag-handle {
    cursor: grab;
    color: var(--muted);
    padding: 0.25rem;
    touch-action: none;
}

/* The How To list is a grid of poster tiles now, not inline players — see
   HANDOFF §-44. The tile and the caption are both real <button>s so they are
   keyboard-focusable and announced as controls, not decorative text. */
.how-to-video-thumb {
    position: relative;
    /* Fixed-ish small thumbnail, not a flexible gallery tile — a real
       `width`, not a `flex-basis`, so it doesn't stretch to fill the row;
       the caption takes the remaining space instead (see .how-to-video-caption
       below). flex-shrink:0 keeps it from being squeezed by a long caption;
       flex-wrap on the parent is still what sends the caption to its own
       line if there truly isn't room, rather than crushing the thumb. */
    width: 190px;
    flex: 0 0 190px;
    max-width: 100%;
    aspect-ratio: 16 / 9;
    padding: 0;
    margin: 0;
    border: 0;
    border-radius: 8px;
    overflow: hidden;
    background: #000;
    cursor: pointer;
    display: block;
}

@media (max-width: 640px) {
    .how-to-video-thumb {
        width: 150px;
        flex-basis: 150px;
    }
}

/* MUST be positioned, not static. The poster layer below it is
   position:absolute, and a positioned element paints above a later
   NON-positioned sibling regardless of DOM order (DEV_LESSONS.md) — a static
   <img> here would be hidden behind the placeholder it is supposed to cover.
   Both positioned with auto z-index, DOM order decides, and the image wins;
   .how-to-video-play comes last and stays on top of both. */
.how-to-video-thumb img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* EVERY tile gets this poster layer, always, underneath whatever image is
   (or isn't) available — see how_to_video_thumbnail_markup() and HANDOFF
   §-46. It is what guarantees a video never renders as a blank rectangle:
   an uploaded-file video with no custom thumbnail shows it outright, and a
   YouTube tile whose hqdefault.jpg is slow, blocked or missing falls back to
   it when the <img> removes itself on error. A custom upload only ever
   covers it — it is never what makes a thumbnail exist. */
.how-to-video-poster {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #2b2b33, #14141a);
}

.how-to-video-poster-icon {
    font-size: 2.75rem;
    line-height: 1;
    opacity: 0.55;
    /* Sits behind the ▶ badge, offset so both read as one "video" mark
       rather than overlapping into mush. */
    transform: translateY(-0.55rem);
}

/* Legacy: kept so an older cached page that still emits this class renders
   the same dark panel rather than a transparent tile. */
.how-to-video-thumb-empty {
    background: linear-gradient(135deg, #2b2b33, #14141a);
}

.how-to-subsection-delete {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}

.how-to-video-play {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2.5rem;
    line-height: 1;
    color: #fff;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
    background: rgba(0, 0, 0, 0.18);
    transition: background 0.15s ease, transform 0.15s ease;
}

.how-to-video-thumb:hover .how-to-video-play,
.how-to-video-thumb:focus-visible .how-to-video-play {
    background: rgba(0, 0, 0, 0.35);
    transform: scale(1.06);
}

.how-to-video-caption {
    /* Sits beside the thumbnail on the same row now, not on its own
       full-width line below it — see .how-to-video-item's row layout
       above. flex:1 lets it take whatever width the thumbnail doesn't. */
    flex: 1 1 10rem;
    min-width: 8rem;
    color: var(--text);
    font-size: 1.15rem;
    font-weight: 700;
    /* Reset the site's default button chrome — this reads as a caption, but
       it must behave (and focus) as the control it is. */
    background: none;
    border: 0;
    padding: 0;
    text-align: left;
    font-family: inherit;
    cursor: pointer;
}

/* Deliberately no text-decoration on hover/focus — the user explicitly
   asked for no underline even though this behaves like a link. Hover
   feedback is a color shift instead. */
.how-to-video-caption:hover,
.how-to-video-caption:focus-visible {
    color: var(--accent);
}

.how-to-video-edit {
    flex-basis: 100%;
}

/* Any container that gets a custom, admin/user-picked background color
   needs every control inside it (icon buttons, links, the delete "X",
   etc.) to switch to that same computed text color too — otherwise a
   fixed-color icon/button can become invisible or unreadable purely by
   coincidence of matching (or nearly matching) whatever background color
   was picked. Apply .tinted-banner alongside the inline background/color
   style ONLY when a custom color is actually set (leave the default,
   uncustomized look completely alone otherwise). */
.tinted-banner .nav-icon-btn,
.tinted-banner .item-edit-btn,
.tinted-banner .favorite-star-btn,
.tinted-banner button.secondary,
.tinted-banner .btn.secondary,
.tinted-banner a {
    color: inherit;
}

/* Match the plain, borderless pencil-icon (.item-edit-btn) look — no box/
   border/background chip, just the icon in the computed text color. The
   nav-icon-btn/favorite-star-btn/secondary-button classes normally DO
   have a border+background box (that's correct on their default,
   uncustomized surface), but a boxed control on top of a solid custom
   banner color reads as visually inconsistent/garish next to the plain
   pencil icon beside it, so strip that box treatment specifically here. */
.tinted-banner .nav-icon-btn,
.tinted-banner .favorite-star-btn,
.tinted-banner button.secondary,
.tinted-banner .btn.secondary {
    border: none;
    background: none;
    padding: 0.2rem 0.4rem;
}

/* The browser's own default :visited purple would otherwise ignore
   .tinted-banner's color:inherit (a same-specificity sibling rule loses
   to :visited's user-agent styles unless explicitly matched) — force it
   to track the same computed text color rather than risk an unreadable
   default purple on a dark custom background. */
.tinted-banner a:visited {
    color: inherit;
}

.tinted-banner .nav-icon-btn:hover,
.tinted-banner .item-edit-btn:hover,
.tinted-banner button.secondary:hover,
.tinted-banner .btn.secondary:hover,
.tinted-banner .favorite-star-btn:hover {
    opacity: 0.7;
}

.tinted-banner .favorite-star-btn.is-favorited {
    color: #ffd75e;
}

/* Non-link muted/secondary text elements that appear inside item rows
   with a custom background (itinerary time column, notes, tag badges) —
   these use var(--muted) by default, tuned for the default surface color,
   not whatever custom background was picked. */
.tinted-banner .itin-time,
.tinted-banner .itin-notes,
.tinted-banner .role-badge,
.tinted-banner .field-label,
.tinted-banner .muted {
    color: inherit;
    opacity: 0.85;
}

.tinted-banner .role-badge {
    background: rgba(255,255,255,0.2);
    border-color: currentColor;
}

/* --- Halloween easter eggs (Part 1: ambient random animations) ---------
   The layer is a direct child of <body> (appended by easter-eggs.js), so
   it never inherits main.page's stacking context — see DEV_LESSONS.md on
   fixed elements trapped behind lower ones. pointer-events:none on both
   the layer and the sprites so a sprite drifting over a button can never
   eat a click. Positions/rotations are written per frame by JS; nothing
   here animates on its own. */
.easter-egg-layer {
    position: fixed;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 9997;
}

.easter-egg-sprite {
    position: absolute;
    top: 0;
    left: 0;
    line-height: 1;
    pointer-events: none;
    user-select: none;
    will-change: transform;
    filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}

.easter-egg-sprite img {
    display: block;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    /* Belt and braces — easter-eggs.js already refuses to run at all under
       reduced motion; this makes sure nothing can appear even if a sprite
       node were somehow created. */
    .easter-egg-layer { display: none; }
}

/* --- Scavenger-hunt score overlay (HALLOWEEN-EASTER-EGGS-DESIGN.md Part 3)
   ------------------------------------------------------------------------
   The user's styling requirement, verbatim: "large, bold, white text, with
   a thick stroke/outline in one of the DARKER colors from the current
   viewer's own active theme palette — chosen so it stays legible overlaid
   on a wide variety of different avatar images/colors."

   So: white fill, --accent-dark outline. --accent-dark is redefined by
   every [data-theme] block near the top of this file (and by any theme
   added later), and the viewer's theme is stamped on <html> by header.php,
   which is what makes the outline follow the *viewer's own* palette with
   no per-theme rules here — the same trick §-25's theme-following banner
   defaults use.

   The outline is drawn twice on purpose:
     - `-webkit-text-stroke` + `paint-order: stroke fill` gives a real,
       even stroke that is painted UNDER the fill, so a thick stroke never
       eats into the white glyph. Supported in Chromium/WebKit/Gecko.
     - the eight-way `text-shadow` ring is the fallback for anything that
       does not do text-stroke, and is harmless where it does (it sits
       exactly under the stroke).
   Either way the number stays readable on a dark avatar, a light one, or a
   busy photo. */
.egg-score-host {
    position: relative;
}

.egg-score-badge {
    position: absolute;
    right: -0.15em;
    bottom: -0.1em;
    z-index: 2;
    pointer-events: none;      /* never eat a click meant for the avatar */
    font-size: 1.6rem;
    font-weight: 900;
    line-height: 1;
    color: #ffffff;
    -webkit-text-stroke: 3px var(--accent-dark);
    paint-order: stroke fill;
    text-shadow:
        -2px -2px 0 var(--accent-dark),  2px -2px 0 var(--accent-dark),
        -2px  2px 0 var(--accent-dark),  2px  2px 0 var(--accent-dark),
         0   -2px 0 var(--accent-dark),  0    2px 0 var(--accent-dark),
        -2px  0   0 var(--accent-dark),  2px  0   0 var(--accent-dark);
}

/* The member-card modal's avatar is much larger than a cloud avatar, so its
   badge scales up to match rather than looking like a stray digit. */
.member-card .egg-score-badge {
    font-size: 2.2rem;
    -webkit-text-stroke-width: 4px;
}

/* Positioned host for the member card's own score badge. The avatar img
   centres itself with `margin: 0 auto`, which needs a block box of a known
   width to centre *inside* — so the wrapper takes the avatar's width and
   does the centring, rather than being a shrink-to-fit inline span. */
.member-card-avatar-wrap {
    display: block;
    position: relative;
    width: 96px;
    margin: 0 auto 0.75rem;
}

.member-card-avatar-wrap .member-card-avatar {
    margin: 0;   /* the wrapper now owns the centring and the gap below */
}

/* ── YouTube embeds, "Getting Started" card, intro-video modal (§-43) ── */

/* Shared responsive 16:9 embed wrapper — see app/lib/youtube.php's
   youtube_embed_html(). Uses aspect-ratio rather than the padding-bottom
   percentage hack to match the rest of this stylesheet, which already uses
   aspect-ratio for gallery tiles and the home banner. */
.video-embed {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
    border-radius: 8px;
    overflow: hidden;
}

.video-embed iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}

.login-video {
    margin-bottom: 1.25rem;
}

/* A How To item that is a YouTube link sits where the <video> would, so it
   needs the same flex behaviour as .how-to-video-item video. */
.how-to-video-item .video-embed {
    flex: 1 1 12rem;
    min-width: 0;
}

/* The Home page "Getting Started" card. The card itself carries the TITLE's
   font size so the subtitle can be a literal 0.65em of it — the requested
   ~65% — rather than a second hard-coded rem value that would drift if the
   title size ever changes. */
.getting-started-card {
    display: block;
    text-align: center;
    text-decoration: none;
    color: inherit;
    font-size: 1.6rem;
    cursor: pointer;
}

.getting-started-card:hover,
.getting-started-card:focus-visible {
    border-color: var(--accent, var(--border));
}

.getting-started-title {
    font-size: 1em;
    font-weight: 700;
    line-height: 1.2;
}

.getting-started-sub {
    font-size: 0.65em;
    color: var(--muted);
    line-height: 1.3;
}

@media (max-width: 600px) {
    .getting-started-card { font-size: 1.35rem; }
}

/* Near-fullscreen intro-video overlay. Deliberately a modal rather than the
   Fullscreen API — see HANDOFF.md §-43 for the reasoning (the click that
   opens it happens on a DIFFERENT page, so the user-gesture context needed
   by requestFullscreen() is gone by the time the player exists). */
body.intro-video-open { overflow: hidden; }

.intro-video-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    background: rgba(0,0,0,0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 3.25rem 1rem 1rem;
    box-sizing: border-box;
}

.intro-video-overlay[hidden] { display: none; }

.intro-video-stage {
    width: min(100%, 1600px);
    max-height: 100%;
}

.intro-video-stage .video-embed {
    max-height: calc(100vh - 4.5rem);
    margin: 0 auto;
    /* aspect-ratio + a max-height needs a width cap too, or a short, wide
       viewport overflows vertically instead of shrinking. */
    max-width: calc((100vh - 4.5rem) * 16 / 9);
}

.intro-video-close {
    position: absolute;
    top: 0.75rem;
    right: 0.9rem;
    background: rgba(0,0,0,0.5);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.4);
    border-radius: 50%;
    width: 2.5rem;
    height: 2.5rem;
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}

.intro-video-title {
    color: #fff;
    text-align: center;
    margin: 0 0 0.6rem;
    font-size: 1rem;
}

/* Add-a-How-To-video source picker (upload vs. YouTube link). */
.how-to-source-choice {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.75rem 0.9rem 0.9rem;
    margin: 0.5rem 0 0.75rem;
}

.how-to-source-choice legend {
    padding: 0 0.35rem;
}
