/* Profile — the member's own corner of the Matrix: the welcome
   questionnaire (asked once, after payment) and the Profile panel it
   becomes afterwards (edit answers, change PIN, sign out).

   Built on the join overlay's grammar (same scrim/panel/entrance, same
   --cta token) so the two read as one product, but tuned by the
   research brief for an audience that is ~35% older, low-confidence,
   phone-first:
     · body text 18px (senior-legibility floor; capped at 20 — past the
       critical size reading speed DROPS again), inputs 16px+ so iOS
       never zoom-jumps on focus, weight 500 in dark (light-on-dark text
       optically thins).
     · every target ≥48px, primary CTA 56px, ≥8px apart.
     · labels are STATIC and above the field — never floating, never
       placeholder-as-label (Baymard: on error users delete their whole
       input just to see the label again).
     · selection state never rides colour alone — ring + check + weight.
     · nothing is icon-only; the escape hatch is the word "Skip".
   Keep the three theme blocks in step (app.css convention). */

:root {
  --prof-ok: #2e7d32;
  /* One step brighter than --text-2 in dark, one step darker in light —
     the same legibility correction the login card got. Light-on-dark
     text also optically thins, hence the extra weight above. */
  --prof-dim: #b0b9c1;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) { --prof-ok: #1b5e20; --prof-dim: #443a48; }
}
:root[data-theme="light"] { --prof-ok: #1b5e20; --prof-dim: #443a48; }

/* ── topbar entry point (replaces the bare Sign out button) ── */
.logout-btn { display: none !important; }

.prof-btn {
  display: flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 4px 14px 4px 4px;
  font: inherit; font-size: 14px; font-weight: 500;
  color: var(--text); background: transparent;
  border: 1px solid var(--line-strong); border-radius: 22px;
  cursor: pointer; transition: background var(--dur-hover) ease;
}
.prof-btn:hover { background: var(--surface); }
.prof-btn img, .prof-btn .prof-blank {
  width: 34px; height: 34px; border-radius: 50%; object-fit: cover;
  background: var(--surface-2); flex: 0 0 auto;
}
.prof-btn .prof-blank {
  display: grid; place-items: center;
  color: var(--text-2); font-size: 17px; font-weight: 700;
}
/* Fills the disc and is clipped by it — head centred, shoulders running
   off the bottom edge. Inset at 66% it floated, with a visible gap under
   the shoulders, which is what made it look off-centre. */
.prof-blank { overflow: hidden; }
.prof-blank svg { width: 100%; height: 100%; display: block; }

/* Signed out there is no name, so the 14px kept on the right for one is
   just a gap beside the avatar. Square the padding and it is a round
   icon button. */
/* .prof-btn.anon is gone with the signed-out seat itself: the topbar's
   "Sign in" button (css/signin.css) is the way into an account now, and
   a blank avatar beside it was two controls for one job. Nothing sets
   the class any more, so the rule went with it. */
/* An unfinished profile earns one quiet dot — not a red badge. */
.prof-btn.todo { position: relative; }
.prof-btn.todo::after {
  content: ""; position: absolute; top: 3px; right: 10px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--cta); border: 2px solid var(--topbar-bg);
}
@media (max-width: 640px) {
  .prof-btn { padding: 0; border: none; width: 44px; min-height: 44px; justify-content: center; }
  .prof-btn span { display: none; }
  .prof-btn.todo::after { right: 0; top: 0; }
}

