/* ==========================================================================
   Memorial platform design system

   Spec §50: elegant, quiet, warm, minimal, spacious, contemporary. Never
   corporate. Never overtly religious. Never overly sombre.

   No candles, no doves, no black ribbons. The person's photograph and the
   memories people share provide the emotional character; everything here gets
   out of their way.

   No build step and no web fonts: a contributor standing outside a church on
   a weak connection should not be waiting on a font file.
   ========================================================================== */

/* --------------------------------------------------------------- tokens */

/* ---------------------------------------------------------------- fonts */

/* Served from this domain — no third-party request, so nothing about a
   visitor is disclosed to anyone in order to render her name, and nothing
   breaks if an outside service goes away. Licences alongside them. 180KB.

   Newsreader carries her name and the editorial text: an old-style face
   drawn for screen reading, warm rather than austere, with an optical-size
   axis so it holds together at 56px and at 20px.

   Its variable italic is another 144KB on its own, so it is deliberately not
   shipped — the dedication is set in the roman, which reads better on a
   phone anyway. Nothing on the public pages asks the serif for italics.

   Source Sans 3 carries the interface: navigation, buttons, filters,
   metadata, forms. Quiet, highly legible, and content to stay out of the way.

   swap: her name renders immediately in a fallback and reflows once the file
   lands. On a memorial the wait must never be a blank page. */
@font-face {
  font-family: "Newsreader";
  src: url("../fonts/newsreader-latin.0a301b6df4c7.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Source Sans 3";
  src: url("../fonts/sourcesans3-latin.fb6579a30024.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Source Sans 3";
  src: url("../fonts/sourcesans3-latin-italic.d36405dc9763.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

:root {
  --paper:        #faf8f4;
  --paper-2:      #ffffff;
  --paper-3:      #f1ede7;   /* soft UI background */
  --paper-sunk:   #ebe6de;

  --ink:          #282522;   /* warm charcoal, never pure black */
  --ink-2:        #5b544e;
  --ink-3:        #77706a;   /* secondary text — 4.6:1 on paper */
  --ink-inverse:  #faf8f4;

  --line:         #ded7cf;
  --line-2:       #ece7e0;
  --line-strong:  #cdc4ba;

  /* Warm stone. Used for rules and quiet marks, not as a brand colour. */
  --stone:        #968a80;
  /* The one dark action colour. */
  --action:       #2d2926;

  --accent:       #5c7365;
  --accent-deep:  #435449;
  --accent-wash:  #edf1ed;
  --accent-line:  #c6d2c7;

  --good:         #4f7355;
  --good-wash:    #eef3ee;
  --warn:         #8a6534;
  --warn-wash:    #f7f1e7;
  --bad:          #96453b;
  --bad-wash:     #f8eeec;

  --focus:        #6f8c7b;

  /* Her name, headings, and editorial text meant to be read slowly. */
  --font-display: "Newsreader", "Iowan Old Style", "Source Serif 4", Georgia, serif;
  /* The interface: navigation, buttons, filters, metadata, forms. */
  --font-ui:      "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI",
                  Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-body:    var(--font-ui);

  --step--1: clamp(0.82rem, 0.80rem + 0.10vw, 0.88rem);
  --step-0:  clamp(1.00rem, 0.97rem + 0.15vw, 1.08rem);
  --step-1:  clamp(1.15rem, 1.09rem + 0.30vw, 1.32rem);
  --step-2:  clamp(1.36rem, 1.24rem + 0.58vw, 1.72rem);
  --step-3:  clamp(1.62rem, 1.38rem + 1.10vw, 2.30rem);
  --step-4:  clamp(2.45rem, 1.95rem + 2.10vw, 3.50rem);

  --sp-1: 0.25rem;  --sp-2: 0.5rem;   --sp-3: 0.75rem;  --sp-4: 1rem;
  --sp-5: 1.5rem;   --sp-6: 2rem;     --sp-7: 3rem;     --sp-8: 4rem;
  --sp-9: 6rem;

  /* Clearance for a home indicator or an overlaid navigation bar.
     Capped, and deliberately so. Phones disagree about this: some report
     nothing, some report the whole height of the navigation bar even though
     the page already stops above it. Added raw to a bar that carries its own
     padding, the larger readings doubled the bar's height — which is what
     made the "Add memory" bar look twice as tall on some phones and not
     others. Bounded, the worst case is a slightly deeper bar.

     Split in two so the cap can be tested: a desktop browser always reports
     zero, so the tests set --safe-b-raw to what a phone reports. */
  --safe-b-raw: env(safe-area-inset-bottom, 0px);
  --safe-b: min(var(--safe-b-raw), var(--sp-4));

  --radius:    10px;
  --radius-lg: 16px;
  --radius-sm: 6px;

  /* One hero shape, used by the public page AND the family's cropper, and
     matching the ratio the server actually renders (images.crop_to_hero).
     When these three disagree the photograph gets cropped twice and the
     family cannot see what they are framing. */
  --hero-ratio: 4 / 3;

  --shadow:    0 1px 2px rgba(34, 31, 27, 0.04), 0 8px 24px -16px rgba(34, 31, 27, 0.18);
  --shadow-lg: 0 2px 6px rgba(34, 31, 27, 0.06), 0 24px 60px -28px rgba(34, 31, 27, 0.30);

  --measure: 34rem;
  --page:    72rem;
  --page-narrow: 44rem;

  --ease: cubic-bezier(0.2, 0, 0.15, 1);
  --tap: 44px;
  --nav-h: 60px;

  color-scheme: light;
}

/* System preference, unless the viewer has explicitly asked for light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:       #16150f;
    --paper-2:     #1e1d18;
    --paper-3:     #262520;
    --paper-sunk:  #121108;

    --ink:         #f0ebe2;
    --ink-2:       #bcb5a9;
    --ink-3:       #8d8579;
    --ink-inverse: #16150f;

    --line:        #34322b;
    --line-2:      #262520;
    --line-strong: #4a463d;

    --stone:       #8d8579;
    --action:      #f0ebe2;

    --accent:      #98b0a0;
    --accent-deep: #b6c9bb;
    --accent-wash: #222a24;
    --accent-line: #3c4a40;

    --good:        #8fb396;   --good-wash: #202a22;
    --warn:        #c69a63;   --warn-wash: #2b2419;
    --bad:         #d0897e;   --bad-wash:  #2d1f1c;

    --focus:       #9dbcab;

    --shadow:    0 1px 2px rgba(0, 0, 0, 0.30), 0 8px 24px -16px rgba(0, 0, 0, 0.60);
    --shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.35), 0 24px 60px -28px rgba(0, 0, 0, 0.70);

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --paper:       #16150f;
  --paper-2:     #1e1d18;
  --paper-3:     #262520;
  --paper-sunk:  #121108;

  --ink:         #f0ebe2;
  --ink-2:       #bcb5a9;
  --ink-3:       #8d8579;
  --ink-inverse: #16150f;

  --line:        #34322b;
  --line-2:      #262520;
  --line-strong: #4a463d;

  --stone:       #8d8579;
  --action:      #f0ebe2;

  --accent:      #98b0a0;
  --accent-deep: #b6c9bb;
  --accent-wash: #222a24;
  --accent-line: #3c4a40;

  --good:        #8fb396;   --good-wash: #202a22;
  --warn:        #c69a63;   --warn-wash: #2b2419;
  --bad:         #d0897e;   --bad-wash:  #2d1f1c;

  --focus:       #9dbcab;

  --shadow:    0 1px 2px rgba(0, 0, 0, 0.30), 0 8px 24px -16px rgba(0, 0, 0, 0.60);
  --shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.35), 0 24px 60px -28px rgba(0, 0, 0, 0.70);

  color-scheme: dark;
}

/* ----------------------------------------------------------------- base */

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

html { -webkit-text-size-adjust: 100%; }

/* `hidden` has to win. It is a user-agent rule, so any class of ours that sets
   `display` quietly beats it — `.btn` does, which left "Retry 0 uploads" sitting
   on the share form whenever nothing had failed, and asked a contributor to
   retry an upload that had gone perfectly. Two elements had already been
   patched for this one at a time; this is the rule they were working around. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-wrap: break-word;
}

/* Headings are Inter too. The serif is a guest on this page — her name, the
   dedication, a quoted memory — and headings that all shouted in it would
   spend the effect before it reached her. */
h1, h2, h3, h4 {
  font-family: var(--font-ui);
  font-weight: 600;
  line-height: 1.18;
  letter-spacing: -0.01em;
  margin: 0 0 var(--sp-4);
  color: var(--ink);
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

p { margin: 0 0 var(--sp-4); }
p:last-child { margin-bottom: 0; }

a {
  color: var(--accent-deep);
  text-decoration-color: var(--accent-line);
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-color: currentColor; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: 3px;
}

img, video, svg { max-width: 100%; height: auto; display: block; }

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--sp-7) 0;
}

small { font-size: var(--step--1); }

::selection { background: var(--accent-wash); color: var(--ink); }

/* ------------------------------------------------------------- utilities */

.wrap        { width: 100%; max-width: var(--page); margin-inline: auto; padding-inline: var(--sp-5); }
.wrap--narrow{ max-width: var(--page-narrow); }
.wrap--wide  { max-width: 84rem; }

.measure     { max-width: var(--measure); }
.centre      { text-align: center; }
.centre-x    { margin-inline: auto; }

.stack > * + * { margin-top: var(--sp-4); }
.stack-sm > * + * { margin-top: var(--sp-2); }
.stack-lg > * + * { margin-top: var(--sp-6); }

.row { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; }
.row--between { justify-content: space-between; }
.row--end { justify-content: flex-end; }
.row--tight { gap: var(--sp-2); }
.row--top { align-items: flex-start; }

.grow { flex: 1 1 auto; min-width: 0; }
.muted { color: var(--ink-2); }
.quiet { color: var(--ink-3); font-size: var(--step--1); }
.display { font-family: var(--font-display); }
.nowrap { white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }

.hide { display: none !important; }
@media (max-width: 47.9em) { .hide-sm { display: none !important; } }
@media (min-width: 48em)   { .only-sm { display: none !important; } }

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

.skip-link {
  position: absolute; left: var(--sp-4); top: -100px; z-index: 100;
  background: var(--paper-2); color: var(--ink);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.skip-link:focus { top: var(--sp-4); }

/* --------------------------------------------------------------- buttons */

.btn {
  --btn-bg: var(--paper-2);
  --btn-ink: var(--ink);
  --btn-line: var(--line-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0.7rem 1.15rem;
  background: var(--btn-bg);
  color: var(--btn-ink);
  border: 1px solid var(--btn-line);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--step-0);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.16s var(--ease), border-color 0.16s var(--ease),
              transform 0.16s var(--ease), color 0.16s var(--ease);
}
/* Sets the token, not the property. Setting `background` directly here beat
   every variant's --btn-bg — same specificity, and a direct declaration wins
   over one that only redefines a variable — so *every* button went pale cream
   on hover while keeping its own text colour. On the primary button that put
   near-white text on cream: invisible, on the one control the page exists to
   get people to press. */
.btn:hover { --btn-bg: var(--paper-3); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }

.btn--primary {
  --btn-bg: var(--ink);
  --btn-ink: var(--ink-inverse);
  --btn-line: var(--ink);
}
.btn--primary:hover { --btn-bg: #383029; --btn-line: #383029; }
/* On dark, "ink" is already light — brighten rather than darken on hover. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn--primary:hover { --btn-bg: #ffffff; --btn-line: #ffffff; }
}
:root[data-theme="dark"] .btn--primary:hover { --btn-bg: #ffffff; --btn-line: #ffffff; }

.btn--accent {
  --btn-bg: var(--accent-deep);
  --btn-ink: #ffffff;
  --btn-line: var(--accent-deep);
}
/* Without its own hover this would fall through to .btn:hover and put white
   text on pale cream, the same fault as the primary button. */
.btn--accent:hover { --btn-bg: var(--accent); --btn-line: var(--accent); }

.btn--ghost { --btn-bg: transparent; --btn-line: transparent; }
.btn--ghost:hover { --btn-bg: var(--paper-3); }

.btn--outline { --btn-bg: transparent; }

.btn--danger { --btn-ink: var(--bad); --btn-line: var(--line-strong); }
.btn--danger:hover { --btn-bg: var(--bad-wash); }

.btn--lg {
  min-height: 56px;
  padding: 1rem 2rem;
  font-size: var(--step-1);
  border-radius: 14px;
}
.btn--sm { min-height: 36px; padding: 0.35rem 0.75rem; font-size: var(--step--1); }
.btn--block { width: 100%; }
.btn--icon { padding: 0.55rem; min-width: var(--tap); }

.btn-group { display: inline-flex; gap: var(--sp-2); flex-wrap: wrap; }

/* ----------------------------------------------------------------- forms */

.field { margin-bottom: var(--sp-5); }
.field:last-child { margin-bottom: 0; }

label, .label {
  display: block;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.01em;
  margin-bottom: var(--sp-2);
  color: var(--ink-2);
}

input[type="text"], input[type="email"], input[type="password"], input[type="url"],
input[type="date"], input[type="time"], input[type="number"], input[type="search"],
input[type="tel"], select, textarea {
  width: 100%;
  min-height: var(--tap);
  padding: 0.65rem 0.85rem;
  background: var(--paper-2);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: max(16px, var(--step-0)); /* never trigger iOS zoom */
  transition: border-color 0.16s var(--ease), box-shadow 0.16s var(--ease);
}
textarea { min-height: 7rem; resize: vertical; line-height: 1.55; }

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

input::placeholder, textarea::placeholder { color: var(--ink-3); }

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
                    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position: right 1.05rem center, right 0.75rem center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 2.25rem;
}

.input--code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 1.35rem;
  letter-spacing: 0.28em;
  text-align: center;
  text-transform: uppercase;
}

.help { font-size: var(--step--1); color: var(--ink-3); margin-top: var(--sp-2); }
.error-text { font-size: var(--step--1); color: var(--bad); margin-top: var(--sp-2); }

/* A field that has been asked about and answered wrongly, or not at all.
   `_field.html` has emitted this class since the beginning and nothing ever
   styled it, so a rejected form went red in the small print underneath and
   left the box itself looking exactly like the ones that were fine. */
.field--error input,
.field--error select,
.field--error textarea {
  border-color: var(--bad);
  background: color-mix(in srgb, var(--bad) 5%, var(--paper-2));
}
.field--error input:focus,
.field--error select:focus,
.field--error textarea:focus {
  border-color: var(--bad);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad) 22%, transparent);
}

/* "(required)", set exactly like the "(optional)" it answers. Both are needed
   or neither reads as information: a lone "(optional)" on two fields out of
   three leaves the third looking the same as an unmarked one. */
.req { font-weight: 400; color: var(--ink-3); }

.check {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  padding: var(--sp-3) 0;
  cursor: pointer;
  font-weight: 400;
  font-size: var(--step-0);
  color: var(--ink);
  letter-spacing: 0;
  margin: 0;
}
.check input[type="checkbox"], .check input[type="radio"] {
  width: 1.25rem; height: 1.25rem;
  margin: 0.2rem 0 0;
  accent-color: var(--accent-deep);
  flex: none;
}
.check__body { flex: 1 1 auto; }
.check__note { display: block; font-size: var(--step--1); color: var(--ink-3); }

.choice-list { display: grid; gap: var(--sp-2); }
.choice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  cursor: pointer;
  transition: border-color 0.16s var(--ease), background 0.16s var(--ease);
  margin: 0; font-weight: 400; font-size: var(--step-0); color: var(--ink);
  letter-spacing: 0;
}
.choice:hover { border-color: var(--line-strong); }
.choice:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
.choice input { margin-top: 0.25rem; accent-color: var(--accent-deep); flex: none;
                width: 1.15rem; height: 1.15rem; }

