/* site/app/screens/profile.css — the Profile, and the avatar in the corner that opens it.
 * Depends on tokens.css, ui.css, shell.css. Reuses .card .list .row .btn .tag .avatar
 * .grabber .pane-scroll .screen-head .sec .chev .popover .enter — nothing here reinvents one.
 *
 * ⚠ THIS FILE CARRIES TWO THINGS, AND ONE OF THEM IS NOT A SCREEN.
 *   Everything under `.pf` is the profile screen. Everything under `.hdr` is the corner
 *   avatar from ../header.js, which the shell mounts ONCE and which floats over all seven
 *   tabs. header.js has no stylesheet of its own, so its rules live here — which means
 *   app.html MUST link this file, or the avatar renders as an unstyled button on every
 *   screen in the app, not just this one. That is the one dependency to remember.
 *
 * ⚠ EVERY `pf-` AND `hdr-` NAME IS PREFIXED, AND THAT IS NOT STYLE POLICING.
 *   app.html's own comment records the last time it went wrong: this project already shipped
 *   two stylesheets that both invented `.pcard`, and whichever loaded second broke the other
 *   silently. Load order does not decide anything here because no two files can collide.
 */

/* ═══════════════════════════════════════════════════════════════════════════
 * 1 · THE CORNER — header.js
 * ═══════════════════════════════════════════════════════════════════════════
 * Syd, 2026-09-02: "top right of all the screens next to the header right aligned to page".
 */

/* ⚠ ABSOLUTE IN `.screen`, NOT IN A PANE, AND THE 54px IS THE STATUS BAR.
 *   ui.css gives .statusbar a fixed 52px and it is a flex child of .screen, so anything
 *   absolutely positioned at top:0 lands UNDERNEATH the clock. 54px clears it by two pixels
 *   and puts the 44px hit box beside the screen title's cap-height rather than above it.
 *   ⚠ z-index sits BETWEEN the collapsing navbar (40) and the floating nav (60): the avatar
 *   must stay above scrolling content and translucent chrome, and must never cover the nav,
 *   because the nav is how you leave. The profile's own sheets are 70+ and cover it, which is
 *   correct — while the profile is open the corner is not a destination any more. */
.hdr {
  position: absolute; z-index: 55;
  top: 54px; right: 12px;
  display: flex; align-items: center;
}

/* The hit box is 44px (--tap-min) and the ink inside it is 36px. A 36px button is what
   Apple's own bars look like; a 36px HIT TARGET is what people miss. */
