/*
  Magic WishList — shared styling for the MAUI Blazor Hybrid host and the web app.

  ── The idea ──────────────────────────────────────────────────────────────────────────────────
  Every screen sits on one side of a secret. Your own list is open: there is nothing on it you do
  not already know. Someone else's list is where the secrets live — suggestions they will never see,
  claims they will never hear about.

  So the GROUND COLOUR says whose knowledge you are in. Light means open. Deep plum means you are
  holding something back from someone, and is a standing reminder not to hand your phone over.

  In dark mode "open" becomes a near-neutral charcoal rather than plum, so the two worlds still read
  as different places: the secret side keeps its colour, the open side gives it up.

  The one ornament is the gift tag: the brand mark, the sign-in story, and on a wish an empty outline
  (unclaimed) or a filled brass tag with initials (spoken for). Your own list shows no tag at all,
  because there is nothing there for you to know.

  ── Layout ────────────────────────────────────────────────────────────────────────────────────
  Phone first: an app bar, one column, a tab bar. From 960px the same markup becomes a rail down the
  left and a content column that lets cards sit side by side. Nothing is duplicated per platform.
*/

@font-face {
    font-family: "Bricolage Grotesque";
    /* Bundled rather than fetched: the phone app is offline-first, and a CDN font would silently fall
       back on exactly the days it matters. Latin subset, variable weight and optical size. */
    src: url("fonts/BricolageGrotesque-latin.woff2") format("woff2");
    font-weight: 200 800;
    font-display: swap;
}

:root {
    color-scheme: light;

    /* Open surfaces — your own things. */
    --bone: #F4F2F7;
    --paper: #FFFFFF;
    --card: var(--paper);
    --line: #E4DFEC;
    --text: #1F1830;
    --muted: #6C6383;

    /* Secret surfaces — someone else's list, your shopping list, writing a suggestion. */
    --ink: #2A1B3D;
    --ink-deep: #21152F;
    --ink-raise: #372550;
    --ink-line: #4B3769;
    --ink-text: #F3EFF8;
    --ink-muted: #B3A7C8;

    /* The ribbon. Two weights: one that passes on white, one that passes on plum. */
    --brass: #8F5F14;
    --brass-lit: #F2BC6B;
    --on-brass: #FFFFFF;

    /* The primary action — plum on paper, brass on plum, lavender on charcoal. Kept apart from
       --ink so the secret ground and the primary button can move independently. */
    --accent: var(--ink);
    --accent-hover: #3B2857;
    --on-accent: #FFFFFF;

    --sage: #2F6B50;
    --sage-lit: #86D0A9;
    --rose: #B23A55;
    --rose-lit: #FF9DAE;

    --r-sm: 10px;
    --r: 16px;
    --r-lg: 26px;
    --gap: 0.85rem;
    --gutter: 1.1rem;
    --tab-h: 4.25rem;
    --rail-w: 248px;

    /* Plum-tinted, never grey: a shadow on a lilac ground that is pure black reads as dirt. */
    --lift: 0 1px 2px rgb(42 27 61 / .05), 0 10px 30px -18px rgb(42 27 61 / .35);
    --lift-hover: 0 1px 2px rgb(42 27 61 / .06), 0 22px 40px -22px rgb(42 27 61 / .5);

    --sans: "Bricolage Grotesque", ui-rounded, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

    /* Shiny.Blazor.Controls reads these, so table cells, sheets, dialogs and toasts wear the app's
       colours and type instead of the library's default blue. The base palette is set as well as the
       pinned roles: the library derives its containers, scrims and inverse surfaces from it, and
       flips it to its own blue-grey dark theme when the OS goes dark unless we say otherwise. */
    --shiny-background: var(--bone);
    --shiny-foreground: var(--text);
    --shiny-card: var(--paper);
    --shiny-popover: var(--paper);
    --shiny-muted: #ECE8F1;
    --shiny-muted-foreground: var(--muted);
    --shiny-primary: var(--accent);
    --shiny-primary-foreground: var(--on-accent);
    --shiny-destructive: var(--rose);
    --shiny-success: var(--sage);
    --shiny-border: var(--line);
    --shiny-input: #9E95B0;
    --shiny-ring: var(--accent);

    --shiny-type-font-family: var(--sans);
    --shiny-color-primary: var(--accent);
    --shiny-color-on-primary: var(--on-accent);
    --shiny-color-surface: var(--paper);
    --shiny-color-on-surface: var(--text);
    --shiny-color-on-surface-variant: var(--muted);
    --shiny-color-outline: #9E95B0;
    --shiny-color-outline-variant: var(--line);
    --shiny-color-surface-container-lowest: var(--paper);
    --shiny-color-surface-container-low: #F9F8FB;
    --shiny-color-surface-container: var(--bone);
    --shiny-color-surface-container-high: #ECE8F1;
    --shiny-color-surface-container-highest: #E4DFEC;
    --shiny-color-surface-variant: #ECE8F1;
    --shiny-color-error: var(--rose);
    --shiny-color-success: var(--sage);
    --shiny-shape-corner-medium: var(--r);
    --shiny-shape-corner-large: var(--r-lg);
}

/* Dark: the open side goes to charcoal with a trace of violet; the secret side keeps its plum, which
   now reads as the lighter, warmer of the two. Matches the library's selector so it wins over the
   library's own dark block (same specificity, loaded later). */