/* ── overlay shell (mirrors .join) ── */
.prof {
  position: fixed; inset: 0; z-index: 60;
  /* Above the watch overlay (100) and its Close button (110). The profile
     opens from the topbar, which stays visible while a video is playing,
     so at 60 it opened *behind* the player — a button that appeared to do
     nothing. Its own scrim covers the video, which is what should happen:
     the video is no longer the thing being pointed at. */
  display: none; justify-content: center; align-items: flex-start;
  padding: 40px 20px 60px; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.prof.open { display: flex; }
body.eon-watching .prof { z-index: 140; }
.prof:not(.open) { display: none !important; }
/* A REAL element, not a `::before` with z-index:-1. A large pseudo
   element sitting behind a sliding sheet is a known-bad combination in
   Safari (it breaks slide animations — see vaul#573), and negative
   z-index on a pseudo invites the parent to become its own paint root.
   A plain sibling has none of those failure modes. */
.prof-scrim {
  position: fixed; inset: 0; z-index: 0;
  background: color-mix(in srgb, #17101c 62%, transparent);
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
  /* Transition, NOT an animation: a filling animation would pin opacity
     at 1 and beat the fade-out (animated values outrank declarations).
     The .in class drives it in both directions, so the scrim and the
     sheet arrive and leave together. */
  opacity: 0;
  transition: opacity 0.28s ease;
}
.prof.in .prof-scrim { opacity: 1; }
.prof-panel { z-index: 1; }
body.prof-open { overflow: hidden; }
@keyframes prof-scrim { from { opacity: 0 } to { opacity: 1 } }
/* Both ends keep a 3d transform: animating TO `none` drops the element
   out of its composited layer at the exact moment the animation ends,
   which is when WebKit mis-rasterises. */
@keyframes prof-in {
  from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.985) }
  to { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
}

.prof-panel {
  position: relative; width: 100%; max-width: 620px;
  padding: 28px 28px 32px; border-radius: 16px;
  /* 94% solid, not 84%: the login card learned the same lesson — a
     translucent ground lets the pattern sit under words and costs
     legibility, which this audience cannot spare. */
  background: color-mix(in srgb, var(--bg-elev) 94%, transparent);
  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--line);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45),
              0 0 0 1px color-mix(in srgb, var(--cta) 14%, transparent);
  animation: prof-in 0.42s var(--ease-move) both;
}
/* Sticky inside the panel, right-aligned — stays reachable while the
   sheet scrolls, and belongs to the sheet rather than floating over the
   library. margin-bottom pulls the content back up under it. */
/* The login card's EON letter-grid, same construction and the same
   two recipes: dark .10 / blur 3.5px, light .14 / blur 2px. Keep in
   step with homepage_login.html .card-pt and join.css's port. */
.prof-pt {
  position: absolute; inset: 0; z-index: 0; overflow: hidden;
  border-radius: inherit; opacity: .10; filter: blur(3.5px);
  pointer-events: none;
}
.prof-pt svg {
  position: absolute; top: -12%; left: -8%;
  width: 116%; height: 124%; transform: rotate(-8deg) scale(1.5);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .prof-pt { opacity: .14; filter: blur(2px); }
}
:root[data-theme="light"] .prof-pt { opacity: .14; filter: blur(2px); }
/* everything except the pattern rides above it */
.prof-panel > *:not(.prof-pt) { position: relative; z-index: 1; }

/* ── the card's top row ──
   Theme on the left; Close (panel) or Skip (questionnaire) on the right.
   A flex row, because the previous arrangement — loose children of a
   block panel, kept side by side by a negative bottom margin on the
   theme button — only held while exactly one button followed it. In the
   questionnaire Close is display:none, so that margin reached past it
   and pulled Skip up onto the theme button and the progress bar. */
.prof-topbar {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 14px; min-height: 44px;
}
.prof-topbar > :first-child { margin-right: auto; }

/* Mirrors Close: same pill, opposite corner, icon only (by request —
   the aria-label and title carry the meaning for anyone who needs it). */
.prof-theme {
  position: sticky; top: 0; z-index: 5;
  flex: 0 0 auto; width: 44px; height: 44px;
  display: grid; place-items: center;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--line-strong); border-radius: 50%; cursor: pointer;
  padding: 0;
}
.prof-theme:hover { background: var(--surface); }
.prof-theme svg { width: 19px; height: 19px; }

/* Same shape and size as the theme button on the other side — two
   equal circles read as a pair; a wide pill beside a small circle did
   not. Icon-only by request, so it keeps aria-label + title. */
.prof-close {
  position: sticky; top: 0; z-index: 5;
  flex: 0 0 auto; width: 44px; height: 44px; padding: 0;
  display: grid; place-items: center;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--line-strong); border-radius: 50%; cursor: pointer;
}
.prof-close svg { width: 19px; height: 19px; }
.prof-close:hover { background: var(--surface); }
/* The panel takes focus when the sheet opens (see profile.js) — it is a
   container, so it must never draw a ring. Buttons keep theirs. */