.hdr-btn {
  min-width: var(--tap-min); height: var(--tap-min);
  display: grid; place-items: center;
  border: 0; padding: 0; background: transparent; color: var(--ink);
  cursor: pointer; border-radius: var(--r-full);
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
[data-ready] .hdr-btn { transition: transform var(--t-press) var(--ease-out); }

/* ⚠ THE RING IS THE PAGE COLOUR, NOT A BORDER COLOUR. Content scrolls UNDER this button, so
   at some scroll position the avatar sits on top of a photo, a card edge or a line of text.
   A 2px ring in --bg reads as a gap punched through the content and keeps the circle legible
   on every one of them, in both themes, without a second translucent layer — DESIGN.md §5
   forbids stacking translucency on translucency and a blurred circle over a blurred bar is
   exactly that. */
.hdr-av {
  width: 36px; height: 36px;
  box-shadow: 0 0 0 2px var(--bg), var(--shadow-1);
}
.hdr .avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ⚠ THE SIGNED-OUT STATE IS THE WORDS "Sign in", NOT AN EMPTY CIRCLE. A grey ring with
   nothing in it reads as a photo that failed to load, so a signed-out player taps it
   expecting their own account and gets a sign-in card they did not ask for. */
.hdr-out { padding-inline: 2px; }
.hdr-word {
  display: inline-flex; align-items: center; height: 32px; padding: 0 13px;
  border-radius: var(--r-full);
  background: var(--chrome);
  backdrop-filter: var(--chrome-blur); -webkit-backdrop-filter: var(--chrome-blur);
  border: 1px solid var(--chrome-edge);
  font: 600 13px/1 var(--font-ui); letter-spacing: -.006em; color: var(--ink);
  white-space: nowrap;
}

/* The shell hides the corner on any tab that already owns it — see setHeaderVisible() in
   header.js and the note in this file's header. `visibility` rather than `display` so the
   button keeps its box and the layout above it never reflows on a tab change. */
.hdr.is-hidden { visibility: hidden; pointer-events: none; }

@media (hover: hover) and (pointer: fine) {
  .hdr-btn:hover .hdr-av { box-shadow: 0 0 0 2px var(--bg), var(--shadow-2); }
  .hdr-btn:hover .hdr-word { border-color: var(--ink-tertiary); }
}
@media (prefers-reduced-transparency: reduce) {
  .hdr-word { background: var(--bg-raised); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ⚠ THE PROFILE RESERVES THE CORNER IN ITS OWN TITLE ONLY, AND THE OTHER SIX SCREENS DO NOT.
 *   A blanket `.screen-head { padding-right: 76px }` from this file would be the right rule
 *   in the wrong place — it belongs in shell.css, and worse, proshop.css already parks a cart
 *   button in the top-right of every one of its views. Reserving the corner globally from
 *   here would put a strip of air beside a cart that is already there, and would still not
 *   stop the avatar landing on top of it. That collision is a shell decision, not a
 *   stylesheet one; it is written up in this task's notes.
 * ⚠ 92px IS SIZED FOR THE WIDER OF THE CORNER'S TWO STATES, NOT THE ONE THAT IS ON SCREEN
 *   WHILE YOU MEASURE IT. Signed in the corner is a 44px circle — 44 + 12 offset + 20 so a
 *   descender never touches it = 76px. Signed OUT it is a "Sign in" pill about 78px wide, and
 *   every one of those numbers grows. 76 looks correct for the whole of development, because
 *   you are signed in the whole time, and clips the title the first time a stranger opens the
 *   app.
 * ⚠ NOT INSIDE THE SHEET. There the panel covers the corner entirely (its z-index is 71 to
 *   the corner's 55) and render() has moved the title into the panel's own bar, so the only
 *   thing 92px of reserved space does there is squeeze the subtitle for a button that is not
 *   on screen. */
.pf:not(.pf-sheet-body) > .pane-scroll > .screen-head { padding-right: 92px; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 2 · THE SCREEN ROOT
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ⚠ THE PROFILE HAS TWO HOSTS AND THE POSITIONING HAS TO WORK IN BOTH.
 *   openProfile() renders into a sheet body; render() can also be called with a pane, the day
 *   it becomes one. The sync pill and the inner sheets are absolutely positioned, so each host
 *   needs a positioned ancestor — but NOT the same one:
 *     · as a pane   → `.pane.pf` is it (below).
 *     · in a sheet  → `.pf-sheet-body` is deliberately left STATIC so they resolve against
 *                     `.pf-sheet`, the panel. If the body were the ancestor, an "absolute,
 *                     bottom:0" sheet would sit at the bottom of the SCROLLED CONTENT and
 *                     scroll away with it, which looks like the sheet failed to open. */
.pane.pf { position: relative; }

.pf .avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ⚠ WRAPPED IN :where() SO IT WEIGHS NOTHING, AND THAT IS NOT TIDINESS — IT WAS A BUG.
   Written plainly as `.pf p { margin: 0 }` this reset is 0,1,1 and it silently beat every
   0,1,0 class rule below it: .pf-note lost its 20px inset, .pf-signin-p lost its 10px,
   .pf-fnote lost its 7px. The notes rendered flush to the phone's edge — OUTSIDE the list
   they belong to — and nothing warned, because a margin that does not apply looks like a
   margin nobody wrote. :where() makes the whole selector 0,0,0, so any class rule wins
   whatever order it appears in. */
:where(.pf) :where(h1, h2, h3, p, ul) { margin: 0; }

/* ⚠ THE LABEL GETS ITS WIDTH FIRST AND THE VALUE IS CAPPED. THIS IS THE SAFETY NET FOR A BUG
   ALREADY FIXED AT THE SOURCE — see the comment on the signed-out facts in profile.js.
   ui.css sizes .row .value for "₱250": `line-height: 1`, no shrink, no cap, and .grow on a
   `flex: 1 1 0%` basis so it only ever receives LEFTOVER width. Hand that column a sentence
   and it takes its full content width, the label is squeezed to what remains, and a
   single-word label cannot wrap — "Verification" ran straight through the sentence beside it
   and rendered as "VerificatiAnstaff member, once, at the desk". Silent, and it looks like a
   font failure rather than a layout one.
   `flex: 1 1 auto` gives the label a content-sized basis so it is not the only thing that
   shrinks; 62% stops any future long value from taking the row again. */
.pf .row .grow { flex: 1 1 auto; }
.pf .row .value {
  flex: 0 1 auto; min-width: 0; max-width: 62%;
  line-height: 1.35; text-align: right; text-wrap: pretty;
}

/* ── The sync pill ─────────────────────────────────────────────────────────
 * Syd's standing rule: optimistic UI with a VISIBLE sync pill. The screen changes
 * immediately; this is the only thing on it that tells the truth about the write.
 * ⚠ IT SITS ABOVE THE FLOATING NAV, NOT UNDER IT. --fnav-clear is shell.css's published
 *   footprint of the nav; re-deriving that number is what put book.js's commit bar 2px
 *   inside the nav for a week. In a sheet there is no nav, so the offset is overridden. */
.pf { --pf-pill-bottom: calc(var(--fnav-clear) + 8px); }
.pf-sheet-body.pf { --pf-pill-bottom: calc(20px + var(--bottom-inset)); }

.pf-pill {
  position: absolute; z-index: 58;
  left: 50%; bottom: var(--pf-pill-bottom);
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 15px; border-radius: var(--r-full);
  font: 600 13px/1 var(--font-ui); letter-spacing: -.004em;
  white-space: nowrap; box-shadow: var(--shadow-2);
  background: var(--bg-raised); color: var(--ink);
  border: 1px solid var(--rule);
  opacity: 1; transform: translate3d(-50%, 0, 0);
}
/* ⚠ allow-discrete IS WHAT MAKES THE FADE HAPPEN AT ALL. profile.js toggles `hidden`, which
   is display:none — and a display change cancels a transition rather than animating through
   it, so without this the pill would snap in and snap out. */
[data-ready] .pf-pill {
  transition: opacity var(--t-menu) var(--ease-out),
              transform var(--t-menu) var(--ease-out),
              display var(--t-menu) allow-discrete;
}
@starting-style { .pf-pill { opacity: 0; transform: translate3d(-50%, 8px, 0); } }
.pf-pill[hidden] { display: none; opacity: 0; transform: translate3d(-50%, 8px, 0); }

/* ⚠ EVERY STATE CARRIES A WORD — the colours below only agree with a word profile.js already
   wrote into the element. "Waiting for signal" is the one that matters: it is the pill saying
   the change is on the phone and not on the server yet, and it must never be green. */
.pf-pill[data-state="saved"] {
  background: color-mix(in srgb, var(--ok) 16%, var(--bg-raised));
  border-color: color-mix(in srgb, var(--ok) 40%, transparent);
  color: var(--ok);
}
.pf-pill[data-state="queued"] {
  background: color-mix(in srgb, var(--warn) 18%, var(--bg-raised));
  border-color: color-mix(in srgb, var(--warn) 42%, transparent);
  color: var(--warn);
}

/* ── The preview switch. NOT PART OF THE APP. ───────────────────────────────
 * Dashed, unpainted and deliberately unlovely, because the day the API lands this block and
 * the fixtures behind it come out together. A preview control that looks like a real control
 * is one that ships. */
.pf-preview {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  margin: 0 16px 18px; padding: 10px 12px;
  border: 1px dashed var(--steel-40); border-radius: var(--r-md);
}
.pf-preview-lab {
  font: 600 10px/1 var(--font-ui); letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-tertiary);
}
.pf-preview-group { display: flex; gap: 6px; flex: 1; flex-wrap: wrap; }
/* 26px tall before the min-height — scaffolding is still tapped by a thumb. */
.pf-preview-btn {
  display: inline-flex; align-items: center;
  border: 1px solid var(--rule); background: transparent; color: var(--ink-secondary);
  font: 500 12px/1 var(--font-ui); padding: 7px 11px; border-radius: var(--r-full);
  min-height: var(--tap-min);
  cursor: pointer; touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.pf-preview-btn[aria-pressed="true"] {
  background: var(--ink); color: var(--bg); border-color: var(--ink);
}
[data-ready] .pf-preview-btn {
  transition: transform var(--t-press) var(--ease-out),
              background-color var(--t-press) var(--ease-out),
              color var(--t-press) var(--ease-out);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * 3 · WHO OWNS WHAT — the boundary, drawn
 * ═══════════════════════════════════════════════════════════════════════════
 * ⚠ THE WHOLE POINT OF THIS SCREEN, AND THE REASON THE TWO GROUPS DO NOT LOOK ALIKE.
 *   ARCHITECTURE.md invariants 18–20 keep rating, ranking, trust and match record out of
 *   every player-realm write. Those are server facts. What this section does is stop the UI
 *   from teaching the opposite: a rating rendered inside the same raised, chevroned, tappable
 *   card as a display name looks like a field, so the next developer adds it to the form, the
 *   server refuses, and the refusal gets "fixed" by widening the allow-list.
 *   So: YOURS is a raised white surface with chevrons and a press animation. THE VENUE'S is a
 *   SUNKEN surface with a hairline, a lock glyph, no chevrons and no press. Different
 *   furniture, not a different tint — a colour difference alone disappears in sunlight.
 */
.pf-sec { margin: 0 0 24px; }

.pf-ghead { padding: 0 20px 9px; }
.pf-ghead-top { display: flex; align-items: center; gap: 6px; }
.pf-ghead-top h2 {
  font: 600 12px/1 var(--font-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-tertiary);
}
.pf-ghead-lock { color: var(--ink-tertiary); flex: none; display: block; }
.pf-ghead p {
  margin-top: 5px; font: 400 13px/1.45 var(--font-ui); color: var(--ink-tertiary);
  max-width: 40ch;
}

.pf-sec .list { margin: 0 16px; }
.pf-sec .list + .list { margin-top: 14px; }

/* The venue's surface. Sunken and outlined instead of raised and shadowed: it is a printed
   record, not a control panel. */
.pf-venue .list {
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  box-shadow: none;
}
.pf-venue .row { cursor: default; }

/* The sentence that turns the boundary from a look into a statement. */
.pf-note {
  margin: 10px 20px 0; font: 400 13px/1.5 var(--font-ui); color: var(--ink-tertiary);
  max-width: 46ch;
}
.pf-note-tight { font-size: 12px; line-height: 1.5; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 4 · IDENTITY CARD
 * ═══════════════════════════════════════════════════════════════════════════ */

.pf-id { padding: 20px 18px 16px; }
.pf-id-top { display: flex; align-items: center; gap: 16px; }
.pf-id-who { min-width: 0; flex: 1; }

/* ⚠ THE NAME WRAPS AND THE HANDLE DOES NOT. A display name is 1..40 characters of anything —
   "Ma. Kristina Villanueva-Sandoval" is a real length, and truncating a person's own name on
   their own profile is the wrong trade. A handle is capped at 20, never wraps, and is the
   thing that identifies them, so it gets the ellipsis if anything does. */
.pf-name {
  font-family: var(--font-display); font-weight: 600;
  font-size: 24px; line-height: 1.15; letter-spacing: -.016em; color: var(--ink);
  overflow-wrap: anywhere;
}
.pf-handle {
  margin-top: 3px;
  font: 500 15px/1.2 var(--font-ui); letter-spacing: -.006em; color: var(--ink-tertiary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.pf-id-facts {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 15px;
}
.pf-since { font: 400 13px/1.3 var(--font-ui); color: var(--ink-tertiary); }

.pf-bio {
  margin-top: 12px;
  font: 400 15px/1.5 var(--font-ui); color: var(--ink-secondary);
  overflow-wrap: anywhere;
}

.pf-id-actions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 16px;
}
.pf-btn { flex: 1; min-width: 150px; font-size: 15px; padding: 11px 16px; }
.pf-btn-ico { flex: none; display: block; }

/* The photo button. ⚠ Setting an avatar is its own server action (invariant 20), so it is its
   own control here — a camera badge on the photo, not a row in the form. */
.pf-photo {
  position: relative; flex: none;
  border: 0; padding: 0; background: transparent; cursor: pointer;
  border-radius: 50%; line-height: 0;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
[data-ready] .pf-photo { transition: transform var(--t-press) var(--ease-out); }
/* ⚠ INK, NOT --action. DESIGN.md: "If two things on a screen are orange, one of them is
   wrong" — and this sheet had four at once: Done, this badge, and both switches. The badge is
   an affordance ON a control, not the control the sheet is asking you to press; Done is. Ink
   is what every other "this is selected / this is a control" fill in the app uses. */
.pf-photo-badge {
  position: absolute; right: -2px; bottom: -2px;
  width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--ink); color: var(--bg-raised);
  box-shadow: 0 0 0 3px var(--bg-raised);
}
.pf-photo-badge svg { display: block; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 5 · ROWS
 * ═══════════════════════════════════════════════════════════════════════════ */

/* .pf-row is a <button> wearing ui.css's .row. A button brings its own font, colour, border
   and centring, and every one of them is wrong inside a list. */
.pf-row {
  width: 100%; border: 0; border-bottom: 1px solid var(--rule);
  background: transparent; color: inherit; text-align: left;
  font: inherit; cursor: pointer;
}
.pf-row:last-child { border-bottom: 0; }
.pf-chev { color: var(--ink-tertiary); }

/* ⚠ A LOCKED ROW STAYS TAPPABLE. It is not disabled, because the sheet behind it is where the
   date and the reason live — and "you cannot do this yet" with no date is the message people
   ask the desk about. The tag is the signal; the row still opens. */
.pf-row-locked .title { color: var(--ink-secondary); }
.pf-tag-lock { background: var(--steel-20); color: var(--ink-secondary); }

/* A read-only fact. ⚠ NO CHEVRON, and the absence is the design: a chevron is a promise that
   tapping leads somewhere, and nothing in the venue's group leads anywhere. */
.pf-fact { cursor: default; }
.pf-fact .value { font-weight: 500; color: var(--ink); }
.pf-fact .meta { color: var(--ink-tertiary); }

.pf-partner .pf-partner-lab {
  font: 600 10px/1 var(--font-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-tertiary); text-align: right; flex: none;
}

/* ── The switch ────────────────────────────────────────────────────────────
 * ⚠ NOT A NATIVE CHECKBOX, AND THE WORD INSIDE THE TRACK IS NOT DECORATION. A switch that is
 *   only a knob left-or-right is state carried by geometry alone; at a glance, outdoors, on a
 *   small phone, people read the wrong one and turn their own ranking off. On/Off is two
 *   characters and it removes the doubt (DESIGN.md §2).
 * The knob travels 32px: the track is 64px border-box, so its padding box is 62px, and a 24px
 * knob inset 3px runs 3→27 and must land 35→59. Change any of those and redo the arithmetic. */
.pf-switch { align-items: center; }
.pf-sw {
  position: relative; box-sizing: border-box; flex: none;
  width: 64px; height: 32px; padding: 0;
  border: 1px solid var(--rule); border-radius: var(--r-full);
  background: var(--bg-sunken); cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
/* ⚠ 32px DRAWN, 44px TAPPABLE. A switch drawn at 44 next to a row of text is a slab; a switch
   TAPPED at 32 is one a thumb slides off, and this is the control that turns a player's own
   ranking off. The pseudo-element does the growing — the same trick cafe.css and proshop.css
   use on their steppers, and the pattern the review asked to be copied. */
.pf-sw::before{ content: ""; position: absolute; inset: -6px; border-radius: inherit; }
.pf-sw-word {
  position: absolute; top: 0; bottom: 0; display: grid; place-items: center;
  font: 700 10px/1 var(--font-ui); letter-spacing: .06em; text-transform: uppercase;
  pointer-events: none;
}
.pf-sw[aria-checked="false"] .pf-sw-word { right: 9px; color: var(--ink-tertiary); }
.pf-sw[aria-checked="true"]  .pf-sw-word { left: 10px; color: var(--bg-raised); }
.pf-sw-knob {
  position: absolute; top: 3px; left: 3px; width: 24px; height: 24px;
  border-radius: 50%; background: var(--bg-raised); box-shadow: var(--shadow-1);
  transform: translate3d(0, 0, 0);
}
/* ⚠ ON IS A STATE, NOT THE ACTION. Two orange switches and an orange Done button put three
   orange things on one sheet; the word inside the track is what says which way this is set,
   and the fill only has to be unmistakably different from Off. Ink is, in both themes. */
.pf-sw[aria-checked="true"] { background: var(--ink); border-color: var(--ink); }
.pf-sw[aria-checked="true"] .pf-sw-knob { transform: translate3d(32px, 0, 0); }
[data-ready] .pf-sw {
  transition: background-color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}
[data-ready] .pf-sw-knob { transition: transform 220ms var(--ease-out); }

/* ── The visibility dropdown ───────────────────────────────────────────────
 * ⚠ A CUSTOM MENU, NEVER A NATIVE <select> — Syd's standing rule, and on this control it also
 *   earns itself: each option needs a sentence saying what it actually exposes, and a native
 *   <option> cannot hold one. "Members only" and "Nobody" are indistinguishable without it. */
/* ⚠ THE LIST HOLDING THE DROPDOWN HAS TO STOP CLIPPING, AND THIS IS NOT OBVIOUS FROM EITHER
   FILE. ui.css gives .list `overflow: hidden` so the rows' square corners are cut to the
   list's radius. The visibility menu is absolutely positioned inside one of those rows, so
   the same rule cut it off at the list's bottom edge: the menu opened, "Anyone" and "Members
   only" were visible, and the third option — "Nobody", the one a person is most likely to be
   looking for — was simply not there. No overflow, no scrollbar, no error; the menu just had
   two items.
   Nothing is lost by turning it off here: these rows have transparent backgrounds, so the
   list's own radius is still what paints the corners. */
.pf .list:has(> .pf-dd) { overflow: visible; }

.pf-dd { position: relative; }
/* ⚠ THE VALUE IS A SIZE SMALLER THAN THE LABEL AND flex:none. At 16px, "Members only" plus
   the label plus the chevron is 4px wider than a 390px phone's list, so the LABEL is what
   flex shrinks — "Who can see your profile" wraps to two lines and the row grows by 20px for
   no reason a reader can see. Shrinking the value instead keeps the question on one line,
   which is the half a person is scanning for. */
/* ⚠ `.pf ` IS ON THE FRONT BECAUSE THE GENERAL VALUE RULE ABOVE IS 0,3,0 AND THIS WOULD BE
   0,2,0. Written as `.pf-dd-trigger .value` it lost the flex declaration to it silently, and
   the symptom was two words: "Members only" broke onto two lines inside a row that had 90px
   of empty space in it. Nothing about that looks like a specificity fight. */
.pf .pf-dd-trigger .value { flex: none; font-size: 15px; font-weight: 500; color: var(--ink); }
.pf-dd-trigger .meta { max-width: 34ch; }
.pf-dd-chev { transform: rotate(90deg); transform-origin: 50% 50%; }
.pf-dd-trigger[aria-expanded="true"] .pf-dd-chev { transform: rotate(-90deg); }
[data-ready] .pf-dd-chev { transition: transform var(--t-menu) var(--ease-out); }

.pf-dd-menu {
  position: absolute; z-index: 30; top: calc(100% - 4px); left: 8px; right: 8px;
  background: var(--bg-raised); border-radius: var(--r-lg);
  box-shadow: var(--shadow-3); border: 1px solid var(--rule); overflow: hidden;
}
.pf-dd-opt {
  width: 100%; display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 15px; border: 0; border-bottom: 1px solid var(--rule);
  background: transparent; color: var(--ink); text-align: left; cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.pf-dd-opt:last-child { border-bottom: 0; }
.pf-dd-opt-t { flex: 1; min-width: 0; }
.pf-dd-opt-l { font: 500 15px/1.3 var(--font-ui); letter-spacing: -.006em; }
.pf-dd-opt-n { margin-top: 3px; font: 400 12px/1.45 var(--font-ui); color: var(--ink-tertiary); }
/* Ink plus weight, not orange. A 15px orange label on --bg-raised was the least readable line
   in the menu, and it was on the option the player had already chosen. */
.pf-dd-opt-c { flex: none; color: var(--ink); margin-top: 2px; display: block; }
.pf-dd-opt[aria-selected="true"] .pf-dd-opt-l { color: var(--ink); font-weight: 700; }
[data-ready] .pf-dd-opt { transition: transform var(--t-press) var(--ease-out); }

/* Exit is faster than enter, and back along the path it entered by. */
.pf-dd-menu.is-out {
  opacity: 0; transform: scale(.97) translateY(-3px);
  transition: opacity var(--t-exit) var(--ease-out), transform var(--t-exit) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pf-dd-opt:hover { background: var(--bg-sunken); }
}

/* ── Sign out ──────────────────────────────────────────────────────────────
 * ⚠ RED, BUT NOT ALARMING, AND THE SECOND ROW'S SUBTITLE IS AN INVARIANT RATHER THAN COPY.
 *   ARCHITECTURE.md invariant 17: revocation is instant for WRITES and takes up to 15 minutes
 *   for READS, "and the UI says exactly that". The person that sentence is written for is
 *   signing out a phone they have just lost, and "immediately" would be a promise the session
 *   design does not make. The styling here keeps that sentence readable rather than shrinking
 *   it into fine print. */
.pf-outs { margin-top: 20px; }
/* ⚠ TOP-ALIGNED, NOT CENTRED, BECAUSE ONE OF THESE TWO ROWS IS THREE LINES TALL. Centred, the
   door glyph on "Sign out everywhere" floats beside the middle of the sentence and stops
   reading as the icon FOR the heading. The 2px nudges it onto the cap-height of the title. */
.pf-danger { align-items: flex-start; color: var(--danger); }
.pf-danger .title { color: var(--danger); font-weight: 600; }
.pf-danger .meta { color: var(--ink-tertiary); max-width: 34ch; }
.pf-danger-ico { flex: none; display: block; margin-top: 2px; color: var(--danger); }

/* ── Text-button links ─────────────────────────────────────────────────────
 * Not a .btn: "Report" and "Remove photo" must not look as pressable as the thing beside
 * them. A quiet link is the honest weight for an action nobody should take by accident. */
.pf-link {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: var(--tap-min); padding: 10px 14px;
  border: 0; background: transparent; border-radius: var(--r-full);
  font: 600 15px/1 var(--font-ui); letter-spacing: -.008em; cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
[data-ready] .pf-link { transition: transform var(--t-press) var(--ease-out); }
.pf-link-quiet  { color: var(--ink-tertiary); }
.pf-link-danger { color: var(--danger); width: 100%; margin-top: 4px; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 6 · MATCH HISTORY — moved here from history.css, 2026-09-03
 * ═══════════════════════════════════════════════════════════════════════════
 * Syd, 2026-09-03: "i want the match history to be on the profile instead". The old
 * screens/history.css is deleted and its `hx-` rules live here under `pf-mh-`, renamed rather
 * than merged so a match card can never collide with a profile row.
 *
 * ⚠ WHAT DID NOT COME ACROSS: the three-number summary strip (.hx-summary). Matches, win rate
 *   and facility rank are all in the venue's read-only group four inches up the same screen
 *   now, and two copies of a rank on one page is two things to disagree.
 *
 * ⚠ WHAT LEFT THIS FILE IN THE OTHER DIRECTION: .pf-strip, .pf-search and .pf-refuse — the
 *   friends strip, the @username box and its refusals. They are screens/players.css now, as
 *   `pl-`. Do not re-add them here; a friends list on two screens is two lists to keep in step.
 *
 * This section is a RECORD, not a dashboard. Nothing is bigger than it needs to be: the result
 * is a word, and the only thing allowed to feel expensive is the rally replay — the one thing
 * on the screen no other app has.
 */

/* ── The filter ───────────────────────────────────────────────────────────── */
/* Thumb width is a calc of the track, so it never needs measuring: 2px padding each side plus
   two 2px gaps = 8px of furniture, and the rest splits three ways. */
.pf-mh-seg { margin: 0 16px 14px; }
.pf-mh-seg .thumb { width: calc((100% - 8px) / 3); }
/* ⚠ 30px PAINTED. ui.css's .segmented button is 8px of padding around a 14px line and leans on
   an invisible overlay for the 44px floor; this restates the floor on the control itself. */
.pf-mh-seg button { min-height: var(--tap-min); }
.pf-mh-seg button[aria-pressed="true"] { font-weight: 600; }

/* ── Day headers ──────────────────────────────────────────────────────────── */
.pf-mh-day { padding-top: 4px; }
.pf-mh-day h2 {
  /* mm/dd/yyyy, tabular, so a column of dates lines up down the scroll. */
  font-variant-numeric: tabular-nums; letter-spacing: .06em;
}
.pf-mh-day-n { font: 400 12px/1 var(--font-ui); color: var(--ink-tertiary); }

/* ── Match card ───────────────────────────────────────────────────────────── */
.pf-mh-match {
  background: var(--bg-raised); border-radius: var(--r-lg);
  box-shadow: var(--shadow-1); margin: 0 16px 10px; overflow: hidden;
}
/* Swapped, not transitioned. Only transform and opacity are allowed to animate here, and a
   shadow that fades over 200ms buys nothing the spring under it is not already selling. */
.pf-mh-match.is-open { box-shadow: var(--shadow-2); }

.pf-mh-head {
  display: block; width: 100%; text-align: left;
  border: 0; background: transparent; cursor: pointer;
  padding: 13px 15px 14px; color: inherit; font: inherit;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}

.pf-mh-top { display: flex; align-items: center; gap: 8px; margin-bottom: 11px; }

.pf-mh-kind {
  font: 600 10px/1 var(--font-ui); letter-spacing: .08em; text-transform: uppercase;
  padding: 5px 8px; border-radius: var(--r-full);
}
.pf-mh-kind.is-open { background: var(--bg-sunken); color: var(--ink-secondary); }
/* Private reads as an outline rather than a fill — a quieter thing than a venue session, and
   distinguishable without relying on the two greys being told apart. */
.pf-mh-kind.is-private {
  background: transparent; color: var(--ink-secondary);
  box-shadow: inset 0 0 0 1px var(--rule);
}

.pf-mh-where {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 400 12px/1 var(--font-ui); color: var(--ink-tertiary);
}

/* ⚠ The result is a WORD first. The colour is a second, redundant channel — never the only
   one. Reading a match card in daylight through polarised sunglasses is a real condition of
   use at this venue. */
.pf-mh-res {
  font: 700 11px/1 var(--font-ui); letter-spacing: .09em; text-transform: uppercase;
  padding: 5px 9px; border-radius: var(--r-full);
}
.pf-mh-res.is-won  { color: var(--ok);     background: color-mix(in srgb, var(--ok) 14%, transparent); }
.pf-mh-res.is-lost { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }

.pf-mh-body { display: flex; align-items: center; gap: 12px; }
.pf-mh-teams { flex: 1; min-width: 0; }
.pf-mh-side { min-width: 0; }
.pf-mh-names {
  font: 500 15px/1.25 var(--font-ui); letter-spacing: -.006em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-mh-handles {
  margin-top: 1px;
  font: 400 12px/1.3 var(--font-ui); color: var(--ink-tertiary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-mh-vs {
  font: 600 10px/1 var(--font-ui); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-tertiary); margin: 6px 0;
}

.pf-mh-score { text-align: right; flex: none; }
.pf-mh-score-main {
  font-family: var(--font-display); font-weight: 600;
  font-size: 24px; line-height: 1; letter-spacing: -.02em; color: var(--ink);
}
.pf-mh-score-sub { margin-top: 4px; font: 400 11px/1.2 var(--font-ui); color: var(--ink-tertiary); }

/* ⚠ THE CHEVRON IS ROTATED BY A SPRING IN JS, so it must not also carry a CSS transition on
   transform — the two would fight and the arrow would lag the panel it belongs to. */
.pf-mh-chev {
  flex: none; color: var(--ink-tertiary); opacity: .55;
  transform-origin: 50% 50%; display: block;
}

/* ── The expanding panel ──────────────────────────────────────────────────── */
/* ⚠ 0fr → 1fr, driven by a spring in JS. A pixel height has to be measured, is wrong after
   any reflow, and cannot be re-targeted mid-flight; an fr track animates to whatever the
   content turns out to be. The inner element MUST have min-height:0 and overflow:hidden or
   the grid refuses to squeeze it. */
.pf-mh-detail { display: grid; grid-template-rows: 0fr; }
.pf-mh-detail-in { min-height: 0; overflow: hidden; will-change: transform, opacity; }

/* ── The rally replay ─────────────────────────────────────────────────────── */
.pf-mh-game-head {
  display: flex; align-items: baseline; gap: 9px;
  padding: 12px 15px 8px; border-top: 1px solid var(--rule);
}
.pf-mh-game-n {
  font: 600 11px/1 var(--font-ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-tertiary);
}
.pf-mh-game-s {
  font: 600 14px/1 var(--font-ui); letter-spacing: -.01em; color: var(--ink); margin-left: auto;
}
.pf-mh-game-r { font: 600 11px/1 var(--font-ui); letter-spacing: .04em; }
.pf-mh-game-r.is-won  { color: var(--ok); }
.pf-mh-game-r.is-lost { color: var(--danger); }

.pf-mh-rallies { padding: 2px 15px 6px; }

.pf-mh-rally {
  display: flex; align-items: baseline; gap: 10px;
  padding: 4px 0 4px 9px;
  border-left: 2px solid var(--rule);
  font: 400 12.5px/1.45 var(--font-ui); color: var(--ink-secondary);
}
/* The rule down the left is a second reading of the same fact the word already carries —
   scanning a whole game for "who was scoring" is a different task from reading one line. */
.pf-mh-rally.is-us   { border-left-color: var(--ok); }
.pf-mh-rally.is-them { border-left-color: var(--danger); }

/* The call is the anchor of the whole replay, so it gets its own column and never wraps.
   5em fits the widest call a game to 11 can produce, "13-11-2". */
.pf-mh-call {
  flex: none; width: 5em; text-align: right;
  font: 600 12px/1.5 var(--font-mono); color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pf-mh-say { flex: 1; min-width: 0; }
.pf-mh-word { font-weight: 600; color: var(--ink); }
/* The server's name is context, not the event — it steps back so the eye lands on the word. */
.pf-mh-serve { color: var(--ink-tertiary); }

.pf-mh-foot {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 15px 15px; border-top: 1px solid var(--rule);
}
.pf-mh-foot-t { font: 400 12px/1.3 var(--font-ui); color: var(--ink-tertiary); }

.pf-mh .empty { margin: 0 16px; }

/* ── Wide screens ─────────────────────────────────────────────────────────── */
/* ⚠ THE CONTAINER IS THE SECTION, NOT `.pf`. A desktop browser should get the tablet layout in
   a readable column (DESIGN.md §6) rather than a rally line seventeen words long — but the
   query has to ask the APP FRAME, not the browser window, because the app is drawn inside a
   390px phone frame and a media query would fire while that frame was still phone-width.
   ⚠ It is declared on this section and NOT on `.pf` itself, deliberately. container-type
   brings layout containment with it, which makes the element a containing block for every
   absolutely positioned descendant — and `.pf` holds the sync pill, the scrim and the sheet
   panel, whose positioning is load-bearing and belongs to the sheet machinery. Containing the
   one section that needs it costs nothing and touches none of that. */
.pf-mh { container: pf-mh / inline-size; }
@container pf-mh (min-width: 700px) {
  .pf-mh-match, .pf-mh-seg, .pf-mh-day {
    max-width: 620px; margin-left: auto; margin-right: auto;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * 7 · SIGNED OUT
 * ═══════════════════════════════════════════════════════════════════════════ */

.pf-signin { padding: 22px 18px; }
.pf-signin .pf-name { font-size: 22px; }
/* ⚠ .pf-signin-p IS GONE. The paragraph it styled ("Your @username, your match record… There
   is no password to lose") was deleted on 2026-09-03, not restyled — Syd's example of the app
   talking is almost word for word this one. The rule is removed with it rather than left
   behind: a dead selector is an invitation to put the sentence back. */
/* An <a> wearing .btn — a real navigation to /auth/start, which is one of the only two routes
   that actually exist. It needs the text centring and the underline removal a <button> gets
   for free. */
.pf-signin-go { margin-top: 18px; text-decoration: none; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 8 · SHEETS
 * ═══════════════════════════════════════════════════════════════════════════
 * Two layers, because the profile itself is a sheet and the edit forms open on top of it.
 * "app" is the profile over whatever tab you were on; "in" is a form over the profile.
 */

.pf-scrim {
  position: absolute; inset: 0;
  background: rgba(0, 10, 22, .42);
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
  opacity: 0;
}
.pf-scrim.pf-layer-app { z-index: 70; }
.pf-scrim.pf-layer-in  { z-index: 72; }

/* ⚠ A SOLID GROUND, NEVER A TRANSLUCENT ONE. DESIGN.md §5: two translucent surfaces stacked
   lose legibility completely, and this panel sits over a blurred scrim which sits over the
   translucent nav. --bg-raised is opaque in both themes. */
.pf-sheet {
  position: absolute; z-index: 71; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column;
  max-height: 88%;
  background: var(--bg-raised);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--shadow-3);
  will-change: transform;
}
.pf-sheet.pf-layer-in { z-index: 73; }

/* ⚠ THE APRON UNDER THE SHEET. SPRING.sheet is damping 0.8 — UNDERdamped, deliberately,
   because a sheet is a thing you throw — so `to(0)` does not stop at 0, it crosses it. For a
   few frames the panel sits ABOVE its docked position and a strip of the screen behind shows
   through under it, which reads as the sheet having failed to reach the bottom. This paints
   the sheet's own colour below the panel so an overshoot reveals more sheet instead of a gap.
   ⚠ position:absolute on a pseudo-element, so it contributes nothing to offsetHeight — the
   drag arithmetic in openSheet() measures that height every time it is grabbed, and an apron
   that counted toward it would make every 1:1 drag slightly slower than the finger. */
.pf-sheet::after {
  content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 140px;
  background: var(--bg-raised); pointer-events: none;
}

/* ⚠ 52px IS THE STATUS BAR, NOT A GAP FOR LOOKS. A "full" sheet that starts at 0 covers the
   clock, and on a real phone the OS draws its own status bar on top of the app — so a preview
   that hides ours stops matching the device it is previewing. */
.pf-sheet-full { top: 52px; max-height: none; }

/* ⚠ touch-action:none ON THE HEAD IS WHAT MAKES THE DRAG WORK AT ALL. Without it the browser
   claims a vertical drag for scrolling before pointermove ever fires, and the sheet simply
   does not follow the finger — with no error anywhere. It is on the head only, because the
   BODY has to keep scrolling normally. */
.pf-sheet-head {
  flex: none; padding: 10px 0 0;
  touch-action: none; -webkit-tap-highlight-color: transparent; cursor: grab;
}
.pf-sheet-head .grabber { margin-bottom: 10px; }
.pf-sheet-bar {
  display: flex; align-items: center; gap: 12px;
  padding: 0 16px 12px; border-bottom: 1px solid var(--rule);
}
.pf-sheet-title {
  flex: 1; min-width: 0;
  font: 600 17px/1.2 var(--font-ui); letter-spacing: -.012em; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Done, not a ✕. The word says what happens; a glyph asks people to guess whether it saves or
   discards, and on a sheet that has already applied its changes the guess is usually wrong. */
/* ⚠ A FILLED PILL, NOT ORANGE TEXT. Orange is the right colour here — Done IS the one action
   on this sheet — but as 16px text on --bg-raised it was the least readable thing on the
   screen, which is the opposite of what the one action should be. Filled, the same orange
   passes comfortably against --action-ink and stops competing with the badge and the switches
   for the eye, both of which are now ink. */
.pf-done {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  border: 0; cursor: pointer;
  padding: 8px 16px; min-height: var(--tap-min); border-radius: var(--r-full);
  background: var(--action); color: var(--action-ink);
  font: 600 15px/1 var(--font-ui); letter-spacing: -.008em;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.pf-done:active { background: var(--action-press); }
[data-ready] .pf-done { transition: transform var(--t-press) var(--ease-out); }

.pf-sheet-body {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding-bottom: calc(20px + var(--bottom-inset));
}
/* ⚠ WHEN THE PROFILE ITSELF IS THE SHEET, THE BODY MUST NOT BE A SECOND SCROLLER.
   render() puts a .pane-scroll inside, and two nested scrollers means a flick that reaches
   the end of the inner one starts moving the outer one — the content visibly slips out from
   under the finger. The inner .pane-scroll wins; the body just holds it. */
.pf-sheet-body.pf {
  overflow: visible; display: flex; flex-direction: column; padding-bottom: 0;
}
/* No floating nav inside a sheet, so shell.css's nav clearance would be 96px of dead air. */
.pf-sheet-body.pf > .pane-scroll { padding-bottom: calc(28px + var(--bottom-inset)); }

/* ── Forms inside sheets ───────────────────────────────────────────────── */

.pf-form { padding: 18px 16px calc(8px + var(--bottom-inset)); }
.pf-fgroup + .pf-fgroup { margin-top: 18px; }
.pf-flabel {
  display: block; padding: 0 4px 7px;
  font: 600 12px/1 var(--font-ui); letter-spacing: .05em; text-transform: uppercase;
  color: var(--ink-tertiary);
}

.pf-field {
  display: flex; align-items: center; gap: 8px;
  padding: 0 14px; min-height: 50px;
  background: var(--bg-sunken); border: 1px solid var(--rule); border-radius: var(--r-md);
}
/* ⚠ focus-within, NOT :focus ON THE INPUT. The ring belongs to the box the person sees; put
   it on the input and it draws inside the box, a rounded rectangle inset in another one. */
.pf-field:focus-within {
  border-color: var(--focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 22%, transparent);
}
.pf-at { flex: none; font: 500 16px/1 var(--font-ui); color: var(--ink-tertiary); }

/* ⚠ 16px MINIMUM, AND IT IS NOT A TASTE CALL. Safari zooms the whole page in when a focused
   input's text is under 16px, and it does not zoom back out — the player is left on a
   sideways-scrolling profile wondering what they broke. */
.pf-input {
  flex: 1; min-width: 0; width: 100%;
  border: 0; background: transparent; outline: none; padding: 14px 0;
  font: 400 16px/1.35 var(--font-ui); letter-spacing: -.006em; color: var(--ink);
}
.pf-input::placeholder { color: var(--ink-tertiary); }
.pf-textarea { resize: none; padding: 14px 0; font-family: var(--font-ui); }
.pf-count {
  flex: none; font: 400 12px/1 var(--font-ui); color: var(--ink-tertiary);
  font-variant-numeric: tabular-nums;
}
.pf-fnote {
  margin: 7px 4px 0; font: 400 12px/1.5 var(--font-ui); color: var(--ink-tertiary);
  max-width: 44ch;
}

/* ⚠ THE FILE INPUT IS REALLY IN THE DOM AND REALLY HIDDEN — not display:none. A
   display:none input cannot be .click()ed reliably on iOS Safari, and the failure is that
   nothing happens when the player taps "Choose a photo". Clipped to a 1px box instead, which
   keeps it focusable and clickable while occupying nothing. */
.pf-file {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.pf-save { margin-top: 20px; }
.pf-save:disabled { opacity: .42; cursor: not-allowed; }

/* ── The @username sheet ───────────────────────────────────────────────── */

.pf-current {
  display: flex; align-items: baseline; gap: 10px;
  padding: 0 4px 16px;
}
.pf-current-lab {
  font: 600 11px/1 var(--font-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-tertiary);
}
.pf-current-val {
  font: 600 19px/1.1 var(--font-ui); letter-spacing: -.012em; color: var(--ink);
  overflow-wrap: anywhere;
}

/* ⚠ THE VERDICT IS A WORD FIRST AND A COLOUR SECOND. "AVAILABLE" / "TAKEN" / "TOO CLOSE" —
   a green or amber pill alone is unreadable to a colour-blind player and invisible in sun,
   and this is the control where being wrong costs a 180-day tombstone on a name. */
.pf-verdict {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  min-height: 22px; margin-top: 10px; padding: 0 2px;
}
.pf-verdict-why {
  font: 400 12px/1.45 var(--font-ui); color: var(--ink-tertiary); flex: 1; min-width: 12ch;
}

.pf-rules { margin-top: 16px; padding-left: 20px; list-style: disc; }
.pf-rules li {
  font: 400 13px/1.6 var(--font-ui); color: var(--ink-tertiary);
}
.pf-rules li::marker { color: var(--steel-40); }

/* The consequence, stated BEFORE the commit rather than in a toast after it. Amber-tinted
   because it is a warning, and worded because amber alone is not one. */
.pf-consequence {
  margin-top: 18px; padding: 14px 16px;
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  border-radius: var(--r-md);
}
.pf-consequence h3 {
  font: 600 13px/1.2 var(--font-ui); letter-spacing: -.004em; color: var(--ink);
}
.pf-consequence p {
  margin-top: 7px; font: 400 12px/1.5 var(--font-ui); color: var(--ink-secondary);
}

.pf-lockbox {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 14px 16px; margin-bottom: 4px;
  background: var(--bg-sunken); border-radius: var(--r-md);
}
.pf-lockbox p {
  flex: 1; font: 400 13px/1.5 var(--font-ui); color: var(--ink-secondary);
}
.pf-lockbox .tag { flex: none; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 9 · ACCESSIBILITY
 * ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  /* Gentler, not absent (DESIGN.md §4.8). What carries meaning stays — the switch still
     changes colour, the menu still fades — what moves stops moving. */
  .pf-sw-knob { transition: none; }
  .pf-dd-chev { transition: none; }
  .pf-dd-trigger[aria-expanded="true"] .pf-dd-chev { transform: rotate(90deg); }
  .pf-dd-menu.is-out { transform: none; transition-property: opacity; }
  /* The pill still fades; it stops sliding up. Both the resting and the starting transform
     have to be flattened together — leave either one at 8px and the pill jumps rather than
     appears, which is a worse artefact than the animation it replaced. */
  .pf-pill { transition-property: opacity, display; }
  .pf-pill[hidden] { transform: translate3d(-50%, 0, 0); }
  @starting-style { .pf-pill { transform: translate3d(-50%, 0, 0); } }
}

@media (prefers-reduced-transparency: reduce) {
  .pf-scrim { backdrop-filter: none; -webkit-backdrop-filter: none; background: rgba(0, 10, 22, .62); }
}

@media (prefers-contrast: more) {
  .pf-venue .list { border-color: var(--ink); }
  .pf-sw { border-color: var(--ink); }
  .pf-preview { border-color: var(--ink); }
}

/* ⚠ KEYBOARD FOCUS IS VISIBLE ON EVERY CUSTOM CONTROL. These are all <button>s and <div>s
   rather than native inputs, so nothing gives them a focus ring for free — and the profile is
   the one screen a staff member is most likely to reach on a desk keyboard. */
.pf-row:focus-visible,
.pf-sw:focus-visible,
.pf-dd-opt:focus-visible,
.pf-mh-head:focus-visible,
.pf-preview-btn:focus-visible,
.pf-link:focus-visible,
.pf-photo:focus-visible,
.pf-done:focus-visible,
.hdr-btn:focus-visible {
  /* ⚠ THEME-AWARE, NOT ORANGE. An orange ring works on the navy page at night and is close to
     invisible on the cream one — which is the theme a staff member on a desk keyboard is most
     likely to be in. --focus is the token that carries this: navy in light, orange in dark.
     ⚠ It was written here as var(--focus-ring, …) on a guess at the name, which never resolved
     and ran on the fallback — right by luck in light, and losing the orange the dark page
     wants. Use --focus; there is no --focus-ring. */
  outline: 2px solid var(--focus); outline-offset: 2px;
  border-radius: var(--r-sm);
}
.pf-photo:focus-visible, .hdr-btn:focus-visible { border-radius: 50%; }

/* ⚠ INSET, BECAUSE THE MATCH CARD CLIPS. .pf-mh-match carries `overflow: hidden` so the
   0fr→1fr replay panel can be squeezed shut — which also clips anything drawn OUTSIDE the
   card's box, and a 2px outward focus ring on a head that fills the card's full width is
   exactly that. Keyboard focus on a match would have been invisible on all four edges. */
.pf-mh-head:focus-visible { outline-offset: -2px; }

/* ── Appearance ────────────────────────────────────────────────────────────
   Light · Dark · Auto. The control is ui.css's .segmented; only the three-up thumb width and
   the glyph-over-word stacking are new. */
.pf-theme { margin: 0 20px 20px; }
/* Three segments: 2px of padding either side plus two 2px gaps = 8. Same formula as the match
   filter above — if a fourth option is ever added, this number changes with it. */
.pf-theme .thumb { width: calc((100% - 8px) / 3); }
.pf-theme .pf-th {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 9px 4px 8px;
}
/* ⚠ THE GLYPH CARRIES THE STATE AS WELL AS THE WORD. DESIGN.md §2 — the moving thumb is a
   surface tint, and a surface tint is the first thing to go in direct sun. The selected glyph
   also gains ink and a little size, so the state survives a greyscale screenshot. */
.pf-theme .pf-th-i {
  color: var(--ink-tertiary);
  scale: 1;
  transition: color 200ms var(--ease-out), scale 300ms var(--ease-out);
}
.pf-theme .pf-th[aria-selected="true"] .pf-th-i { color: var(--ink); scale: 1.1; }
.pf-theme .pf-th-w {
  font: 500 12px/1 var(--font-ui); letter-spacing: -.004em; color: var(--ink-secondary);
}
.pf-theme .pf-th[aria-selected="true"] .pf-th-w { font-weight: 600; color: var(--ink); }

/* ⚠ THE PANEL IS FOCUSED ON OPEN (see openSheet) AND MUST NOT DRAW A RING FOR IT. A dialog is
   focused so a screen reader announces it, not because the person moved a keyboard to it —
   and a 2px outline around the whole sheet is a box drawn around the entire screen. Keyboard
   focus on the controls INSIDE it is unaffected; those keep their own :focus-visible rings. */
.pf-sheet:focus, .pf-sheet:focus-visible { outline: none; }