fieldset { border: 0; padding: 0; margin: 0 0 var(--sp-5); }
legend { padding: 0; margin-bottom: var(--sp-3); font-weight: 600; font-size: var(--step--1);
         color: var(--ink-2); }

.form-actions {
  display: flex; gap: var(--sp-3); flex-wrap: wrap; align-items: center;
  margin-top: var(--sp-6); padding-top: var(--sp-5);
  border-top: 1px solid var(--line-2);
}

/* ----------------------------------------------------------------- cards */

.card {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--sp-5);
}
.card--flush { padding: 0; overflow: hidden; }
.card--sunk { background: var(--paper-3); border-color: transparent; }
.card__head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-4); margin-bottom: var(--sp-4);
}
.card__head h2, .card__head h3 { margin: 0; }

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper-2);
  overflow: hidden;
}
.panel__head {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line-2);
  display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4);
}
.panel__body { padding: var(--sp-5); }

.note {
  padding: var(--sp-4);
  border-radius: var(--radius);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  font-size: var(--step--1);
  color: var(--ink-2);
}
.note--warn { background: var(--warn-wash); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.note--bad  { background: var(--bad-wash);  border-color: color-mix(in srgb, var(--bad) 30%, transparent); }

/* ----------------------------------------------------------- link sharing */

.linkcard { margin-bottom: var(--sp-6); }
.linkcard__title { margin: 0 0 var(--sp-4); font-size: var(--step-1); }

.linkrow {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line-2);
}
.linkrow:first-of-type { border-top: 0; padding-top: 0; }
.linkrow__body { min-width: 0; flex: 1 1 auto; }
.linkrow__label {
  display: block;
  font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 2px;
}
/* The address is the point of the row, so it is set larger than the label and
   allowed to wrap rather than being cut off mid-domain on a phone. */
.linkrow__url {
  display: block;
  font-size: var(--step-0);
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.linkrow__url:hover { text-decoration: underline; }
@media (max-width: 30em) {
  .linkrow { align-items: flex-start; gap: var(--sp-3); }
}

/* A word, not a dot. Sits along the bottom of the thumbnail so it is legible
   against a photograph without hiding much of it. */
.mcard__state {
  position: absolute;
  left: var(--sp-2);
  bottom: var(--sp-2);
  z-index: 2;
  font-size: 0.66rem;
  padding: 0.15em 0.5em;
  pointer-events: none;
  box-shadow: 0 1px 2px rgba(20, 18, 15, 0.18);
}

/* --------------------------------------------------------------- sendbar */

/* Sticks to the bottom of the screen while the form is still being filled in,
   and settles into place at the end of it. Sticky rather than fixed, so it
   never covers the last field on a small screen. */
.sendbar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  margin-inline: calc(-1 * var(--sp-5));
  padding: var(--sp-4) var(--sp-5) calc(var(--sp-4) + var(--safe-b));
  background: color-mix(in srgb, var(--paper) 94%, transparent);
  backdrop-filter: saturate(140%) blur(10px);
  border-top: 1px solid var(--line);
}
/* Sticky only travels inside its own containing block, and the tray of
   thumbnails grows as a sibling above this bar rather than around it — so
   once photographs were queued the bar sat below the fold, which is exactly
   when someone needs to watch it. While an upload is running it comes off
   the flow entirely and floats, so the progress is on screen wherever the
   contributor happens to be scrolled. */
.sendbar[data-floating="true"] {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  margin-inline: 0;
  padding-inline: var(--sp-5);
  box-shadow: 0 -8px 28px -18px rgba(0, 0, 0, 0.55);
}
/* Holds the space the bar vacated, so the page does not jump under the
   contributor's thumb the moment an upload starts. */
.sendbar__spacer { display: none; }
.sendbar__spacer[data-active="true"] { display: block; }
.sendbar__btn {
  font-size: var(--step-1);
  min-height: 60px;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.sendbar__note {
  margin: var(--sp-3) 0 0;
  text-align: center;
  font-size: var(--step--1);
  color: var(--ink-3);
}
/* What is still missing, in the bar the contributor is actually looking at.
   The form's own error line sits above a bar that is `position: fixed` once
   anything is queued, so on a phone it was rendered off the top of the screen
   — the send was refused, and the only sign of it was out of sight. */
.sendbar__need {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  color: var(--ink);
  font-size: var(--step--1);
  text-align: center;
}

.sendbar__progress { margin-bottom: var(--sp-3); }
.sendbar__track {
  height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--line);
}
.sendbar__fill {
  height: 100%; width: 0; border-radius: 999px;
  background: var(--accent);
  transition: width 0.3s var(--ease);
}
.sendbar__status {
  display: block;
  margin-top: var(--sp-2);
  text-align: center;
  font-size: var(--step--1);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------ access code */

.codecard__title { margin: 0 0 var(--sp-2); font-size: var(--step-1); }
.codecard__lead { color: var(--ink-2); margin: 0 0 var(--sp-4); }

/* The code is read off this screen and said down a telephone, so it is set
   large, spaced, and in a face where 0 and O cannot be confused. */
.codeshow {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: clamp(2rem, 1.2rem + 3.5vw, 3rem);
  letter-spacing: 0.22em;
  /* The tracking is trailing whitespace on the last glyph; nudge it back so
     the code sits optically centred in its panel. */
  text-indent: 0.22em;
  text-align: center;
  color: var(--ink);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-3);
  margin: 0 0 var(--sp-5);
  user-select: all;
}

/* ----------------------------------------------------------------- pills */