.prof-panel:focus, .prof-panel:focus-visible { outline: none; }
/* In the questionnaire, "Skip" IS the way out — a second Close button
   would collide with it on phones and read as a different (scarier)
   action. Esc still closes. */
.prof.wizard .prof-close { display: none; }

/* ── headings + progress ── */
.prof-eyebrow {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 14px; font-size: 13px; font-weight: 800;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--prof-dim);
}
.prof-h {
  margin: 0 0 8px; font-size: clamp(24px, 4.4vw, 32px);
  font-weight: 800; line-height: 1.18; color: var(--text);
}
.prof-sub {
  margin: 0 0 22px; font-size: 18px; line-height: 1.5;
  font-weight: 600; color: var(--prof-dim);
}
.prof-why { /* the "why we ask" line — sits WITH the question */
  /* Was -10px, to tuck it under the heading. A negative margin against a
     heading whose line-height already leaves room meant the two boxes
     genuinely intersected — measurably, at every width. Tighten the
     heading instead; the pairing reads the same and nothing overlaps. */
  margin: 0 0 18px; font-size: 15px; line-height: 1.45; color: var(--text-2);
}
.prof-h + .prof-why { margin-top: 2px; }

/* Segmented progress. Filled segments are never colour-only — they also
   carry a border, so a colour-blind or low-contrast display still reads
   the state (WCAG 1.4.11 applies to state indicators too). */
.prof-steps { display: flex; gap: 6px; margin: 0 0 18px; }
.prof-steps i {
  flex: 1; height: 6px; border-radius: 3px;
  background: var(--surface-2); border: 1px solid transparent;
}
.prof-steps i.on {
  background: var(--cta); border-color: color-mix(in srgb, var(--cta) 70%, #000);
}
.prof-stepnote { font-size: 14px; font-weight: 700; color: var(--prof-dim); }
.prof-stepnote .ok { color: var(--prof-ok); font-weight: 800; }

/* ── choice grids (radio semantics, card targets) ── */
.prof-opts { display: grid; gap: 10px; margin: 0 0 8px; }
.prof-opts.two { grid-template-columns: repeat(2, 1fr); }
/* Gender is three options — "Man", "Woman", "Prefer not to say" — and one
   full-width row each pushed the whole question below the buttons, where
   people reported missing it. Sized to their own words they take one row
   on a laptop and two on a phone, instead of three. */
.prof-opts:not(.two) { display: flex; flex-wrap: wrap; }
.prof-opts:not(.two) > .prof-opt { flex: 0 1 auto; }
.prof-opt {
  display: flex; align-items: center; gap: 12px;
  min-height: 56px; padding: 12px 16px;
  font: inherit; font-size: 18px; font-weight: 600; text-align: left;
  color: var(--text); background: var(--bg-elev);
  border: 1.5px solid var(--line-strong); border-radius: 12px;
  cursor: pointer; transition: border-color var(--dur-hover) ease,
    background var(--dur-hover) ease;
}
.prof-opt:hover { background: var(--surface); }
.prof-opt[aria-checked="true"] {
  border-color: var(--cta); border-width: 2px; padding: 11px 15px;
  background: color-mix(in srgb, var(--cta) 12%, var(--bg-elev));
}
/* the tick is the non-colour half of the selected state */
.prof-opt .tick {
  margin-left: auto; width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; flex: 0 0 auto;
  background: var(--cta); color: var(--cta-ink);
  font-size: 16px; font-weight: 900; opacity: 0; transform: scale(0.6);
  transition: opacity var(--dur-hover) ease, transform var(--dur-hover) ease;
}
.prof-opt[aria-checked="true"] .tick { opacity: 1; transform: none; }

/* one-tap suggestion chips above a free-text field */
.prof-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
.prof-chip {
  min-height: 48px; padding: 10px 16px;
  font: inherit; font-size: 16px; font-weight: 600;
  color: var(--text); background: var(--bg-elev);
  border: 1.5px solid var(--line-strong); border-radius: 24px; cursor: pointer;
}
.prof-chip:hover { background: var(--surface); }
.prof-chip[aria-pressed="true"] {
  border-color: var(--cta); border-width: 2px; padding: 9px 15px;
  background: color-mix(in srgb, var(--cta) 14%, var(--bg-elev));
}

/* ── avatar grid: 4×2 so all eight sit above the fold at 390px ── */
.prof-avatars {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 12px; margin: 0 0 8px;
  /* Grid items STRETCH to the row height by default, which silently
     overrides aspect-ratio and turns the avatars into ovals whenever a
     row is taller than it is wide. Anchoring them to the row start lets
     aspect-ratio decide the height, so they stay circles. */
  align-items: start;
}
.prof-av {
  position: relative; aspect-ratio: 1; padding: 4px;
  box-sizing: border-box; /* padding must not inflate the square */
  background: var(--bg-elev); border: 2px solid transparent;
  border-radius: 50%; cursor: pointer; overflow: visible;
  transition: transform var(--dur-hover) ease, border-color var(--dur-hover) ease;
}
.prof-av img {
  width: 100%; height: 100%; aspect-ratio: 1;
  border-radius: 50%; object-fit: cover; display: block;
}
.prof-av:hover { transform: scale(1.04); }
.prof-av[aria-checked="true"] { border-color: var(--cta); transform: scale(1.04); }
.prof-av .tick {
  position: absolute; right: -2px; bottom: -2px;
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--cta); color: var(--cta-ink);
  font-size: 15px; font-weight: 900;
  border: 2px solid var(--bg-elev);
  opacity: 0; transform: scale(0.5);
  transition: opacity var(--dur-hover) ease, transform var(--dur-hover) ease;
}
.prof-av[aria-checked="true"] .tick { opacity: 1; transform: none; }