@media (prefers-color-scheme: dark) {
    :root:not(.shiny-theme-light) {
        color-scheme: dark;

        --bone: #131116;
        --paper: #1D1A22;
        --line: #2F2A37;
        --text: #EEEAF3;
        --muted: #A69EB5;

        --brass: var(--brass-lit);
        --on-brass: var(--ink);
        --sage: var(--sage-lit);
        --rose: var(--rose-lit);

        --accent: #D9CBF2;
        --accent-hover: #E7DDF8;
        --on-accent: var(--ink);

        --lift: 0 1px 0 rgb(255 255 255 / .03) inset, 0 12px 30px -20px rgb(0 0 0 / .7);
        --lift-hover: 0 1px 0 rgb(255 255 255 / .05) inset, 0 22px 40px -22px rgb(0 0 0 / .85);

        --shiny-muted: #26222C;
        --shiny-input: #6E667D;
        --shiny-color-on-primary: var(--on-accent);
        --shiny-color-outline: #6E667D;
        --shiny-color-surface-container-low: #18161C;
        --shiny-color-surface-container-high: #26222C;
        --shiny-color-surface-container-highest: #2F2A37;
        --shiny-color-surface-variant: #26222C;
    }
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
    margin: 0;
    background: var(--bone);
    color: var(--text);
    font-family: var(--sans);
    font-optical-sizing: auto;
    font-size: 16px;
    line-height: 1.55;
    -webkit-tap-highlight-color: transparent;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

@media (prefers-reduced-motion: reduce) {
    /* A looping animation cut to .01ms would flicker rather than stop, so it runs once, at once. */
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
    }
}

.icon { width: 1.25rem; height: 1.25rem; flex: none; display: block; }

/* ── Shell ─────────────────────────────────────────────────────────────────────────────────── */

.app {
    min-height: 100dvh;
    background: var(--bone);
    color: var(--text);
    transition: background-color .35s ease;
}

/* The whole point: a screen where you are holding a secret is a different place. */
.app.secret {
    color-scheme: dark;
    background: var(--ink);
    color: var(--ink-text);
    --card: var(--ink-raise);
    --line: var(--ink-line);
    --text: var(--ink-text);
    --muted: var(--ink-muted);
    --brass: var(--brass-lit);
    --sage: var(--sage-lit);
    --rose: var(--rose-lit);
    --on-brass: var(--ink);
    --accent: var(--brass-lit);
    --accent-hover: #F6CC8C;
    --on-accent: var(--ink);
    --lift: 0 1px 0 rgb(255 255 255 / .04) inset, 0 12px 30px -20px rgb(0 0 0 / .6);
    --lift-hover: 0 1px 0 rgb(255 255 255 / .06) inset, 0 22px 40px -22px rgb(0 0 0 / .75);

    --shiny-color-primary: var(--accent);
    --shiny-color-on-primary: var(--on-accent);
    --shiny-color-surface: var(--ink-raise);
    --shiny-color-on-surface: var(--ink-text);
    --shiny-color-on-surface-variant: var(--ink-muted);
    --shiny-color-outline-variant: var(--ink-line);
    --shiny-color-surface-container: var(--ink-deep);
    --shiny-color-surface-container-high: #432E60;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 1.08rem;
    font-weight: 750;
    letter-spacing: -0.025em;
    color: var(--text);
    text-decoration: none;
}

.brand-tag { width: 1.3rem; height: auto; overflow: visible; flex: none; }
.brand-tag .card { fill: var(--brass); }
.brand-tag .string { stroke: var(--muted); stroke-width: 1.8; stroke-linecap: round; }

.status {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--muted);
}

.status .icon { width: 1rem; height: 1rem; }
.status.offline { color: var(--rose); }

/* Phone: app bar on top, tab bar underneath. */

.rail { display: none; }

.app-bar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap);
    padding: max(0.8rem, env(safe-area-inset-top)) var(--gutter) 0.8rem;
    background: color-mix(in srgb, var(--bone) 86%, transparent);
    -webkit-backdrop-filter: saturate(1.4) blur(14px);
    backdrop-filter: saturate(1.4) blur(14px);
}

.app.secret .app-bar { background: color-mix(in srgb, var(--ink) 86%, transparent); }

.secret-banner {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    margin: 0;
    padding: 0.65rem var(--gutter);
    font-size: 0.86rem;
    line-height: 1.35;
    color: var(--ink-muted);
    background: var(--ink-deep);
    border-block: 1px solid var(--ink-line);
}

.secret-banner .icon { color: var(--brass-lit); }
.secret-banner strong { color: var(--ink-text); font-weight: 650; }

.app-body {
    width: 100%;
    max-width: 680px;
    margin: 0 auto;
    padding: 0.9rem var(--gutter) calc(var(--tab-h) + env(safe-area-inset-bottom) + 2.5rem);
}

.tab-bar {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 10;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    padding-bottom: env(safe-area-inset-bottom);
    background: color-mix(in srgb, var(--paper) 90%, transparent);
    -webkit-backdrop-filter: saturate(1.4) blur(14px);
    backdrop-filter: saturate(1.4) blur(14px);
    border-top: 1px solid var(--line);
}

.app.secret .tab-bar { background: color-mix(in srgb, var(--ink-deep) 92%, transparent); }

.tab-bar a {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.22rem;
    min-height: var(--tab-h);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--muted);
    text-decoration: none;
}

.tab-bar a .icon { width: 1.5rem; height: 1.5rem; }
.tab-bar a.active { color: var(--text); }
.tab-bar a.active .icon { color: var(--brass); stroke-width: 2.1; }

/* A short ribbon above the live tab — the quietest way to say "you are here". */
.tab-bar a.active::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    width: 1.8rem;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--brass);
    transform: translateX(-50%);
}

/* Desk: a rail down the side, content that can breathe. */

@media (min-width: 960px) {
    :root { --gutter: 3rem; }

    .app {
        display: grid;
        grid-template-columns: var(--rail-w) minmax(0, 1fr);
    }

    .app-bar, .tab-bar { display: none; }

    .rail {
        position: sticky;
        top: 0;
        height: 100dvh;
        display: flex;
        flex-direction: column;
        gap: 2rem;
        padding: 1.75rem 1rem 1.5rem;
        background: var(--paper);
        border-right: 1px solid var(--line);
    }

    .app.secret .rail { background: var(--ink-deep); }

    .rail .brand { padding: 0 0.75rem; font-size: 1.15rem; }
    .rail .brand-tag { width: 1.45rem; }

    .rail-nav { display: flex; flex-direction: column; gap: 0.2rem; }

    .rail-nav a {
        display: flex;
        align-items: center;
        gap: 0.75rem;
        padding: 0.62rem 0.75rem;
        border-radius: 12px;
        font-weight: 600;
        color: var(--muted);
        text-decoration: none;
        transition: background-color .15s ease, color .15s ease;
    }

    .rail-nav a:hover { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }

    /* The live item wears the primary button's colours, like the live filter chip: one visual word
       for "this one". */
    .rail-nav a.active { color: var(--on-accent); background: var(--accent); }

    .rail .status { margin-top: auto; padding: 0 0.75rem; }

    .stage { min-width: 0; }

    .secret-banner { padding-inline: var(--gutter); }

    .app-body {
        max-width: 1180px;
        padding: 2.75rem var(--gutter) 4rem;
    }
}