.pill {
  display: inline-flex; align-items: center; gap: 0.35em;
  padding: 0.2em 0.6em;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: var(--paper-3);
  color: var(--ink-2);
  border: 1px solid var(--line);
  white-space: nowrap;
}
.pill--good { background: var(--good-wash); color: var(--good); border-color: transparent; }
.pill--warn { background: var(--warn-wash); color: var(--warn); border-color: transparent; }
.pill--bad  { background: var(--bad-wash);  color: var(--bad);  border-color: transparent; }
.pill--wait { background: var(--paper-3); color: var(--ink-3); }
.pill--accent { background: var(--accent-wash); color: var(--accent-deep); border-color: transparent; }

/* -------------------------------------------------------------- messages */

.flash-stack { position: sticky; top: var(--sp-3); z-index: 40; }
.flash {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-4);
  margin-bottom: var(--sp-3);
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: var(--step--1);
}
.flash--error { border-left-color: var(--bad); }
.flash--warning { border-left-color: var(--warn); }

/* =====================================================  public memorial  */

.memorial-nav {
  position: sticky; top: 0; z-index: 30;
  backdrop-filter: saturate(140%) blur(12px);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s var(--ease);
}
.memorial-nav[data-stuck="true"] { border-bottom-color: var(--line); }
.memorial-nav__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); min-height: var(--nav-h);
}
/* Her name is in the bar from the moment the page opens. It used to fade in
   only once you had scrolled past the hero, which left the top of the page
   looking empty and half-built — and meant that anyone landing on the gallery
   or the memories page saw a nameless bar. */
.memorial-nav__name {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.memorial-nav__links {
  display: flex; align-items: center; gap: var(--sp-5);
  font-family: var(--font-ui);
  /* Fluid, like the rest of the type. Pinned at 0.76rem it stayed 12px on a
     27-inch display while the body text around it grew — small capitals that
     read as an afterthought rather than as navigation. */
  font-size: clamp(0.84rem, 0.74rem + 0.34vw, 1rem);
  /* Eased back from 0.14em: enough to read as small capitals, not so much
     that the words come apart. */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  flex: 0 1 auto; min-width: 0;
}
.memorial-nav__links a {
  /* A step darker than before: this was quiet to the point of being hard to
     read against warm paper. */
  color: var(--ink-2); text-decoration: none; white-space: nowrap;
  padding-block: 0.45rem;
  border-bottom: 1px solid transparent;
  transition: color 0.16s var(--ease), border-color 0.16s var(--ease);
}
.memorial-nav__links a:hover { color: var(--ink); border-bottom-color: var(--stone); }
.memorial-nav__links a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--stone); }

/* The family's own door, set apart from the visitor navigation. */
.memorial-nav__family {
  display: inline-flex; align-items: center; gap: 0.4em;
  margin-left: var(--sp-3);
  padding-left: var(--sp-5);
  border-left: 1px solid var(--line);
  color: var(--ink-3) !important;
}
.memorial-nav__family svg { width: 11px; height: 11px; opacity: 0.75; }

/* The short form is only for narrow screens; both are in the markup so the
   swap needs no JavaScript. */
.memorial-nav__short { display: none; }

@media (max-width: 40em) {
  .memorial-nav__inner { gap: var(--sp-3); }
  /* This used to shrink to 0.72rem — about 11.5px on a large phone, which is
     smaller than the navigation on the same phone's own settings screen, and
     too small for the people this site is mostly for. The width it needs comes
     back out of the tracking instead: small capitals survive tighter spacing
     far better than they survive being set at eleven pixels. */
  .memorial-nav__links {
    gap: var(--sp-2);
    font-size: clamp(0.84rem, 0.72rem + 0.72vw, 0.95rem);
    letter-spacing: 0.06em;
    /* The safety net, and it must be here rather than only on the narrowest
       phones: with the links set large enough to read, the row can exceed a
       390px screen, and spilling visibly dragged the whole page sideways. */
    overflow-x: auto;
    scrollbar-width: none;
  }
  .memorial-nav__links::-webkit-scrollbar { display: none; }

  /* Inner pages keep a name — but "Val" rather than "Valerie Ann Bennett",
     which was being cut off mid-word to make room. */
  .memorial-nav__full { display: none; }
  .memorial-nav__short { display: inline; }
  .memorial-nav__name { font-size: var(--step-0); flex: 0 1 auto; }

  /* On the landing page the name is set enormous a few centimetres below, so
     repeating it in the bar bought nothing and cost the links their room.
     Dropped here, the links take the full width instead of scrolling. */
  .memorial-nav--home .memorial-nav__name { display: none; }
  .memorial-nav--home .memorial-nav__links {
    width: 100%;
    justify-content: space-between;
    gap: var(--sp-2);
  }
}

/* Narrowest phones: tighten the tracking a little further, then let the row
   scroll. Scrolling is the better trade — the words stay readable, and the
   three of them are in a fixed order people learn. */
@media (max-width: 24em) {
  .memorial-nav__links { font-size: 0.82rem; letter-spacing: 0.04em; }
  .memorial-nav__links {
    overflow-x: auto;
    scrollbar-width: none;
  }
  .memorial-nav__links::-webkit-scrollbar { display: none; }
}

.hero {
  /* Tight at the top on a phone. Everything that matters — her name, the
     dates, and the one thing we are asking people to do — has to be reachable
     without scrolling, and padding here is the cheapest thing to give up. */
  padding: var(--sp-4) 0 var(--sp-5);
  text-align: center;
}
/* A mounted photograph: a hairline warm border, an ivory mount, and the
   picture inside. The previous taupe surround read as a heavy frame and
   competed with the photograph; a mount should be almost unnoticed. */
.hero__frame {
  width: min(100%, 30rem);
  margin: 0 auto var(--sp-5);
  aspect-ratio: var(--hero-ratio);
  /* Belt and braces for short screens (a phone held sideways, a small
     laptop): the photograph never takes more than half the window. */
  max-height: 52svh;
  padding: 7px;
  background: #fdfcfa;
  border: 1px solid var(--line);
  box-shadow: none;
  border-radius: 2px;
}
.hero__frame > img {
  border-radius: 1px;
}
@media (min-width: 48em) {
  /* Tightened so that the photographs below are reachable without scrolling
     on a laptop. Everything the family wrote is still here — her name, the
     dates, the place, the dedication, the invitation — it simply sits closer
     together. */
  .hero { padding: var(--sp-5) 0 var(--sp-5); }
  .hero__frame {
    /* Still the emotional centre of the page (spec §50), but measured
       against the window rather than given a fixed 40rem: on a short laptop
       screen that alone was more than half the height before her name. */
    width: min(100%, 34rem);
    max-height: min(46svh, 26rem);
    margin-bottom: var(--sp-5);
    padding: 10px;
  }
}
.hero__frame img { width: 100%; height: 100%; object-fit: cover; }

.hero__frame--empty {
  display: grid; place-items: center;
  border: 1px dashed var(--line-strong);
  box-shadow: none;
  color: var(--ink-3);
  font-size: var(--step--1);
  padding: var(--sp-5);
}

/* Her name, set as it would be on the card itself: large, lightly letterspaced
   rather than tightened — a Garamond at display size wants air, not the
   negative tracking a grotesque needs. */
/* Her name is the masthead of the whole site, so it is stated properly: the
   display serif at a genuine bold, not the interface font at 600.
   The font-family is set explicitly here because the global heading rule
   points h1 at the UI font — without this line her name quietly rendered in
   Inter while the bar above it was in the serif, which is what made it look
   unfinished. */
.hero__name {
  font-family: var(--font-display);
  font-size: var(--step-4);
  margin: 0 0 var(--sp-4);
  /* Newsreader is an old-style face and does not want the tight tracking a
     high-contrast display serif did. */
  letter-spacing: -0.005em;
  line-height: 1.08;
  font-weight: 500;
  text-wrap: balance;
}

/* The hairline rule and the dates sit together as one device, the way a
   printed order of service sets them. */
.hero__rule {
  width: 4.5rem;
  height: 1px;
  margin: 0 auto var(--sp-4);
  border: 0;
  background: var(--line-strong);
}
/* Spaced lining figures, as they would be engraved or letterpressed. The
   wide tracking is the whole effect, so it is deliberately generous. */
.hero__dates {
  font-family: var(--font-ui);
  font-size: var(--step-1);
  color: var(--ink-2);
  letter-spacing: 0.18em;
  font-variant-numeric: lining-nums;
  /* Tracking adds space after the final glyph; pull it back so the line sits
     optically centred under her name. */
  text-indent: 0.18em;
  margin: 0 0 var(--sp-3);
}
/* The dates and the place read as one line of biography, so they sit close
   together. Browsers without :has() simply leave the usual gap. */
.hero__dates:has(+ .hero__place) { margin-bottom: var(--sp-2); }
/* Small capitals, widely spaced — the convention for a place name on a
   memorial card, and it stops "Brentwood" competing with the dates. */
.hero__place {
  font-size: 0.78rem;
  color: var(--ink-3);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-indent: 0.22em;
  margin: 0 0 var(--sp-5);
}
/* The dedication. Editorial in character but set in the roman rather than a
   sweeping italic: at phone sizes an italic serif of this size is markedly
   harder to read, and this is the sentence the family most want read. */
.hero__intro {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--step-1);
  color: var(--ink-2);
  max-width: 30rem;
  margin: 0 auto var(--sp-5);
  line-height: 1.55;
  text-wrap: pretty;
}
@media (min-width: 48em) {
  .hero__intro { font-size: var(--step-2); line-height: 1.5; }
}
@media (min-width: 48em) {
  .hero__dates { margin-bottom: var(--sp-3); }
  .hero__place { margin-bottom: var(--sp-4); }
  .hero__intro { margin-bottom: var(--sp-5); line-height: 1.5; }
}

.gallery-locked {
  margin-top: var(--sp-6);
  text-align: center;
}
.gallery-locked__note {
  max-width: 26rem;
  margin: 0 auto var(--sp-4);
  color: var(--ink-2);
  font-size: var(--step--1);
}

.share-cta { margin-top: var(--sp-5); }
.share-cta__note {
  max-width: 26rem;
  margin: var(--sp-4) auto 0;
  color: var(--ink-3);
  font-size: var(--step--1);
  line-height: 1.55;
}
@media (min-width: 48em) {
  .share-cta { margin-top: var(--sp-5); }
  .share-cta__note { margin-top: var(--sp-3); }
}

/* The invitation on a memorial page should be quietly worded and quietly set.
   A solid near-black block was the loudest thing on the page and the least in
   keeping with it — this is an engraved-looking outline that fills on hover,
   so it still reads unmistakably as the thing to press. */