/* ── text fields ── */
.prof-field { display: block; margin: 0 0 16px; }
.prof-field > span {
  display: block; margin-bottom: 7px;
  font-size: 16px; font-weight: 700; color: var(--text);
}
.prof-field input {
  width: 100%; box-sizing: border-box; min-height: 56px;
  padding: 12px 16px; font: inherit;
  font-size: 17px; /* ≥16px or iOS zooms the whole page on focus */
  font-weight: 500; color: var(--text); background: var(--bg-elev);
  border: 1.5px solid var(--line-strong); border-radius: 12px;
}
.prof-field input:focus { outline: none; border-color: var(--focus); border-width: 2px; padding: 11px 15px; }
.prof-field input::placeholder { color: var(--text-3); opacity: 0.75; }

/* ── actions ── */
.prof-go {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 56px; padding: 14px 20px;
  font: inherit; font-size: 18px; font-weight: 800;
  color: var(--cta-ink); background: var(--cta);
  border: 0; border-radius: 28px; cursor: pointer;
}
.prof-go:disabled { opacity: 0.55; cursor: default; }
.prof-ghost {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 52px; padding: 12px 20px;
  font: inherit; font-size: 17px; font-weight: 700;
  color: var(--text); background: transparent;
  border: 1.5px solid var(--line-strong); border-radius: 26px; cursor: pointer;
}
.prof-ghost:hover { background: var(--surface); }
/* Destructive actions are tinted red on BOTH breakpoints. Keyed on an
   explicit class, never `:last-child` — the last button on the edit and
   change-PIN screens is "Cancel", which must not read as destructive. */
.prof-ghost.prof-danger { color: var(--danger); font-weight: 700; }
.prof-actions { display: grid; gap: 10px; margin-top: 22px; }
/* Second question on a step, spaced off the first (was an inline style). */
.prof-field-gap { margin-top: 20px; }

/* Skip is a plain WORD, top-right: discoverable by eye, out of the
   thumb's easy zone so it is never the accidental tap. Never an X —
   novice users don't reliably read bare icons. */
/* No longer absolute. It never actually was on desktop: the panel's own
   `.prof-panel > :not(.prof-pt) { position: relative }` (specificity
   0,2,0) outranked this rule (0,1,0), so `right: 24px` was applied to a
   RELATIVE box and shifted Skip 24px to the LEFT, straight onto the
   progress bar. It lives in .prof-topbar now and needs no coordinates. */
.prof-skip {
  flex: 0 0 auto;
  min-height: 44px; padding: 8px 12px;
  font: inherit; font-size: 16px; font-weight: 600;
  color: var(--text-2); background: transparent; border: 0;
  border-radius: 10px; cursor: pointer; text-decoration: underline;
}
.prof-skip:hover { color: var(--text); background: var(--surface); }

