/* site/app/screens/profile.css — the Profile screen.
 * 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.
 *
 * ⚠ IT CARRIES ONE THING NOW. It used to hold header.js's corner avatar as well, because
 *   header.js has no stylesheet of its own — see the note below where those rules were. Both
 *   of header.js's buttons live in the tab bar since 2026-09-04, so their rules moved to
 *   shell.css with it and everything here is the profile screen.
 *
 * ⚠ EVERY `pf-` 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 — REMOVED 2026-09-04
 * ═══════════════════════════════════════════════════════════════════════════
 * ⚠⚠ ABOUT SEVENTY LINES OF `.hdr`, `.hdr-btn`, `.hdr-av`, `.hdr-out` AND `.hdr-word` STOOD
 *   HERE AND NOTHING HAD RENDERED ANY OF IT FOR A DAY. header.js used to mount a floating
 *   avatar in the screen's top-right; on 2026-09-04 the profile moved into the tab bar as
 *   `.fnav-me` and the player code moved above the bar as `.qrbtn`, and neither writes an
 *   `.hdr` class any more. Grepped before deleting: `hdr` appears in no .js file under site/.
 *   Kept as a note rather than deleted silently, because dead CSS is not inert — it is the
 *   next reader's evidence that a mechanism is still in use, and there was a `.hdr.is-hidden`
 *   rule down here paired with a `setHeaderVisible()` that no longer exists either.
 *   The rules that replaced them live in shell.css, with the tab bar they now belong to. */

/* ⚠ THE 92px TITLE RESERVE THAT WAS HERE IS GONE, WITH THE BUTTON IT WAS FOR. It held a gap in
 *   the Profile's title row for the player-code button in the screen's top-right corner. That
 *   button moved to the tab bar on 2026-09-04 (shell.css, `.qrbtn`), so the reserve became 92px
 *   of dead air beside nothing and wrapped the title early. The Shop keeps its own reserve
 *   because its cart is still up there; nothing else does. */

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

/* ⚠ THE PROFILE HAS TWO HOSTS AND THE POSITIONING HAS TO WORK IN BOTH.
 *   openPlayerSheet() renders into a sheet body; showProfile() renders into the Profile TAB's
 *   pane — which it now is, since 2026-09-04. 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. */
/* ⚠ THE PILL IS FIXED, LIKE THE DOCKS, so in the pane it sits in the band above the tab bar
   (`--band-bottom`, shell.css) and in a sheet — whose transform makes it the containing block —
   it sits above the sheet's own bottom inset. */
.pf { --pf-pill-bottom: var(--band-bottom); }
/* In a sheet there is no bar under the pill, so there is no band to follow either — nav.js only moves
   the pill that is the PANE's (`.pane.on .pf-pill` outside `.pf-sheet-body`). */
.pf-sheet-body.pf { --pf-pill-bottom: calc(20px + var(--safe-bottom)); }

.pf-pill {
  position: fixed; 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);
  /* ⚠ THE BAND'S SHIFT IS nav.js's, WRITTEN INLINE AS `translate` (2026-09-15), its own property so it
     composes with the centring transform. Its `bottom` used to change with the tab bar's state, and with
     no transition on it the pill jumped whenever the labels opened or the bar minimised; then it followed
     `--band-shift` on a 360ms transition, which a shift written every frame would restart on every
     write. So it is not declared here and no list below transitions it (scripts/checks/riders.js).
     ⚠ AN EXPLICIT LIST EVEN HERE, BEFORE `[data-ready]` gives it the real one: left at the initial
     `transition-property: all`, tokens.css's reduced-motion `160ms !important` would slide every
     inline write. No duration is set, so outside reduced motion this list animates nothing. */
  transition-property: opacity, transform, display;
  will-change: translate;
}
/* ⚠ 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 var(--gutter) 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 var(--gutter) 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 var(--gutter); }
.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 var(--gutter) 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;
}
/* ⚠⚠ THE MARK IS A KICKER ABOVE THE NAME, NOT A BADGE IN FRONT OF IT, AND THE FIRST VERSION WAS
   THE OTHER WAY ROUND. Beside a 108px avatar this column is about 190px wide; an 18px badge and
   four characters in front of a 24px display name broke "Syd Tanchan" — eleven characters — onto
   two lines, on the one screen whose subject is that name. The note above says the name WRAPS and
   is never truncated, so the mark cannot compete with it for the same line: it goes on its own,
   immediately before, which is still in front of the name in reading order and is how the rest of
   the app's cards introduce a heading.
   ⚠ justify-items: start, or the grid stretches the mark to the column and the badge's tap-sized
     ghost sits under the whole width of the name. */