.memorial-page .btn {
  font-family: var(--font-ui);
  letter-spacing: 0.06em;
  border-radius: 2px;
}
.memorial-page .btn--primary {
  --btn-bg: transparent;
  --btn-ink: var(--ink);
  --btn-line: var(--ink-3);
  padding: 0.85rem 2.25rem;
}
.memorial-page .btn--primary:hover {
  --btn-bg: var(--ink);
  --btn-ink: var(--ink-inverse);
  --btn-line: var(--ink);
}
/* The sticky bar that follows you down the page is the exception: it is a
   piece of interface, not part of the card, and it has to hold its own
   against whatever photograph is scrolling behind it. */

.section {
  padding: var(--sp-6) 0;
  border-top: 1px solid var(--line-2);
}
@media (min-width: 48em) { .section { padding: var(--sp-7) 0; } }
/* The first section after the hero sits closer to it: the border already
   separates them, and a full gap on both sides was pushing the photographs
   below the fold for no gain. */
.hero + .section,
.hero + section.section { padding-top: var(--sp-5); }
/* Section headings as small spaced capitals rather than big serif headlines.
   On a memorial card the person's name is the only thing set large; every
   other heading is a label, and labels should not compete with her. */
.section__title {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-indent: 0.2em;
  color: var(--ink-3);
  text-align: center;
  margin-bottom: var(--sp-5);
}
/* A hairline above each heading, the way sections are ruled off in print. */
.section__title::before {
  content: "";
  display: block;
  width: 2.5rem;
  height: 1px;
  margin: 0 auto var(--sp-4);
  background: var(--line-strong);
}
.section__title--left { text-align: left; }

.statement {
  max-width: var(--measure);
  margin-inline: auto;
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.7;
  color: var(--ink-2);
}
.statement p + p { margin-top: var(--sp-4); }
.statement strong { color: var(--ink); font-weight: 600; }

.funeral {
  max-width: 38rem; margin-inline: auto;
}
.funeral__when {
  font-family: var(--font-display);
  font-size: var(--step-2);
  text-align: center;
  margin-bottom: var(--sp-3);
}
.funeral__where {
  text-align: center; color: var(--ink-2); margin-bottom: var(--sp-5);
  font-size: var(--step-0); line-height: 1.5;
}
.funeral__grid { display: grid; gap: var(--sp-5); margin-top: var(--sp-6); }
@media (min-width: 40em) { .funeral__grid { grid-template-columns: 1fr 1fr; } }
.funeral__item h3 {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--sp-2);
}
.funeral__item p { color: var(--ink-2); font-size: var(--step-0); }

/* ---------------------------------------------------------------- galleries */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--sp-3);
}
@media (min-width: 48em) {
  .tiles { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--sp-4); }
}
/* On a phone the gallery runs edge to edge with hairline gaps, the way a
   camera roll does. Inset by 24px each side with a 12px gutter, the same two
   columns were a third smaller, and the photographs are the point of the
   page. */
@media (max-width: 47.9em) {
  .memorial-page [data-gallery-grid] {
    margin-inline: calc(-1 * var(--sp-5));
    gap: 3px;
    grid-template-columns: repeat(2, 1fr);
  }
  .memorial-page [data-gallery-grid] .tile { border-radius: 0; }
}

.tiles--dense { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--sp-2); }
.tiles--large { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
/* auto-fill leaves a handful of photographs stranded against the left edge;
   auto-fit + a centred track keeps a small preview looking deliberate. */
.tiles--preview {
  grid-template-columns: repeat(auto-fit, minmax(160px, 220px));
  justify-content: center;
}

.tile {
  position: relative;
  /* Portrait, not square. These are photographs of people, and a 1:1 crop
     takes the top off their heads — the one thing this product must never
     do. 4:5 holds a standing figure or a face-and-shoulders intact, and the
     lightbox shows the whole uncropped frame. */
  aspect-ratio: 4 / 5;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
  border: 0;
  padding: 0;
  cursor: pointer;
  display: block;
  width: 100%;
}
.tile img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 0.5s var(--ease), opacity 0.4s var(--ease);
  opacity: 0;
}
.tile img.is-loaded { opacity: 1; }
.tile:hover img { transform: scale(1.03); }

.tile__video-mark {
  position: absolute; inset: auto var(--sp-2) var(--sp-2) auto;
  display: inline-flex; align-items: center; gap: 0.3em;
  padding: 0.2em 0.5em;
  background: rgba(20, 18, 15, 0.72);
  color: #fff;
  border-radius: 999px;
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  backdrop-filter: blur(4px);
}
.tile__caption {
  position: absolute; inset: auto 0 0 0;
  padding: var(--sp-5) var(--sp-3) var(--sp-3);
  background: linear-gradient(transparent, rgba(20, 18, 15, 0.78));
  color: #fff;
  font-size: var(--step--1);
  text-align: left;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}
/* Hover reveals nothing on a touchscreen, so the caption would simply never
   be seen on a phone. There, it stays visible; the lightbox carries the full
   attribution either way. */
@media (hover: hover) {
  .tile__caption { opacity: 0; }
  .tile:hover .tile__caption, .tile:focus-visible .tile__caption { opacity: 1; }
}
@media (hover: none) {
  .tile__caption {
    opacity: 1;
    font-size: 0.72rem;
    padding: var(--sp-6) var(--sp-2) var(--sp-2);
  }
}

/* A play triangle in the corner is easy to miss at thumbnail size; centred
   and sized properly, a video reads as a video at a glance. */
.tile__play {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  pointer-events: none;
}
.tile__play span {
  display: grid; place-items: center;
  width: 3rem; height: 3rem;
  border-radius: 50%;
  background: rgba(20, 18, 15, 0.55);
  backdrop-filter: blur(4px);
  color: #fff;
  font-size: 1rem;
  padding-left: 3px;
}

/* ------------------------------------------------------------- memories */

.memory {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--sp-6) 0;
  border-bottom: 1px solid var(--line-2);
}
.memory:last-child { border-bottom: 0; }
.memory__text {
  font-family: var(--font-ui);
  font-size: var(--step-1);
  line-height: 1.62;
  color: var(--ink-2);
}
/* Deliberately small and few. The words are what someone came to read; the
   photographs sit under them as evidence, not as a gallery. Four across at
   most, and never wider than a thumb on a phone. */
.memory__photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  max-width: 22rem;
}
.memory__photos .tile { border-radius: var(--radius-sm); }

.memory__by {
  margin-top: var(--sp-4);
  font-size: var(--step--1);
  color: var(--ink-3);
  letter-spacing: 0.02em;
}

.featured-memory {
  max-width: 38rem; margin-inline: auto; text-align: center;
  padding: var(--sp-6) var(--sp-4);
}
.featured-memory__text {
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: 1.5;
  color: var(--ink);
}
.featured-memory__by { margin-top: var(--sp-4); color: var(--ink-3); font-size: var(--step-0); }




/* ===========================================================  uploader  */

.dropzone {
  position: relative;
  display: grid;
  place-items: center;
  gap: var(--sp-3);
  min-height: 12rem;
  padding: var(--sp-6) var(--sp-5);
  border: 2px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--paper-2);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.dropzone:hover { border-color: var(--accent); background: var(--accent-wash); }
.dropzone[data-dragging="true"] {
  border-color: var(--accent);
  background: var(--accent-wash);
  border-style: solid;
}
.dropzone__mark {
  width: 46px; height: 46px;
  fill: none; stroke: var(--ink-3); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  transition: stroke 0.2s var(--ease);
}
.dropzone:hover .dropzone__mark,
.dropzone[data-dragging="true"] .dropzone__mark { stroke: var(--accent-deep); }
.dropzone__title { font-family: var(--font-ui); font-size: var(--step-2); }
.dropzone__hint { color: var(--ink-3); font-size: var(--step--1); max-width: 24rem; }
.dropzone input[type="file"] { display: none; }

/* The three things a hesitant contributor needs to know before tapping. */
/* ------------------------------------------------------------ share page */

.share-page { padding-block: var(--sp-5) var(--sp-9); }

.share-head { margin-bottom: var(--sp-5); }
.share-head h1 {
  font-size: var(--step-3);
  margin: 0 0 var(--sp-2);
}
.share-head p { margin: 0; }

.share-reassure {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3);
  justify-content: center;
  margin: var(--sp-3) 0 0;
  font-size: var(--step--1);
  color: var(--ink-3);
}
.share-reassure span { display: inline-flex; align-items: center; gap: 0.45em; }
/* A tick, not an empty circle — an empty circle reads as an unticked box. */
.share-reassure span::before {
  content: "✓";
  display: inline-grid; place-items: center;
  width: 15px; height: 15px; flex: none;
  border-radius: 50%;
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-size: 9px; line-height: 1; font-weight: 700;
}

.tray { margin-top: var(--sp-5); }
.tray__head {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--sp-4); margin-bottom: var(--sp-3); flex-wrap: wrap;
}
.tray__count { font-family: var(--font-ui); font-size: var(--step-1); }

.tray__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: var(--sp-2);
}
@media (min-width: 40em) {
  .tray__grid { grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: var(--sp-3); }
}

.item {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--paper-3);
  border: 1px solid var(--line);
}
.item img { width: 100%; height: 100%; object-fit: cover; }
.item__file {
  display: grid; place-items: center; height: 100%; padding: var(--sp-2);
  font-size: 0.68rem; color: var(--ink-3); text-align: center; word-break: break-all;
}
.item__remove {
  position: absolute; top: 4px; right: 4px;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(20, 18, 15, 0.66);
  color: #fff; cursor: pointer;
  font-size: 15px; line-height: 1;
  backdrop-filter: blur(4px);
}
.item__remove:hover { background: rgba(20, 18, 15, 0.85); }

.item__bar {
  position: absolute; inset: auto 0 0 0; height: 4px;
  background: rgba(255, 255, 255, 0.35);
}
.item__bar span {
  display: block; height: 100%; width: 0;
  background: var(--accent);
  transition: width 0.25s var(--ease);
}

.item__state {
  position: absolute; inset: auto 0 0 0;
  padding: var(--sp-4) 4px 4px;
  background: linear-gradient(transparent, rgba(20, 18, 15, 0.75));
  color: #fff; font-size: 0.66rem; text-align: center;
}
.item[data-state="done"] .item__state { background: none; }
.item[data-state="done"]::after {
  content: "✓";
  position: absolute; bottom: 5px; left: 6px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--good); color: #fff;
  display: grid; place-items: center; font-size: 12px;
}
.item[data-state="failed"] { border-color: var(--bad); }
.item[data-state="failed"] .item__state { background: var(--bad); }