.prof-err {
  margin: 12px 0 0; padding: 12px 14px; border-radius: 10px;
  font-size: 16px; line-height: 1.45; display: none;
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
  color: var(--text);
}
.prof-err.on { display: block; }
.prof-saved {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 14px; font-weight: 700; color: var(--prof-ok);
}

/* ── the settled Profile panel ── */
/* Centred identity on BOTH breakpoints — the picture is the control
   (tap it to change), so it wants to be the focal point, not a 72px
   thumbnail beside the name. */
.prof-id {
  display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: 10px; margin: 4px 0 22px;
}
.prof-id img, .prof-id .prof-blank {
  width: 84px; height: 84px; border-radius: 50%; object-fit: cover;
  background: var(--surface-2); flex: 0 0 auto;
}
.prof-id .prof-blank { display: grid; place-items: center; color: var(--text-2); font-size: 30px; font-weight: 700; }
/* The avatar is a button: tapping the picture changes the picture, so
   the settings list loses a whole row. The pencil badge is the
   affordance — it is decorative (aria-hidden); the button carries a
   real label. */
.prof-avedit {
  position: relative; padding: 0; border: 0; background: none;
  cursor: pointer; border-radius: 50%; flex: 0 0 auto;
  line-height: 0; /* no descender gap under the image */
}
.prof-avedit img, .prof-avedit .prof-blank { display: block; }
.prof-pencil {
  position: absolute; right: -2px; bottom: -2px;
  width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--cta); color: var(--cta-ink);
  border: 2px solid var(--bg-elev);
  transition: transform var(--dur-hover) ease;
}
.prof-pencil svg { width: 14px; height: 14px; }
.prof-avedit:hover .prof-pencil { transform: scale(1.08); }
.prof-avedit:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.prof-id h2 { margin: 0 0 2px; font-size: 24px; font-weight: 800; color: var(--text); }
.prof-id p { margin: 0; font-size: 16px; font-weight: 600; color: var(--prof-dim); }

/* ── membership badge (name → number → plan) ──
   The pill wears the tier's own colour, set inline from the very array
   the join/upgrade overlay uses, so the plan is the same colour
   wherever the member meets it. It starts neutral and is tinted a
   microtask later (see profile.js) — never a wrong colour first.
   The line under it says when the membership runs out the optimistic
   way: a real date while that is far off, "N days of access left" once
   it is close. Deliberately NOT a warning colour — the panel states the
   member's position, it doesn't chase them. */
.prof-plan { display: flex; flex-direction: column; align-items: center; gap: 5px; margin-top: 10px; }
.prof-plan-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 14px 5px 11px; border-radius: 999px;
  font-size: 15px; font-weight: 800; line-height: 1.3;
  background: var(--surface-2); color: var(--text);
  /* the tint arrives a frame later — ease it in rather than snap */
  transition: background 0.2s ease, color 0.2s ease;
}
/* The trial marker sits BESIDE the tier pill, never inside it: the pill
   wears the tier's own colour and that colour means the tier. Filled
   rather than outlined because it is the one thing on this panel with a
   clock on it, and --new-ink/--new-on is the project's settled pairing
   for a filled accent chip (Eon Yellow on dark, Eon Orange on cream,
   dark ink on both — white on #db8037 is 2.93:1). Tokens come from
   app.css, which loads before this file. */
.prof-plan-row {
  display: flex; align-items: center; justify-content: center;
  gap: 8px; flex-wrap: wrap;
}
.prof-plan-trial {
  display: inline-flex; align-items: center;
  padding: 4px 10px; border-radius: 999px;
  background: var(--new-ink); color: var(--new-on);
  font-size: 12px; font-weight: 800; letter-spacing: 0.04em;
  text-transform: uppercase; line-height: 1.3; white-space: nowrap;
}
.prof-crown { display: grid; place-items: center; line-height: 0; }
.prof-crown svg { display: block; width: 17px; height: 17px; }
/* .prof-id p is `element + class`, so it outranks a bare class here —
   scope the override or the badge line inherits the phone number's size. */
.prof-id .prof-plan-until { font-size: 14px; font-weight: 600; color: var(--prof-dim); }
/* Screen-reader only: the crown is decorative, so the pill needs a word
   to say what it is a pill OF. */