.pf-name-line { display: grid; justify-items: start; gap: 5px; min-width: 0; }
.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); }

/* ══ THE HANDLE, WHICH IS ALSO THE WAY TO CHANGE IT ════════════════════════
   The Editable list was removed on 2026-09-04 and it held the only route to the @username
   sheet, so the handle under the name became the control. It has to read as pressable without
   turning the identity card into a form: a pencil at 13px and a press response, and nothing
   else — no border, no fill, no chevron. */
.pf-handle-btn {
  display: inline-flex; align-items: center; gap: 6px;
  border: 0; background: transparent; padding: 3px 0; margin: 0;
  cursor: pointer; color: inherit; font: inherit;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.pf-handle-ico { color: var(--ink-tertiary); flex: none; }
.pf-handle-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 6px; }
@media (hover: hover) and (pointer: fine) {
  .pf-handle-btn:hover .pf-handle-ico { color: var(--ink-secondary); }
}

/* ══ THE RECORD, AS CARDS THAT OPEN ════════════════════════════════════════
 * SPEC (Syd, 2026-09-04): "i want the stats page to look like this [minimal-carousel] … it
 * should be more obvious that the cards are clickable and when they are expanded they show
 * more stats."
 *
 * ⚠⚠ THE LAYOUT SNAPS. IT DOES NOT ANIMATE, AND THAT IS THE SECOND ATTEMPT. The first animated
 *   the widths and played the moves with a FLIP pass, and cards flew off the screen. These sit
 *   in a WRAPPING flex row and the animated property is their width, so half-way through the
 *   opened card is 300px and the others 160 — a combination that wraps differently from both
 *   the start and the end state, and cards jump rows no matter how correct the transforms are.
 *   A layout whose line breaks depend on the values being animated cannot be smoothed from
 *   outside; it needs the elements out of flow for the duration, which is a projection engine
 *   and is not worth one grid of four cards. What the reader came for — the extra rows — still
 *   animates open INSIDE the card, where it is bounded.
 *
 * ⚠ `order: -1` PUTS THE OPEN CARD ON TOP. In DOM order the third card opening would wrap to a
 *   line of its own with the others stranded around it.
 *
 * ⚠ PRESS FEEDBACK IS `scale`, and so is the settle. `transform` belongs to motion.js's
 *   pressable(), which blanks it on release; `scale` is its own property and composes. */
/* ⚠ 16px, THE SAME INSET `.pf-sec .list` AND `.card` USE. It had none — the cards ran edge to
   edge while everything above and below them was inset, which is what Syd saw as "margins
   broken". Nothing else on the screen touches the edge. */
.pf-cards { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 var(--gutter); }

/* ⚠ THE SETTLE, AND IT IS THE WHOLE ANIMATION NOW. One compositor-only keyframe on each card
   when the open state changes: it cannot re-wrap, it cannot leave the screen, and it reads as
   the grid responding rather than the grid teleporting. profile.js restarts it by removing the
   class, reflowing and re-adding — adding a class that is already there restarts nothing. */