.progress {
  margin-top: var(--sp-5);
  padding: var(--sp-4);
  border-radius: var(--radius);
  background: var(--paper-3);
}
.progress__label {
  display: flex; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--step--1); margin-bottom: var(--sp-2);
}
.progress__track {
  height: 8px; border-radius: 999px; background: var(--line); overflow: hidden;
}
.progress__fill {
  height: 100%; width: 0; border-radius: 999px;
  background: var(--accent);
  transition: width 0.3s var(--ease);
}
.progress__breakdown {
  display: flex; gap: var(--sp-4); margin-top: var(--sp-3);
  font-size: var(--step--1); color: var(--ink-2); flex-wrap: wrap;
}

/* ==========================================================  family app  */

.app { display: flex; min-height: 100vh; }

.side {
  flex: 0 0 254px;
  border-right: 1px solid var(--line);
  background: var(--paper-2);
  padding: var(--sp-5) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-5);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.side__brand { display: flex; flex-direction: column; gap: 2px; text-decoration: none; }
.side__brand strong { font-family: var(--font-display); font-size: var(--step-1); color: var(--ink); }
.side__brand span { font-size: 0.72rem; color: var(--ink-3); letter-spacing: 0.06em;
                    text-transform: uppercase; }

.side__nav { display: grid; gap: 2px; }
.side__link {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--step--1);
  white-space: nowrap;
  transition: background 0.14s var(--ease), color 0.14s var(--ease);
}
.side__link:hover { background: var(--paper-3); color: var(--ink); }
.side__link[aria-current="page"] { background: var(--accent-wash); color: var(--accent-deep);
                                   font-weight: 600; }
.side__badge {
  background: var(--ink); color: var(--ink-inverse);
  border-radius: 999px; padding: 0.05em 0.5em; font-size: 0.7rem; font-weight: 600;
}
.side__group-label {
  font-size: 0.68rem; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--ink-3); padding: var(--sp-3) 0.7rem var(--sp-1);
}
.side__foot { margin-top: auto; font-size: var(--step--1); }
/* These are anchors; without this they flow inline and read as one sentence. */
.side__foot a { display: block; padding: 3px 0; color: var(--ink-3); text-decoration: none; }
.side__foot a:hover { color: var(--ink); text-decoration: underline; }

/* Sign out is a form button so the request can be a POST, but it belongs in
   the same list as the links above it and has to look like one. */
.side__signout,
.linkish {
  display: block;
  padding: 3px 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink-3);
  text-align: left;
  cursor: pointer;
}
.side__signout:hover,
.linkish:hover { color: var(--ink); text-decoration: underline; }

/* On the memorial list it sits inline in a sentence of links instead. */
.linkish { display: inline; padding: 0; }

/* On desktop the menu is not a menu — it is simply the sidebar, always open.
   The disclosure arrow and its label belong to the phone layout only. */
.side__menu { display: contents; }
.side__menu-toggle { display: none; }
.side__quick { display: none; }

/* ------------------------------------------------- family app on a phone
   This block must stay AFTER the component rules above: they share its
   specificity, so anything declared later would quietly win over it — which
   is exactly what used to happen to the mobile navigation. */
@media (max-width: 61.9em) {
  .app { flex-direction: column; }

  .side {
    position: sticky; top: 0; z-index: 30;
    height: auto; flex: none;
    border-right: 0; border-bottom: 1px solid var(--line);
    flex-direction: column; align-items: stretch; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
  }

  .side__top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
  .side__brand { flex-direction: row; align-items: baseline; gap: var(--sp-2); }
  .side__brand span { font-size: 0.62rem; }

  /* The four things worth a thumb. Scrolls if it must, but on a 360px screen
     the first two — the ones with work waiting in them — are always visible. */
  .side__quick {
    display: flex; gap: var(--sp-2);
    overflow-x: auto;
    scrollbar-width: none;
    /* Full-bleed, so the row visibly runs off the edge and reads as scrollable
       rather than as a list that happens to be clipped. */
    margin-inline: calc(-1 * var(--sp-4));
    padding-inline: var(--sp-4);
    scroll-padding-inline: var(--sp-4);
  }
  .side__quick::-webkit-scrollbar { display: none; }

  .quick {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    flex: 0 0 auto;
    /* 44px tall: a comfortable tap target for someone in their seventies
       holding a phone, which is who is actually using this. */
    min-height: 44px;
    padding: 0 var(--sp-4);
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--paper);
    color: var(--ink-2);
    font-size: var(--step--1);
    text-decoration: none;
    white-space: nowrap;
  }
  .quick[aria-current="page"] {
    background: var(--accent-wash);
    border-color: var(--accent-line);
    color: var(--accent-deep);
    font-weight: 600;
  }

  .side__menu { display: block; border-top: 1px solid var(--line-2); }
  .side__menu-toggle {
    display: flex; align-items: center; gap: var(--sp-2);
    min-height: 44px;
    font-size: var(--step--1);
    color: var(--ink-2);
    cursor: pointer;
    list-style: none;
  }
  .side__menu-toggle::-webkit-details-marker { display: none; }
  .side__menu-toggle::after {
    content: "";
    width: 0.5em; height: 0.5em;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform 0.18s var(--ease);
  }
  .side__menu[open] .side__menu-toggle::after { transform: rotate(-135deg) translate(-2px, -2px); }

  .side__nav { padding-bottom: var(--sp-3); }
  .side__link { min-height: 44px; }
  .side__foot { margin-top: 0; padding-bottom: var(--sp-3); }
}

.main { flex: 1 1 auto; min-width: 0; padding: var(--sp-6) var(--sp-5) var(--sp-9); }
@media (min-width: 62em) { .main { padding: var(--sp-7) var(--sp-7) var(--sp-9); } }
.main__inner { max-width: 68rem; }

.page-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-6);
}
.page-head h1 { font-size: var(--step-3); margin: 0 0 var(--sp-1); }
.page-head p { color: var(--ink-2); margin: 0; font-size: var(--step-0); max-width: 42rem; }

/* ------------------------------------------------------------- stat tiles */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}
.stat {
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  text-decoration: none;
  display: block;
  transition: border-color 0.16s var(--ease);
}
a.stat:hover { border-color: var(--line-strong); }
.stat__value {
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.stat__label {
  font-size: var(--step--1); color: var(--ink-3); margin-top: var(--sp-2);
}
.stat--accent { background: var(--accent-wash); border-color: var(--accent-line); }
.stat--accent .stat__value { color: var(--accent-deep); }

/* Money and counts line up on the decimal point. A commission column that
   ragged-rights is unreadable the moment there is more than one partner. */
.table th.num, .table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A row's actions, folded away until asked for. Three forms per row rendered
   flat turns the partner table into a wall of inputs. */
.staff-actions { position: relative; }
.staff-actions > summary { list-style: none; cursor: pointer; }
.staff-actions > summary::-webkit-details-marker { display: none; }
.staff-actions__body {
  position: absolute;
  right: 0;
  z-index: 5;
  width: 15rem;
  margin-top: var(--sp-2);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  box-shadow: var(--shadow-lg);
}
.staff-actions__body label {
  display: block;
  font-size: var(--step--1);
  font-weight: 600;
  margin-bottom: var(--sp-1);
}
.staff-actions__body input { width: 100%; }

/* ------------------------------------------------------------------ paying */

/* The banner that carries the buy button, shown on every page of the family
   area once a trial has run out. A row on desktop so the sentence and the
   button read as one thing; stacked on a phone so the button keeps a full
   tap target. */
.plan-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}
.plan-note__text { flex: 1 1 20rem; }
.plan-note .btn { flex: none; }

/* The checkout itself: the card form, and what it buys beside it. */
.pay {
  display: grid;
  gap: var(--sp-6);
  align-items: start;
}
@media (min-width: 62em) {
  .pay { grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr); gap: var(--sp-7); }
}

/* Stripe mounts an iframe here and sizes it itself. The min-height stops the
   page collapsing to nothing and then jumping once the iframe loads. */
.pay__form { min-height: 22rem; }

.pay__aside {
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper-3);
}
.pay__aside h2 { font-size: var(--step-1); margin: 0 0 var(--sp-4); }

.pay__list { list-style: none; margin: 0 0 var(--sp-5); padding: 0; }
.pay__list li {
  position: relative;
  padding-left: 1.4rem;
  margin-bottom: var(--sp-3);
  color: var(--ink-2);
  line-height: 1.5;
}
.pay__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.45em;
  width: 0.5rem;
  height: 0.28rem;
  border-left: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: rotate(-45deg);
}

.pay__note {
  font-size: var(--step--1);
  color: var(--ink-2);
  line-height: 1.55;
  margin: 0 0 var(--sp-3);
}
.pay__note--quiet { color: var(--ink-3); margin-bottom: 0; }

/* ------------------------------------------------------------ media grid */

.mgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-3);
}
@media (min-width: 62em) {
  .mgrid { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
}

.mcard {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
  border: 1px solid var(--line);
  transition: border-color 0.16s var(--ease), box-shadow 0.16s var(--ease);
}
.mcard[data-selected="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
}
.mcard__media { position: relative; aspect-ratio: 1; display: block; }
.mcard__media img { width: 100%; height: 100%; object-fit: cover; opacity: 0;
                    transition: opacity 0.35s var(--ease); }
.mcard__media img.is-loaded { opacity: 1; }

.mcard__pick {
  position: absolute; top: 6px; left: 6px; z-index: 2;
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.9);
  background: rgba(20, 18, 15, 0.32);
  cursor: pointer;
  display: grid; place-items: center;
  color: #fff; font-size: 13px; line-height: 1;
  backdrop-filter: blur(3px);
  padding: 0;
}
.mcard[data-selected="true"] .mcard__pick { background: var(--accent-deep); border-color: #fff; }

.mcard__flags {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  display: flex; gap: 3px; flex-direction: column; align-items: flex-end;
}
.flag {
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: rgba(20, 18, 15, 0.66);
  color: #fff; font-size: 11px; line-height: 1;
  backdrop-filter: blur(3px);
}
.flag--gallery { background: rgba(72, 96, 78, 0.9); }
.flag--funeral { background: rgba(120, 88, 52, 0.9); }
.flag--featured { background: rgba(60, 66, 96, 0.9); }
.flag--rejected { background: rgba(150, 69, 59, 0.92); }
.flag--pending  { background: rgba(20, 18, 15, 0.55); }

/* Four labels never fit across a 150px card, so they wrapped mid-word.
   A two-column grid fits every label at a legible size and gives a much
   bigger tap target than a squeezed row. */
.mcard__foot {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px; padding: 4px;
  border-top: 1px solid var(--line-2);
  background: var(--paper-2);
}
.mcard__act {
  min-width: 0;
  border: 0; background: transparent;
  padding: 7px 4px;
  border-radius: var(--radius-sm);
  font-size: 0.72rem; color: var(--ink-3);
  cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background 0.14s var(--ease), color 0.14s var(--ease);
}
.mcard__act:hover { background: var(--paper-3); color: var(--ink); }
.mcard__act[data-on="true"] {
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-weight: 700;
}

.mcard__note {
  padding: 4px 6px; font-size: 0.66rem; color: var(--ink-3);
  background: var(--paper-2); border-top: 1px solid var(--line-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --------------------------------------------------- recent contributions */

.recent { padding: var(--sp-4) 0; border-bottom: 1px solid var(--line-2); }
.recent:last-child { border-bottom: 0; }
.recent__strip {
  display: flex; gap: var(--sp-2); margin-top: var(--sp-3);
  overflow-x: auto; scrollbar-width: thin; padding-bottom: 2px;
}
.recent__strip img {
  width: 74px; height: 74px; flex: none;
  object-fit: cover; border-radius: var(--radius-sm);
  background: var(--paper-3);
  opacity: 0; transition: opacity 0.35s var(--ease);
}
.recent__strip img.is-loaded { opacity: 1; }

/* ----------------------------------------------------------- bulk bar */

.bulkbar {
  position: fixed; inset: auto var(--sp-4) var(--sp-4) var(--sp-4); z-index: 45;
  max-width: 56rem; margin-inline: auto;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  background: var(--paper-2);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  box-shadow: var(--shadow-lg);
  transform: translateY(160%);
  transition: transform 0.28s var(--ease);
}
.bulkbar[data-visible="true"] { transform: none; }
.bulkbar__count { font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
.bulkbar__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; flex: 1 1 auto; }

@media (max-width: 47.9em) {
  .bulkbar { inset: auto var(--sp-2) var(--sp-2) var(--sp-2); padding: var(--sp-3); }
  .bulkbar__actions { width: 100%; }
  .bulkbar__actions .btn { flex: 1 1 auto; }
}

/* -------------------------------------------------------------- filters */

/* Search + two selects + a button used to stack into four full-width rows,
   pushing the photographs below the fold on a laptop. */
.toolbar {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  align-items: center; margin-bottom: var(--sp-4);
}
.toolbar__search { flex: 1 1 16rem; min-width: 0; }
.toolbar select { width: auto; flex: 0 1 12rem; min-width: 9rem; }
.toolbar input, .toolbar select { min-height: 40px; padding-block: 0.4rem; }
@media (max-width: 47.9em) {
  .toolbar__search { flex-basis: 100%; }
  .toolbar select { flex: 1 1 40%; }
}

.filters {
  display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center;
  margin-bottom: var(--sp-5);
}
.chip {
  display: inline-flex; align-items: center; gap: 0.35em;
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper-2);
  color: var(--ink-2);
  font-size: clamp(0.86rem, 0.78rem + 0.40vw, 0.98rem);
  text-decoration: none;
  white-space: nowrap;
  min-height: 38px;
}
.chip:hover { border-color: var(--line-strong); color: var(--ink); }
.chip[aria-current="true"] {
  background: var(--ink); color: var(--ink-inverse); border-color: var(--ink);
}
.chip__n { opacity: 0.65; font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------- filter bar */

/* One line that scrolls sideways rather than two rows that wrap. On a phone
   the wrapped version cost about a third of the screen before a single
   photograph appeared. */
.filterbar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
  /* Wraps rather than scrolls.

     It used to be a full-bleed horizontal scroller, on the reasoning that a
     row visibly running past the edge reads as scrollable. Measured on a 390px
     phone it hid 42px, which cut "Timeline" through the middle of the word and
     put "By who" half off the screen. Those two are the controls that make a
     large collection navigable at all, so hiding them is the one thing this
     row must not do. Two short centred lines cost about 40px of height and
     hide nothing. */
  flex-wrap: wrap;
  row-gap: var(--sp-2);
}
/* The divider only makes sense while both sets share a line. */
@media (max-width: 30em) {
  .filterbar__divider { display: none; }
}

/* Each set is a segmented control: one connected object, so it reads as a
   single choice rather than three unrelated buttons. */
.filterbar__set {
  display: inline-flex;
  flex: 0 0 auto;
  background: var(--paper-3);
  border-radius: 999px;
  padding: clamp(3px, 2px + 0.1vw, 5px);
}
.filterbar__divider {
  flex: 0 0 auto;
  width: 1px; height: 20px;
  background: var(--line);
}
.chip--seg {
  border: 0;
  background: transparent;
  /* Sized up from 13px/32px: this is the control the gallery is navigated
     with, and on a large phone it was smaller than the body text beneath it. */
  min-height: clamp(38px, 34px + 0.9vw, 44px);
  padding: 0.3rem clamp(0.8rem, 0.58rem + 0.55vw, 1.25rem);
  font-size: clamp(0.92rem, 0.82rem + 0.55vw, 1.05rem);
}
.chip--seg:hover { background: color-mix(in srgb, var(--paper-2) 70%, transparent); }
.chip--seg[aria-current="true"] {
  background: var(--paper-2);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(34, 31, 27, 0.10);
}

/* ------------------------------------------------------- discourage saving */

/* This cannot stop a determined person — a screenshot defeats all of it, and
   anything the browser displays it has already decoded. What it does stop is
   the casual save: the long-press menu on iOS and Android, the drag-to-
   desktop, the right-click. That is where nearly all of it happens.

   pointer-events on the image rather than the tile, so the tile itself still
   takes the tap and opens the viewer. */
.memorial-page .tile img,
.memorial-page .tile video,
.lightbox__figure img,
.lightbox__figure video {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.memorial-page .tile img,
.lightbox__figure img {
  pointer-events: none;
}

/* ------------------------------------------------- family link recipients */

.linkpeople {
  margin: var(--sp-3) 0 var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--paper-3);
  border-radius: var(--radius);
}
.linkpeople__label {
  display: block;
  font-size: 0.7rem; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--sp-3);
}
.linkpeople__list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.linkpeople__list li {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3);
  font-size: var(--step--1);
  padding: 2px 0;
}
.linkpeople__add { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.linkpeople__add input { flex: 1 1 14rem; min-width: 0; }

/* ------------------------------------------------------------------ people */

/* A list, not a grid: it reads the same at four names and at forty, and the
   whole row is the tap target rather than the name alone. */
.people { list-style: none; margin: 0; padding: 0; }
.people__row + .people__row { border-top: 1px solid var(--line-2); }
.people__link {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-2);
  min-height: 56px;
  text-decoration: none;
  color: var(--ink);
}
.people__link:hover { background: var(--paper-3); }
.people__name { font-size: var(--step-0); }
.people__count {
  flex: 0 0 auto;
  font-size: var(--step--1);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------- video cover pick */

.cover-pick {
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
}
.cover-pick .quiet { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- contribution row */

.contrib {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper-2);
  overflow: hidden;
  margin-bottom: var(--sp-4);
}
.contrib__head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--sp-4); padding: var(--sp-4) var(--sp-5); flex-wrap: wrap;
}
.contrib__who { font-family: var(--font-display); font-size: var(--step-1); }
.contrib__meta { font-size: var(--step--1); color: var(--ink-3); }
.contrib__memory {
  padding: 0 var(--sp-5) var(--sp-4);
  font-family: var(--font-display);
  color: var(--ink-2);
  font-size: var(--step-0);
  line-height: 1.55;
}
.contrib__strip {
  display: flex; gap: var(--sp-2); padding: 0 var(--sp-5) var(--sp-4);
  overflow-x: auto; scrollbar-width: thin;
}
.contrib__strip img {
  width: 92px; height: 92px; flex: none;
  object-fit: cover; border-radius: var(--radius-sm);
  background: var(--paper-3);
}
.contrib__actions {
  display: flex; gap: var(--sp-2); padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--line-2); background: var(--paper-3); flex-wrap: wrap;
}

/* ---------------------------------------------------------- review mode */

.review {
  display: grid; gap: var(--sp-5);
  grid-template-columns: 1fr;
}
@media (min-width: 62em) { .review { grid-template-columns: minmax(0, 1fr) 20rem; } }

.review__stage {
  display: grid; place-items: center;
  background: var(--paper-sunk);
  border-radius: var(--radius-lg);
  padding: var(--sp-4);
  min-height: 50vh;
}
.review__stage img, .review__stage video {
  max-height: 68vh; width: auto; border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.review__keys {
  display: grid; gap: var(--sp-2); font-size: var(--step--1); color: var(--ink-3);
}
.review__keys kbd {
  display: inline-grid; place-items: center; min-width: 22px; padding: 1px 5px;
  border: 1px solid var(--line-strong); border-radius: 4px;
  background: var(--paper-2); font-family: ui-monospace, Menlo, monospace;
  font-size: 0.72rem; color: var(--ink-2);
}

/* Stacked directly above the state pill, which occupies the same corner.
   Both are read together — "Waiting for you" and "Already here" is the whole
   answer to whether this one needs keeping. */
.mcard__dupe {
  position: absolute;
  left: var(--sp-2);
  bottom: calc(var(--sp-2) + 1.65rem);
  z-index: 2;
  font-size: 0.66rem;
  padding: 0.15em 0.5em;
  pointer-events: none;
  box-shadow: 0 1px 2px rgba(20, 18, 15, 0.18);
}

/* ------------------------------------------------------- gallery timeline */

/* One row per submission. The date and the name sit in a column of their own
   on a wide screen and stack above the photographs on a phone, so the reading
   order is the same either way: when, who, then what they brought. */
.tl { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-7); }

.tl__entry { display: grid; gap: var(--sp-4); }

.tl__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-4); }

.tl__when {
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft, #6b655d);
  font-size: var(--step--1);
}