.prof-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.prof-rows { display: grid; gap: 2px; margin: 0 0 20px; }
.prof-row {
  /* min-width:0 — grid items default to `auto`, which means "at least as
     wide as my content", so a long name made the ROW wider than its
     track and pushed Change/chevron off the end instead of truncating. */
  min-width: 0;
  display: flex; align-items: center; gap: 14px;
  width: 100%; min-height: 56px; padding: 12px 14px;
  font: inherit; text-align: left; color: var(--text);
  background: transparent; border: 0; border-radius: 12px;
  border-bottom: 1px solid var(--line); cursor: pointer;
}
.prof-row:hover { background: var(--surface); }
/* A tight label column leaves the most room for the value; long values
   truncate with an ellipsis rather than wrapping or pushing the
   chevron off the row. */
.prof-row .k { font-size: 16px; font-weight: 600; color: var(--prof-dim); flex: 0 0 84px; }
.prof-row .v { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prof-row .v { font-size: 18px; font-weight: 700; color: var(--text); }
/* NB .unset, not .empty — app.css owns a global .empty
   empty-state block that would swallow this span. */
.prof-row .v.unset { font-weight: 500; color: var(--text-3); }
.prof-row .go { margin-left: auto; font-size: 15px; font-weight: 700; color: var(--cta); }

/* "2 of 5 done" nudge — endowed progress, the one framing with real
   evidence behind it (pre-filled progress ~doubled completion). */
.prof-nudge {
  display: grid; gap: 12px; margin: 0 0 22px; padding: 16px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--cta) 10%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--cta) 34%, transparent);
}
.prof-nudge p { margin: 0; font-size: 17px; line-height: 1.45; font-weight: 600; color: var(--text); }

/* ── phones: a bottom SHEET, not a full-screen takeover ──
   It rises from the bottom, stops short of the top so the library stays
   visible behind it (you can see what you're returning to, and the gap
   is an obvious tap-to-dismiss target), and the sheet's own bottom is
   the thumb zone — so the actions simply stick there. No viewport-fixed
   bar, no duplicated buttons. */