.pf-cards.is-settling .pf-card { animation: pf-settle 260ms var(--ease-out) both; }
@keyframes pf-settle { from { scale: .965; } to { scale: 1; } }
@media (prefers-reduced-motion: reduce) {
  .pf-cards.is-settling .pf-card { animation: none; }
}

.pf-card {
  position: relative;
  flex: none;
  width: calc(50% - 5px); height: 108px;
  display: flex; flex-direction: column; align-items: flex-start;
  padding: 12px 13px;
  border: 1px solid var(--rule); border-radius: 22px;
  background: var(--bg-raised);
  color: var(--ink); text-align: left;
  cursor: pointer; overflow: hidden;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
  scale: 1;
  /* ⚠ NO width/height/transform TRANSITION. Those three were the re-wrap engine described
     above; the colours still cross-fade, which is the part that can be animated safely. */
  transition: background-color 200ms var(--ease-out), color 200ms var(--ease-out),
              border-color 200ms var(--ease-out), scale var(--t-press) var(--ease-out);
}
.pf-card:active { scale: .975; }
.pf-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.pf-card-head { display: flex; align-items: center; gap: 6px; width: 100%; }
.pf-card-ico { color: var(--ink-tertiary); flex: none; }
.pf-card-label {
  font: 600 11.5px/1.2 var(--font-ui); letter-spacing: .02em; text-transform: uppercase;
  color: var(--ink-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ⚠ THE CHEVRON IS THE "THIS OPENS" SIGN AND THE "THIS IS OPEN" SIGN, which is one mark doing
   two jobs rather than two marks disagreeing. A phone has no hover, so a card that only
   announces itself under a cursor announces itself to nobody at the venue. */
.pf-card-chev {
  margin-left: auto; flex: none; color: var(--ink-tertiary);
  rotate: 90deg;
  transition: rotate 320ms var(--ease-out), color 200ms var(--ease-out);
}
.pf-card.is-big .pf-card-chev { rotate: -90deg; color: var(--ink-secondary); }

.pf-card-value {
  margin-top: auto;
  font-family: var(--font-display); font-weight: 600;
  font-size: 30px; line-height: 1; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  transition: font-size 420ms var(--ease-out);
}
/* ⚠⚠ IT IS PULLED OUT OF FLOW WHEN THE CARD IS SMALL, AND ONLY THEN. In flow it sits under the
   number and pushes it up — so Rating, which is the one card with a tag, had its 1642 sitting
   seven pixels higher than the #14 beside it. Four numbers in a grid that do not share a
   baseline read as a rendering fault, and it is the kind nobody can name; they just think the
   screen looks slightly wrong. Cornered, every value sits on the same line.
   ⚠ AND IT GOES BACK INTO FLOW WHEN THE CARD OPENS, because there the bottom-right corner is
   where the detail rows are, and a chip parked on top of them is worse than an uneven baseline
   ever was. */
.pf-card-tag { position: absolute; right: 13px; bottom: 12px; margin: 0; }
.pf-card.is-big .pf-card-tag { position: static; margin-top: 8px; align-self: flex-start; }

/* Closed, the extra lines are not merely hidden — they take no height, so the card is the size
   of the number in it. */
.pf-card-more {
  display: grid; gap: 3px; width: 100%;
  margin-top: 0; max-height: 0; opacity: 0;
  overflow: hidden;
  transition: max-height 420ms var(--ease-out), opacity 200ms var(--ease-out),
              margin-top 420ms var(--ease-out);
}
.pf-card.is-big .pf-card-more { max-height: 120px; opacity: 1; margin-top: 10px; }
.pf-card-mrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  border-top: 1px solid var(--rule); padding-top: 5px;
}
.pf-card-mk { font: 500 12.5px/1.3 var(--font-ui); color: var(--ink-tertiary); }
.pf-card-mv {
  font: 600 13px/1.3 var(--font-ui); color: var(--ink); font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Open: the card takes the full width and goes to the top; the other three share a row. */
.pf-card.is-big {
  order: -1;
  width: 100%; height: auto; min-height: 168px;
  background: var(--ink); color: var(--bg); border-color: var(--ink);
}
.pf-card.is-big .pf-card-ico,
.pf-card.is-big .pf-card-label,
.pf-card.is-big .pf-card-mk { color: color-mix(in srgb, var(--bg) 66%, transparent); }
.pf-card.is-big .pf-card-mv { color: var(--bg); }
.pf-card.is-big .pf-card-chev { color: color-mix(in srgb, var(--bg) 66%, transparent); }
.pf-card.is-big .pf-card-mrow { border-top-color: color-mix(in srgb, var(--bg) 22%, transparent); }
.pf-card.is-big .pf-card-value { font-size: 42px; }

.pf-card.is-mini {
  width: calc(33.333% - 6.667px); height: 84px;
  padding: 9px 10px;
}
.pf-card.is-mini .pf-card-label { font-size: 10px; }
.pf-card.is-mini .pf-card-ico, .pf-card.is-mini .pf-card-chev { display: none; }
.pf-card.is-mini .pf-card-value { font-size: 22px; }
.pf-card.is-mini .pf-card-tag { display: none; }

@media (prefers-reduced-motion: reduce) {
  .pf-card-more, .pf-card-chev { transition-property: opacity, color; }
}
@media (prefers-contrast: more) {
  .pf-card { border-color: var(--ink); }
}

/* 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 var(--gutter) 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 var(--gutter) 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 var(--gutter); }

/* ── 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);
  /* ⚠ NO backdrop-filter, AND IT HAD ONE: blur(2px) under a 42% dim. Nobody can see two pixels of blur
     through that, and it cost a full-screen blur pass on every frame the scrim fades — at 120 Hz an
     8.3ms frame shared with the sheet it sits under (2026-10-04, the 120 Hz work; motion.js glide()). */
  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. */
/* ⚠ FIXED — anchored to the visible bottom of the viewport in every browser's toolbar state, the
   same reasoning as the tab bar (shell.css, the .fnav note). Its bottom paddings read
   --safe-bottom for the same reason. */
.pf-sheet {
  position: fixed; 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. */
/* ⚠ AND BELOW THE ISLAND ON A REAL PHONE. 52px is the desk preview's painted status bar; an iPhone
   17 Pro's inset is 62, so the sheet began 10px up inside the clock's strip. The larger of the two:
   the inset plus 10px, so the sheet's rounded top clears the island rather than touching it. */
.pf-sheet-full { top: max(52px, calc(var(--safe-top) + 10px)); 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 var(--gutter) 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(--safe-bottom));
}
/* ⚠ 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(--safe-bottom)); }

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

.pf-form { padding: 18px var(--gutter) calc(8px + var(--safe-bottom)); }
.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. */
  /* ⚠ `[data-ready]` IN THE SELECTOR, OR THIS LOSES: the transition list it narrows is set on
     `[data-ready] .pf-pill`, one class stronger, and at plain `.pf-pill` the pill went on sliding
     with the band under reduced motion (review, 2026-09-14). */
  [data-ready] .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 { background: rgba(0, 10, 22, .62); }   /* no blur to take away any more — see .pf-scrim */
}

@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 var(--gutter) 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; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 13 · THE REAL PROFILE — profile.js §13
 * ═══════════════════════════════════════════════════════════════════════════
 * ⚠ THE SAMPLE'S OWN PIECES, SWITCHED OFF. The real profile is drawn from the same classes as the
 *   sample one — the identity card, the group head, the cards, the fact row, the switch, Appearance —
 *   so the two modes are one design. Everything below is the `.is-off` state of a sample piece: the
 *   same box, greyed, with no press and no pointer, because nothing behind it works yet. Nothing
 *   here moves, so there is nothing for a reduced-motion block to turn down. */

/* Where verification happens, under the "Unverified" tag. A fact in the tertiary ink the card's
   other small print uses, not a call to action: it cannot be done in the app. Drawn only when
   views/profile.js TRUST_WORDS has a line, and today none has. */
.pf-trust-line {
  margin-top: 8px;
  font: 400 13px/1.45 var(--font-ui); color: var(--ink-tertiary);
  max-width: 40ch;
}

/* "Not in the app yet: …" at the foot of the page. `.pf-note`'s inset and voice; the top margin
   goes, because Appearance above already ends on its own 20px. */
.pf-real-foot { margin: 0 var(--gutter) 8px; }

/* The photo: a picture, not a button, and its camera badge greyed — the sunken fill and tertiary ink
   every switched-off control below uses, not the ink disc that says "press me". */
.pf-photo.is-off { cursor: default; }
.pf-photo.is-off .pf-photo-badge { background: var(--bg-sunken); color: var(--ink-tertiary); }

/* The pencil on the @username. On the handle line it sits where the sample's does; on the big line
   (no display name yet) it rides the name's last line, a size smaller than the type around it. */
.pf-handle-btn.is-off { cursor: default; }
.pf-name .pf-at-ico { display: inline-block; vertical-align: .1em; margin-left: 7px; }
/* The @username in the name's slot (no display name yet) is a handle, so it never wraps: one line,
   an ellipsis if it must, and the pencil kept beside it rather than pushed off the card.
   ⚠ min-width: 0 on the h2 too — a grid item's default min-width is its content, which defeats the cut. */
.pf-name.is-handle { display: flex; align-items: center; min-width: 0; max-width: 100%; }
.pf-name.is-handle .pf-name-text { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pf-name.is-handle .pf-at-ico { flex: none; }

/* Edit profile: the quiet button, greyed, with "Coming soon" inside it after the words — in the
   secondary ink, so the tag does not outweigh the greyed label it explains. */
.pf-btn.is-off { cursor: default; color: var(--ink-tertiary); }
.pf-btn.is-off .pf-btn-tag { margin-left: 2px; padding: 4px 8px; font-size: 10px; color: var(--ink-secondary); }

/* "Coming soon" beside a group's title: smaller than a status tag, so the 12px heading keeps its line. */
.pf-ghead-tag { padding: 3px 7px; font-size: 10px; letter-spacing: .06em; }

/* The four cards: no press, no chevron, and the dash in the tertiary ink, so "—" reads as "nothing
   here yet" and not as a value. */
.pf-card.is-off { cursor: default; }
.pf-card.is-off:active { scale: 1; }
.pf-card.is-off .pf-card-value { color: var(--ink-tertiary); }

/* Best partner: a grey disc where the face goes, and "None yet" in the tertiary ink. */
.pf-skel-av { width: 40px; height: 40px; border-radius: 50%; }
.pf-partner .pf-none { color: var(--ink-tertiary); }

/* Partners' empty box on the page gutter, as the match list's is (`.pf-mh .empty`). */
.pf-sec > .empty { margin: 0 var(--gutter); }

/* The match filter on All, every segment greyed; All keeps its thumb and weight, so the state reads. */
.pf-mh-seg.is-off button { cursor: default; color: var(--ink-tertiary); }
.pf-mh-seg.is-off button[aria-pressed="true"] { color: var(--ink-secondary); }

/* Settings and sign-out rows: the label kept, the control greyed. A switch with no word in its track
   and the knob at rest — On or Off would be a claim about a setting that does not exist yet. */
.pf .row.is-off { cursor: default; }
.pf-sw.is-off { cursor: default; opacity: .5; }
.pf-dd-trigger.is-off .value,
.pf-dd-trigger.is-off .pf-dd-chev { color: var(--ink-tertiary); }
.pf-danger.is-off .title,
.pf-danger.is-off .pf-danger-ico { opacity: .5; }
.pf-danger.is-off .meta { color: var(--ink-tertiary); }