/* ── Type ──────────────────────────────────────────────────────────────────────────────────── */

h1 {
    font-size: clamp(1.9rem, 1.35rem + 1.9vw, 2.8rem);
    font-weight: 720;
    letter-spacing: -0.035em;
    line-height: 1.04;
    margin: 0.35rem 0 0.35rem;
    text-wrap: balance;
}

h2 {
    font-size: 1.22rem;
    font-weight: 680;
    letter-spacing: -0.02em;
    line-height: 1.2;
    margin: 2.4rem 0 0.65rem;
}

h3 { font-size: 1rem; font-weight: 680; margin: 1.4rem 0 0.5rem; }

p { margin: 0 0 0.6rem; }
.app-body p { max-width: 64ch; }

.lede { font-size: 1.03rem; color: var(--muted); margin: 0 0 1.5rem; text-wrap: pretty; }
.muted { color: var(--muted); }
.small { font-size: 0.84rem; }
.centered { text-align: center; }
.error { color: var(--rose); font-weight: 550; }

.price {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--text);
}

.clamp {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

a { color: var(--brass); text-underline-offset: 3px; text-decoration-thickness: 1px; }

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.75rem var(--gap);
    margin-bottom: 1.25rem;
}

.page-head h1 { margin: 0; }
.head-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.site-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem 1.25rem; margin-top: 2rem; }

/* A way back sits above the heading it leaves, not off to the side of it. */
.back-link { flex-basis: 100%; }
.page-head .back-link { justify-content: flex-start; margin: 0 0 0.2rem -0.3rem; }
.app-body > .back-link { margin: 0 0 0.5rem -0.3rem; }
.page-head + .lede { margin-top: -0.6rem; }

/* The desk rail already lists these screens, so there they have nothing to go back to. */
@media (min-width: 960px) {
    .back-link.phone-only { display: none; }
}

.group > h2 {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    color: var(--muted);
    font-size: 0.88rem;
    font-weight: 680;
    margin-top: 0;
}

.group > h2::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.group + .group { margin-top: 1.75rem; }

/* ── Notices ───────────────────────────────────────────────────────────────────────────────── */

.notice {
    max-width: 68ch;
    margin: 0 0 1.25rem;
    padding: 0.75rem 1rem;
    font-size: 0.9rem;
    color: var(--text);
    background: color-mix(in srgb, var(--brass) 9%, var(--card));
    border-left: 3px solid var(--brass);
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
}

.notice strong { font-weight: 680; }
.notice.warning { border-left-color: var(--rose); background: color-mix(in srgb, var(--rose) 9%, var(--card)); }

/* ── Forms ─────────────────────────────────────────────────────────────────────────────────── */

label { display: block; margin-bottom: 1rem; font-size: 0.86rem; font-weight: 620; color: var(--muted); }
label.inline { display: flex; align-items: center; gap: 0.55rem; font-weight: 500; }
label em { font-style: normal; font-weight: 400; }

input, textarea, select {
    width: 100%;
    margin-top: 0.35rem;
    padding: 0.72rem 0.85rem;
    font: inherit;
    font-size: 1rem;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    transition: border-color .15s ease, box-shadow .15s ease;
}

input:hover, textarea:hover, select:hover { border-color: color-mix(in srgb, var(--text) 25%, var(--line)); }

input:focus, textarea:focus, select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

input::placeholder { color: var(--muted); opacity: .7; }
label.inline input, label.check input { width: auto; margin: 0; accent-color: var(--accent); }

a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible {
    outline: 2px solid var(--brass);
    outline-offset: 2px;
}

/* The router moves focus to the heading after navigation; that is for screen readers, not a
   visible ring. */
h1:focus, h1:focus-visible { outline: none; }

button, a.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    min-height: 44px;
    padding: 0.6rem 1.1rem;
    font: inherit;
    font-weight: 650;
    line-height: 1.2;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 12px;
    cursor: pointer;
    transition: background-color .15s ease, border-color .15s ease, color .15s ease, transform .12s ease;
}

/* A link that leaves the app, dressed as the button it sits beside. */
a.button { text-decoration: none; }

button:hover:not(:disabled), a.button:hover { border-color: color-mix(in srgb, var(--text) 28%, var(--line)); }
button .icon, a.button .icon { width: 1.15rem; height: 1.15rem; }

/* Primary inverts with the ground, which keeps the worlds coherent: plum on paper, brass on plum,
   lavender on charcoal. The tokens carry it; see --accent. */
button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
button.primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }

button.danger { color: var(--rose); border-color: color-mix(in srgb, var(--rose) 40%, transparent); background: transparent; }
button.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--rose) 8%, transparent); border-color: var(--rose); }

button.linkish {
    min-height: 0;
    padding: 0.35rem 0.3rem;
    border: none;
    background: none;
    color: var(--brass);
    font-weight: 620;
}

button.linkish:hover:not(:disabled) { text-decoration: underline; text-underline-offset: 3px; }
button.linkish.danger { color: var(--rose); }

button.icon-button {
    min-height: 0;
    width: 2.6rem;
    height: 2.6rem;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--muted);
}

button.icon-button:hover:not(:disabled) { color: var(--text); background: color-mix(in srgb, var(--text) 7%, transparent); }