@keyframes prof-up {
  from { transform: translateY(100%); }
  to { transform: none; }
}
@media (max-width: 640px) {
  /* The sheet is ABSOLUTELY anchored to the bottom rather than relying
     on flex alignment: on the first open (before the panel has content
     and a settled height) flex alignment resolved against the wrong box
     on iOS and the sheet painted at the TOP of the screen, correcting
     itself only on a second open. Absolute bottom can't do that. */
  /* NB: no `display` on the bare .prof — setting it here would override
     the base `display: none` for ALL states, leaving a full-screen
     scrim in the page after close that swallows every tap. Only the
     .open state may set display. */
  .prof { padding: 0; overflow: hidden; }
  .prof.open { display: block; }
  /* No blur behind an opaque sheet on phones: backdrop-filter is the one
     effect no major modal library ships, and it is the prime suspect for
     stale/offset rasterisation on iOS. Desktop keeps it. */
  .prof-scrim { backdrop-filter: none; -webkit-backdrop-filter: none;
    background: color-mix(in srgb, #17101c 74%, transparent); }
  .prof-panel {
    /* FIXED, not absolute: production sheets (vaul, Ionic, Radix) pin
       the sheet to the viewport rather than to the overlay's box, so a
       momentarily-wrong overlay box can't drag the sheet with it. */
    position: fixed; left: 0; right: 0; bottom: 0;
    /* Opaque, and NO backdrop-filter here. A blurred backdrop on an
       element that is simultaneously animating `transform` is the
       combination iOS Safari rasterises badly — the layer paints stale
       or offset while hit-testing stays correct (visible buttons that
       do nothing). The scrim keeps the blur; the sheet does not need
       it, since it sits on a solid ground anyway. */
    background: var(--bg-elev);
    backdrop-filter: none; -webkit-backdrop-filter: none;
    /* own compositing layer, declared up front (helps, but is not
       load-bearing — iOS drops these hints under memory pressure) */
    will-change: transform;
    max-width: none; width: 100%;
    /* Height is capped TWO ways so the sheet can never eat the screen:
       a share of the viewport, AND a hard 84px of library that always
       stays visible above it (on a short phone 88% left barely a
       sliver). Measured in svh — the SMALL viewport, i.e. with the
       browser's URL bar showing — because vh/dvh are larger than what
       the member can actually see, which is what pushed the sheet up
       past the top edge on real phones. svh also means the sheet does
       not resize when the URL bar hides mid-scroll. */
    max-height: 78vh; /* fallback for browsers without svh */
    max-height: min(78svh, calc(100svh - 84px));
    overflow-y: auto;
    -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    border-radius: 22px 22px 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
    padding: 10px 16px 0;
    /* A TRANSITION driven by a class added on the next frame — not a CSS
       animation that fires the instant the element is displayed. The
       animation could start against a box iOS had not finished laying
       out, which is how the sheet ended up painted in the wrong place on
       the very first open. This is the pattern the sheet libraries use:
       mount → let layout settle → then move. */
    transform: translate3d(0, 100%, 0);
    transition: transform 0.34s var(--ease-move);
    animation: none;
  }
  .prof.in .prof-panel { transform: translate3d(0, 0, 0); }
  /* grab-handle: the standard "this sheet pulls down" affordance */
  .prof-panel::before {
    content: ""; display: block; width: 40px; height: 4px; margin: 0 auto 8px;
    border-radius: 2px; background: var(--line-strong);
  }
  /* Skip sticks with the sheet instead of scrolling away — on a long
     step (the age grid) it must stay one tap away. It gets its OWN row
     here: at 390px the step eyebrow runs the full width, so overlaying
     it would collide. */
  .prof-topbar { position: sticky; top: 0; z-index: 5; margin-bottom: 8px; }
  .prof-skip { margin-left: auto; }
  .prof-h { font-size: 26px; letter-spacing: -0.02em; }
  .prof-sub { font-size: 17px; margin-bottom: 18px; }
  .prof-avatars { gap: 10px; }

  /* ── Apple-style sheet furniture (HIG grouped list) ──
     Identity centred at the top of the sheet, settings as ONE inset
     rounded group with hairline separators and a value+chevron per row,
     actions as a separate group below. Same grammar as an iOS Settings
     screen, which is the pattern this audience already knows. */
  .prof-id {
    flex-direction: column; text-align: center; gap: 10px;
    margin: 6px 0 22px;
  }
  .prof-id img, .prof-id .prof-blank { width: 84px; height: 84px; }
  /* 84px circle → keep the badge clear of the rim, and give it a real
     44px tap area without making the dot itself huge */
  .prof-pencil { right: 0; bottom: 0; width: 30px; height: 30px; }
  .prof-pencil svg { width: 15px; height: 15px; }
  .prof-id h2 { font-size: 22px; letter-spacing: -0.02em; }
  .prof-id p { font-size: 15px; }
  .prof-plan { margin-top: 9px; }
  .prof-plan-pill { font-size: 14px; padding: 5px 13px 5px 10px; }
  .prof-plan-trial { font-size: 11px; padding: 3px 9px; }
  .prof-crown svg { width: 16px; height: 16px; }
  .prof-id .prof-plan-until { font-size: 13.5px; }

  .prof-rows {
    gap: 0; margin-bottom: 18px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 14px; overflow: hidden;
  }
  .prof-row {
    min-height: 52px; padding: 12px 14px; gap: 10px;
    border-bottom: 1px solid var(--line); border-radius: 0;
  }
  .prof-row:last-child { border-bottom: 0; }
  .prof-row:active { background: var(--surface); }
  /* Label column then value, both LEFT aligned — values ragged against
     the right edge read as unrelated to their labels. A fixed label
     width lines every value up in one column. */
  .prof-row .k { font-size: 16px; font-weight: 500; flex: 0 0 74px; }
  .prof-row .v {
    margin-left: 0; text-align: left; flex: 1 1 auto; min-width: 0;
    font-size: 16px; font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .prof-row .go { margin-left: auto; }
  /* the word stays (icons alone read as decoration to novice users) but
     it shrinks to a hint, with the platform chevron after it */
  .prof-row .go { margin-left: 0; font-size: 13px; font-weight: 600; opacity: .75; flex: 0 0 auto; }
  .prof-row::after {
    content: ""; flex: 0 0 auto; width: 7px; height: 7px; margin-left: 2px;
    border-right: 2px solid var(--text-3); border-bottom: 2px solid var(--text-3);
    transform: rotate(-45deg); opacity: .8;
  }

  .prof-actions { gap: 10px; }
  .prof-actions .prof-ghost {
    background: var(--bg-elev); border-color: var(--line);
    border-radius: 14px; min-height: 50px;
  }
  .prof-go { border-radius: 14px; }
  /* the one set of buttons, docked to the bottom of the sheet */
  .prof-actions {
    position: sticky; bottom: 0; z-index: 4;
    margin-top: 22px; padding: 12px 0 calc(12px + env(safe-area-inset-bottom));
    background: var(--bg-elev); border-top: 1px solid var(--line);
  }
}

/* ── desktop: stop rendering a phone column on a wide screen ──
   The card was a single measure of stacked full-width controls whatever
   the viewport, so the last step (seven ages + four genders, each on its
   own row) came out 956px tall against a 757px window: the question you
   were being asked scrolled off the top, and Continue sat below the fold
   with nothing to say it was there.
   Two changes fix it and neither touches the phone layout. The choice
   grids use the width they already have — the options are short words,
   not sentences, and one per row was never a reading decision. And the
   card is capped to the window with its own body scrolling inside, so
   the buttons stay on screen no matter how long a step gets. */
@media (min-width: 641px) {
  /* 40/60 of chrome around the card cost more than it bought once the
     card started capping itself to the window — every pixel of it came
     straight off the question. 32/32 still reads as a floating dialog. */
  .prof { padding: 32px 20px 32px; }
  .prof-panel {
    display: flex; flex-direction: column;
    max-height: calc(100vh - 64px);
    max-height: calc(100svh - 64px);
  }
  /* min-height:0 or a flex item refuses to shrink below its content and
     the scroll never happens — the whole card just overflows instead. */
  .prof-body {
    min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    /* let the scrollbar sit at the card's edge, not inset in the padding */
    margin: 0 -28px; padding: 0 28px;
  }
  .prof-actions {
    position: sticky; bottom: 0; z-index: 4;
    padding-top: 20px; padding-bottom: 6px;
    /* Solid under the buttons, fading up: content has to READ as passing
       beneath the bar. A short or weak fade makes the same pixels look
       like a card that has been cut off, which is the one thing a
       scrolling region must never look like. */
    background: linear-gradient(to bottom,
      transparent 0, var(--bg-elev) 40%, var(--bg-elev) 100%);
  }
  /* Age: 8 short labels — 3 across is 3 rows instead of 4.
     Gender: 4 labels that were one per row for no reason at all. */
  .prof-opts.two { grid-template-columns: repeat(3, 1fr); }
  /* (gender's flex row is in the base rules — it helps at every width) */
  /* Three to a row leaves ~150px of text width, and 18px was enough to
     wrap "Rather not say" onto two lines — which made one option taller
     than its neighbours and broke the grid's rhythm. */
  .prof-opt { font-size: 16px; min-height: 52px; padding: 10px 14px; gap: 8px; }
  .prof-opt[aria-checked="true"] { padding: 9px 13px; }
  .prof-opt .tick { width: 22px; height: 22px; font-size: 14px; }

  /* ── the buttons, side by side ──
     Two full-width buttons stacked cost 144px of a capped card, and on
     the last step that was the difference between seeing the second
     question and never knowing it was there. A dialog on a wide screen
     puts its pair on one line anyway: secondary left, primary right,
     which is also the order a mouse expects to travel.
     Wizard only — the settled panel's actions are Change PIN and Sign
     out, which are unrelated and must not read as a pair.
     grid-auto-flow rather than an explicit two-column template, so the
     first step — which has a Continue and no Back — gets one full-width
     button without needing a :has() to notice. */
  .prof.wizard .prof-actions {
    grid-auto-flow: column; grid-auto-columns: 1fr; align-items: center;
  }
  .prof.wizard .prof-actions .prof-go { order: 2; }
  .prof.wizard .prof-actions .prof-ghost { order: 1; }

  /* Every one of these is a few pixels off the header so the questions
     get them instead. */
  .prof-sub { margin-bottom: 12px; }
  .prof-why { margin-bottom: 12px; }
  .prof-field-gap { margin-top: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .prof-panel, .prof::before { animation: none; }
  .prof-av, .prof-opt, .prof-av .tick, .prof-opt .tick { transition: none; }
}