.tl__who { margin: 0; }
.tl__who--quiet { font-weight: 500; color: var(--ink-soft, #6b655d); }
.tl__count { color: var(--ink-soft, #6b655d); }

/* Same tile treatment as the grid, so a photograph looks identical wherever
   it is met. */
.tl__items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--sp-3);
}

@media (min-width: 60em) {
  .tl__entry { grid-template-columns: 14rem 1fr; gap: var(--sp-5); align-items: start; }
  .tl__meta { flex-direction: column; align-items: flex-start; gap: var(--sp-1); }
  .tl__items { grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); }
}

/* -------------------------------------------------------------- lightbox */

.lightbox {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(16, 14, 12, 0.94);
  display: none;
  place-items: center;
  padding: var(--sp-4);
  /* The viewer's own height, which everything inside is sized from. Normally
     the screen; on a page somebody has pinched into, the part of it they can
     actually see, which the viewer measures and sets here. */
  --viewer-h: 100dvh;
}
.lightbox[open], .lightbox[data-open="true"] { display: grid; }
.lightbox__figure {
  /* A <figure> carries a UA margin of 1em 40px. Unreset, it pushed the
     photograph 16px down inside a frame sized to hold it exactly, so the
     bottom strip of every picture on a phone sat below the screen. */
  margin: 0;
  max-width: min(100%, 82rem); max-height: 100%;
  display: grid; gap: var(--sp-3); justify-items: center;

  /* Kept below the controls, explicitly.
     Zooming sets a `transform` on the picture, and a transform makes a
     stacking context — which lifts the picture out of the ordinary flow and
     into the same paint layer as the absolutely positioned buttons. It comes
     after them in the markup, so it painted over them: the close and share
     controls vanished the instant anybody zoomed, on every photograph, whatever
     colour it was. At life size there is no transform, no stacking context, and
     nothing looked wrong — which is why this survived. */
  position: relative;
  z-index: 0;
}
.lightbox__figure img, .lightbox__figure video {
  max-height: calc(var(--viewer-h) * 0.78); width: auto; border-radius: var(--radius);
}
.lightbox__caption { color: rgba(255, 255, 255, 0.86); text-align: center;
                     font-size: var(--step--1); max-width: 40rem; }
.lightbox__caption strong { display: block; font-family: var(--font-display);
                            font-size: var(--step-1); color: #fff; font-weight: 400; }
/* 44px: the accessible minimum, and no larger. The previous 52px circles
   were the heaviest objects in the product, sitting on top of the one thing
   the page exists to show.

   The disc has to carry a white icon over a photograph whose colour nobody
   knows, and zooming in makes that worse: a picture scaled up fills the
   corners the controls sit in, so a bright sky or a white wall ends up
   directly behind them. At the 30% black it used to be, a white cross on a
   white background measured 2.1:1 — under the 3:1 that WCAG 1.4.11 asks of a
   control, and in practice invisible. 55% measures 4.7:1 and is still plainly
   a translucent disc rather than a button drawn on top of her. */
.lightbox__btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  /* Above the photograph, whatever zooming does to it. */
  z-index: 2;
  width: 44px; height: 44px; border-radius: 50%;
  /* Two rings, because one cannot do it: the white inner edge defines the disc
     against a dark photograph, and the dark outer shadow defines it against a
     light one. The white ring alone rendered pure white over white — gone at
     exactly the moment it was needed. */
  border: 1px solid rgba(255, 255, 255, 0.26);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.22), 0 1px 6px rgba(0, 0, 0, 0.28);
  background: rgba(0, 0, 0, 0.55);
  color: #fff; cursor: pointer;
  display: grid; place-items: center;
  backdrop-filter: blur(6px);
  transition: background 0.16s var(--ease);
}
/* And the glyph carries its own shadow, so the stroke separates even where the
   disc is thinnest — at its blurred edge, and behind a very bright highlight. */
.lightbox__btn svg {
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.55));
}
/* Darker on hover, not lighter. This was 0.5 — below the resting state once
   that went to 0.55, so pointing at a control made it harder to see. */
.lightbox__btn:hover { background: rgba(0, 0, 0, 0.72); }
.lightbox__btn:focus-visible {
  outline: 2px solid #fff; outline-offset: 2px;
}
/* "5 of 12", which sits between the two buttons and had no backing at all.
   White at 72% over a white photograph rendered as white on white: a contrast
   ratio of 1.00, which is not low but absent. It gets the same disc as its
   neighbours, shaped to the text. */
.lightbox__count {
  position: absolute;
  z-index: 2;
  top: calc(var(--sp-4) + env(safe-area-inset-top));
  left: 50%; transform: translateX(-50%);
  font-family: var(--font-ui);
  color: #fff;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  pointer-events: none;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.22);
  backdrop-filter: blur(6px);
}
.lightbox__prev { left: var(--sp-4); }
.lightbox__next { right: var(--sp-4); }
.lightbox__close { top: var(--sp-4); right: var(--sp-4); transform: none; }
.lightbox__share { top: var(--sp-4); left: var(--sp-4); transform: none; }

/* On a phone this stops being a dialog on a web page and becomes the whole
   screen, the way a photograph opens in a camera roll: edge to edge, no
   letterboxing margin, the caption floating over the image rather than
   stealing height from it. The photographs are why anyone came. */
@media (max-width: 47.9em) {
  .lightbox__prev, .lightbox__next { display: none; }

  .lightbox {
    padding: 0;
    background: #0d0c0a;
    /* dvh, not vh: with vh the image is sized for a viewport taller than the
       one actually visible once the browser's own bars are showing, and the
       bottom of every photograph sits under them. */
    height: var(--viewer-h);
    grid-template-rows: 1fr;
  }
  .lightbox__figure {
    width: 100%;
    height: var(--viewer-h);
    max-width: none;
    max-height: none;
    gap: 0;
    align-content: center;
  }
  .lightbox__figure img,
  .lightbox__figure video {
    width: 100%;
    height: var(--viewer-h);
    max-height: none;
    object-fit: contain;
    border-radius: 0;
  }
  /* A video's native controls are drawn at the very bottom edge of the
     element. At the full 100dvh that edge sits under the home indicator and
     beneath the caption gradient, so the time bar could not be dragged — or
     often even seen. Videos give that strip back; photographs, which have no
     controls, keep the whole height. */
  .lightbox__figure video {
    height: calc(var(--viewer-h) - var(--safe-b-raw) - var(--sp-7));
    align-self: center;
  }
  /* Never let the caption take a touch meant for the scrubber underneath it. */
  .lightbox__caption {
    pointer-events: none;
  }
  .lightbox__caption {
    position: absolute;
    inset: auto 0 0 0;
    margin: 0;
    max-width: none;
    padding: var(--sp-7) var(--sp-5)
             calc(var(--sp-5) + var(--safe-b));
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
    text-align: left;
  }
  /* Position only. This block used to re-state the background at 0.4 and the
     ring at 0.2, quietly undoing the contrast the base rule sets — on a phone,
     which is where a photograph fills the screen behind these and where the
     problem was reported. */
  .lightbox__close,
  .lightbox__share {
    top: calc(var(--sp-3) + env(safe-area-inset-top));
  }
  .lightbox__close { right: var(--sp-3); }
  .lightbox__share { left: var(--sp-3); }
}

/* ------------------------------------------------------------ hero crop */

.cropper {
  position: relative;
  /* Same shape as the published hero, so this really is a preview. */
  aspect-ratio: var(--hero-ratio);
  max-width: 22rem;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--paper-3);
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.cropper:active { cursor: grabbing; }
/* Fills the frame at zoom 1 and is magnified from there — which is what the
   server does (images.crop_to_hero takes the whole photograph at zoom 1).
   Sizing this to the file's natural pixels instead, as it used to, showed a
   1600px photograph at 1600px inside a 350px box: the family saw a drastic
   close-up at minimum zoom and no way to pull back.

   Panning is object-position rather than a percentage translate, because a
   percentage translate resolves against the image's own width — which is now
   the frame's width, so it would cancel itself out and nothing would move.
   Anchoring transform-origin to the same point means zooming pushes in on
   whatever the family has centred, rather than drifting off it. */
.cropper img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* ---------------------------------------------------------------- tables */

.table { width: 100%; border-collapse: collapse; font-size: var(--step--1); }
.table th, .table td {
  text-align: left; padding: var(--sp-3) var(--sp-3);
  border-bottom: 1px solid var(--line-2);
  vertical-align: top;
}
.table th {
  font-weight: 600; color: var(--ink-3); font-size: 0.72rem;
  letter-spacing: 0.06em; text-transform: uppercase;
}
.table tr:last-child td { border-bottom: 0; }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* -------------------------------------------------------------- timeline */

.timeline { display: grid; gap: var(--sp-4); }
.timeline__item {
  display: grid; grid-template-columns: 5.5rem 1fr; gap: var(--sp-4);
  font-size: var(--step--1);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--line-2);
}
.timeline__item:last-child { border-bottom: 0; padding-bottom: 0; }
.timeline__when { color: var(--ink-3); font-variant-numeric: tabular-nums; }
.timeline__what { color: var(--ink-2); }
@media (max-width: 34em) {
  .timeline__item { grid-template-columns: 1fr; gap: var(--sp-1); }
}

/* ------------------------------------------------------------ empty state */

.empty {
  text-align: center;
  padding: var(--sp-8) var(--sp-5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  color: var(--ink-2);
}
.empty h3 { font-size: var(--step-2); margin-bottom: var(--sp-2); }
.empty p { max-width: 30rem; margin-inline: auto; }

/* ------------------------------------------------------------ pagination */

.pager {
  display: flex; gap: var(--sp-3); align-items: center; justify-content: center;
  margin-top: var(--sp-6); font-size: var(--step--1); color: var(--ink-3);
}

/* ------------------------------------------------------------------ auth */

.auth {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: var(--sp-6) var(--sp-4);
}
.auth__card {
  width: 100%; max-width: 25rem;
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--sp-6);
  box-shadow: var(--shadow);
}
.auth__head { text-align: center; margin-bottom: var(--sp-6); }
.auth__head h1 { font-size: var(--step-2); margin-bottom: var(--sp-2); }
.auth__head p { color: var(--ink-2); font-size: var(--step--1); margin: 0; }
.auth__foot { margin-top: var(--sp-5); text-align: center; font-size: var(--step--1);
              color: var(--ink-3); }

/* --------------------------------------------------------------- footer */

.foot {
  padding: var(--sp-7) 0 var(--sp-8);
  border-top: 1px solid var(--line-2);
  text-align: center;
  color: var(--ink-3);
  font-size: var(--step--1);
}
.foot a { color: var(--ink-3); }

/* ---------------------------------------------------------------- print */

@media print {
  .side, .memorial-nav, .bulkbar, .btn, .filters { display: none !important; }
  body { background: #fff; color: #000; }
  .card, .panel { border-color: #ccc; box-shadow: none; }
  .qr-print { break-inside: avoid; }
}

/* --------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}


/* ================================================== refinement pass  */

/* ----------------------------------------------------------------- toast */

/* One quiet confirmation, bottom-centre, out of the way of the thumb. */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--sp-5) + var(--safe-b));
  transform: translate(-50%, 0.5rem);
  z-index: 100;
  max-width: calc(100vw - 2rem);
  padding: 0.65rem 1.1rem;
  border-radius: 999px;
  background: var(--action);
  color: var(--ink-inverse);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  box-shadow: 0 6px 20px -8px rgba(34, 31, 27, 0.45);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s var(--ease), transform 0.18s var(--ease);
}
.toast[data-visible="true"] { opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity 0.18s linear; transform: translate(-50%, 0); }
}