button.block { width: 100%; }
button:disabled { opacity: .42; cursor: default; }

.actions { display: flex; flex-wrap: wrap; gap: var(--gap); margin: 1.6rem 0; }
.invite-row { display: flex; gap: var(--gap); align-items: center; max-width: 560px; }
.invite-row input { margin-top: 0; }

/* ── Shiny table cells ─────────────────────────────────────────────────────────────────────────
   Forms read best at a form's width: a text box stretched across a desk monitor is hard to scan. */

.shiny-tv-section { max-width: 680px; }

/* ── Auth ──────────────────────────────────────────────────────────────────────────────────── */

.auth-shell {
    min-height: 100dvh;
    display: grid;
    grid-template-rows: auto 1fr;
    background: var(--bone);
}

.auth-story {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    padding: calc(env(safe-area-inset-top) + 2.25rem) 1.5rem 4.75rem;
    background: var(--ink);
    color: var(--ink-text);
}

.auth-story .brand-tag { width: 2.4rem; }
.auth-story .brand-tag .card { fill: var(--brass-lit); }
.auth-story .brand-tag .string { stroke: var(--ink-muted); }

.auth-promise {
    margin: 0;
    max-width: 17ch;
    font-size: 1.6rem;
    font-weight: 680;
    line-height: 1.1;
    letter-spacing: -0.03em;
    text-wrap: balance;
}

.auth-panel {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 0 1rem 2.5rem;
    margin-top: -3.25rem;
}

.auth-card {
    width: min(420px, 100%);
    padding: 1.9rem 1.6rem;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--lift);
}

.auth-card h1 { margin: 0 0 0.2rem; font-size: 1.75rem; }
.auth-card .lede { margin-bottom: 1.4rem; }
.auth-card button.primary { margin-top: 0.4rem; }
.auth-card > p:last-child { margin: 1.2rem 0 0; }

/* The tag arrives on its string and settles. The rest of the app's motion is under Motion, below. */
.swing {
    transform-origin: 50% 0;
    animation: swing 2.2s cubic-bezier(.3, .7, .35, 1) .15s both;
}

@keyframes swing {
    0%   { transform: rotate(-16deg); }
    30%  { transform: rotate(10deg); }
    55%  { transform: rotate(-5deg); }
    78%  { transform: rotate(2deg); }
    100% { transform: rotate(0); }
}

@media (min-width: 960px) {
    .auth-shell { grid-template-rows: none; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }

    .auth-story {
        justify-content: space-between;
        padding: 4rem clamp(3rem, 5vw, 6rem);
    }

    .auth-story .brand-tag { width: 6.5rem; margin-left: 0.5rem; }

    .auth-promise {
        font-size: clamp(2.5rem, 1.1rem + 2.7vw, 4rem);
        max-width: 13ch;
    }

    .auth-panel {
        align-items: center;
        margin: 0;
        padding: 3rem;
    }

    .auth-card {
        width: min(400px, 100%);
        padding: 0;
        background: transparent;
        border: none;
        box-shadow: none;
    }

    .auth-card h1 { font-size: 2.2rem; }
}

/* ── Cards ─────────────────────────────────────────────────────────────────────────────────────
   One column on a phone; on a desk they fill the width in columns of at least ~19rem. */

.cards {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr));
    gap: var(--gap);
    align-items: start;
}

.card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--lift);
    overflow: hidden;
}

.card.row { display: flex; align-items: center; gap: var(--gap); padding: 0.9rem 0.75rem 0.9rem 1rem; }
.card.tappable { cursor: pointer; transition: border-color .15s ease; }
.card.tappable:hover { border-color: color-mix(in srgb, var(--text) 26%, var(--line)); }
.card.done { opacity: .58; }

.card-body { padding: 1rem 1.1rem 1.05rem; display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; flex: 1; }
.card.row .card-body, .wish .card-body { padding: 0; }
.card-body p { margin: 0; font-size: 0.92rem; }
.card-body strong { font-size: 1.08rem; font-weight: 660; letter-spacing: -0.015em; line-height: 1.25; }

.card-title { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; }

/* A wish is laid out around its tag: the tag first, then the item it is hanging from. */
.wish { display: flex; gap: 1rem; padding: 1.05rem 1.1rem; align-items: flex-start; }

.meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 0.9rem;
    align-items: center;
    margin-top: 0.15rem;
    font-size: 0.84rem;
    color: var(--muted);
}

.meta a { display: inline-flex; align-items: center; gap: 0.3rem; font-weight: 620; }
.meta a .icon { width: 0.95rem; height: 0.95rem; }

.card-actions { display: flex; flex-wrap: wrap; gap: 0.5rem 0.9rem; align-items: center; margin-top: 0.7rem; }
.check { display: flex; align-items: center; gap: 0.45rem; margin: 0; color: var(--text); font-size: 0.9rem; font-weight: 550; }


/* ── The gift tag ──────────────────────────────────────────────────────────────────────────────
   It carries the information the whole app exists for. An outline is unclaimed; a filled tag
   names who is buying it. Never shown on the owner's own list. */

.tag {
    flex: none;
    position: relative;
    display: inline-block;
    width: 2.9rem;
    height: 3.7rem;
    color: var(--muted);
}

.tag svg { display: block; width: 100%; height: 100%; }
.tag path { fill: currentColor; stroke: none; }

.tag.empty path {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    opacity: .55;
}

.tag.claimed { color: var(--brass); }
.tag.mine { color: var(--sage); }

/* The initials sit below the punched hole, in the ground colour so they read as ink on the tag. */
.tag .face {
    position: absolute;
    inset: 34% 0 0;
    display: grid;
    place-items: center;
    font-size: 0.74rem;
    font-weight: 750;
    letter-spacing: 0.02em;
    color: var(--card);
}

.app.secret .tag .face { color: var(--ink); }


/* ── Filters ───────────────────────────────────────────────────────────────────────────────────
   The live chip wears the primary button's colours, so "what am I looking at" reads the same way
   as "what will this do". */

