/* portal.css — the member area.
 *
 * Same palette, same type, same paper as the photograph archive, 90 Passes and The Peoples Page,
 * because Charley's first instruction on the rebuild was "the overall designs of these pages are
 * great, we will want to keep the overall look of it all."
 *
 * What is NEW here is only what a signed-in area needs and a public page does not: a rail, a
 * progress meter, form fields, a save state, a drop zone. Each is built out of the tokens below
 * rather than out of a new palette, so the portal reads as another room in the same building.
 *
 * The motif: the public rooms each carry one — a chairlift for The Peoples Page, a pass card for
 * 90 Passes. The portal's is a TRIPLE CHAIR on its cable — Charley, 2026-08-28: "The background
 * icon of what looks like a gondola, should look like one of our chairlifts, like the triple
 * chairlift. We don't have gondolas at showdown." (The old luggage-tag mark read as a gondola
 * cabin hanging from a line, which is exactly what Showdown has never had.)
 */

:root{
  --navy:#041E42; --navy-2:#0B2A5B; --orange:#FF9228; --pink:#E85D9E; --sky:#9FC5E8;
  --cream:#F6F1E7; --paper:#FFFDF8; --ink:#1B1B1B; --mute:#6B6F76; --rule:#D9D2C3;
  --gold:#C79A3B; --green:#2E7D52; --red:#B3261E;
  --serif:"Rockwell","Roboto Slab","Iowan Old Style","Palatino Linotype",Georgia,serif;
  --sans:-apple-system,BlinkMacSystemFont,"Inter","Poppins","Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  --shadow:0 1px 0 rgba(4,30,66,.05),0 8px 22px rgba(4,30,66,.07);
  --shadow-lift:0 14px 30px rgba(4,30,66,.15);
}
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{font-family:var(--sans);color:var(--ink);background:var(--cream);-webkit-font-smoothing:antialiased;
  background-image:
    radial-gradient(1100px 460px at 50% -12%,rgba(255,255,255,.8),transparent 70%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170' viewBox='0 0 150 150'%3E%3Cg fill='none' stroke='%23041E42' stroke-opacity='.05' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M4 16h64'/%3E%3Cpath d='M36 16v9'/%3E%3Cpath d='M18 25h36'/%3E%3Cpath d='M18 25v13M54 25v13'/%3E%3Cpath d='M18 31h36'/%3E%3Cpath d='M18 38h36'/%3E%3Cpath d='M30 31v7M42 31v7'/%3E%3Cpath d='M24 42h24'/%3E%3Cpath d='M82 96h64'/%3E%3Cpath d='M114 96v9'/%3E%3Cpath d='M96 105h36'/%3E%3Cpath d='M96 105v13M132 105v13'/%3E%3Cpath d='M96 111h36'/%3E%3Cpath d='M96 118h36'/%3E%3Cpath d='M108 111v7M120 111v7'/%3E%3Cpath d='M102 122h24'/%3E%3C/g%3E%3C/svg%3E");
  min-height:100vh}
h1,h2,h3,h4{font-family:var(--serif);font-weight:700;line-height:1.1}
a{color:var(--navy)}
img{max-width:100%}
:focus-visible{outline:3px solid var(--orange);outline-offset:2px;border-radius:4px}

/* ═══════════════════════ Header ═══════════════════════
   ⭐ GONE (2026-08-27): the portal pages now carry the one site header that home/sitenav.js
   generates, like every other page — the second navy bar and the .whochip went with it. The
   signed-in marker is the account chip in that header's corner (portal/session.js), and Sign out
   lives at the foot of the dashboard rail (.railout below). */

.wrap{max-width:1180px;margin:0 auto;padding:0 20px}

/* ═══════════════════════ The two-column shell ═══════════════════════ */
.shell{display:grid;grid-template-columns:232px 1fr;gap:30px;padding:26px 0 80px;align-items:start}
@media(max-width:900px){.shell{grid-template-columns:1fr;gap:18px;padding-top:16px}}

/* ── The rail ── */
.rail{position:sticky;top:92px;display:flex;flex-direction:column;gap:3px}
@media(max-width:900px){.rail{position:static;flex-direction:row;overflow-x:auto;gap:5px;
  padding-bottom:7px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
  .rail::-webkit-scrollbar{display:none}}
.rail a{display:flex;align-items:center;gap:10px;padding:10px 13px;border-radius:7px;
  text-decoration:none;color:var(--navy);font-size:14.5px;font-weight:600;white-space:nowrap;
  border:1px solid transparent;transition:background .12s,border-color .12s}
.rail a:hover{background:var(--paper);border-color:var(--rule)}
.rail a[aria-current]{background:var(--navy);color:#fff;border-color:var(--navy)}
.rail a[aria-current] .ic{color:var(--orange)}
.rail a .ic{width:19px;height:19px;flex:none;color:var(--navy);opacity:.85}
.rail a[aria-current] .ic{opacity:1}
.rail .pip{margin-left:auto;background:var(--orange);color:var(--navy);border-radius:999px;
  font:700 10.5px/1 var(--sans);padding:4px 7px;min-width:20px;text-align:center}
.rail a[aria-current] .pip{background:var(--orange);color:var(--navy)}
@media(max-width:900px){.rail a{font-size:13px;padding:9px 11px}.rail a .ic{display:none}}
/* ⭐ The rail used to end in a dashed rule and four links back out to the public rooms. The site
   header injected by home/sitenav.js carries those now, in the same place as on every other page,
   so the rail is the member area's own rows — plus the way out. (The old rule hiding the header's
   account chip on this page is gone too: with the dashboard's second header removed, that chip IS
   the signed-in marker, on this page like every other.) */
.railout{display:flex;align-items:center;gap:10px;padding:10px 13px;border-radius:7px;margin-top:10px;
  background:none;border:1px dashed var(--rule);color:var(--mute);font:600 13.5px/1.4 var(--sans);
  cursor:pointer;white-space:nowrap;text-align:left;transition:background .12s,border-color .12s,color .12s}
.railout:hover{background:var(--paper);border-style:solid;border-color:var(--navy);color:var(--navy)}
.railout .ic{width:19px;height:19px;flex:none;opacity:.8}
@media(max-width:900px){.railout{margin-top:0;font-size:13px;padding:9px 11px}.railout .ic{display:none}}

/* ⭐⭐ 2026-09-09 — DELETE MY ACCOUNT, in the menu (Charley: "The Delete my account is hard to
   find"). It sits below Sign out and is deliberately the quietest row at rest: it is the most
   destructive thing in the room and it must not compete for the eye with the six rows that are
   the reason anybody came. The red arrives on hover, when the pointer is already on it — the
   moment it stops being decoration and starts being a warning. */
/* ⛔⭐ NO `opacity` HERE, AND THAT IS THE POINT. The first draft of this row was `--mute` at
   opacity:.75, which composites to #8E9092 on the cream and measures **2.85:1** — a straight WCAG
   failure, and on the one row in the menu somebody might be hunting for in a hurry. Quiet is done
   with the border and the absent background instead, and the ink is a shade DARKER than --mute
   (which is itself 4.48:1, a hair under AA). Measured with _tools/contrast: 5.36:1. */
.raildel{display:flex;align-items:center;gap:10px;padding:10px 13px;border-radius:7px;margin-top:3px;
  background:none;border:1px dashed transparent;color:#5F636A;font:600 13.5px/1.4 var(--sans);
  cursor:pointer;white-space:nowrap;text-align:left;
  transition:background .12s,border-color .12s,color .12s}
/* 6.19:1 — measured, same tool. */
.raildel:hover,.raildel:focus-visible{background:#FFF7F6;border-color:#E9BFBB;border-style:solid;
  color:var(--red)}
.raildel .ic{width:19px;height:19px;flex:none;opacity:.8}
@media(max-width:900px){.raildel{margin-top:0;font-size:13px;padding:9px 11px}.raildel .ic{display:none}}

/* ═══════════════════════ Panels ═══════════════════════ */
.panel{background:var(--paper);border:1px solid var(--rule);border-radius:9px;box-shadow:var(--shadow);
  padding:22px 24px;margin-bottom:20px}
@media(max-width:560px){.panel{padding:17px 15px;border-radius:7px}}
.panel > h2{font-size:20px;color:var(--navy);letter-spacing:.01em}
.panel > h2 + .sub{margin-top:6px;color:var(--mute);font-size:14px;line-height:1.55;max-width:62ch}
.panel .hd{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:15px}
.panel .hd h2{font-size:20px;color:var(--navy)}
.panel .hd .spacer{flex:1}
.panel .hd .meta{font-size:12px;letter-spacing:.09em;text-transform:uppercase;color:var(--mute)}

/* ── The hearts tally, beside the greeting (2026-09-07 — website/portal/HEARTS-PLAN.md §15) ──
   Charley: "a tally in the members dashboard that read how many hearts they have pushed… Make the
   'heart' a little bigger here."
   ⛔ THIS IS NOT `.sgdh`. That badge lives over a photograph: dimmed to 85% at rest, drop-shadowed
   and text-shadowed so a red outline and white digits survive a bright snow picture. None of that
   belongs on a cream panel — a shadow here reads as a smudge — so the mark is drawn again, plain,
   at 27px instead of 15, with the count in the panel's own navy. Nothing below touches `.sgdh`.

   ⭐⭐ 2026-09-08 (§16) — the mark here is now ALWAYS the pink snowflake on its white coin, and the
   count is always drawn, 0 included. Charley: "make the symbol they see the pink snowflake, and if
   they haven't like anything, it should still be a Pink snowflake, but it would just show 0."
   The red heart is gone from this block: an empty heart on a photograph invites a press, and there
   is nothing here to press. ⛔ The white coin needs its hairline on cream just as it does on snow —
   without it the disc dissolves into the panel and the flake looks unmoored. */
.dhearts{position:relative;display:inline-flex;align-items:center;align-self:center;flex:none}
/* ⛔ NO HOVER BOX. The first draft lit a pale rounded ground under the heart on hover; it read as
   exactly the grey box Charley had just had taken off the photographs. The hover answer is the
   tooltip, and a 1px lift — nothing is drawn behind the mark. The padding stays, transparent, so
   the tap target is 35px tall on a phone. */
.dh{display:inline-flex;align-items:center;gap:7px;text-decoration:none;color:var(--navy);
  padding:4px 6px;margin:-4px -6px;border-radius:8px;line-height:1;
  -webkit-tap-highlight-color:transparent;transition:transform .12s}
.dh:hover{transform:translateY(-1px)}
@media (prefers-reduced-motion:reduce){.dh{transition:none}.dh:hover{transform:none}}
.dh svg{width:27px;height:27px;display:block;overflow:visible}
/* the pink is photo-heart.js §4's, written down a second time for the same reason the shape is */
.dh .dh-disc{fill:#fff;stroke:rgba(0,0,0,.11);stroke-width:.8}
.dh .dh-flake{stroke:var(--pink);stroke-width:1.4;stroke-linecap:round;fill:none}
.dh .dh-n{font:700 15.5px/1 var(--sans);color:var(--navy)}
/* the hover — and the keyboard's focus, and a first tap on a touch screen; see wireHeartTally() */
.dhtip{position:absolute;top:calc(100% + 10px);left:-6px;z-index:30;width:264px;
  max-width:calc(100vw - 44px);background:var(--navy);color:var(--cream);border-radius:8px;
  padding:11px 13px;font:400 12.5px/1.55 var(--sans);box-shadow:var(--shadow-lift)}
.dhtip::before{content:"";position:absolute;top:-4px;left:17px;width:9px;height:9px;
  background:var(--navy);transform:rotate(45deg);border-radius:2px}
/* ⚠️ display:block, not inline-block — inline-block let the link ride up onto the last line of
   the sentence whenever that line had room, which it does in the empty state. */
.dhtip .dh-go{display:block;margin-top:8px;color:var(--orange);font-weight:700;
  text-decoration:none;white-space:nowrap}
.dhtip .dh-go:hover{text-decoration:underline}
@media(max-width:560px){.dhtip{left:auto;right:-6px}.dhtip::before{left:auto;right:17px}}

.srule{display:flex;align-items:center;gap:14px;margin:34px 0 16px}
.srule h2{font-size:20px;color:var(--navy);letter-spacing:.03em;white-space:nowrap}
.srule hr{flex:1;border:0;border-top:1px dashed var(--rule)}

/* ═══════════════════════ Buttons ═══════════════════════ */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;border:0;cursor:pointer;
  font:700 13px/1 var(--sans);letter-spacing:.09em;text-transform:uppercase;text-decoration:none;
  padding:13px 20px;border-radius:999px;transition:transform .12s,box-shadow .12s,background .12s;
  background:var(--navy);color:#fff;box-shadow:0 2px 9px rgba(4,30,66,.2)}
.btn:hover{transform:translateY(-2px);box-shadow:0 7px 16px rgba(4,30,66,.26)}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none;box-shadow:none}
/* ⛔⛔ 2026-09-14 — WITHOUT THIS LINE, `el.hidden = true` DOES NOTHING TO A BUTTON. The browser's own
   sheet says [hidden]{display:none}, but that is a USER-AGENT rule and the author rule above it
   (display:inline-flex) outranks it — so every place in the dashboard that says "hidden at zero"
   (edPendButton, edLegButton, edUnButton) went on drawing an empty button with no number in it.
   Found in the pane while walking the Legends pile, which is the first group that is often empty.
   ⚠️ The rule is the same one behind the guidance to toggle `el.hidden` rather than style.display:
   it only works if [hidden] is allowed to win. */
.btn[hidden]{display:none}
.btn.go{background:var(--orange);color:var(--navy)}
.btn.bad{background:var(--red);color:#fff}
.btn.bad:hover{box-shadow:0 7px 16px rgba(179,38,30,.3)}
.btn.ghost{background:transparent;color:var(--navy);border:1.5px solid var(--rule);box-shadow:none}
.btn.ghost:hover{border-color:var(--navy);background:var(--cream)}
/* ⭐ A destructive action that is NOT shouting. Charley's delete (2026-08-31) is legitimate, quiet
   and reversible — a red block beside Save would read as a warning about pressing it, which is the
   wrong thing to tell somebody who is meant to use it. It only turns red under the pointer, which
   is the moment the warning is actually useful. */
.btn.quiet{background:transparent;color:var(--mute);border:1.5px solid transparent;box-shadow:none;
  font-weight:600;letter-spacing:.06em}
.btn.quiet:hover{color:var(--red);border-color:#E9BFBB;background:#FFF7F6;transform:none;box-shadow:none}
.btn.sm{padding:9px 15px;font-size:11.5px}
.btn.wide{width:100%}
.btn svg{width:17px;height:17px;flex:none}

/* ⭐ The one button the dashboard is built around. Charley: "The dashboard will have an obvious
   'upload your memories' button." So it is not a button in a row of buttons — it is a panel. */
.bigup{display:flex;align-items:center;gap:20px;width:100%;text-align:left;cursor:pointer;
  background:linear-gradient(135deg,var(--navy) 0%,var(--navy-2) 100%);color:#fff;border:0;
  border-radius:11px;padding:24px 26px;box-shadow:0 3px 14px rgba(4,30,66,.28);
  transition:transform .14s,box-shadow .14s;font-family:var(--sans);margin-bottom:20px;
  border-bottom:4px solid var(--orange)}
.bigup:hover{transform:translateY(-3px);box-shadow:0 16px 34px rgba(4,30,66,.32)}
.bigup .ring{width:60px;height:60px;border-radius:50%;background:var(--orange);color:var(--navy);
  display:grid;place-items:center;flex:none;box-shadow:0 3px 10px rgba(0,0,0,.25)}
.bigup .ring svg{width:31px;height:31px}
.bigup b{display:block;font-family:var(--serif);font-size:25px;letter-spacing:.01em;line-height:1.12}
.bigup span{display:block;margin-top:5px;font-size:13.5px;color:var(--sky);line-height:1.5}
.bigup .arw{margin-left:auto;flex:none;opacity:.55;font-size:26px}
@media(max-width:560px){.bigup{padding:18px 17px;gap:14px}.bigup b{font-size:20px}
  .bigup .ring{width:48px;height:48px}.bigup .ring svg{width:25px;height:25px}.bigup .arw{display:none}}

/* ═══════════════════════ Stat tiles ═══════════════════════ */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:12px}
.tile{background:var(--paper);border:1px solid var(--rule);border-radius:8px;padding:14px 15px;
  text-decoration:none;color:inherit;display:block;transition:border-color .12s,transform .12s}
a.tile:hover{border-color:var(--navy);transform:translateY(-2px)}
.tile b{display:block;font-family:var(--serif);font-size:29px;color:var(--navy);line-height:1}
.tile span{display:block;margin-top:5px;font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--mute)}
.tile.warn b{color:var(--orange)}
.tile.warn{border-color:var(--orange);background:#FFF8EF}

/* ═══════════════════════ The profile meter ═══════════════════════ */
.meter{display:flex;align-items:center;gap:14px;margin:4px 0 0}
.meter .bar{flex:1;height:11px;border-radius:999px;background:var(--cream);
  border:1px solid var(--rule);overflow:hidden}
.meter .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--orange),var(--gold));
  transition:width .5s cubic-bezier(.2,.8,.2,1)}
.meter .bar i.done{background:linear-gradient(90deg,var(--green),#4FA574)}
.meter .pct{font-family:var(--serif);font-size:21px;color:var(--navy);min-width:56px;text-align:right}

/* ═══════════════════════ Forms ═══════════════════════ */
.field{margin-bottom:19px}
.field > label,.lbl{display:block;font-weight:700;font-size:14px;color:var(--navy);margin-bottom:6px}
.field .hint{display:block;font-weight:400;font-size:12.5px;color:var(--mute);margin-top:3px;line-height:1.5}
.req{color:var(--pink)}
/* ⭐ 2026-09-20 — 16px, WAS 15.5px. iOS Safari zooms the whole page when the focused box's type is
   under 16px and never zooms back (Charley's list, item 1, with a screen recording). Half a pixel
   costs nothing anywhere and this is the base every portal form inherits; the smaller, designed
   sizes on the editor cards keep their look on a desktop and are lifted to 16px on phones by the
   block at the foot of this file. */
input[type=text],input[type=email],input[type=password],input[type=tel],input[type=number],select,textarea{
  width:100%;font:400 16px/1.5 var(--sans);color:var(--ink);background:#fff;
  border:1.5px solid var(--rule);border-radius:7px;padding:11px 13px;transition:border-color .12s,box-shadow .12s}
textarea{resize:vertical;min-height:92px}
input:focus,select:focus,textarea:focus{outline:0;border-color:var(--navy);box-shadow:0 0 0 3px rgba(4,30,66,.1)}
input::placeholder,textarea::placeholder{color:#AEB3BA}
select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236B6F76' d='M6 8 0 0h12z'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 13px center;padding-right:34px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:560px){.row2{grid-template-columns:1fr}}

/* The media release — it gates everything, so it does not look like a footnote. */
.agree{display:flex;gap:12px;align-items:flex-start;background:var(--cream);border:1.5px solid var(--rule);
  border-left:4px solid var(--orange);border-radius:7px;padding:15px 16px;cursor:pointer;font-size:14.5px;line-height:1.55}
.agree input{margin-top:2px;width:19px;height:19px;flex:none;accent-color:var(--navy);cursor:pointer}
.agree b{color:var(--navy)}

/* ═══════════════════════ The season-pass tick ═══════════════════════
   ⭐⭐ CHARLEY, 2026-09-11: "When a member uploads their photos, we should put a check mark below
   their name if it is a season pass. When they check that box, it is automatically entered into
   the season pass drawing for them. But we want them to know that happened."

   Three states live in this block and all three are visible at once on a busy screen, so they are
   told apart by ground and not by wording alone: UNTICKED is the cream strip everything else on
   the page is; ASKING is the same strip with the two fields open under it; DONE is the gold one
   with the wall's own ink. ⛔ The gold is measured against its ground, not picked — see .chip.pass. */
.passtick{background:var(--cream);border:1.5px solid var(--rule);border-left:4px solid var(--gold);
  border-radius:7px;padding:14px 16px;margin-bottom:19px}
.passtick.on{background:#FBF3E0;border-color:#E3C88A;border-left-color:var(--gold)}
.passtick > label{display:flex;gap:12px;align-items:flex-start;cursor:pointer;
  font-size:14.5px;line-height:1.55;margin:0}
.passtick > label input{margin-top:2px;width:19px;height:19px;flex:none;accent-color:var(--gold);cursor:pointer}
.passtick > label b{color:var(--navy)}
.passtick .hint{display:block;font-size:12.5px;color:var(--mute);margin:7px 0 0 31px;line-height:1.5}
.passbox{margin:14px 0 0 31px;padding-top:13px;border-top:1px solid #E3C88A}
.passbox .row2{grid-template-columns:150px 1fr}
.passbox label{display:block;font-weight:700;font-size:12.5px;color:var(--navy);margin-bottom:5px}
.passbox .acts{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:13px}
.passbox .msg{font-size:13px;line-height:1.5;color:var(--red);flex-basis:100%}
.passbox .msg.good{color:#1F6B44}
@media(max-width:560px){ .passbox .row2{grid-template-columns:1fr} }

/* ═══════════════════════ "Have you got your passes?" ═══════════════════════
   ⭐ Charley, same day: "we also want to make them aware of the 90passes page, and prompt them to
   send their passes in, anywhere we can. Especially the photos upload area and the memories area."
   ⛔ It is a PANEL and not a banner, and it never covers anything: the upload room and the
   memories room both have a job somebody came there to do, and a prompt that interrupts it would
   be paid for out of the labelling. */
.pxpromo{display:flex;gap:16px;align-items:flex-start;background:#FBF3E0;border:1.5px solid #E3C88A;
  border-left:4px solid var(--gold);border-radius:10px;padding:16px 18px;margin-bottom:18px}
.pxpromo .ic{flex:none;width:42px;height:42px;border-radius:8px;background:#fff;border:1px solid #E3C88A;
  display:grid;place-items:center;color:#7A5200}
.pxpromo h3{font-size:16px;color:var(--navy);margin-bottom:5px}
.pxpromo p{font-size:13.5px;line-height:1.6;color:#3A3A3A;margin:0}
.pxpromo .go{margin-top:11px;display:flex;gap:9px;flex-wrap:wrap}
@media(max-width:560px){ .pxpromo{flex-direction:column;gap:12px} }

/* ═══════════════════════ Save state ═══════════════════════ */
.saved{display:inline-flex;align-items:center;gap:7px;font-size:12px;letter-spacing:.07em;
  text-transform:uppercase;color:var(--mute);opacity:0;transition:opacity .25s}
.saved.on{opacity:1}
.saved.ok{color:var(--green)}
.saved .dot{width:8px;height:8px;border-radius:50%;background:currentColor;flex:none}
@keyframes pulse{0%,100%{opacity:.35}50%{opacity:1}}
.saved.busy .dot{animation:pulse 1s infinite}

/* ═══════════════════════ Media grid ═══════════════════════ */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(178px,1fr));gap:14px}
@media(max-width:480px){.grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}}
.card{background:var(--paper);border:1px solid var(--rule);border-radius:8px;overflow:hidden;
  cursor:pointer;text-align:left;padding:0;font:inherit;color:inherit;display:block;width:100%;
  transition:transform .13s,box-shadow .13s,border-color .13s;position:relative}
.card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lift);border-color:var(--navy)}
.card .well{aspect-ratio:4/3;background:var(--cream);display:grid;place-items:center;overflow:hidden;
  box-shadow:inset 0 -1px 0 var(--rule);position:relative}
.card .well img{width:100%;height:100%;object-fit:cover;display:block}
.card .well .ph{color:var(--navy);opacity:.22}
.card .well .ph svg{width:46px;height:46px}
.card .body{padding:10px 11px 12px}
.card .cap{font-size:13px;line-height:1.4;color:var(--ink);overflow:hidden;display:-webkit-box;
  -webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.8em}
.card .cap.empty{color:var(--mute);font-style:italic}
.card .foot{margin-top:8px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}

/* The label state, at a glance across a whole grid. */
.chip{font:700 10px/1 var(--sans);letter-spacing:.09em;text-transform:uppercase;
  padding:5px 8px;border-radius:999px;white-space:nowrap}
.chip.need{background:#FFF1DF;color:#8A5000;border:1px solid #F0C68A}
.chip.done{background:#E8F5EE;color:var(--green);border:1px solid #A9D8BF}
.chip.film{background:#FDE9F3;color:#9B2A69;border:1px solid #F3BEDA}
.chip.pend{background:var(--cream);color:var(--mute);border:1px dashed var(--rule)}
.chip.pub{background:#EAF1FB;color:var(--navy-2);border:1px solid #BFD4EE}
/* ⭐⭐ 2026-09-11 — the season-pass tick. Gold, because that is the 90 Passes room's own ink.
   ⛔ MEASURED, not chosen by eye (_tools/contrast, the book's WCAG rule applies to the site too):
   #7A5200 on #FBF3E0 is 6.26:1 — AA at this 10px uppercase, with room to spare. */
.chip.pass{background:#FBF3E0;color:#7A5200;border:1px solid #E3C88A}

.card .flag{position:absolute;top:9px;right:9px;width:24px;height:24px;border-radius:50%;
  background:var(--orange);color:var(--navy);display:grid;place-items:center;
  font:700 13px/1 var(--sans);box-shadow:0 2px 7px rgba(0,0,0,.25)}
.card.ok .flag{background:var(--green);color:#fff}

/* A tile for something sent that has not reached us yet — hollow, like The Peoples Page's. */
.card.ghost{border-style:dashed;cursor:default;background:transparent}
.card.ghost:hover{transform:none;box-shadow:none;border-color:var(--rule)}
.card.ghost .well{background:transparent;box-shadow:none}

/* ═══ The member's own delete (2026-09-01) ═══════════════════════════════════════════════════
   ⛔ WHY THE WRAPPER EXISTS. `.card` is a <button>, and a <button> may not contain another one —
   the parser un-nests it and the × ends up as a sibling of the grid, floating over the page. So
   the grid item is a wrapper and the × is the card's sibling, positioned over its corner.
   ⚠️ It sits TOP-LEFT: the top-right corner already holds `.flag`, the ✓ / count. */
.cardwrap{position:relative;display:block}
.cardx{position:absolute;top:8px;left:8px;width:26px;height:26px;border-radius:50%;
  border:1px solid rgba(255,255,255,.55);background:rgba(4,30,66,.62);color:#fff;cursor:pointer;
  font:400 17px/1 var(--sans);display:grid;place-items:center;padding:0 0 2px;
  box-shadow:0 2px 7px rgba(0,0,0,.28);
  /* ⭐ Hidden until wanted on a mouse, so it never competes with the photograph — but it must
     never be hover-only, because a phone has no hover and this is the button a member on a phone
     most needs. The coarse-pointer rule below puts it back. */
  opacity:0;transform:scale(.86);transition:opacity .13s,transform .13s,background .13s;z-index:2}
.cardwrap:hover .cardx,.cardx:focus-visible{opacity:1;transform:scale(1)}
.cardx:hover{background:var(--red);border-color:var(--red)}
@media(hover:none),(pointer:coarse){.cardx{opacity:1;transform:scale(1)}}

/* Taken down. Greyed and unclickable, but still showing its picture and its caption — a member
   has to be able to recognise what they removed in order to decide to put it back. */
.card.gone{cursor:default;border-style:dashed;background:var(--cream)}
.card.gone:hover{transform:none;box-shadow:none;border-color:var(--rule)}
.card.gone .well{opacity:.4;filter:grayscale(1)}
.card.gone .cap{color:var(--mute)}

/* ═══════════════════════ The labelling drawer ═══════════════════════ */
.scrim{position:fixed;inset:0;background:rgba(4,30,66,.55);backdrop-filter:blur(2px);z-index:60;
  display:grid;place-items:center;padding:20px;opacity:0;pointer-events:none;transition:opacity .18s}
.scrim.on{opacity:1;pointer-events:auto}
/* ⛔ `height`, NOT `max-height`. A grid container with an auto height lays its rows out at content
   size and only then gets clipped by max-height — so the row stayed 932px inside an 860px sheet,
   the form's own overflow never engaged, and "Save & next" was cut off below the fold. Measured:
   gridTemplateRows read back as `932.5px` with max-height:860px in force. A DEFINITE height plus
   `minmax(0,1fr)` is what actually constrains the row; `min-height:0` on the form alone is not
   enough, because there was never a smaller row for it to shrink into. */
.sheet{background:var(--paper);border-radius:12px;width:min(1000px,100%);
  height:min(90vh,860px);
  display:grid;grid-template-columns:1.15fr 1fr;grid-template-rows:minmax(0,1fr);
  overflow:hidden;box-shadow:0 30px 70px rgba(0,0,0,.4);
  transform:translateY(14px) scale(.99);transition:transform .2s}
.scrim.on .sheet{transform:none}
/* ⚠️ On the stacked layout the picture's height is set on the ROW, not on `.look`. An `auto` row
   sizes to its item's content and the item's own `max-height:34vh` does not pull the row down with
   it — measured at ~54% of the sheet before this. Capping the track is what actually works, and it
   matters: this is the phone layout, where most labelling will happen and where every pixel the
   photograph takes is a pixel off the form. */
@media(max-width:820px){.sheet{grid-template-columns:1fr;
  grid-template-rows:minmax(0,34vh) minmax(0,1fr);
  height:min(94vh,900px)}}
.sheet .look{background:#0A1730;display:grid;place-items:center;position:relative;min-height:230px}
.sheet .look img,.sheet .look video{max-width:100%;max-height:100%;object-fit:contain;display:block}
@media(max-width:820px){.sheet .look{max-height:34vh}}
.sheet .look .nav{position:absolute;bottom:12px;left:0;right:0;display:flex;justify-content:center;gap:8px}
.sheet .look .nav button{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.24);
  color:#fff;border-radius:999px;width:38px;height:38px;cursor:pointer;font-size:16px;line-height:1;
  display:grid;place-items:center;transition:background .12s}
.sheet .look .nav button:hover:not(:disabled){background:rgba(255,255,255,.28)}
.sheet .look .nav button:disabled{opacity:.3;cursor:not-allowed}
.sheet .look .count{position:absolute;top:12px;left:14px;color:#fff;font-size:11.5px;
  letter-spacing:.1em;text-transform:uppercase;opacity:.72}
.sheet .look .x{position:absolute;top:9px;right:11px;background:rgba(0,0,0,.4);border:0;color:#fff;
  width:34px;height:34px;border-radius:50%;cursor:pointer;font-size:19px;line-height:1}
/* ⭐⭐ 2026-09-11 — THE TURN. Charley: "add this as a feature for members for their own photos in
   their memories." It sits beside the ×, at the same weight, because it is the same kind of thing:
   something you do TO this photograph rather than something you write about it.
   ⛔ ALWAYS VISIBLE, never a hover-reveal. Most of this labelling happens on a phone, where there
   is no hover and a control that only appears on one does not exist. */
.sheet .look .turn{position:absolute;top:9px;right:51px;background:rgba(0,0,0,.4);border:0;color:#fff;
  width:34px;height:34px;border-radius:50%;cursor:pointer;font-size:17px;line-height:1;
  display:grid;place-items:center;transition:background .12s}
.sheet .look .turn:hover{background:rgba(0,0,0,.62)}
.sheet .look .turn:disabled{opacity:.4;cursor:default}
/* ⛔ `min-height:0` IS LOad-BEARING. A grid item defaults to `min-height:auto`, which means it
   refuses to shrink below its content — so the form grew to 932px inside an 860px sheet, its own
   `overflow-y:auto` never engaged, and the sheet's `overflow:hidden` simply cut the bottom off.
   Measured, not guessed. Only the FORM needs it — `.look` keeps its 230px floor, because at 230
   it never drives the row height and on the stacked mobile layout that floor is what stops the
   picture collapsing to nothing. */
.sheet .form{padding:22px 24px 0;overflow-y:auto;min-height:0}
@media(max-width:560px){.sheet .form{padding:17px 15px 0}}
.sheet .form h3{font-size:19px;color:var(--navy);margin-bottom:3px}
.sheet .form .why{color:var(--mute);font-size:13px;line-height:1.55;margin-bottom:17px}
/* ⚠️ The action bar sticks to the FOOT of the scrolling form, and it must span the form's full
   width — so it cancels the form's side padding with negative margins and re-applies it as its
   own. An earlier version used `bottom:-22px` against a padded form and the bar was clipped off
   the bottom of the sheet: "Save & next" was unreachable on any screen tall enough to scroll. */
.sheet .form .acts{display:flex;gap:10px;align-items:center;flex-wrap:wrap;
  position:sticky;bottom:0;z-index:2;background:var(--paper);
  margin:22px -24px 0;padding:15px 24px;border-top:1px solid var(--rule);
  box-shadow:0 -6px 14px rgba(255,253,248,.9)}
@media(max-width:560px){.sheet .form .acts{margin:18px -15px 0;padding:13px 15px}}

/* ═══════════════════════ Who-is-in-it ═══════════════════════
   Removable bubbles rather than a comma-soup text field, and the bubbles live INSIDE the box with
   the cursor — which is the whole reason the box can hold as many names as there are people in the
   picture without the field growing a scrollbar or the list getting away from whoever is typing.
   ⭐ One component drives both of these: the drawer's `#lbChips` and the editor card's `.chips`. */
.chips{display:flex;flex-wrap:wrap;align-items:center;gap:7px;width:100%;cursor:text;
  background:#fff;border:1.5px solid var(--rule);border-radius:7px;padding:7px 9px;
  transition:border-color .12s,box-shadow .12s}
.chips:focus-within{border-color:var(--navy);box-shadow:0 0 0 3px rgba(4,30,66,.1)}
.chips .tags{margin-top:0;gap:6px}
.chips .tags:empty{display:none}
.chips input[type=text]{flex:1 1 9em;min-width:9em;width:auto;border:0;padding:4px 3px;border-radius:0}
.chips input[type=text]:focus{outline:0;border:0;box-shadow:none}

.tags{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}
.tag{display:inline-flex;align-items:center;gap:7px;background:var(--cream);border:1px solid var(--rule);
  border-radius:999px;padding:6px 7px 6px 12px;font-size:13.5px;color:var(--navy)}
.tag button{background:none;border:0;cursor:pointer;color:var(--mute);font-size:15px;line-height:1;
  width:20px;height:20px;border-radius:50%;display:grid;place-items:center}
.tag button:hover{background:var(--rule);color:var(--ink)}
/* ⭐ 2026-09-10 — A BUBBLE IS EDITED WHERE IT STANDS. Charley: "once you enter the name it won't let
   you edit the name, it makes you erase and start over. Lets make it so once the name is in the
   bubble, you just need to click it to edit it." Click the name and the bubble becomes a box holding
   it: Enter or clicking away keeps the change, Escape puts it back, emptying it removes the bubble.
   See nameChips() in dashboard/index.html. */
.tag .tagname{cursor:text}
.tag:hover .tagname{text-decoration:underline dotted;text-underline-offset:3px}
.tag.editing{background:#fff;border-color:var(--orange);box-shadow:0 0 0 2px rgba(255,146,40,.22)}
.chips .tag input.tagedit,.tags .tag input.tagedit{flex:none;width:auto;min-width:3ch;max-width:100%;
  margin:0;padding:0 2px;border:0;border-radius:0;background:transparent;box-shadow:none;
  font:inherit;color:inherit;line-height:1.3}
.chips .tag input.tagedit:focus,.tags .tag input.tagedit:focus{outline:0;border:0;box-shadow:none}
/* ⚠️ Inside an editor card the name is a <span> inside a <label>, and `.ecard .efields label span`
   is the box CAPTION's rule — block, uppercase, bold. The same trap the bubble itself fell into
   (see the note on `.ecard .efields .chips .tag` below); everything it sets is set back here. */
.ecard .efields .chips .tag .tagname{display:inline;margin:0;font:inherit;letter-spacing:normal;
  text-transform:none;color:inherit}
.ecard .efields .chips .tag.editing{background:#fff;padding:2px 7px}
.ecard .efields .chips .tag input.tagedit{flex:none;width:auto;min-width:3ch;padding:0 2px;border:0;
  background:transparent;font-size:12.4px;line-height:1.3}
.sugg{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
.sugg button{background:transparent;border:1px dashed var(--rule);border-radius:999px;padding:5px 11px;
  font:400 12.5px/1.3 var(--sans);color:var(--mute);cursor:pointer}
.sugg button:hover{border-style:solid;border-color:var(--navy);color:var(--navy);background:var(--paper)}

/* ═══════════════════════ Drop zone ═══════════════════════ */
.drop{border:2.5px dashed var(--rule);border-radius:11px;background:var(--cream);padding:38px 22px;
  text-align:center;transition:border-color .14s,background .14s;cursor:pointer;display:block;width:100%;
  font-family:var(--sans)}
.drop:hover,.drop.over{border-color:var(--orange);background:#FFF8EF}
.drop .ic{color:var(--navy);opacity:.45;margin-bottom:11px}
.drop .ic svg{width:44px;height:44px}
.drop b{display:block;font-family:var(--serif);font-size:20px;color:var(--navy)}
.drop span{display:block;margin-top:7px;color:var(--mute);font-size:13.5px;line-height:1.55}

.qrow{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px dashed var(--rule)}
.qrow:last-child{border-bottom:0}
.qrow .th{width:52px;height:40px;border-radius:5px;background:var(--cream);flex:none;overflow:hidden;
  display:grid;place-items:center;border:1px solid var(--rule)}
.qrow .th img{width:100%;height:100%;object-fit:cover}
.qrow .nm{flex:1;min-width:0;font-size:13.5px}
.qrow .nm b{display:block;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.qrow .nm small{color:var(--mute);font-size:12px}
/* ⭐⭐ 2026-09-09 — the same complaint the /submit/ bar answers, in the row form. Charley: "I have
   uploaded a few things and am not sure if it is going." A file's `session` call to Apps Script
   runs BEFORE its first byte and takes one to five seconds, so this bar used to sit at exactly 0
   and then leap. The row now carries a number beside the bar, and dashboard/index.html creeps the
   figure while that call is away (see `send()` there). The barber-pole is the part that keeps
   moving once the creep has all but converged — off under prefers-reduced-motion. */
.qrow .prog{width:110px;height:8px;border-radius:999px;background:var(--cream);border:1px solid var(--rule);
  overflow:hidden;flex:none}
.qrow .prog i{display:block;position:relative;height:100%;overflow:hidden;background:var(--orange);transition:width .2s linear}
.qrow .prog i::after{content:"";position:absolute;inset:0;
  background-image:linear-gradient(115deg,rgba(255,255,255,.5) 25%,transparent 25%,transparent 50%,
    rgba(255,255,255,.5) 50%,rgba(255,255,255,.5) 75%,transparent 75%,transparent);
  background-size:20px 20px;animation:qbarber .75s linear infinite}
@keyframes qbarber{from{background-position:0 0}to{background-position:20px 0}}
/* tabular figures so the bar does not shuffle sideways as the digits change */
.qrow .pct{flex:none;min-width:2.9em;text-align:right;font-size:12.5px;font-weight:700;color:var(--navy);
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
@media(max-width:560px){.qrow .prog{width:64px}.qrow .pct{min-width:2.6em;font-size:11.5px}}
@media(prefers-reduced-motion:reduce){
  .qrow .prog i{transition:none}
  .qrow .prog i::after{animation:none;opacity:.25}
}

/* ═══════════════════════ Notices ═══════════════════════ */
.note{border-radius:8px;padding:14px 16px;font-size:14px;line-height:1.6;margin-bottom:18px;
  border:1px solid var(--rule);background:var(--cream)}
.note b{color:var(--navy)}
.note.warn{background:#FFF8EF;border-color:#F0C68A}
.note.good{background:#E8F5EE;border-color:#A9D8BF}
.note.bad{background:#FDECEA;border-color:#F0B3AC}

/* ═══════════════════════ The empty states ═══════════════════════
   The negative space rule, borrowed from the sketchbook: no page shows what it has without
   showing the shape of what it lacks. An empty grid says what would be here and how to fill it. */
.empty{text-align:center;padding:46px 22px;color:var(--mute)}
.empty .ic{opacity:.2;color:var(--navy);margin-bottom:14px}
.empty .ic svg{width:56px;height:56px}
.empty b{display:block;font-family:var(--serif);font-size:21px;color:var(--navy);margin-bottom:7px}
.empty p{max-width:44ch;margin:0 auto 18px;font-size:14.5px;line-height:1.6}

/* ═══════════════════════ Pass cards ═══════════════════════ */
.passgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}
.passcard{background:var(--paper);border:1px solid var(--rule);border-radius:8px;overflow:hidden;
  box-shadow:var(--shadow)}
.passcard .well{aspect-ratio:3/4;background:var(--cream);display:grid;place-items:center;overflow:hidden}
.passcard .well img{width:100%;height:100%;object-fit:cover}
.passcard .body{padding:12px 13px 14px}
.passcard .yr{font-family:var(--serif);font-size:25px;color:var(--navy);line-height:1}
.passcard .hold{font-size:13px;color:var(--mute);margin-top:4px}

/* ═══════════════════════ The media release ═══════════════════════
   Shared by the dashboard's release gate (the first screen a new member sees) and anywhere else
   the agreement renders. A 700-word legal document inline would push the button off the bottom
   of a phone, so the text scrolls in its own box. Moved here from index.html when the release
   moved off the join form (2026-08-28). */
.release{background:var(--cream);border:1px solid var(--rule);border-radius:11px;padding:15px 16px;
  margin-bottom:16px}
.release .relnote{font-size:13.6px;line-height:1.6;color:var(--mute);margin-bottom:12px}
.release .body{font-size:12.9px;line-height:1.62;color:#3b4046;max-height:280px;overflow-y:auto;
  background:var(--paper);border:1px solid var(--rule);border-radius:8px;padding:13px 14px;
  margin-bottom:13px}
.release .body p{margin-bottom:9px}
.release .body ul{margin:0 0 9px 17px}
.release .body li{margin-bottom:6px}

/* ═══════════════════════ The danger zone ═══════════════════════
   Deleting an account is a soft delete — everything withdrawn and marked unusable, nothing
   destroyed — but the panel still has to look like what it is. */
.panel.dangerzone{border-color:#E9BFBB;border-left:4px solid var(--red)}
.panel.dangerzone h2{color:var(--red)}

/* ═══════════════════════ Footer ═══════════════════════ */
footer.pt{border-top:1px solid var(--rule);margin-top:20px;padding:26px 0 46px;color:var(--mute);font-size:13px}
footer.pt .wrap{display:flex;gap:18px;flex-wrap:wrap;align-items:center}
footer.pt a{color:var(--navy)}

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

/* ⛔⛔ `display:flex` AND `display:grid` BEAT THE `hidden` ATTRIBUTE. The UA rule for [hidden] is
   `display:none` at author-origin-zero, so ANY display in this file overrides it and the element
   goes on showing while the code reads as though it were gone. portal/index.html has carried this
   rule in its inline CSS since 2026-08-27; portal.css never got it, and the dashboard loads only
   portal.css — so `#railEditor`, which is `.rail a{display:flex}` and starts `hidden`, was on
   screen for EVERY member, editor or not. The room itself was never reachable (route() sends a
   non-editor straight back to home, and acct.editorGet refuses server-side), but a dead link
   labelled "Edit photos" in a member's own sidebar is its own small breach of trust.
   Found 2026-09-01 while wiring the members' delete. Keep this rule at the bottom, where it wins. */
[hidden]{display:none!important}

/* ⚠️ The design-phase banner. It is loud on purpose and it is removed with the mock store — while
   it is on screen, nothing typed into this portal is being kept anywhere but this browser. */
.mockbar{background:repeating-linear-gradient(45deg,#2B2B2B,#2B2B2B 11px,#242424 11px,#242424 22px);
  color:#FFD9A0;font:600 11.5px/1.5 var(--mono);letter-spacing:.03em;padding:7px 16px;text-align:center;
  position:relative;z-index:40}
.mockbar b{color:#fff}
.mockbar button{background:rgba(255,255,255,.13);border:1px solid rgba(255,255,255,.25);color:#fff;
  border-radius:999px;padding:3px 11px;font:inherit;font-size:11px;cursor:pointer;margin-left:9px}
.mockbar button:hover{background:rgba(255,255,255,.24)}


/* ═══════════════════════ The profile questions ═══════════════════════
   Charley, 2026-08-26: "the 'complete your profile' — make that look more appealing for each
   question. We really want people to fill this info out."

   The nine were a stack of bold labels over grey boxes inside one panel, which read as a form to
   be got through. Each is now its OWN card, and each card does three things a form field does not:
   it numbers itself so nine feels finite, it says WHY the answer is wanted, and it turns green the
   moment it holds one. The number becomes a tick — the whole page is a row of ticks to collect.

   Nothing is required, nothing blocks anything, and there is still no submit button. */
.qlist{display:grid;gap:15px;margin-bottom:20px}

.qcard{position:relative;background:var(--paper);border:1.5px solid var(--rule);border-radius:10px;
  padding:18px 20px 19px;box-shadow:var(--shadow);
  transition:border-color .15s,box-shadow .15s,background .2s}
.qcard:hover{border-color:#C2B9A6}
.qcard:focus-within{border-color:var(--navy);box-shadow:0 0 0 3px rgba(4,30,66,.09),var(--shadow)}
@media(max-width:560px){.qcard{padding:15px 15px 16px;border-radius:8px}}

/* answered — the reward for the answer is that the card changes state under you */
.qcard.done{border-color:#A9D8BF;background:linear-gradient(180deg,#F4FBF7,var(--paper) 90px)}
.qcard.done:hover{border-color:#8CC9A9}

.qcard .qhd{display:flex;align-items:flex-start;gap:13px;margin-bottom:12px}
@media(max-width:560px){.qcard .qhd{gap:11px}}

/* the numeral, which becomes a tick */
.qcard .num{flex:none;width:29px;height:29px;border-radius:50%;display:grid;place-items:center;
  background:var(--cream);border:1.5px solid var(--rule);color:var(--mute);
  font:700 13px/1 var(--sans);margin-top:1px;transition:background .2s,border-color .2s,color .2s}
.qcard .num svg{width:15px;height:15px;display:none}
.qcard.done .num{background:var(--green);border-color:var(--green);color:#fff}
.qcard.done .num span{display:none}
.qcard.done .num svg{display:block}

.qcard .qtx{min-width:0;flex:1}
.qcard .qq{display:block;font-family:var(--serif);font-weight:700;font-size:17.5px;line-height:1.25;
  color:var(--navy);letter-spacing:.005em;cursor:pointer}
@media(max-width:560px){.qcard .qq{font-size:16px}}
.qcard .why{display:block;margin-top:5px;font-size:13px;line-height:1.55;color:var(--mute);max-width:60ch}

/* "counts double" — the three questions the archive actually needs, said out loud */
.qcard .wt{display:inline-flex;align-items:center;gap:5px;margin-top:9px;
  background:#FDE9F3;color:#9B2A69;border:1px solid #F3BEDA;border-radius:999px;
  padding:3px 10px;font:700 9.5px/1.35 var(--sans);letter-spacing:.11em;text-transform:uppercase}

.qcard textarea,.qcard input[type=text]{background:var(--cream)}
.qcard.done textarea,.qcard.done input[type=text]{background:var(--paper)}
.qcard textarea{min-height:74px;overflow:hidden}

/* the jump-to chips on the meter panel */
.togo{display:flex;flex-wrap:wrap;gap:7px;margin-top:15px}
.togo button{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-family:inherit;
  background:var(--cream);border:1px dashed var(--rule);border-radius:999px;padding:6px 13px;
  font-size:12.5px;font-weight:600;color:var(--navy);transition:border-color .12s,background .12s}
.togo button:hover{border-style:solid;border-color:var(--navy);background:var(--paper)}
.togo button::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--orange);flex:none}
.togo .lead{font-size:12.5px;color:var(--mute);align-self:center;margin-right:2px}

/* a card you have just jumped to says so for a moment, or the chip looks like it did nothing */
@keyframes qflash{0%{box-shadow:0 0 0 0 rgba(255,146,40,.55)}100%{box-shadow:0 0 0 13px rgba(255,146,40,0)}}
.qcard.flash{border-color:var(--orange);animation:qflash .9s ease-out 1}
@media(prefers-reduced-motion:reduce){.qcard.flash{animation:none}}


/* ═══════════════════════ The editor floor ═══════════════════════
   ⭐ Charley, 2026-08-28: "each photo could live on a card of some kind that has fields to fill
   out … Year photo was taken, People in the photo, Where is it, What is happening."

   The shape is a working surface, not a gallery: the photograph is fixed at the left so the eye
   lands in the same place on every card down the page, and the four boxes sit beside it so an
   editor can look and type without either one moving. Twenty-five of these is a sitting.

   The one visual decision worth defending: a card that holds an answer turns GREEN and says so.
   Dave Elmquist has three hundred photographs to get through, and the only thing that makes that
   bearable is being able to see, by scrolling, how far he has got. */

.edsearch{display:flex;gap:10px;margin-top:17px}
.edsearch input{flex:1;min-width:0;padding:13px 15px;font:inherit;font-size:15.5px;color:var(--ink);
  background:var(--paper);border:1.5px solid var(--rule);border-radius:8px}
.edsearch input:focus{outline:0;border-color:var(--navy);box-shadow:0 0 0 3px rgba(4,30,66,.09)}
.edsearch .btn{flex:none}
/* ⚠️ `.empty b` is a block heading in this stylesheet; a bold word INSIDE the empty state's
   paragraph must stay inline or it breaks the sentence in half. */
.empty p b{display:inline;font-family:inherit;font-size:inherit;color:var(--navy);margin:0}
@media(max-width:560px){.edsearch{flex-direction:column}.edsearch .btn{width:100%}}

.panel.editorcall{border-left:4px solid var(--orange)}

/* ⭐⭐ 2026-09-10 — HOW MUCH YOU HAVE EDITED, and the list behind the number. Charley: "Lets have a
   way editors can see how many things they have edited … It should land in the 'edit photos' page
   with the total number of photos that person has been credited for 'editing'. Below that number,
   there should be a link or button that says 'See Edits'." */
.edtop{display:flex;gap:20px;align-items:flex-start}
.edtop > .sub{flex:1;min-width:0;margin:0}
.edmine{flex:none;width:190px;text-align:center;background:var(--cream);border:1px solid var(--rule);
  border-radius:9px;padding:13px 14px 14px}
.edmine b{display:block;font-family:var(--serif);font-size:36px;line-height:1;color:var(--navy)}
.edmine > span{display:block;margin:6px 0 11px;font-size:10.5px;line-height:1.4;letter-spacing:.1em;
  text-transform:uppercase;color:#5F636A}
.edmine .btn{width:100%}
@media(max-width:700px){.edtop{flex-direction:column}.edmine{width:100%}}

/* the See Edits page — one panel a day, one line a photograph */
.edsum{margin:0;color:#5F636A;font-size:14px;line-height:1.55}
.edsum b{color:var(--navy)}
.edday .hd{margin-bottom:8px}
.edlist{list-style:none;margin:0;padding:0}
.edlist li + li{border-top:1px solid var(--rule)}
.edli{display:flex;align-items:center;gap:13px;padding:9px 8px;border-radius:7px;text-decoration:none;color:var(--ink)}
a.edli:hover{background:var(--cream)}
a.edli:hover .tx b{text-decoration:underline;text-underline-offset:3px}
/* ⚠️ a crop is right HERE and wrong on the editor card: this thumbnail is a pointer back to a
   photograph already described, not the question being asked about it (see `.ecard .shot`) */
.edli .th{flex:none;width:68px;height:51px;border-radius:5px;background:#0A1730;overflow:hidden;
  display:grid;place-items:center}
.edli .th img{width:100%;height:100%;object-fit:cover;display:block}
.edli .tx{flex:1;min-width:0}
.edli .tx b{display:block;font-size:14px;font-weight:600;line-height:1.35;color:var(--navy);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.edli .ds{display:block;margin-top:2px;font-size:12.5px;line-height:1.4;color:#5F636A;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.edli .gn{display:inline-block;margin-top:3px;font:700 10.5px/1.3 var(--sans);letter-spacing:.07em;
  text-transform:uppercase;color:var(--red)}
.edli .tm{flex:none;font-size:12px;color:#5F636A;white-space:nowrap}
.edli.gone .th{opacity:.55}
@media(max-width:560px){.edli{gap:10px}.edli .th{width:56px;height:42px}.edli .tm{display:none}}

/* the results bar */
.edbar{padding-bottom:16px}
.edmeter{display:flex;align-items:center;gap:12px;margin-bottom:13px}
.edmeter .bar{flex:1;height:8px;border-radius:999px;background:var(--cream);
  border:1px solid var(--rule);overflow:hidden}
.edmeter .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--orange),var(--gold));
  transition:width .3s}
.edmeter .bar i.done{background:linear-gradient(90deg,var(--green),#4FA574)}
.edmeter span{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--mute);flex:none}

.edrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.edrow .spacer{flex:1}
.edfoot{justify-content:center;margin:22px 0 10px}
.edfoot .meta{font-size:12px;letter-spacing:.09em;text-transform:uppercase;color:var(--mute)}
.edtoggle{display:inline-flex;align-items:center;gap:9px;font-size:13.5px;color:var(--mute);cursor:pointer}
.edtoggle input{width:17px;height:17px;accent-color:var(--navy);cursor:pointer}

/* the cards */
/* ⚠️ ONE CARD PER ROW, at every width. Two columns of cards was tried first and is wrong: the
   media query would be on the VIEWPORT while the cramping is in the CARD, so a 1280px window gave
   two 432px cards whose four boxes were 110px wide each — unusable for typing a sentence about
   what is happening. Full width gives the photograph a fixed left edge all the way down the page
   and the boxes room to hold an answer. Twenty-five in a column is the sitting. */
.edgrid{display:grid;gap:14px}

.ecard{display:flex;gap:0;background:var(--paper);border:1.5px solid var(--rule);border-radius:10px;
  overflow:hidden;box-shadow:var(--shadow);transition:border-color .15s,background .25s}
.ecard:focus-within{border-color:var(--navy);box-shadow:0 0 0 3px rgba(4,30,66,.09),var(--shadow)}
.ecard.done{border-color:#A9D8BF;background:linear-gradient(180deg,#F4FBF7,var(--paper) 110px)}
.ecard.done:focus-within{border-color:var(--green)}

/* ⭐⭐ THE WHOLE PHOTOGRAPH, NEVER A CROP — Charley, 2026-08-28: "lets make sure you can see the
   whole photo right there on the card."
   ⛔ `object-fit: cover` was wrong here and it is worth saying why, because it is the right answer
   almost everywhere else on this site. A cropped thumbnail is fine when the picture is DECORATION
   and the visitor can click through. On this card the picture is the QUESTION — the editor is being
   asked who is in it and what is happening — and `cover` silently eats the edges of every frame,
   which on a group photograph is exactly where the person nobody has named is standing.
   So: `contain`, on the dark ground, and the box is big enough to work from. */
/* ⚠️ The box STRETCHES to the card and a portrait frame therefore makes its own card taller, which
   is right — a tall photograph earns the room. The cap stops a panorama or a full-page newspaper
   scan running a single card down the screen. */
.ecard .shot{flex:none;width:290px;min-height:236px;max-height:400px;background:#0A1730;display:grid;
  place-items:center;align-self:stretch;position:relative;overflow:hidden}
.ecard .shot img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block}
/* ⭐ 2026-09-11 — the turn on an editor's card is a word in the FOOTER, next to the delete, and not
   a button over the picture. ⛔ `.shot` is an <a> to the full-size frame: a <button> inside it is
   invalid markup, the browser un-nests it, and the click navigates instead of turning. The
   thumbnail turning under their hand is the feedback; the control does not have to sit on it. */
.ecard .efoot .turn{font-variant-numeric:tabular-nums}
/* a quiet affordance rather than a zoom — the thumbnail is 480px and the full 1400px frame is one
   click away, which is where a face actually gets identified */
.ecard .shot::after{content:"open full size";position:absolute;left:0;right:0;bottom:0;
  padding:6px 8px;font:600 9.5px/1 var(--sans);letter-spacing:.1em;text-transform:uppercase;
  color:#fff;text-align:center;background:linear-gradient(transparent,rgba(4,23,48,.85));
  opacity:0;transition:opacity .15s;pointer-events:none}
.ecard .shot:hover::after,.ecard .shot:focus-visible::after{opacity:1}
@media(max-width:980px){.ecard .shot{width:220px;min-height:200px}}
@media(max-width:620px){
  .ecard{flex-direction:column}
  .ecard .shot{width:100%;min-height:0;aspect-ratio:4/3}
}

.ecard .ebody{flex:1;min-width:0;padding:13px 15px 14px}
.ecard .ehd{display:flex;align-items:flex-start;gap:10px;margin-bottom:11px}
.ecard .ehd b{flex:1;min-width:0;font-size:13.4px;font-weight:600;line-height:1.4;color:var(--navy);
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.ecard .edone{flex:none;font:700 10px/1 var(--sans);letter-spacing:.09em;text-transform:uppercase;
  color:var(--green);white-space:nowrap;padding-top:2px}

/* ⭐⭐ 2026-09-08 — THE TITLE IS PRINTED ON THE CARD, NOT TYPED INTO IT (Charley: "no one should be
   able to directly edit the Title … That can be taken out"). `.ehd b` above already carried the
   look, because the members'-uploads card next door has always shown its title this way; these two
   rules are what make it read as a RESULT rather than as a box somebody forgot to enable. */
/* ⛔ NO BORDER — see the note on #sgded .sgded-title b in portal/photo-edit.js. With a rule
   around it the title read as an input somebody had disabled. */
.ecard .ehd b[data-title]{padding:7px 10px;border-radius:6px;background:var(--cream);
  font-size:14.4px;-webkit-line-clamp:3}
.ecard.done .ehd b[data-title]{background:rgba(255,255,255,.7)}
.ecard .ehd b[data-title].none{font-weight:400;font-style:italic;color:var(--red)}

/* Where a prefilled year came from. Quieter than the red `.eerr` above it — this is provenance,
   not a complaint, and both can be on screen at once. */
.ecard .eguess{display:block;margin-top:5px;font-size:11.4px;font-style:normal;line-height:1.45;
  color:var(--mute)}
.ecard .eguess b{color:var(--navy);font-weight:600}

/* A tidy 2×2: Year | People over Where | What. The right-hand column is wider because both of the
   answers that land in it — who is in it, and what is going on — are sentences, and both of the
   left-hand ones are a few words. */
.ecard .efields{display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr);gap:10px 13px}
@media(max-width:820px){.ecard .efields{grid-template-columns:1fr}}
.ecard .efields label{display:block;min-width:0}
.ecard .efields label span{display:block;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute);font-weight:700;margin-bottom:4px}
.ecard .efields input,.ecard .efields textarea{width:100%;padding:8px 10px;font:inherit;font-size:13.6px;
  color:var(--ink);background:var(--paper);border:1.5px solid var(--rule);border-radius:6px;
  line-height:1.45;resize:vertical}
.ecard .efields input:focus,.ecard .efields textarea:focus{outline:0;border-color:var(--navy);
  background:#fff}
.ecard.done .efields input,.ecard.done .efields textarea{background:rgba(255,255,255,.75)}
/* ⭐⭐ 2026-09-11 — the season-pass tick, on an editor's card. Charley: "we as editors should be
   able to mark season passes for other people. If people send something in, they might not realize
   their is a second competition for the passes."
   ⚠️ THE OVERRIDES ARE NOT DECORATION. `.ecard .efields label` is the stronger selector, so
   without these three the tick row would lose its flex and the box would draw as a plain stack of
   editor fields with a checkbox floating above it. The two inputs inside it are LEFT to the card's
   own styling on purpose — they should look like every other box on the card. */
.ecard .efields .passtick{grid-column:1/-1;margin-bottom:0}
.ecard .efields .passtick > label{display:flex;gap:10px;align-items:center;font-size:13.5px;
  text-transform:none;letter-spacing:normal;color:var(--ink);margin:0}
.ecard .efields .passtick > label input[type=checkbox]{width:17px;height:17px;flex:none;padding:0;
  border:0;background:none;accent-color:var(--gold)}
.ecard .efields .passtick .hint{margin-left:27px}
.ecard .efields .passbox{margin-left:27px}
/* ⚠️ The People box is a chip strip in here too, and `.ecard .efields input` is the stronger
   selector — without these the cursor would sit inside a second full-width bordered box drawn
   inside the first one. Same component, editor-sized. */
.ecard .efields .chips{background:var(--paper);border:1.5px solid var(--rule);border-radius:6px;
  padding:5px 7px;gap:5px}
.ecard .efields .chips:focus-within{border-color:var(--navy);background:#fff;box-shadow:none}
.ecard .efields .chips input[type=text]{flex:1 1 8em;min-width:8em;width:auto;padding:3px 2px;
  border:0;background:transparent;font-size:13.6px;border-radius:0}
.ecard .efields .chips input[type=text]:focus{border:0;background:transparent;box-shadow:none}
/* ⚠️ A `.tag` IS A <span>, and `.ecard .efields label span` above is the box caption's rule —
   block, uppercase, 10.5px, bold, with a margin under it. Left alone it broke every bubble onto
   two lines with the × underneath the name. Everything it sets has to be set back here. */
.ecard .efields .chips .tag{display:inline-flex;align-items:center;gap:6px;
  font:400 12.4px/1.3 var(--sans);letter-spacing:normal;text-transform:none;color:var(--navy);
  margin-bottom:0;padding:3px 4px 3px 9px;background:var(--cream)}
.ecard .efields .chips .tag button{display:grid;place-items:center;width:17px;height:17px;
  font-size:13px;margin-bottom:0;color:var(--mute)}
.ecard.done .efields .chips{background:rgba(255,255,255,.75)}

/* ⭐ 2026-08-31 — the members' uploads share the .ecard shell, with two differences: some have no
   full-size link to hang on (`span.shot` rather than `a.shot`, so the hover caption must not
   promise one), and the heading is whose photograph it is rather than what it is called. */
.ecard span.shot{position:relative;display:grid;place-items:center}
.ecard span.shot::after{content:none}
.ecard .ehd b{line-height:1.45}
.ecard .efoot .credit{font-size:11.5px;color:var(--mute)}
.ecard .edsearch-inline{flex:1;min-width:200px}

.ecard .efoot{display:flex;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}
.ecard .efoot .spacer{flex:1}
.ecard .efoot .saved{font-size:11.5px}
.ecard .efoot .saved.bad{color:var(--red)}
.ecard .efoot .credit{font-size:11.5px;color:var(--mute);font-style:italic;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}

/* ⭐⭐ THE TITLE BOX — Charley, 2026-08-29: "the label that i first create is the label until
   someone updates the label itself … Once it is edited, whatever the edit is, should become the
   label." It sits where the filename used to be printed, at the top of the card and in the same
   navy, because it is the same sentence — only now it can be typed into. It writes itself out of
   the boxes below until somebody puts a hand on it. */
.ecard .etitle{flex:1;min-width:0;display:block}
.ecard .etitle span{display:block;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute);font-weight:700;margin-bottom:4px}
.ecard .etitle input{width:100%;padding:7px 10px;font:inherit;font-size:14.6px;font-weight:600;
  line-height:1.35;color:var(--navy);background:var(--cream);border:1.5px solid transparent;
  border-radius:6px;transition:background .15s,border-color .15s}
.ecard .etitle input:hover{border-color:var(--rule)}
.ecard .etitle input:focus{outline:0;background:#fff;border-color:var(--navy)}
.ecard.done .etitle input{background:rgba(255,255,255,.7)}

/* The name off the scan, kept in front of the editor rather than thrown away — it is what the
   search index still holds and what Charley typed at ingest. */
.ecard .efrom{margin:7px 0 11px;font-size:11.6px;line-height:1.5;color:var(--mute)}
.ecard .efrom b{font-weight:600;color:var(--ink)}

/* ⭐ THE ONE REQUIRED ANSWER. "The only real requirement an editor should have is putting the
   year." Marked on the label, said in red under the box when Save is pressed without one. */
.ecard .ereq span i{display:inline-block;margin-left:6px;font-style:normal;font-size:9.5px;
  letter-spacing:.09em;color:var(--orange)}
.ecard .efields input.bad{border-color:var(--red);background:#FFF7F6;border-width:2px}
.ecard .eerr{display:block;margin-top:5px;font-size:11.6px;font-style:normal;line-height:1.45;
  color:var(--red)}

/* ⭐ 2026-09-17 — A RING ON THE REFUSED BOX. Charley: *"if there is an error, it could pop to
   that spot on the part that is not put in… a red field showing the error and where it is on the
   page so its easy to change."*

   ⚠️ WHAT WAS ALREADY TRUE HERE, measured before changing anything: pressing Save on a card with
   no year ALREADY turned this box red, ALREADY put the red line under it, ALREADY marked the whole
   card amber (`.ecard.needsyear` below) with "⚠ needs a year" on its header, and the bare
   `box.focus()` in the dashboard ALREADY scrolled the box fully into view — in a harness with a
   111px sticky header the box went from 460px above the fold to 344px down the viewport, clear of
   the header. ⛔ **So this is a prominence change, not a repair.** The claim that the field was
   being scrolled under the header was mine and was wrong; it did not survive being tested.

   What is new is the RING — the orange `qflash` above, in red, because the eye needs something
   that moves to land on a border that has changed colour on a page full of borders. Same gesture
   as the jump-to-card flash, so it reads the same way everywhere on the floor.

   ⛔ NO CARD-LEVEL RULE HERE. A first pass added `.ecard.hasbad{border-color:var(--red)}` and it
   was wrong twice over: `.ecard.needsyear` below already marks the card for exactly this, in the
   amber this floor uses for "not finished yet" — and being LATER in the file at equal specificity
   it wins the cascade anyway, so the red rule would not even have painted. */
@keyframes ebadflash{0%{box-shadow:0 0 0 0 rgba(179,38,30,.5)}100%{box-shadow:0 0 0 13px rgba(179,38,30,0)}}
.ecard .efields input.bad.flash{animation:ebadflash .9s ease-out 1}
@media(prefers-reduced-motion:reduce){.ecard .efields input.bad.flash{animation:none}}

/* Whose box of photographs it came out of. Full width under the 2×2, because it is the one answer
   that is usually the same for three hundred cards in a row. */
.ecard .ecoll{grid-column:1/-1}
.ecard .ecollrow{display:flex;gap:9px;flex-wrap:wrap;align-items:center}
.ecard .ecollrow select{flex:0 1 300px;min-width:0;padding:8px 10px;font:inherit;font-size:13.6px;
  color:var(--ink);background:var(--paper);border:1.5px solid var(--rule);border-radius:6px}
.ecard .ecollrow select:focus{outline:0;border-color:var(--navy);background:#fff}
.ecard .ecollrow input{flex:1 1 220px;min-width:0}

/* ⭐ An answer with no year is not finished, and the card says so in amber rather than green. */
.ecard.needsyear{border-color:#EFC98A;background:linear-gradient(180deg,#FFFAF1,var(--paper) 110px)}
.ecard.needsyear:focus-within{border-color:var(--orange)}
.ecard.needsyear .edone{color:var(--orange)}
.edneed{margin:-4px 0 12px;font-size:12.5px;color:var(--orange)}

/* ⭐⭐ A PHOTOGRAPH TAKEN OFF THE WEBSITE (2026-08-31). Charley: "editors should be able to delete
   photos out of the website."

   The card STAYS ON THE PAGE, greyed, with its boxes disabled and its one button now reading "Put
   it back". That is the whole design decision and it is worth defending: a delete that made the
   card vanish would give somebody working at speed through three hundred photographs no way to
   see what they had just done, and no way back except finding the picture again in a search that
   no longer returns it.

   The photograph itself is desaturated rather than hidden — an editor has to be able to see WHICH
   one they took down. */
.ecard.removed{border-color:var(--rule);background:var(--cream);box-shadow:none;opacity:.86}
.ecard.removed:focus-within{border-color:var(--rule);box-shadow:none}
.ecard.removed .shot img{filter:grayscale(1);opacity:.5}
.ecard.removed .etitle input{text-decoration:line-through;color:var(--mute)}
.ecard.removed .edone{color:var(--red)}
.ecard.removed .efields{opacity:.55}
.ecard.removed .efields input,.ecard.removed .efields textarea,
.ecard.removed .ecollrow select{background:var(--cream)}
/* The sentence that says what "removed" means, on the card where the question is being asked. */
.ecard .egone{margin:0 0 12px;padding:9px 11px;font-size:12.2px;line-height:1.5;color:var(--ink);
  background:#FFF7F6;border:1px solid #E9BFBB;border-radius:6px}
.ecard .egone b{font-weight:700;color:var(--red)}
/* …and the same fact said once for the whole search, up in the results bar. */
.edgone{margin:-4px 0 12px;font-size:12.5px;color:var(--mute)}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   THE PROFILE, 2026-09-01 — the masthead, the congratulation, and the finished page

   Charley: "once you complete your profile, the page changes and says 'Congratulations for
   completing your profile!' and then rebuilds the page to look nice with all that info all put
   together. From here they should also be able to edit that anytime. Lets also add a way they can
   add a profile photo to their page, and also the signed in icon in the top right. The option to
   add a photo should be at the top of the page."

   Three blocks below: the masthead that carries the portrait (both faces, always first), the
   congratulation, and the assembled page. Nothing here invents a colour — it is the same navy,
   orange, cream and dashed rule the rest of the room is built from.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── 1 · The masthead ────────────────────────────────────────────────────────────────────────
   ⭐ ONE PORTRAIT ON THE PAGE. This panel is the top of the profile in BOTH faces — the form and
   the finished page — so the picture is never shown twice on one screen, and "the option to add a
   photo should be at the top of the page" is true whichever face is showing. */
.pfhead{padding-bottom:22px}
.pfid{display:flex;align-items:center;gap:24px}
@media(max-width:620px){.pfid{flex-direction:column;align-items:flex-start;gap:16px}}

.pfav{position:relative;flex:none;width:132px;height:132px;border-radius:50%;cursor:pointer;
  padding:0;overflow:hidden;background:var(--cream);border:3px solid var(--rule);
  display:grid;place-items:center;transition:border-color .14s,box-shadow .14s,transform .14s}
.pfav:hover{border-color:var(--orange);box-shadow:0 8px 20px rgba(4,30,66,.16);transform:translateY(-2px)}
.pfav img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.pfav .ini{font-family:var(--serif);font-size:40px;color:var(--navy);opacity:.42;letter-spacing:.02em}
/* the dashed ring is the empty state's invitation; a portrait covers it */
.pfav .ring{position:absolute;inset:9px;border-radius:50%;border:2px dashed rgba(4,30,66,.22);
  pointer-events:none}
.pfav.has .ring{display:none}
/* the camera badge — always there, so "this is the thing you press" needs no hover to discover */
.pfav .cam{position:absolute;right:4px;bottom:4px;width:36px;height:36px;border-radius:50%;
  background:var(--orange);color:var(--navy);display:grid;place-items:center;
  box-shadow:0 2px 7px rgba(4,30,66,.3);border:2px solid var(--paper)}
.pfav .cam svg{width:18px;height:18px}
/* uploading. ⚠️ The bar is a straight strip clipped by the round frame, which reads as a filling
   arc — no second element, no animation to keep in step with the real progress number. */
.pfav.busy::after{content:'';position:absolute;left:0;bottom:0;height:6px;width:var(--p,0%);
  background:var(--orange);transition:width .2s}
.pfav.busy img{opacity:.55}
@media(max-width:620px){.pfav{width:104px;height:104px}.pfav .ini{font-size:32px}
  .pfav .cam{width:30px;height:30px}.pfav .cam svg{width:15px;height:15px}}

.pfwho{min-width:0;flex:1}
.pfwho h2{font-size:25px;color:var(--navy);letter-spacing:.01em}
.pfwho .sub{margin-top:7px;color:var(--mute);font-size:14px;line-height:1.55;max-width:56ch}
.pfbtns{display:flex;flex-wrap:wrap;gap:9px;margin-top:15px;align-items:center}
.pfwho .hint{margin-top:9px;font-size:12.5px;color:var(--mute);min-height:1px}

/* ── 2 · The congratulation ──────────────────────────────────────────────────────────────────
   ⭐ SAID ONCE, and then never again — the flag is in localStorage (see celebrated() in the
   dashboard). A banner that fires on every visit stops being a congratulation and becomes
   furniture, and this project's whole tone argument is against that. */
.celebrate{position:relative;overflow:hidden;background:var(--navy);border-color:var(--navy);
  color:var(--cream);padding:30px 30px 28px}
/* ⭐ A starburst BEHIND THE TICK, masked to a circle so it fades out rather than running off into
   a smudge in one corner. Charley's standing note on the book's tone is subtlety, so this is a
   glow at the one point the eye is already on, not confetti across the panel. */
.celebrate .rays{position:absolute;left:-52px;top:50%;width:400px;height:400px;
  transform:translateY(-50%);pointer-events:none;opacity:.85;
  background:repeating-conic-gradient(from 8deg,
    rgba(255,146,40,.30) 0deg 4deg, rgba(255,146,40,0) 4deg 15deg,
    rgba(159,197,232,.20) 15deg 19deg, rgba(159,197,232,0) 19deg 30deg);
  -webkit-mask-image:radial-gradient(circle at 50% 50%,#000 0 12%,rgba(0,0,0,.55) 30%,transparent 60%);
  mask-image:radial-gradient(circle at 50% 50%,#000 0 12%,rgba(0,0,0,.55) 30%,transparent 60%)}
@media(max-width:560px){.celebrate .rays{width:280px;height:280px;left:-70px}}
.celebrate .cwrap{position:relative;display:flex;align-items:flex-start;gap:18px}
.celebrate .ctx{min-width:0;flex:1}
@media(max-width:560px){.celebrate{padding:24px 20px 22px}.celebrate .cwrap{gap:14px}}
.celebrate h2{font-size:27px;color:#fff;line-height:1.14;letter-spacing:.01em}
@media(max-width:560px){.celebrate h2{font-size:21px}}
.celebrate p{margin-top:9px;font-size:14.5px;line-height:1.62;color:var(--sky);max-width:58ch}
.cmark{flex:none;width:46px;height:46px;border-radius:50%;background:var(--orange);color:var(--navy);
  display:grid;place-items:center;box-shadow:0 5px 16px rgba(255,146,40,.4);
  animation:cpop .5s cubic-bezier(.2,1.5,.4,1) 1}
.cmark svg{width:24px;height:24px}
@keyframes cpop{0%{transform:scale(.3);opacity:0}100%{transform:scale(1);opacity:1}}
@media(prefers-reduced-motion:reduce){.cmark{animation:none}}

/* the same news, said inline in the form, at the moment the ninth answer lands */
.bang{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:18px;
  padding:14px 16px;border-radius:9px;background:#E8F5EE;border:1.5px solid #A9D8BF}
.bang .cmark{width:36px;height:36px;background:var(--green);color:#fff;box-shadow:none}
.bang .cmark svg{width:19px;height:19px}
.bang b{display:block;font-family:var(--serif);font-size:17px;color:var(--navy);line-height:1.2}
.bang span{display:block;margin-top:3px;font-size:13px;color:var(--mute)}
.bang .btn{margin-left:auto}
@media(max-width:560px){.bang .btn{margin-left:0;width:100%}}

/* ── 3 · The finished profile ────────────────────────────────────────────────────────────────
   Set as a PAGE, not as a form with the inputs taken out: the question becomes a heading, the
   answer becomes prose, and the one-word answer becomes the pull quote it is going to be on The
   Peoples Page anyway. */
.pfpage{padding-bottom:26px}
.pfmeta{display:flex;flex-wrap:wrap;gap:9px 26px;margin:-4px 0 0;padding-bottom:18px;
  border-bottom:1px dashed var(--rule)}
.pfmeta span{font-size:14px;color:var(--ink)}
.pfmeta b{display:block;font:700 10px/1 var(--sans);letter-spacing:.11em;text-transform:uppercase;
  color:var(--mute);margin-bottom:5px}

.pfword{position:relative;margin:22px 0 4px;padding:8px 0 18px;text-align:center;
  font-family:var(--serif);font-size:34px;line-height:1.15;color:var(--navy);letter-spacing:.01em}
@media(max-width:560px){.pfword{font-size:26px}}
.pfword .mark{color:var(--orange);margin-right:2px}
.pfword cite{display:block;margin-top:11px;font:400 11.5px/1 var(--sans);letter-spacing:.13em;
  text-transform:uppercase;color:var(--mute);font-style:normal}

.pfans{margin-top:8px}
.pfans > div{padding:20px 0;border-top:1px dashed var(--rule)}
.pfans > div:first-child{border-top:0;padding-top:14px}
.pfans dt{font-family:var(--serif);font-weight:700;font-size:17.5px;line-height:1.25;
  color:var(--navy);margin-bottom:9px}
@media(max-width:560px){.pfans dt{font-size:16px}}
.pfans dd{font-size:15.5px;line-height:1.72;color:var(--ink);max-width:66ch}
.pfans dd p + p{margin-top:11px}
.pffoot{margin-top:22px;padding-top:16px;border-top:1px dashed var(--rule);
  font-size:12.5px;color:var(--mute)}

/* ── "That's me" — the claims (2026-09-04, website/portal/CLAIMS-PLAN.md) ─────────────────── */
.panel.claimcall{border-left:4px solid var(--navy)}
.eclaims{margin:0 16px 12px;display:flex;flex-direction:column;gap:8px}
.eclaim{display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;padding:9px 12px;border-radius:8px;
  background:#EAF1FB;border:1px solid #BFD4EE;font-size:13.5px;line-height:1.45;color:var(--ink)}
.eclaim b{color:var(--navy)}
.eclaim i{color:var(--mute);font-style:italic}
.eclaim.pend{background:#FFF8EF;border-color:#F0C68A}
.eclaim .btn.sm{padding:5px 11px}

/* ══ ⭐⭐ THE LEGEND TAB (s484, 2026-09-14) ══════════════════════════════════════════════════════
   The frame around portal/legend-editor.js on the dashboard: who the page is, a picker when a member
   has claimed more than one, and the save bar that stays in reach at the foot of the screen. The form
   itself carries its own styles, because the same form also opens in the sheet on the Legend page,
   where this stylesheet is not loaded. */
.legpick{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 14px}
.legpick span{font-size:11px;letter-spacing:.11em;text-transform:uppercase;color:var(--mute);margin-right:4px}
.legpick a{font:600 13px/1 var(--sans);color:var(--navy);text-decoration:none;border:1.5px solid var(--rule);
  background:var(--paper);border-radius:999px;padding:8px 13px}
.legpick a:hover{border-color:var(--navy)}
.legpick a[aria-current]{background:var(--navy);border-color:var(--navy);color:#fff}
.panel.legtop{display:flex;gap:16px;align-items:center;flex-wrap:wrap;border-left:4px solid var(--orange)}
.legthumb{width:84px;height:84px;border-radius:50%;object-fit:cover;flex:none;background:var(--cream);
  border:2px solid var(--paper);box-shadow:0 0 0 1.5px var(--rule)}
.legthumb.none{display:block}
.legwho{flex:1;min-width:200px}
.legk{display:block;font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--gold);font-weight:700}
.legwho h2{font-family:var(--serif);font-size:26px;color:var(--navy);line-height:1.15;margin-top:3px}
.legmeta{margin-top:6px;font-size:13.5px;line-height:1.5;color:var(--mute)}
.legmeta b{font-weight:700}
.legmeta .st-approved{color:var(--green)}
.legmeta .st-pending{color:#8A5000}
.legmeta .st-declined{color:var(--red)}
.legmeta .st-editor{color:var(--navy)}
.legintro{margin:4px 2px 16px;color:var(--mute);font-size:14px;line-height:1.55;max-width:70ch}
/* ⭐ the save bar sticks to the bottom of the screen while the form scrolls under it — a page this long
   must never make somebody scroll back to find the button */
.legsave{position:sticky;bottom:0;z-index:5;display:flex;gap:10px;align-items:center;flex-wrap:wrap;
  margin:6px -4px 0;padding:12px 16px;background:rgba(246,241,231,.96);border:1px solid var(--rule);
  border-radius:10px;box-shadow:0 -6px 20px rgba(4,30,66,.10);backdrop-filter:blur(6px)}
.legst{flex:1;min-width:180px;font-size:13.5px;line-height:1.45;color:var(--mute)}
.legst.warn{color:#8A5000}
.legst.good{color:var(--green)}
.legst.bad{color:var(--red)}
.legst a{color:inherit;font-weight:600}
@media (max-width:620px){
  .legthumb{width:62px;height:62px}
  .legwho h2{font-size:22px}
  .legsave .btn.go{flex:1}
}

/* ══ ⭐⭐ THE LEGENDARY DISCOUNT (2026-09-23) ════════════════════════════════════════════════════════
   The frame the dashboard draws round thebook/legendary.js's banner, for a member acct.legendOffer says is a
   Legend: the two prices, the line about later, and the family invite (Legend tab); the Legend label on #/book's
   own price blocks; and the owners-only line in the editors' Legends pile. The banner styles itself — it is the
   loud one (Charley: "Think late nineties and Lisa Franke"); everything here stays in the dashboard's calm panel
   language so the banner is the only thing shouting.
   Contrast measured with _tools/contrast: #B3306F on cream 5.23:1 · #9A4414 (the site's --orange-ink) on
   cream 5.81:1 · #5E626A on cream 5.44:1 · #1F6B3A on paper 6.41:1 · #6B4A12 on the pending row 7.62:1.
   ⚠️ NOT --mute on the cream cards: 4.48:1, just under AA. */
.panel.lgo{border-left:4px solid var(--pink)}
#legOffer > .sgdl,#bookLegend > .sgdl{margin-bottom:18px}
.lgo-cards{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.lgo-card{display:flex;flex-direction:column;gap:8px;min-width:0;background:var(--cream);
  border:1px solid var(--rule);border-radius:10px;padding:16px 18px}
.lgo-k{display:block;font:800 11px/1 var(--sans);letter-spacing:.14em;text-transform:uppercase;color:#9A4414}
.lgo-card.cus .lgo-k{color:#B3306F}
.lgo-card h3{font-family:var(--serif);font-size:19px;line-height:1.18;color:var(--navy)}
.lgo-pp{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.lgo-pp b{font-family:var(--serif);font-size:30px;line-height:1;color:var(--navy)}
.lgo-pp s{font-family:var(--serif);font-size:18px;color:#5E626A}
.lgo-pp small{font:700 12px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase;color:#B3306F}
.lgo-card .btn{margin-top:auto;align-self:flex-start;min-height:44px}
.lgo-soon{margin-top:auto;font-size:14.5px;line-height:1.5;font-style:italic;color:#5E626A}
.lgo-after{margin:12px 2px 0;font-size:13.5px;line-height:1.5;color:#5E626A}
.lgo-share{margin-top:18px;padding-top:16px;border-top:1px dashed var(--rule)}
.lgo-share h3{font-family:var(--serif);font-size:18px;color:var(--navy);margin-bottom:6px}
.lgo-share p{font-size:14.5px;line-height:1.55;color:#33373D;max-width:62ch;margin-bottom:10px}
.lgo-share p b{color:var(--navy)}
.lgo-link{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
/* the invite link itself — read-only, selected whole on focus, and never taller than the buttons beside it */
.lgo-link input[type=text]{flex:1 1 260px;width:auto;min-width:0;font:400 13.5px/1.4 var(--mono);color:var(--navy);
  padding:10px 11px;background:#fff}
.lgo-link .btn{min-height:42px}
.lgo-copied{display:block;min-height:1.4em;margin-top:6px;font-size:13px;font-weight:600;color:#1F6B3A}
/* #/book: the price the server confirmed is labelled as the Legend's (0,4,1 — beats the page's own .bk-opt.cus rule) */
.bk-choose .bk-opt .pp.lg small{color:#B3306F}
/* the editors' Legends pile: what a non-owner sees where Approve and Decline would be */
.claimrow .acts .st.lgwait{font-weight:600;font-style:italic;color:#6B4A12}
@media (max-width:620px){
  .lgo-cards{grid-template-columns:1fr}
  .lgo-card{padding:14px 15px}
  .lgo-card .btn{align-self:stretch}
  .lgo-link .btn{flex:1 1 0}
}

/* ═══════════════════════ ⭐⭐ 2026-09-20 — NO PAGE ZOOM WHEN A BOX IS TAPPED ON A PHONE ═══════════
   Charley's list, item 1, with a screen recording: tapping a box zoomed the whole page in and pushed
   the × off the screen. iOS Safari does that whenever the focused control's type is under 16px. The
   site header (home/sitenav.js) carries a blanket !important rule for it; this block says the same
   thing at the source, so the portal does not depend on that one rule being on the page.
   ⛔ LAST IN THE FILE ON PURPOSE — every selector below repeats one written earlier at the same
   specificity, and only source order makes this one win. The desktop keeps the designed sizes.
   ⚠️ The bubble being renamed (.tagedit) goes to 16px with the rest: it is a real <input> and zooms
   like one. The bubble grows a little while it is being typed in, and goes back when it is done. */
@media (max-width:900px),(pointer:coarse){
  input[type=text],input[type=email],input[type=password],input[type=tel],input[type=number],
  input[type=search],input[type=url],input[type=date],select,textarea,
  .edsearch input,
  .ecard .efields input,.ecard .efields textarea,
  .ecard .efields .chips input[type=text],
  .chips .tag input.tagedit,.tags .tag input.tagedit,
  .ecard .efields .chips .tag input.tagedit,
  .ecard .etitle input,
  .ecard .ecollrow select,
  .lgo-link input[type=text]{font-size:16px}
}