/* --------------------------------------------------------- share button */

.sharebtn { display: inline-flex; align-items: center; gap: 0.5em; }
.sharebtn svg { flex: 0 0 auto; opacity: 0.75; }

/* ------------------------------------------------------- back to memorial */

/* "‹ Valerie" — the quiet way home from a secondary page. Deliberately not a
   house icon or a HOME button; it reads as part of the editorial page. */
.memorial-nav__back {
  font-family: var(--font-ui);
  font-size: clamp(0.84rem, 0.76rem + 0.28vw, 1rem);
  letter-spacing: 0.02em;
  color: var(--ink-2);
  text-decoration: none;
  white-space: nowrap;
  padding: 0.45rem 0.5rem 0.45rem 0;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 0.15em;
}
.memorial-nav__back:hover { color: var(--ink); }
.memorial-nav__back span { font-size: 1.15em; line-height: 1; }

/* --------------------------------------------------------- sticky header */

/* Lightweight, and only once the visitor has scrolled past the top. */
.memorial-nav { position: sticky; top: 0; z-index: 30; }

/* ------------------------------------------------------- sticky filters */

/* Beneath the header, so the filter is still reachable a long way down a
   gallery. Uses the header's own height as its offset. */
@supports (position: sticky) {
  .memorial-page .filterbar {
    position: sticky;
    top: var(--nav-h);
    z-index: 20;
    background: color-mix(in srgb, var(--paper) 94%, transparent);
    backdrop-filter: saturate(140%) blur(8px);
    padding-block: var(--sp-3);
  }
}

/* ---------------------------------------------- gallery tile attribution */

/* "Shared by John" on all forty tiles was visual noise, and the same three
   words repeated. It is always available in the viewer; here it appears on
   hover, where a pointer exists to hover with. Touch devices — which have no
   hover — simply do not carry it. */
@media (hover: hover) and (pointer: fine) {
  .memorial-page .tile__caption { opacity: 0; }
  .memorial-page .tile:hover .tile__caption,
  .memorial-page .tile:focus-visible .tile__caption { opacity: 1; }
}
@media (hover: none) {
  .memorial-page .tile__caption { display: none; }
}

/* ------------------------------------------------------- viewer gestures */

/* Nothing in the viewer is text to be selected. Scoped to the whole viewer,
   not just the photograph: a double tap that landed on the caption — or on
   the black beside her — was enough for Android to raise a Copy / Select all
   bar and take the picture off the screen behind it. */
.lightbox {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* Every gesture in here belongs to the viewer, which implements all of them
     itself. This was `manipulation`, which still permits the browser's own
     pinch — and a pinch that began on the black beside the photograph rather
     than on it was therefore taken by the browser and magnified the whole
     page. preventDefault cannot call that back once touch-action has allowed
     it; the only place to refuse it is here. */
  touch-action: none;
}

/* The photograph itself is what zooms; the frame stays put. */
.lightbox__figure img,
.lightbox__figure video {
  transform-origin: center center;
  transition: transform 0.18s var(--ease);
  touch-action: none;
}
/* No easing while a finger is on it, or panning lags behind the touch. Keyed
   on the gesture rather than on the zoom level: keyed on zoom, a double tap
   would drop the transition in the same frame as the transform and so jump,
   while the final settle back to life size was the only movement that eased —
   which is what made zooming feel unlike everything else on the phone. */
.lightbox[data-gesture="true"] .lightbox__figure img,
.lightbox[data-gesture="true"] .lightbox__figure video {
  transition: none;
}

/* A single tap gets the controls out of the way of the photograph. They come
   back on the next tap; nothing is lost, so this needs no explanation. */
.lightbox[data-chrome="hidden"] .lightbox__btn,
.lightbox[data-chrome="hidden"] .lightbox__count,
.lightbox[data-chrome="hidden"] .lightbox__caption {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .lightbox__figure img,
  .lightbox__figure video { transition: none; }
}

/* --------------------------------------------------------------- thanks */

.thanks__title {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 400;
  line-height: 1.25;
  max-width: 22ch;
  margin-inline: auto;
  text-wrap: balance;
}
.thanks__what {
  font-family: var(--font-ui);
  color: var(--ink-3);
  font-size: var(--step--1);
  margin-top: var(--sp-4);
}
/* What actually happens next, stated plainly. */
.thanks__next {
  max-width: 32rem;
  margin: var(--sp-6) auto 0;
  padding: var(--sp-5);
  background: var(--paper-3);
  border-radius: var(--radius);
  font-size: var(--step--1);
  color: var(--ink-2);
  text-align: left;
}
.thanks__next p + p { margin-top: var(--sp-3); }

/* --------------------------------------------------- accessibility polish */

/* Navigation links carry a proper touch target. At 0.76rem with 0.45rem of
   padding they were about 34px tall — comfortably tappable for me, and not
   for someone in their seventies with a phone in one hand. */
.memorial-nav__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

@media (max-width: 40em) {
  /* On a phone the family's door is the lock alone: it belongs to one or two
     people and should not compete with the three visitor items. The label
     stays in the accessibility tree. */
  .memorial-nav__family { padding-left: var(--sp-4); margin-left: var(--sp-2); }
  .memorial-nav__family-label {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .memorial-nav__family svg { width: 14px; height: 14px; }
  /* The sticky filter sits under a shorter bar on a phone. */
  .memorial-page .filterbar { top: calc(var(--nav-h) - 4px); }
}

/* The viewer's controls are the one place a focus ring on paper would be
   invisible, so they carry their own. Declared with the rest of the
   accessibility rules rather than buried in the lightbox block. */
.lightbox__count[hidden] { display: none; }

/* While a filter is being fetched. A brief dim rather than a spinner: the
   swap takes a few hundred milliseconds and a spinner would flash. */
[data-gallery-grid][data-busy="true"] {
  opacity: 0.45;
  transition: opacity 0.12s var(--ease);
  pointer-events: none;
}

/* ---------------------------------------------------------- add memory bar */

/* Full width along the foot of the screen on a phone, as it was before —
   this is the one action the page is asking for, and the family preferred it
   stated plainly rather than tucked into a corner.

   It still keeps the manners the corner version was given: it appears only
   once the invitation already on the page has scrolled away, and it never
   sits over a photograph in the viewer. */
.addpill {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 25;

  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + var(--safe-b));

  background: var(--action);
  color: var(--ink-inverse);
  border-top: 1px solid var(--action);

  font-family: var(--font-ui);
  font-size: var(--step-0);
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;

  transform: translateY(110%);
  transition: transform 0.3s var(--ease);
}
.addpill[data-visible="true"] { transform: none; }
.addpill svg { flex: 0 0 auto; opacity: 0.85; }

/* Room at the foot of the document so the bar never covers the last of the
   page. Scoped with :has() so a memorial with submissions closed — which
   renders no bar at all — gets no dead space either. */
@media (max-width: 47.9em) {
  body:has(.addpill) { padding-bottom: 5.5rem; }
}

/* Never over a photograph in the viewer, and never on desktop — the sticky
   header carries Add memory there with room to spare. */
body[data-viewer="open"] .addpill { transform: translateY(110%); }
@media (min-width: 48em) { .addpill { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .addpill { transition: none; }
}

/* ============================================================== setup wizard
   One question per screen, for somebody doing this a few days after a death.
   Wide margins, one clear action, and a visible way out of every step. */

.setup { padding-block: var(--sp-6) var(--sp-9); }

.setup__progress { margin-bottom: var(--sp-6); }

.setup__count {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--ink-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 0 0 var(--sp-3);
}

.setup__steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--step--1);
  color: var(--ink-3);
}

.setup__step { display: flex; align-items: center; gap: var(--sp-2); }

/* A rule under the current step rather than a filled pill: this is a quiet
   progress indicator, not a game. */
.setup__step.is-current > span {
  color: var(--ink);
  font-weight: 500;
  box-shadow: 0 2px 0 var(--accent, var(--ink));
}
.setup__step.is-done a { color: var(--ink-2); }

.setup__head { margin-bottom: var(--sp-6); }
.setup__head h1 { margin-bottom: var(--sp-3); }
.setup__head--done { text-align: center; margin-bottom: var(--sp-7); }

.setup__legend {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0;
  margin-bottom: var(--sp-2);
}

.setup fieldset { border: 0; padding: 0; margin: 0; }

.setup__hero {
  display: flex;
  gap: var(--sp-5);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--sp-5);
}
.setup__hero img {
  width: 7.5rem;
  height: 7.5rem;
  object-fit: cover;
  border-radius: var(--radius);
}

.setup__note { margin-top: var(--sp-5); }

.setup__link {
  font-size: var(--step-1);
  word-break: break-word;
  margin: 0;
}

.setup__next { margin-top: var(--sp-7); }
.setup__next h2 { font-size: var(--step-1); margin-bottom: var(--sp-5); }
.setup__next h3 { font-size: var(--step-0); margin-bottom: var(--sp-2); }

.setup__finish { margin-top: var(--sp-7); text-align: center; }

/* Key/value pairs in the staff area. Two columns where there is room, stacked
   on a phone, because a storage prefix is long and must not be truncated:
   reading it is the entire reason it is on screen. */
.kv {
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: var(--sp-2) var(--sp-5);
  margin: 0;
  font-size: var(--step--1);
}
.kv dt { color: var(--ink-3); }
.kv dd { margin: 0; color: var(--ink-2); overflow-wrap: anywhere; }
.kv code { font-size: 0.95em; }

@media (max-width: 34em) {
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dd { margin-bottom: var(--sp-3); }
}

/* ------------------------------------------------- gallery: the first screen

   The gallery and the timeline exist to be browsed, and their headings were
   costing more of a phone screen than the photographs were getting: the first
   tile began 347px down an 844px viewport, four of sixteen visible. The
   heading is still there, still first in the reading order, just no longer the
   main event on the page whose job is the pictures. */

.gallery-page { padding-block: var(--sp-5) var(--sp-8); }
.gallery-head { margin-bottom: var(--sp-4); }

@media (max-width: 47.9em) {
  .gallery-page { padding-block: var(--sp-4) var(--sp-7); }
  .gallery-head { margin-bottom: var(--sp-3); }
  /* One step down from the page-title scale. The person's name is already on
     the bar above; this heading names the section, not the memorial. */
  .gallery-head h1 { font-size: var(--step-2); margin-bottom: var(--sp-2); }
  .gallery-head p { font-size: var(--step--1); }
}

@media (min-width: 48em) {
  .gallery-page { padding-block: var(--sp-7) var(--sp-9); }
  .gallery-head { margin-bottom: var(--sp-6); }
}