.filters {
    display: flex;
    gap: 0.45rem;
    overflow-x: auto;
    margin: 0 calc(var(--gutter) * -1) 1.25rem;
    padding: 0.1rem var(--gutter) 0.3rem;
    scrollbar-width: none;
}

.filters::-webkit-scrollbar { display: none; }

@media (min-width: 960px) {
    .filters { flex-wrap: wrap; overflow: visible; margin-inline: 0; padding-inline: 0; }
}

.chip {
    flex: none;
    min-height: 38px;
    padding: 0.35rem 0.95rem;
    border-radius: 999px;
    font-size: 0.88rem;
    font-weight: 620;
    white-space: nowrap;
    color: var(--muted);
    background: transparent;
}

.chip:hover:not(.on) { color: var(--text); }
.chip.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.chip .count { margin-left: 0.45rem; font-weight: 500; opacity: .7; font-variant-numeric: tabular-nums; }

/* Where a wish lives. Private is the one worth noticing, in sage — the tag's colour for "yours". */
.where { font-weight: 650; color: var(--text); }
.where.private { color: var(--sage); }

/* ── Flags ─────────────────────────────────────────────────────────────────────────────────── */

.flag {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.78rem;
    font-weight: 680;
    color: var(--muted);
}

.flag::before {
    content: "";
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: currentColor;
}

.flag.given { color: var(--sage); }
.flag.claimed { color: var(--brass); }
.flag.suggested { color: var(--muted); font-weight: 600; }
.flag.suggested::before { display: none; }

/* Something on their list since your last visit — the ribbon colour, like everything waiting on you. */
.new-pill {
    display: inline-flex;
    align-items: center;
    height: 1.25rem;
    padding: 0 0.5rem;
    border-radius: 999px;
    background: var(--brass);
    color: var(--on-brass);
    font-size: 0.72rem;
    font-weight: 750;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.card.has-new { box-shadow: inset 3px 0 0 var(--brass), var(--lift); }

/* ── Photos & avatars ──────────────────────────────────────────────────────────────────────── */

.link-photo { display: flex; gap: 0.75rem; align-items: center; margin: 0.35rem 0 0.6rem; }
.link-photo img { width: 6rem; height: 6rem; object-fit: contain; border-radius: var(--r-sm); background: var(--card); border: 1px solid var(--line); flex: none; }
.link-photo p { margin: 0 0 0.25rem; }

.photo-strip { display: flex; gap: 0.45rem; overflow-x: auto; padding-bottom: 0.2rem; margin: 0.35rem 0 0.1rem; }

.photo-strip img {
    height: 76px;
    width: auto;
    border-radius: var(--r-sm);
    object-fit: cover;
    flex: 0 0 auto;
    background: var(--line);
    cursor: zoom-in;
}

/* On your own list the photos lead the card, edge to edge. */
.card > .photo-strip { margin: 0; padding: 0; gap: 2px; overflow: hidden; }
.card > .photo-strip img { height: 11.5rem; flex: 1 1 0; min-width: 0; border-radius: 0; }
.card > .photo-strip img:nth-child(n+4) { display: none; }

.avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    flex: none;
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: 0.95rem;
    letter-spacing: 0.01em;
    color: var(--brass);
    background: color-mix(in srgb, var(--brass) 15%, var(--card));
}

.family-hero { display: flex; align-items: center; gap: 0.9rem 1.1rem; margin-bottom: 1.5rem; }
.family-hero > div { flex: 1; min-width: 0; }
.family-hero h1 { margin: 0 0 0.15rem; }
.family-hero .avatar { width: 4.25rem; height: 4.25rem; font-size: 1.3rem; }
.family-hero > button { flex: none; }

/* Announcements read as notes pinned above the people: a brass rule on the left, like a notice. */
.announcements { margin-bottom: 1.75rem; }

/* A section heading with its action on the same line: the rule runs from the title to the button. */
.section-head { display: flex; align-items: center; gap: var(--gap); margin-bottom: 0.6rem; }
.section-head > h2 {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 0.7rem;
    margin: 0;
    color: var(--muted);
    font-size: 0.88rem;
    font-weight: 680;
}
.section-head > h2::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.section-head > button { flex: none; }

.announce-hint { max-width: 68ch; margin: 0 0 1rem; }
.cards.announcement-list { grid-template-columns: 1fr; max-width: 68ch; }
.card.announcement { border-left: 3px solid var(--brass); }
.card.announcement .card-body { gap: 0.7rem; }
.announcement-by { display: flex; align-items: center; gap: 0.7rem; }
.announcement-by .avatar { width: 2.4rem; height: 2.4rem; font-size: 0.82rem; flex: none; }
.announcement-by > div { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.announcement-by strong { font-size: 0.98rem !important; }
.announcement-text { white-space: pre-line; overflow-wrap: anywhere; font-size: 0.98rem !important; line-height: 1.5; }
.announcement-actions { display: flex; gap: 1rem; padding: 0 1.1rem 0.85rem; }

/* A member's note at the top of their own list. The whole family reads it, so it wears the brass rule too. */
.my-note { margin-bottom: 1.75rem; }
.list-note {
    display: grid;
    gap: 0.4rem;
    max-width: 68ch;
    margin-bottom: 1.25rem;
    padding: 0.85rem 1.1rem;
    background: var(--card);
    border: 1px solid var(--line);
    border-left: 3px solid var(--brass);
    border-radius: var(--r-sm);
}
.my-note .list-note { margin-bottom: 0; }
.list-note-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.75rem; }
.list-note-text { margin: 0; white-space: pre-line; overflow-wrap: anywhere; line-height: 1.5; }
.list-note-actions { display: flex; gap: 1rem; }

/* ── Comments ──────────────────────────────────────────────────────────────────────────────────
   A note kept from the wish's author wears the same brass rule as an announcement: something said
   among the family. */

.comments { display: grid; gap: 0.75rem; max-width: 68ch; }
.comment-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.comment {
    display: grid;
    gap: 0.45rem;
    justify-items: start;
    padding: 0.75rem 0.9rem;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}
.comment.family-only { border-left: 3px solid var(--brass); }
.comment-by { display: flex; align-items: center; gap: 0.6rem; }
.comment-by .avatar { width: 2rem; height: 2rem; font-size: 0.72rem; flex: none; }
.comment-by > div { display: flex; flex-direction: column; min-width: 0; }
.comment-text { margin: 0; white-space: pre-line; overflow-wrap: anywhere; line-height: 1.5; }
.comment-photo { display: block; max-width: 100%; max-height: 16rem; object-fit: cover; border-radius: var(--r-sm); }
.comment-compose { display: grid; gap: 0.35rem; }
.comment-sheet { color: var(--text); }
.comment-sheet > h2 { margin-top: 0; }
.wish-comments { max-width: 680px; margin-top: 1rem; }
.meta .comment-count { display: inline-flex; align-items: center; gap: 0.3rem; }
.meta .comment-count .icon { width: 0.95rem; height: 0.95rem; }

/* ── Sheets with forms: members who don't use the app, emailing a list ─────────────────────────── */

.managed-sheet > h2, .email-sheet > h2 { margin-top: 0; }
.managed-sheet > label, .email-sheet > label { display: grid; gap: 0.3rem; margin-bottom: 0.8rem; }
.managed-sheet > label.check { display: flex; }
.invite-takes-over { display: grid; gap: 0.3rem; max-width: 560px; margin: 0.6rem 0 1rem; }
.email-picks { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.55rem; }
.email-picks .check { flex-wrap: wrap; }
.email-picks .muted { font-weight: 500; }

/* ── Home ──────────────────────────────────────────────────────────────────────────────────── */

.home-quick { margin-bottom: 2rem; }
.home-quick > h2 { margin-top: 0; }
.home-quick > p { margin-bottom: 0.8rem; }
.home-quick .invite-row input { flex: 1; min-width: 0; }
.home-quick .invite-row > button { flex: none; }
.home-quick-actions { display: flex; flex-wrap: wrap; gap: var(--gap); max-width: 560px; margin-top: 0.75rem; }

/* Suggest a gift: everyone you could buy for, family by family. */
.recipient-family { margin: 1.1rem 0 0.45rem; font-size: 0.78rem; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.recipient-picks { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; max-width: 560px; }
.recipient-picks .check { gap: 0.7rem; padding: 0.5rem 0.75rem; border-radius: 10px; background: var(--card); cursor: pointer; }
.recipient-picks .avatar { flex: none; width: 2rem; height: 2rem; font-size: 0.8rem; }
.recipient-name { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.recipient-name .muted { font-weight: 500; }
.suggest-picked { margin: 0.9rem 0 1.6rem; }
.home-section { margin-bottom: 2rem; }
.home-section > p.muted { max-width: 68ch; margin: 0 0 0.9rem; }
.home-flag { font-weight: 680; color: var(--brass); }
.home-flag.soon { color: var(--rose); }
.new-titles { list-style: disc; margin: 0.15rem 0 0; padding-left: 1.05rem; font-size: 0.9rem; color: var(--text); }
.new-titles li { margin: 0.1rem 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card.row > svg.icon { flex: none; color: var(--muted); }

/* Watching: the person first, then how you hear about them. */
.watch-head { display: flex; align-items: center; gap: var(--gap); padding: 1rem 0.75rem 0.5rem 1rem; }
.watch-head .card-body { padding: 0; gap: 0.1rem; }
.watch .shiny-tv-section { max-width: none; }

.profile-photo { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.5rem; }
.profile-photo .avatar { width: 4.5rem; height: 4.5rem; font-size: 1.4rem; }

/* ── Empty states ──────────────────────────────────────────────────────────────────────────── */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    max-width: 30rem;
    margin: 1rem auto;
    padding: 3.5rem 1.5rem;
    text-align: center;
    color: var(--muted);
}

.empty-state strong { color: var(--text); font-size: 1.2rem; font-weight: 680; letter-spacing: -0.02em; }
.empty-state p { margin: 0; max-width: 36ch; }

.empty-mark {
    display: grid;
    place-items: center;
    width: 4.5rem;
    height: 4.5rem;
    margin-bottom: 0.9rem;
    border-radius: 50%;
    color: var(--brass);
    background: color-mix(in srgb, var(--brass) 12%, transparent);
}

.empty-mark .icon { width: 2rem; height: 2rem; stroke-width: 1.5; }

/* The empty tag as the empty-state mark: the same object the list is made of, waiting. */
.empty-state .tag { margin-bottom: 1rem; width: 3.4rem; height: 4.3rem; }

.boot { padding: 3rem; color: var(--muted); }

#blazor-error-ui {
    display: none;
    position: fixed;
    inset: auto 0 0 0;
    padding: 0.85rem 1.2rem;
    background: #B23A55;
    color: #FFF;
    z-index: 100;
}


/* ── Gifts ─────────────────────────────────────────────────────────────────────────────────────
   Given is where the secret ends, so this is the one place the open side of the app names a giver.
   The sheet that marks it is deliberately slower than a checkbox. */

.gifts-due { margin: 0 0 2rem; }
.gifts-due h2 { display: flex; align-items: center; gap: 0.5rem; font-size: 1.15rem; margin: 0 0 0.8rem; }
.gifts-due h2 .icon { color: var(--brass); }
.card.gift { border-color: color-mix(in srgb, var(--brass) 45%, var(--line)); }

.unsaved { margin: 0 0 2rem; }
.unsaved h2 { display: flex; align-items: center; gap: 0.5rem; font-size: 1.15rem; margin: 0 0 0.35rem; }
.unsaved h2 .icon { color: var(--rose); }
.card.unsaved-change { border-color: color-mix(in srgb, var(--rose) 45%, var(--line)); }
.unsaved-reason { color: var(--text); }

/* This page is always the secret ground, so the sheet takes the card surface and ink explicitly —
   the control's own light default would put the page's pale text on a pale sheet. */
.give-sheet { color: var(--text); }
.give-sheet h2 { margin-top: 0; text-wrap: balance; }
.give-photo { display: block; width: 100%; max-height: 12rem; object-fit: cover; border-radius: var(--r-sm); margin: 0 0 1rem; }
.give-warning {
    margin: 0 0 1rem;
    padding: 0.85rem 1rem;
    color: var(--text);
    background: color-mix(in srgb, var(--rose) 11%, var(--card));
    border: 1px solid color-mix(in srgb, var(--rose) 45%, transparent);
    border-left-width: 4px;
    border-radius: var(--r-sm);
}
.give-warning strong { display: flex; align-items: center; gap: 0.45rem; font-size: 1.05rem; font-weight: 700; color: var(--rose); }
.give-warning p { margin: 0.35rem 0 0; font-size: 0.92rem; }
.give-points { margin: 0 0 1.1rem; padding-left: 1.2rem; display: grid; gap: 0.5rem; }
.give-sheet .check { margin-bottom: 1rem; }

/* ── Buy List: what's left to spend, and what was paid ─────────────────────────────────────────── */

.buy-totals {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
    gap: var(--gap);
    max-width: 44rem;
    margin: 0 0 1.25rem;
}
.buy-totals + .muted { margin-top: -0.6rem; }
.buy-total {
    display: grid;
    gap: 0.15rem;
    padding: 0.85rem 1rem;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}
.buy-total-label { font-size: 0.76rem; font-weight: 650; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }
.buy-total-amount { font-size: 1.45rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; font-variant-numeric: tabular-nums; color: var(--text); }
.buy-total-amount.none { font-size: 1.05rem; font-weight: 600; color: var(--muted); }

/* An item is read top down, so each line is a step down in weight from the title. */
.card.buy .card-body { gap: 0.3rem; padding: 1rem 1.1rem 0.85rem; }
.card.buy .card-title { margin-bottom: 0.1rem; }
.card.buy .card-title strong { font-size: 1.14rem; }
.card.buy .for-whom { font-size: 0.9rem; color: var(--muted); }
.card.buy .for-whom strong { font-weight: 660; color: var(--text); }
.card.buy .card-body > p.muted { font-size: 0.9rem; }
.card.buy .meta { margin-top: 0.25rem; }
.card.buy .meta .price { font-size: 0.95rem; }

/* One thing to do next, then the ways to put it off. */
.card.buy .card-actions { margin-top: 0.75rem; gap: 0.5rem 0.75rem; }
.card.buy .card-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0 0.9rem;
    margin-top: 0.7rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--line);
}
.card.buy .card-links button { font-size: 0.86rem; padding-left: 0; padding-right: 0; }

/* The buyer's own record, marked with the lock: nobody else sees it. */
button.purchase-line {
    display: inline-flex;
    justify-content: flex-start;
    min-height: 0;
    text-align: left;
    align-items: center;
    align-self: flex-start;
    gap: 0.4rem;
    margin-top: 0.4rem;
    padding: 0.28rem 0.7rem;
    font-size: 0.84rem;
    font-weight: 620;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    background: color-mix(in srgb, var(--sage) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--sage) 40%, transparent);
    border-radius: 999px;
}
button.purchase-line:hover:not(:disabled) { border-color: var(--sage); }
button.purchase-line .icon, button.purchase-add .icon { width: 0.85rem; height: 0.85rem; flex: none; }
button.purchase-line .icon, button.purchase-add .icon { color: var(--sage); }

/* A link, not a button: it offers something extra rather than being the thing to do next. */
button.purchase-add {
    align-self: flex-start;
    justify-content: flex-start;
    gap: 0.4rem;
    min-height: 0;
    margin-top: 0.4rem;
    padding: 0.2rem 0;
    border: none;
    background: none;
    color: var(--brass);
    font-size: 0.86rem;
    font-weight: 620;
    text-align: left;
}
button.purchase-add:hover:not(:disabled) { text-decoration: underline; text-underline-offset: 3px; }

.purchase-sheet { color: var(--text); }
.purchase-sheet h2 { margin-top: 0; text-wrap: balance; }
.private-note { display: flex; gap: 0.45rem; align-items: flex-start; font-size: 0.88rem; color: var(--muted); }
.private-note .icon { flex: none; width: 1rem; height: 1rem; margin-top: 0.15rem; color: var(--sage); }
.paid-field { display: grid; gap: 0.3rem; max-width: 14rem; margin-top: 0.9rem; }
.paid-field input { font-variant-numeric: tabular-nums; }
.purchase-sheet .actions { margin: 0.8rem 0 0; }
.receipts {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
    gap: 0.75rem;
}
.receipts li { display: grid; gap: 0.25rem; justify-items: start; min-width: 0; }
.receipts li.open { grid-column: 1 / -1; }
.receipt-photo { display: block; width: 100%; padding: 0; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--line); }
.receipt-photo img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
.receipts li.open .receipt-photo img { aspect-ratio: auto; max-height: 70vh; object-fit: contain; }

.nav-badge {
    display: inline-grid;
    place-items: center;
    min-width: 1.15rem;
    height: 1.15rem;
    padding: 0 0.3rem;
    border-radius: 999px;
    background: var(--brass);
    color: var(--on-brass);
    font-size: 0.7rem;
    font-weight: 750;
    font-variant-numeric: tabular-nums;
}

.rail-nav a .nav-badge { margin-left: auto; }
.tab-bar a { position: relative; }
.tab-bar a .nav-badge { position: absolute; top: 0.25rem; left: calc(50% + 0.4rem); }


/* ── Motion ────────────────────────────────────────────────────────────────────────────────────
   Motion here says one of four things, and everything else stays still:

   Arrived        — a screen and its cards rise in; ghost cards give way to the real ones.
   Changed hands  — a tag swings on its string the moment it is claimed.
   You are here   — the live tab pops and its ribbon draws out.
   Waiting on you — a gift to confirm, the badge that counts them, an empty tag.

   Transform and opacity only, so none of it costs a layout. Icons that move are MotionIcon from
   Shiny.Blazor.Controls, which compiles to @keyframes the same way.

   Entrances fill `backwards`, never `both`: a transform left filling after it ends makes the element
   a containing block, and the sheets and image viewer inside a page are position: fixed. */

:root {
    --ease-out: cubic-bezier(.22, 1, .36, 1);
    --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
}

@keyframes rise {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

@keyframes fade-in { from { opacity: 0; } }

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* Arrived. A new screen is new DOM, so this replays per screen with no code — and a re-render of
   the screen you are already on leaves it alone. */

.page-head, .app-body > h1, .family-hero, .empty-state, .gifts-due, .unsaved, .auth-card {
    animation: rise .5s var(--ease-out) backwards;
}

.lede, .notice, .buy-totals { animation: rise .5s var(--ease-out) 60ms backwards; }
.filters { animation: rise .5s var(--ease-out) 110ms backwards; }

.cards > li { animation: rise .55s var(--ease-out) 140ms backwards; }
.cards > li:nth-child(2) { animation-delay: 185ms; }
.cards > li:nth-child(3) { animation-delay: 230ms; }
.cards > li:nth-child(4) { animation-delay: 275ms; }
.cards > li:nth-child(5) { animation-delay: 320ms; }
.cards > li:nth-child(6) { animation-delay: 365ms; }
.cards > li:nth-child(7) { animation-delay: 410ms; }
.cards > li:nth-child(n+8) { animation-delay: 455ms; }

.auth-promise { animation: rise .8s var(--ease-out) .45s backwards; }

/* Stepping into a secret: the warning drops in over the ground as it turns plum. */
.secret-banner { animation: banner-in .5s var(--ease-out) backwards; }

@keyframes banner-in { from { opacity: 0; transform: translateY(-100%); } }

/* Ghosts wait a beat before showing, so a fast load never flashes them at all. */
.loading { animation: fade-in .35s ease .2s backwards; }
.cards > li.ghost { animation: none; box-shadow: none; }
.ghost .card-body { gap: 0.6rem; }
.ghost-photo { height: 11.5rem; }
.ghost-line { height: 0.8rem; border-radius: 999px; }
.ghost-line.lg { height: 1.05rem; }
.ghost-avatar { width: 48px; height: 48px; border-radius: 50%; flex: none; }
.ghost-tag {
    width: 2.9rem;
    height: 3.7rem;
    flex: none;
    border-radius: 4px;
    /* The tag's clipped corner, so even the ghost is the object the list is made of. */
    clip-path: polygon(34% 0, 100% 0, 100% 100%, 0 100%, 0 27%);
}
.ghost-form { max-width: 680px; }

/* A card you can open answers the pointer. It lifts with its shadow rather than a transform — the
   card holds the image viewer, which a transformed ancestor would trap inside it. The photo is free
   to move: the viewer is its sibling, not its child. */

.card.tappable { transition: border-color .15s ease, box-shadow .3s var(--ease-out); }
.card > .photo-strip img { transition: transform .8s var(--ease-out); }

@media (hover: hover) {
    .card.tappable:hover {
        box-shadow: var(--lift-hover);
    }

    .card.tappable:hover > .photo-strip img { transform: scale(1.045); }

    .brand:hover .brand-tag {
        transform-origin: 50% 0;
        animation: swing 1.5s cubic-bezier(.3, .7, .35, 1);
    }
}

button:active:not(:disabled) { transform: scale(.97); }

/* Changed hands. The tag hangs from its punched hole and swings when the claim lands on it — on the
   tap that claims it, or as the list arrives — and never while it simply sits there. */

.tag { transform-origin: 24% 19%; }
.tag.claimed, .tag.mine { animation: tag-swing 1.3s var(--ease-out) backwards; }

@keyframes tag-swing {
    0%   { transform: rotate(-24deg); }
    32%  { transform: rotate(13deg); }
    58%  { transform: rotate(-6deg); }
    80%  { transform: rotate(2.5deg); }
    100% { transform: rotate(0); }
}

/* You are here. NavLink swaps the class on the same element, so this plays once per arrival. */

.tab-bar a.active .icon, .rail-nav a.active .icon { animation: pop .45s var(--ease-spring); }
.tab-bar a.active::before { animation: ribbon .4s var(--ease-out); }

@keyframes pop { from { transform: scale(.7); } }
@keyframes ribbon { from { transform: translateX(-50%) scaleX(0); } }

/* Waiting on you. The badge pops in and rings three times, then lets you be. */

.nav-badge { animation: pop .5s var(--ease-spring) backwards, halo 2.2s ease-out .6s 3; }

@keyframes halo {
    0%        { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brass) 55%, transparent); }
    70%, 100% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--brass) 0%, transparent); }
}

/* An empty list is still an invitation: the blank tag turns gently on its string, and the mark
   breathes a ring outwards. */

.empty-state .tag { animation: sway 5s ease-in-out .8s infinite; }

@keyframes sway {
    0%, 100% { transform: rotate(0); }
    30%      { transform: rotate(-7deg); }
    70%      { transform: rotate(4deg); }
}

.empty-mark { position: relative; }

.empty-mark::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    opacity: 0;
    animation: ring 3.2s ease-out 1s infinite;
}

@keyframes ring {
    0%   { opacity: .4; transform: scale(.92); }
    100% { opacity: 0; transform: scale(1.5); }
}

/* --- PhotoPicker ---------------------------------------------------------------------------
   Only the upload progress bar is ours; the thumbnails, the add button and the file input all
   belong to Shiny's MediaPickerButton. */
.photo-picker { display: flex; flex-direction: column; gap: .75rem; }

.photo-progress {
    height: .375rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text) 12%, transparent);
    overflow: hidden;
}

.photo-progress > span {
    display: block;
    height: 100%;
    background: var(--brass, #F2BC6B);
    transition: width .2s ease;
}
