/* ===========================
   EyeScore — dial and domain pages
   The whole of the portfolio's own styling. Sits on top of base.css, which
   supplies the reset, the tokens and the pixer face this file references.
   =========================== */

/* ═══════════════════════════════════════════════
   EYESCORE — Iris domain wheel + evidence scoring
   Palette derived from the Eyedle eye SVG:
   iris amber #d4913a · brand cyan #3c9ebf · teal #14b8a6
   ═══════════════════════════════════════════════ */
body { background: #050914; }

.pf {
  --mono: 'IBM Plex Mono', ui-monospace, monospace;
  /* Height of everything above and below the dial. Read by .pf-stage to work
     out how big the dial may be; declared here so a breakpoint that tightens
     the masthead or the footer can correct it in one place. */
  --chrome: calc(256px * var(--ui));
  position: relative;
  min-height: 100vh;
  /* svh, not vh: on a phone 100vh is the viewport with the browser toolbar
     *hidden*, so a page that exactly fills it is ~90px taller than what you
     can actually see, and every screen picks up a phantom scroll. svh is the
     toolbar-visible height, and unlike dvh it does not change as you scroll,
     so the dial cannot resize mid-gesture. */
  min-height: 100svh;
  padding: 1.8rem 1.5rem 2rem;
  /* viewport-fit=cover hands us the full screen including the rounded corners,
     the status bar and the home indicator; these keep content out of them.
     The top inset matters as much as the others: without it the masthead sits
     under the notch or Dynamic Island the moment the page is tall enough that
     the centring stops leaving a margin there by luck. */
  padding-top:   max(1.8rem, env(safe-area-inset-top));
  padding-left:  max(1.5rem, env(safe-area-inset-left));
  padding-right: max(1.5rem, env(safe-area-inset-right));
  padding-bottom: calc(2rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  /* "safe" so a short viewport falls back to flex-start instead of
     overflowing past the top where it can't be scrolled to */
  justify-content: safe center;
}

/* ── Atmosphere ──────────────────────────────────
   A graticule rather than a mood. Drifting blurred orbs are decoration
   that says nothing about the subject and compete with the dial's own
   glow; these spokes are set to the dial's per-point angle (fed in from
   JS) and centred on it, so the page reads as the instrument's field
   extending past its rim. Masked to fade in and out, so it never draws a
   hard edge and never crowds the dial itself. */
.pf-field {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  --fx: 50%; --fy: 46%; --spoke: 6.912deg;
  background: repeating-conic-gradient(from 0deg at var(--fx) var(--fy),
    rgba(126,178,208,0.055) 0deg 0.16deg,
    transparent 0.16deg var(--spoke));
  -webkit-mask-image: radial-gradient(circle at var(--fx) var(--fy),
    transparent 30%, #000 54%, transparent 84%);
  mask-image: radial-gradient(circle at var(--fx) var(--fy),
    transparent 30%, #000 54%, transparent 84%);
}
/* Settles the edges so the field doesn't just stop. */
.pf-vignette {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(ellipse at var(--fx, 50%) var(--fy, 46%),
    transparent 34%, rgba(2,5,12,0.72) 100%);
}

.pf-grain {
  position: fixed; inset: 0; pointer-events: none; z-index: 0; opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px;
}

/* ── Views ───────────────────────────────────── */
.pf-view {
  position: relative; z-index: 1;
  width: 100%;
  display: flex; flex-direction: column; align-items: center;
  transition: opacity .2s ease, transform .2s ease;
}
/* display:flex above would otherwise beat the [hidden] attribute's default
   display:none, leaving both views on screen at once. */
.pf-view[hidden] { display: none; }
.pf-view.is-out { opacity: 0; transform: scale(.97) translateY(-8px); }
.pf-view.is-in  { animation: pf-viewin .42s cubic-bezier(.22,1,.36,1) both; }
@keyframes pf-viewin { from { opacity: 0; transform: translateY(20px); } }

/* ── Masthead ────────────────────────────────── */
.pf-head { text-align: center; max-width: calc(620px * var(--ui)); }

.pf-title {
  font-family: 'pixer', Georgia, serif;
  font-size: clamp(2.1rem, 5.2vw, 3.5rem);
  font-weight: normal;
  letter-spacing: 4px;
  /* The body's 1.6 is set for reading and puts ~34px of dead air around a 56px
     display face. Pulling it to 1.08 lifts the whole masthead and hands the
     space to the dial. */
  line-height: 1.08;
  margin: 0;
  color: #eaf4ff;
  animation: pf-up .8s cubic-bezier(.22,1,.36,1) both;
}

/* The shared cursor is 0.85em, which resolves to ~30px against this title
   and renders the underscore as a slab; 0.4em brings it back to a caret. */
.pf-title .tw-cursor {
  color: #9fdcf2;
  font-size: 0.4em;
  letter-spacing: 0;
  margin-left: 0.06em;
  vertical-align: baseline;
}

.pf-sub {
  font-size: 0.95rem; line-height: 1.6;
  color: rgba(255,255,255,0.8);
  margin-top: 0.45rem; text-wrap: balance;
  animation: pf-up .8s .1s cubic-bezier(.22,1,.36,1) both;
}
.pf-sub strong { color: rgba(255,255,255,0.88); font-weight: 500; }

/* ── Wheel ───────────────────────────────────── */
.pf-stage {
  position: relative;                  /* positioning context for .pf-slot */
  /* The dial takes whatever height the rest of the page doesn't need.
     ~256px is the measured chrome above and below it (padding, title,
     subheading, footer), so this keeps the page inside the fold at any
     height while letting the dial grow as large as the space allows. Tighten
     any of that chrome and this number has to come down with it, or the dial
     simply leaves the reclaimed space empty. */
  /* 88vw rather than 100% is load-bearing: the tool orbit rides at r=0.59 of
     this box, so the 12% left over is the lane those buttons sit in. Widen
     the dial and they leave the screen.
     The floor stops a short phone from shrinking the hero to a token — below
     it the page is allowed to scroll instead, which costs a swipe to read the
     footer rather than making the instrument itself unreadable. */
  /* Fallback first: a browser without svh drops the whole declaration below
     as invalid, and an unsized stage collapses the dial entirely. */
  width: min(88vw, calc(100vh - var(--chrome)), calc(600px * var(--ui)));
  width: max(228px, min(88vw, calc(100svh - var(--chrome)), calc(600px * var(--ui))));
  aspect-ratio: 1;
  /* The bottom margin is the tool bar's lane. The bar is positioned out of
     flow so it cannot stretch this square box, which means its room has to be
     reserved here by hand. */
  margin: 0.65rem 0 3.9rem;
  animation: pf-up 1s .18s cubic-bezier(.22,1,.36,1) both;
}
.pf-wheel { width: 100%; height: 100%; overflow: visible; display: block; }

.pf-wedge {
  cursor: pointer;
  /* view-box + wheel centre so the entrance bloom scales from the pupil
     outward, and the hover offset translates along the wedge bisector */
  transform-box: view-box;
  transform-origin: 200px 200px;
  transition: transform .34s cubic-bezier(.22,1,.36,1);
}
/* Phase 1 of the entrance: the wedges pop in one after another. Visible by
   default, so the bloom only layers on when motion is welcome and a skipped
   animation can never hide them. Phase 2 — the levels rising inside them —
   is driven in JS; see runEntrance. */
@media (prefers-reduced-motion: no-preference) {
  .pf-wedge {
    /* .22,1,.36,1 put 76% of the travel into the first quarter of the run —
       ~6 frames at 60Hz, too few to read as fluid. This spreads the same
       movement across roughly triple the frames without feeling slower. */
    animation: pf-bloom .7s cubic-bezier(.34,.78,.4,1) backwards;
    will-change: transform, opacity;
  }
}
@keyframes pf-bloom { from { opacity: 0; transform: scale(0.72); } }

/* After the first run, the entrance is switched off so coming back from a
   domain only plays the view's own fade-in and the dial is simply there.
   The id keeps this above the plain-class rules, including the bloom
   declared inside the reduced-motion media query. */
#view-wheel.is-settled .pf-wedge,
#view-wheel.is-settled .pf-pulse circle,
#view-wheel.is-settled .pf-stage,
#view-wheel.is-settled .pf-head,
#view-wheel.is-settled .pf-title,
#view-wheel.is-settled .pf-sub,
#view-wheel.is-settled .pf-foot,
#view-wheel.is-settled .pf-slot { animation: none; }
/* Holding a compositor layer forever costs memory for no benefit. */
#view-wheel.is-settled .pf-wedge { will-change: auto; }

/* A filtered child inside a group being scaled has to be re-rasterised
   every frame, so the completion glow waits for the wedges to stop moving.
   Gated on is-filled — set the moment the levels land — rather than on
   is-settled, which also waits out the rim pulse and held the glow back
   more than a second longer than the rasterising ever needed. */
#view-wheel:not(.is-filled) .pf-seg.is-complete .pf-fill { filter: none; }

/* Inert only while the dial is still drawing itself. Gated on is-live (the
   last wedge landing) rather than is-settled, which also waits out the rim
   pulse and held the dial unusable for over a second too long. JS guards
   the handlers too, since pointer-events cannot stop a keyboard activation. */
#view-wheel:not(.is-live) .pf-seg { pointer-events: none; }
#view-wheel:not(.is-live) .pf-wedge { cursor: default; }

/* Track = the domain's full allocation. Fill = points earned so far. */
/* Kept high enough that an empty wheel still reads as nine coloured
   domains rather than a grey ring — the fill carries the progress. */
.pf-track {
  fill-opacity: 0.3;
  stroke: currentColor; stroke-width: 0;
  transition: fill-opacity .28s ease;
}
.pf-fill { fill-opacity: 0.9; transition: fill-opacity .28s ease, filter .5s ease; }

.pf-edge {
  stroke-width: 2.2;
  stroke-opacity: 0.45;
  /* butt, not round: a round cap adds half the stroke width beyond each end of
     the arc, which pushed the line out past the wedge and into the gaps
     between segments. butt ends it exactly on the boundary at any width. */
  stroke-linecap: butt;
  transition: stroke-opacity .28s ease, stroke-width .28s ease;
}
.pf-seg.is-active .pf-edge { stroke-opacity: 0.9; }
.pf-seg.is-dim .pf-edge { stroke-opacity: 0.12; }

/* ── Capped domain ───────────────────────────────
   No badge, line or overlay — the segment simply becomes luminous. A full
   wedge goes to solid colour and gains a soft halo in its own hue, so
   "every point earned" is carried by the segment itself rather than by
   anything laid on top of it. */
.pf-seg.is-complete .pf-fill {
  fill-opacity: 1;
  filter: drop-shadow(0 0 5px var(--dc));
}
.pf-seg.is-complete .pf-track { fill-opacity: 0.44; }
/* A capped domain gets a bright bar across its outer edge — heavier than the
   level line on a partial one, so "topped out" reads at a glance rather than
   only through the luminance change. Struck in a lifted version of the
   domain's own hue rather than white, so the cap still belongs to its segment.
   The CSS stroke beats the presentation attribute set in JS. */
.pf-seg.is-complete .pf-edge {
  /* Fallback for engines without relative colour syntax. */
  stroke: var(--dc);
  /* Mixing toward white in sRGB drags the chroma out with it, which is what
     made these read as grey pastels. Lifting lightness in oklch while pushing
     chroma up instead keeps the hue intact — a brighter fork of the segment's
     own colour rather than a washed version of it. */
  stroke: oklch(from var(--dc) calc(l + 0.17) calc(c * 1.3) h);
  stroke-width: 3.4;
  stroke-opacity: 1;
}

/* The seal: a wave sheds off the rim across just that domain's arc. Same
   vocabulary as the load pulse, but tinted to the domain rather than white
   — a silver line running over the segment read as an applied flourish,
   whereas the dial breathing out in the domain's own colour belongs to it.
   non-scaling-stroke keeps the line from thickening as it expands. */
.pf-seal {
  fill: none;
  stroke-width: 4.5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  transform-box: view-box;
  transform-origin: 200px 200px;
  opacity: 0;
  animation: pf-seal 1.15s cubic-bezier(.16,.75,.3,1) forwards;
}
@keyframes pf-seal {
  0%   { opacity: 0;    transform: scale(1); }
  16%  { opacity: 0.62; }
  100% { opacity: 0;    transform: scale(1.13); }
}
@media (prefers-reduced-motion: reduce) {
  .pf .pf-seal { display: none; }
}
/* Brief swell as the wave lands, settling back to the luminous rest state. */
.pf-seg.is-maxed .pf-fill {
  fill-opacity: 1;
  filter: drop-shadow(0 0 11px var(--dc));
}

/* Pulse on a wedge whose score just moved, so the eye is drawn to the one
   that changed rather than having to spot it. */
.pf-seg.is-gain .pf-fill { fill-opacity: 1; }
.pf-seg.is-gain .pf-edge { stroke-opacity: 1; stroke-width: 2.4; }
.pf-seg.is-loss .pf-fill { fill-opacity: 0.62; }
.pf-seg.is-loss .pf-edge { stroke-opacity: 0.75; stroke-width: 2.4; }
.pf-seg.is-gain .pf-fill,
.pf-seg.is-gain .pf-edge,
.pf-seg.is-loss .pf-fill,
.pf-seg.is-loss .pf-edge { transition: fill-opacity .3s ease, stroke-opacity .3s ease, stroke-width .3s ease; }

/* ── Rim pulse ───────────────────────────────────
   Two rings shed from the dial's edge as the last wedge lands. No fill
   mode, so the base opacity:0 keeps them invisible both before the delay
   and after they finish — they exist only for their moment.
   non-scaling-stroke stops the outward scale from thickening the line as
   it grows, which would fight the fade. */
.pf-pulse circle {
  fill: none;
  stroke: rgba(163,212,236,0.55);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  transform-box: view-box;
  transform-origin: 200px 200px;
  opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .pf-pulse circle { animation: pf-pulse 1.5s cubic-bezier(.16,.75,.3,1); }
}
/* Capped at 1.05: from r=188 that reaches ~197, which keeps the wave clear of
   the tool arc orbiting just beyond it. It used to run to 1.13 (~212) and
   passed straight through the buttons. */
@keyframes pf-pulse {
  0%   { opacity: 0.5;  transform: scale(1); }
  55%  { opacity: 0.2; }
  100% { opacity: 0;    transform: scale(1.05); }
}
/* Purely decorative, so it simply doesn't exist when motion is unwelcome —
   the blanket rule below would otherwise flash it for 0.01ms. */
@media (prefers-reduced-motion: reduce) {
  .pf .pf-pulse { display: none; }
}

/* ── Delta indicator ─────────────────────────────
   Rises above the score inside the pupil when the total moves. Muted sage
   and dusty rose rather than signal green/red: it needs to register as
   direction without shouting over the dial's own palette. */
.pf-delta {
  transform-box: fill-box;
  transform-origin: center;
  animation: pf-delta 1.5s cubic-bezier(.22,1,.36,1) forwards;
}
.pf-delta-arrow {
  fill: none; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}
.pf-delta-num {
  font-family: var(--mono);
  font-size: 16.5px; font-weight: 500;
  text-anchor: middle; letter-spacing: 0.02em;
}
.pf-delta.is-up   .pf-delta-arrow { stroke: #8ecfae; }
.pf-delta.is-up   .pf-delta-num   { fill:   #8ecfae; }
.pf-delta.is-down .pf-delta-arrow { stroke: #d9a0a0; }
.pf-delta.is-down .pf-delta-num   { fill:   #d9a0a0; }
@keyframes pf-delta {
  0%   { opacity: 0; transform: translateY(7px); }
  20%  { opacity: 1; transform: translateY(0); }
  68%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-9px); }
}
@keyframes pf-delta-still { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  /* Needs to outrank the blanket `.pf * { animation-duration: .01ms }`
     further down, which would otherwise fast-forward this straight to its
     opacity:0 end frame and swallow the feedback entirely. This is
     information, not decoration, so it still has to be readable — it just
     holds still instead of rising. */
  .pf .pf-delta { animation: pf-delta-still 1.5s linear forwards !important; }
}


.pf-seg.is-active .pf-wedge { transform: translate(var(--ox), var(--oy)); }
.pf-seg.is-active .pf-track { fill-opacity: 0.3; }
.pf-seg.is-dim .pf-track { fill-opacity: 0.06; }
.pf-seg.is-dim .pf-fill  { fill-opacity: 0.22; }
/* A <g> is not natively focusable, so tabindex makes it so — and the UA
   then draws its focus ring around the group's bounding *box*, which on a
   wedge appears as a rectangle floating over the dial. Suppressed for
   pointer input only; keyboard focus keeps a clear indicator below. */
/* touch-callout: iOS pops a share/copy sheet over an element held down for
   half a second, which is exactly the gesture the dial uses. */
.pf-seg { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
/* Belt and braces with the preventDefault in the touchmove handler: some
   engines consult this again on each move once the gesture is under way. */
.pf-wheel.is-scrubbing { touch-action: none; }
.pf-seg:focus { outline: none; }
.pf-seg:focus-visible { outline: none; }
.pf-seg:focus-visible .pf-track {
  fill-opacity: 0.45;
  stroke: #ffffff;
  stroke-width: 1.6;
}
/* Dragging across the dial would otherwise start a text selection and
   paint the segment with the browser's highlight colour. */
.pf-wheel { -webkit-user-select: none; user-select: none; }

/* The pupil as an "open it" target, live only once a first tap has named a
   segment. Inert the rest of the time so it never swallows a tap aimed at the
   dial, and absent entirely on a mouse, where a single click already opens. */
.pf-open { pointer-events: none; }
.pf-wheel.has-pick .pf-open { pointer-events: auto; cursor: pointer; }

/* A named segment reads as held rather than hovered: the ring around the pupil
   brightens to say the middle is now a button. */
.pf-wheel.has-pick .pf-readout .pf-cap { fill: rgba(240,213,168,0.75); }

.pf-scan { animation: pf-spin 34s linear infinite; transform-origin: 200px 200px; }
@keyframes pf-spin { to { transform: rotate(360deg); } }

/* ── Pupil readout ───────────────────────────── */
.pf-readout { pointer-events: none; }
/* Matches the title face. pixer ships a single weight, so keep font-weight
   normal — a numeric weight would synthesise a fake bold. */
.pf-val {
  font-family: 'pixer', Georgia, serif;
  font-size: 54px; font-weight: normal;
  fill: #fff; text-anchor: middle;
  font-variant-numeric: tabular-nums;
  letter-spacing: 2px;
}
.pf-val-sm { font-size: 40px; }
.pf-den { font-size: 26px; fill: rgba(255,255,255,0.72); }
.pf-cap {
  font-family: var(--mono);
  font-size: 10.5px; letter-spacing: 0.24em; text-transform: uppercase;
  text-anchor: middle; fill: rgba(255,255,255,0.72);
}
.pf-name {
  font-family: 'pixer', Georgia, serif;
  font-size: 16px; font-weight: normal;
  text-anchor: middle; fill: rgba(255,255,255,0.92);
  letter-spacing: 1.4px;
}
.pf-fade { transition: opacity .2s ease; }

/* ── Help trigger ────────────────────────────── */
.pf-help {
  display: inline-flex; align-items: center; justify-content: center;
  width: calc(17px * var(--ui)); height: calc(17px * var(--ui)); margin-left: .45rem;
  vertical-align: 1px;
  border-radius: 50%; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.22);
  background: rgba(255,255,255,0.05);
  color: rgba(255,255,255,0.8);
  font: 600 .62rem/1 var(--font, 'Inter', system-ui, sans-serif);
  padding: 0;
  transition: background .2s, border-color .2s, color .2s, transform .2s;
}
.pf-help:hover {
  background: rgba(212,145,58,0.18);
  border-color: rgba(212,145,58,0.6);
  color: #f0d5a8;
  transform: scale(1.15);
}
.pf-help:focus-visible {
  outline: 2px solid rgba(212,145,58,0.7); outline-offset: 2px;
}

/* ── About dialog ────────────────────────────── */
.pf-modal {
  /* Fills the viewport so any click landing outside the card is a
     click on the dialog itself, which is what dismisses it. */
  width: 100%; height: 100%;
  max-width: 100vw; max-height: 100vh;
  max-height: 100dvh;   /* dvh here: a dialog may be opened either side of a
                           toolbar hide, and it is not animating a size */
  margin: 0; padding: 1.5rem;
  padding-top:    max(1.5rem, env(safe-area-inset-top));
  padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
  border: none; background: transparent; overflow: auto;
  place-items: center;
}
.pf-modal[open] { display: grid; }
.pf-modal::backdrop {
  background: rgba(3,6,14,0.6);
  -webkit-backdrop-filter: blur(3px) saturate(1.1);
  backdrop-filter: blur(3px) saturate(1.1);
  opacity: 0;
  transition: opacity .3s ease, backdrop-filter .3s ease, -webkit-backdrop-filter .3s ease;
}
/* Class-driven rather than [open]-driven: the backdrop is created already
   matching [open], so there'd be no start value to transition from. */
.pf-modal.is-open::backdrop {
  opacity: 1;
  -webkit-backdrop-filter: blur(16px) saturate(1.35);
  backdrop-filter: blur(16px) saturate(1.35);
}

.pf-modal-card {
  position: relative;
  display: flex; flex-direction: column;
  width: min(100%, calc(540px * var(--ui)));
  /* 100% of the dialog's content box, which is the viewport minus its own
     padding — including the safe-area insets, which a hard-coded 3rem could
     not know about. The dialog is the grid container, so this is exact. */
  max-height: 100%;
  /* The card is no longer the scroller. It was both rounded and scrolling, so
     the thumb ran the full height of a box whose corners curve away from it —
     and the close button, absolutely positioned inside a scrolling box, rode
     the content straight out of view. */
  overflow: hidden;
  padding: 0;
  border-radius: 22px;
  background: rgba(11,18,34,0.78);
  border: 1px solid rgba(255,255,255,0.1);
  -webkit-backdrop-filter: blur(34px) saturate(1.6);
  backdrop-filter: blur(34px) saturate(1.6);
  box-shadow:
    0 24px 70px rgba(0,0,0,0.6),
    inset 0 1px 0 rgba(255,255,255,0.09);
  opacity: 0;
  transform: translateY(16px) scale(.96);
  transition: opacity .32s cubic-bezier(.22,1,.36,1),
              transform .32s cubic-bezier(.22,1,.36,1);
}
/* Focus target only — it's a container, so no ring. */
.pf-modal-card:focus { outline: none; }
.pf-modal.is-open .pf-modal-card { opacity: 1; transform: none; }

/* Header stays put; only what is below it moves. That keeps the panel's title
   and its close button on screen through a long list, and starts the scrollbar
   below the card's top corners rather than inside their curve. */
.pf-modal-head { flex: 0 0 auto; padding: 2rem 1.9rem .2rem; }
.pf-modal-scroll {
  flex: 1 1 auto;
  min-height: 0;                       /* without this a flex child will not scroll */
  overflow-y: auto;
  overscroll-behavior: contain;        /* stops the page scrolling once the list ends */
  padding: 0 1.9rem 1.7rem;
}
/* Holds the thumb off the card's bottom curve, the one corner the track still
   reaches now that the header covers the top two. */
.pf-modal-scroll::-webkit-scrollbar-track { margin-bottom: 20px; }

.pf-modal-close {
  position: absolute; top: .95rem; right: .95rem;
  z-index: 2;                          /* above the scrolling content */
  width: calc(30px * var(--ui)); height: calc(30px * var(--ui)); border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.1);
  color: rgba(255,255,255,0.8);
  transition: background .18s, color .18s, border-color .18s, transform .18s;
}
.pf-modal-close:hover {
  background: rgba(255,255,255,0.11);
  color: #fff; border-color: rgba(255,255,255,0.25);
}
.pf-modal-close:active { transform: scale(.9); }

.pf-modal-title {
  font-family: 'pixer', Georgia, serif;
  font-weight: normal; letter-spacing: 2px;
  font-size: 1.35rem; color: #f0d5a8;
  margin: 0 2.2rem .8rem 0;
}
.pf-modal-lead {
  font-size: 1rem; line-height: 1.65;
  color: rgba(255,255,255,0.88);
}
.pf-modal-list {
  list-style: none; margin: 1.15rem 0 0;
  display: flex; flex-direction: column; gap: .8rem;
}
.pf-modal-list li {
  font-size: .88rem; line-height: 1.62;
  color: rgba(255,255,255,0.8);
  padding-left: 1.05rem; position: relative;
}
.pf-modal-list li::before {
  content: ''; position: absolute; left: 0; top: .6em;
  width: 5px; height: 5px; border-radius: 50%;
  background: rgba(212,145,58,0.75);
  box-shadow: 0 0 8px rgba(212,145,58,0.5);
}
.pf-modal-list strong { color: rgba(255,255,255,0.88); font-weight: 600; }
.pf-modal-foot {
  margin-top: 1.3rem; padding-top: 1.1rem;
  border-top: 1px solid rgba(255,255,255,0.07);
  font-size: .86rem; line-height: 1.65;
  color: rgba(255,255,255,0.72);
}
.pf-modal-foot a { color: rgba(60,158,191,0.95); text-decoration: none; }
.pf-modal-foot a:hover { text-decoration: underline; }

/* ── Tool bar ────────────────────────────────────
   A row of buttons in the lane directly beneath the dial. This used to be an
   arc riding the dial's top-right quadrant, which read well on a wide screen
   and badly everywhere else — on a phone the dial nearly fills the width, so
   the arc had nowhere to go. Rather than keep two layouts for one toolbar,
   the bar is now the layout at every size; sideways, where there is width to
   spare and no height, it stands up beside the dial instead (see below).

   Positioned out of flow so it cannot stretch the square stage, which means
   the room it occupies is reserved by hand on .pf-stage's margin-bottom. */
.pf-tools {
  position: absolute;
  inset: 100% 0 auto 0;                /* the lane directly beneath the dial */
  z-index: 6;
  pointer-events: none;                /* only the buttons are hit targets */
  display: flex; justify-content: center; align-items: center;
  gap: .55rem; padding-top: .95rem;
}
/* In DOM order, which is the order they once ran clockwise around the arc. */
.pf-slot { pointer-events: auto; }
.pf-slot-4, .pf-slot-5 { display: none; }

/* Both the feedback button and the account button need a backend, so the bar
   is either three long or five — there is no four. */
.pf-tools.has-fb .pf-slot-4,
.pf-tools.has-fb .pf-slot-5 { display: block; }
.pf-tool {
  width: calc(40px * var(--ui)); height: calc(40px * var(--ui)); border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.1);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  color: rgba(255,255,255,0.8);
  transition: background .2s, border-color .2s, color .2s, transform .2s;
}
/* Picks up the dial's blue family — the tint from Presentations (#4aaed1),
   the icon from the brightest of them, Publications (#5fc6ea). */
.pf-tool:hover {
  background: rgba(74,174,209,0.16);
  border-color: rgba(74,174,209,0.55);
  color: #5fc6ea;
  transform: translateY(-2px);
}
.pf-tool:active { transform: translateY(0) scale(.94); }

/* ── The toolbar's entrance ──────────────────────
   Runs with the dial, not after it. The first button leaves as the first
   wedge blooms and the last lands just as the rim pulse sheds, so the two
   read as one arrival spread over the whole entrance rather than a quick
   flourish tacked on the end.

   No class gates this. An earlier version added one partway through the load,
   which meant the buttons sat there fully visible and then vanished to replay
   themselves the moment it landed. Declaring the animation up front with
   `backwards` instead holds every button at its start frame from the very
   first paint, so nothing is ever seen before its turn. It also survives JS
   failing entirely — the buttons still arrive.

   The direction follows whichever layout is in force. On the arc each button
   rises outward along its own radius, so the group unfurls from the rim; laid
   out as a bar or a rail it is a simple lift, because a radial vector means
   nothing once the buttons are in a straight line.

   The three timing values are set from JS so they stay tied to the dial's own
   constants; these defaults are the same numbers, for the frames before that
   runs and for when it cannot. */
@media (prefers-reduced-motion: no-preference) {
  .pf-tools .pf-slot {
    animation: pf-tool-lift var(--tool-dur, 780ms) cubic-bezier(.34,.78,.4,1) backwards;
  }
  .pf-tools .pf-slot:nth-child(1) { animation-delay: var(--tool-at, 420ms); }
  .pf-tools .pf-slot:nth-child(2) { animation-delay: calc(var(--tool-at, 420ms) + 1 * var(--tool-step, 240ms)); }
  .pf-tools .pf-slot:nth-child(3) { animation-delay: calc(var(--tool-at, 420ms) + 2 * var(--tool-step, 240ms)); }
  .pf-tools .pf-slot:nth-child(4) { animation-delay: calc(var(--tool-at, 420ms) + 3 * var(--tool-step, 240ms)); }
  .pf-tools .pf-slot:nth-child(5) { animation-delay: calc(var(--tool-at, 420ms) + 4 * var(--tool-step, 240ms)); }
}

/* The bar and the rail both place their buttons in normal flow, so one lift is
   all that is needed. A second keyframe used to travel each button outward
   along its own radius; it went with the arc. */
@keyframes pf-tool-lift {
  from { opacity: 0; transform: translateY(11px); }
}
.pf-tool:focus-visible { outline: 2px solid rgba(95,198,234,0.7); outline-offset: 2px; }

/* ── To-do list ──────────────────────────────── */
.pf-todo-group { margin-top: 1.2rem; }

/* The heading is the section's only control, so it has to look like one
   without a hover to tell it: the chevron is the standing affordance, since a
   phone never sees the hover tint. */
.pf-todo-head {
  /* Centred, not baseline: a long domain name wraps to two lines on a phone,
     and baseline would pin "13 left" to the first of them while the chevron
     sat between the two. */
  display: flex; align-items: center; gap: .6rem;
  width: 100%; text-align: left; font: inherit;
  padding: .4rem .4rem .5rem; margin-bottom: .35rem;
  background: none; border: none;
  border-bottom: 1px solid rgba(255,255,255,0.07);
  border-radius: 7px 7px 0 0;
  cursor: pointer;
  transition: background .16s;
}
.pf-todo-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--tc); box-shadow: 0 0 9px var(--tc); flex-shrink: 0;
  align-self: center;
}
.pf-todo-name {
  font-size: .95rem; font-weight: 600; color: rgba(255,255,255,0.88);
  flex: 1; min-width: 0;
  transition: color .16s;
}
.pf-todo-left {
  font-family: var(--mono); font-size: .76rem;
  letter-spacing: .1em; text-transform: uppercase; color: var(--tc);
  white-space: nowrap;
}
.pf-todo-go {
  flex-shrink: 0; align-self: center;
  color: var(--tc); opacity: .8;
  font-size: 1.05rem; line-height: 1;
  transition: transform .16s, opacity .16s;
}
.pf-todo-go::before { content: '\203A'; }   /* › */
@media (hover: hover) {
  .pf-todo-head:hover { background: rgba(255,255,255,0.05); }
  .pf-todo-head:hover .pf-todo-name { color: #fff; }
  .pf-todo-head:hover .pf-todo-go { transform: translateX(3px); opacity: 1; }
}
.pf-todo-head:focus-visible { outline: 1px solid var(--tc); outline-offset: -1px; }

/* Inert from here down: what is left to claim is a reading list. */
.pf-todo-items { list-style: none; margin: 0; padding: 0; }
/* An untiered run straight after a tiered one — "Other discretionary evidence"
   landing under "Meetings attended" — would otherwise read as part of it. */
.pf-todo-items + .pf-todo-items { margin-top: .55rem; }
.pf-todo-item {
  display: flex; align-items: flex-start; gap: .7rem;
  padding: .3rem .4rem;
  color: rgba(255,255,255,0.8);
}
/* .69em puts it on the centre line of the label's first line box
   (.95rem x 1.45), so it stays put when a long label wraps. */
.pf-todo-bullet {
  width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0;
  background: rgba(255,255,255,0.28); margin-top: .69em;
}
.pf-todo-label { flex: 1; font-size: .95rem; line-height: 1.45; }
/* Disambiguates repeated item labels across tiers, e.g. the three
   "Oral presentation, first author" rows under Presentations — one heading
   over the run that shares it. */
.pf-todo-tier {
  font-family: var(--mono); font-size: .7rem;
  letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,0.62);
  padding: .5rem .4rem .1rem;
}
.pf-todo-tier:first-of-type { padding-top: .25rem; }
.pf-todo-pts {
  font-family: var(--mono); font-size: .87rem;
  color: var(--tc); opacity: .75; white-space: nowrap;
}
.pf-todo-done {
  text-align: center; padding: 1.6rem 0 .6rem;
  font-size: .91rem; color: rgba(255,255,255,0.8); line-height: 1.6;
}

/* ── Key dates ───────────────────────────────── */
.pf-dates-list { list-style: none; margin: 1.1rem 0 0; }
/* ── The route ───────────────────────────────────
   Read as a tube line: one continuous track with a station at each milestone.
   The row dividers are gone — the track is the connector now, and a rule
   crossing it every row broke the line into rungs. */
.pf-dates-list { --row-pad: .7rem; }
.pf-date { display: flex; gap: .9rem; padding: var(--row-pad) 0; }

.pf-date-rail {
  position: relative;                  /* stacking context for track vs station */
  display: flex; flex-direction: column; align-items: center;
  width: 14px; flex-shrink: 0;
  padding-top: 7px;                    /* fixed, so the pip centre is knowable */
}
/* Each row draws its own full-height segment, so consecutive rows butt up into
   one unbroken track without needing to know how tall their neighbours are. */
.pf-date-rail::before {
  content: ''; position: absolute; z-index: 0;
  left: 50%; margin-left: -1px;
  /* Pulled out past the row's own padding. The rail is a flex child and so
     stretches only to the content box, which left the track broken into rungs
     with a gap at every row boundary. */
  top: calc(var(--row-pad) * -1); bottom: calc(var(--row-pad) * -1);
  width: 2px;
  background: rgba(255,255,255,0.15);
}
/* Already behind you, so the track fades back with the labels. */
.pf-date.is-past .pf-date-rail::before { background: rgba(255,255,255,0.08); }
/* The line has to terminate at the first and last station rather than running
   off into empty space. 12.5px = 7px padding + half an 11px pip. */
.pf-dates-list > .pf-date:first-child .pf-date-rail::before { top: 12.5px; }
.pf-dates-list > .pf-date:last-child  .pf-date-rail::before {
  bottom: auto; height: calc(var(--row-pad) + 12.5px);
}

/* ── Stations ──────────────────────────────────── */
.pf-date-pip {
  position: relative; z-index: 1;      /* sits on the track, not under it */
  box-sizing: border-box;
  width: 11px; height: 11px; border-radius: 50%;
  background: #0b1220;                 /* matches the card, so it reads hollow */
  border: 2px solid rgba(255,255,255,0.3);
}
.pf-date.is-past .pf-date-pip {
  background: rgba(255,255,255,0.2); border-color: transparent;
}
/* Not yet placed in the calendar at all, so the station is drawn lighter than
   a dated one still to come — it is on the line, but not yet pinned to it. */
.pf-date.is-undated .pf-date-pip { border-color: rgba(255,255,255,0.16); }
.pf-date.is-undated .pf-date-when { color: rgba(255,255,255,0.62); font-style: italic; }
.pf-date.is-next .pf-date-pip {
  background: #f0d5a8; border-color: #f0d5a8;
  box-shadow: 0 0 10px rgba(240,213,168,0.8);
}

/* Where you are now: a single ring leaving the station every five seconds.
   The ping itself is quick — the expansion is over inside the first 30% — and
   the rest of the cycle is deliberately empty, so it registers as an
   occasional signal rather than a blinking light. */
.pf-date.is-next .pf-date-pip::before {
  content: ''; position: absolute; pointer-events: none;
  left: 50%; top: 50%; margin: -5.5px 0 0 -5.5px;
  width: 11px; height: 11px; border-radius: 50%;
  border: 1.5px solid rgba(240,213,168,0.7);
  /* linear here, with the real easing declared inside the keyframe: it has to
     govern the expansion alone, not be stretched across the quiet tail. */
  animation: pf-station 5s linear infinite;
}
@keyframes pf-station {
  0%   { opacity: .7; transform: scale(1);
         animation-timing-function: cubic-bezier(.18,.7,.32,1); }
  30%  { opacity: 0;  transform: scale(3.4); }
  100% { opacity: 0;  transform: scale(3.4); }
}
/* The dialogs live outside .pf, so the blanket reduced-motion rule further
   down never reaches them. Dropped entirely rather than fast-forwarded — the
   station still glows, so "you are here" survives without the movement. */
@media (prefers-reduced-motion: reduce) {
  .pf-date.is-next .pf-date-pip::before { display: none; }
}
.pf-date-body { flex: 1; min-width: 0; }
.pf-date-top {
  display: flex; align-items: baseline; gap: .55rem; flex-wrap: wrap;
}
.pf-date-label {
  font-size: .99rem; font-weight: 600; color: rgba(255,255,255,0.9);
}
.pf-date.is-past .pf-date-label { color: rgba(255,255,255,0.72); }
.pf-date-when {
  font-family: var(--mono); font-size: .78rem;
  letter-spacing: .08em; color: rgba(255,255,255,0.72);
}
.pf-date.is-next .pf-date-when { color: #f0d5a8; }
.pf-date-tbc {
  font-family: var(--mono); font-size: .66rem;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,0.62);
  border: 1px solid rgba(255,255,255,0.14);
  padding: .1rem .34rem; border-radius: 999px;
}
/* ── Disclosure ──────────────────────────────────
   Notes are closed at rest so the cycle reads as a timeline rather than a wall
   of prose. 0fr -> 1fr on a single grid row is what makes this animate to a
   content-driven height; max-height would need a guessed ceiling and stalls
   visibly once the real height is reached. */
.pf-date-note-wrap {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .34s cubic-bezier(.3,.8,.35,1),
              opacity .24s ease;
  opacity: 0;
}
.pf-date:focus-visible .pf-date-note-wrap,
.pf-date.is-open .pf-date-note-wrap { grid-template-rows: 1fr; opacity: 1; }
/* Hover-to-open only where hover is real. A touch browser fires :hover on tap
   and leaves it latched on the last row touched, so the row would open on the
   first tap, then refuse to close on the second: the tap toggles .is-open off
   while the stuck :hover holds it open, leaving the note visible and
   aria-expanded saying "false". Tap toggling is handled in JS. */
@media (hover: hover) {
  .pf-date:hover .pf-date-note-wrap { grid-template-rows: 1fr; opacity: 1; }
}

/* The clipper carries no padding of its own. Anything on this box survives the
   collapse — a 0fr track zeroes the content height, but padding is still part
   of the border box, and 4px of it leaked through on every closed row. */
.pf-date-note-clip {
  /* Both required: the row can only collapse if its content may be clipped,
     and a grid item's implicit min-height:auto would otherwise floor it. */
  overflow: hidden; min-height: 0;
}
.pf-date-note {
  font-size: .89rem; line-height: 1.58;
  color: rgba(255,255,255,0.72);
  padding-top: .25rem;
}
.pf-date.is-past .pf-date-note { color: rgba(255,255,255,0.62); }

/* The whole row is the hit target, so it should look like one. */
.pf-date { cursor: pointer; border-radius: 8px; }
.pf-date:focus { outline: none; }
.pf-date:focus-visible { outline: 2px solid rgba(95,198,234,0.6); outline-offset: 2px; }
@media (hover: hover) { .pf-date:hover .pf-date-label { color: #fff; } }

@media (prefers-reduced-motion: reduce) {
  .pf-date-note-wrap { transition: none; }
}

/* ── Useful links ────────────────────────────────
   Each group takes one of the dial's hue families, so the panel reads as part
   of the same object rather than a generic list of URLs. */
.pf-link-group { margin-bottom: 1.4rem; }
.pf-link-group:last-of-type { margin-bottom: .4rem; }
.pf-link-head {
  font-family: var(--mono); font-size: .68rem;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--lc); opacity: .85;
  display: flex; align-items: center; gap: .5rem;
  margin-bottom: .55rem;
}
/* A short rule running off the heading, picking up the group's hue. */
.pf-link-head::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--lc) 45%, transparent), transparent);
}

.pf-link {
  position: relative; display: flex; align-items: flex-start; gap: .85rem;
  padding: .7rem .85rem .7rem 1rem;
  margin-bottom: .4rem;
  border-radius: 11px;
  text-decoration: none;
  background: rgba(255,255,255,0.022);
  border: 1px solid rgba(255,255,255,0.055);
  transition: background .2s ease, border-color .2s ease, transform .2s cubic-bezier(.3,.8,.35,1);
}
/* The same gesture the evidence meters use: an accent that grows rather than a
   static rule appearing. Compositor-only, so it stays cheap. */
.pf-link::before {
  content: ''; position: absolute; left: 0; top: .55rem; bottom: .55rem;
  width: 3px; border-radius: 3px; background: var(--lc);
  transform: scaleY(0); transform-origin: center;
  transition: transform .26s cubic-bezier(.3,.8,.35,1);
}
.pf-link:hover, .pf-link:focus-visible {
  background: color-mix(in srgb, var(--lc) 7%, rgba(255,255,255,0.022));
  border-color: color-mix(in srgb, var(--lc) 34%, transparent);
  transform: translateX(2px);
}
.pf-link:hover::before, .pf-link:focus-visible::before { transform: scaleY(1); }
.pf-link:focus { outline: none; }
.pf-link:focus-visible { outline: 2px solid color-mix(in srgb, var(--lc) 70%, transparent); outline-offset: 2px; }

.pf-link-body { flex: 1; min-width: 0; }
.pf-link-title {
  display: block; font-size: .95rem; font-weight: 600;
  color: rgba(255,255,255,0.9);
}
.pf-link:hover .pf-link-title { color: #fff; }
.pf-link-note {
  display: block; font-size: .84rem; line-height: 1.5;
  color: rgba(255,255,255,0.72); margin-top: .15rem;
}
.pf-link-host {
  display: block; font-family: var(--mono); font-size: .68rem;
  letter-spacing: .04em; color: rgba(255,255,255,0.62);
  margin-top: .3rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-link-arrow {
  flex-shrink: 0; margin-top: .15rem;
  color: rgba(255,255,255,0.62);
  transition: color .2s ease, transform .26s cubic-bezier(.3,.8,.35,1);
}
.pf-link:hover .pf-link-arrow {
  color: var(--lc); transform: translate(2px, -2px);
}

@media (prefers-reduced-motion: reduce) {
  .pf-link, .pf-link::before, .pf-link-arrow { transition: none; }
}

/* ── Feedback form ───────────────────────────────
   Borrows the evidence controls' vocabulary rather than inventing a second
   one: the grade pills are the option buttons from a choice item, and the
   rating scale is the stepper's tabular numerals. */
.pf-fb-block { margin: 1.3rem 0; border: none; padding: 0; }
.pf-fb-legend {
  display: block; padding: 0;
  font-family: var(--mono); font-size: .7rem;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,0.72); margin-bottom: .55rem;
}

.pf-fb-anchor {
  font-size: .8rem; color: rgba(255,255,255,0.72);
  margin: -.25rem 0 .7rem;
}
.pf-fb-anchor b { color: rgba(255,255,255,0.8); font-weight: 600; }

.pf-fb-grades { display: flex; flex-wrap: wrap; gap: .4rem; }
/* Appears only once "Other" is chosen, so the field never sits there
   unexplained. */
.pf-fb-other { margin-top: .45rem; }
.pf-fb-grade {
  flex: 1 1 auto; min-width: 5.5rem;
  padding: .5rem .6rem; border-radius: 9px; cursor: pointer;
  font: inherit; font-size: .88rem; text-align: center;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.8);
  transition: background .16s, border-color .16s, color .16s;
}
.pf-fb-grade:hover { background: rgba(255,255,255,0.07); color: rgba(255,255,255,0.9); }
.pf-fb-grade.is-on {
  background: rgba(240,213,168,0.14);
  border-color: rgba(240,213,168,0.55);
  color: #f0d5a8;
}

.pf-fb-input {
  width: 100%; font: inherit; font-size: .92rem;
  padding: .6rem .7rem; border-radius: 9px;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.92);
  transition: border-color .16s, background .16s;
}
.pf-fb-input::placeholder { color: rgba(255,255,255,0.55); }
.pf-fb-input:focus {
  outline: none;
  border-color: rgba(95,198,234,0.55);
  background: rgba(255,255,255,0.05);
}
.pf-fb-area { resize: vertical; min-height: 4.6rem; line-height: 1.55; }

/* ── Rating scales ──────────────────────────────── */
.pf-fb-scale { margin-bottom: .9rem; }
.pf-fb-scale-label {
  display: flex; align-items: baseline; justify-content: space-between; gap: .8rem;
  font-size: .9rem; color: rgba(255,255,255,0.88); margin-bottom: .35rem;
}
.pf-fb-scale-value {
  font-family: var(--mono); font-size: .78rem;
  color: rgba(255,255,255,0.62); font-variant-numeric: tabular-nums;
}
.pf-fb-scale.is-set .pf-fb-scale-value { color: #f0d5a8; }
.pf-fb-scale-word { color: rgba(255,255,255,0.72); }
.pf-fb-dots { display: flex; gap: .35rem; }
.pf-fb-dot {
  flex: 1 1 0; height: calc(34px * var(--ui)); border-radius: 8px; cursor: pointer;
  font-family: var(--mono); font-size: .84rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.62);
  transition: background .15s, border-color .15s, color .15s, transform .15s;
}
.pf-fb-dot:hover { border-color: rgba(240,213,168,0.4); color: rgba(255,255,255,0.88); }
.pf-fb-dot:active { transform: scale(.94); }
/* Filled up to the chosen value rather than marking just the one, so the
   control reads as a level the way the domain meters do. */
.pf-fb-dot.is-lit {
  background: rgba(240,213,168,0.16);
  border-color: rgba(240,213,168,0.5);
  color: #f0d5a8;
}
.pf-fb-dot:focus-visible { outline: 2px solid rgba(95,198,234,0.7); outline-offset: 2px; }

.pf-fb-send {
  width: 100%; margin-top: .3rem;
  padding: .75rem 1rem; border-radius: 10px; cursor: pointer;
  font: inherit; font-size: .95rem; font-weight: 600;
  background: rgba(240,213,168,0.16);
  border: 1px solid rgba(240,213,168,0.5);
  color: #f0d5a8;
  transition: background .18s, border-color .18s, transform .18s, opacity .18s;
}
.pf-fb-send:hover:not(:disabled) { background: rgba(240,213,168,0.24); }
.pf-fb-send:active:not(:disabled) { transform: scale(.985); }
.pf-fb-send:disabled { opacity: .5; cursor: default; }

.pf-fb-status {
  font-size: .84rem; line-height: 1.5; margin: .2rem 0 .7rem;
  color: rgba(255,255,255,0.72); min-height: 1.2em;
}
.pf-fb-status.is-error { color: #ffa9a9; }

/* ── Sent ───────────────────────────────────────── */
.pf-fb-thanks { text-align: center; padding: 2.2rem 0 1.4rem; }
.pf-fb-tick {
  width: 58px; height: 58px; margin: 0 auto 1rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; color: #8fe3c2;
  background: rgba(143,227,194,0.12);
  border: 1px solid rgba(143,227,194,0.4);
}
@media (prefers-reduced-motion: no-preference) {
  .pf-fb-tick { animation: pf-fb-pop .5s cubic-bezier(.22,1,.36,1) both; }
}
@keyframes pf-fb-pop { from { opacity: 0; transform: scale(.7); } }
.pf-fb-thanks-title {
  font-family: 'pixer', Georgia, serif; font-size: 1.2rem;
  letter-spacing: 2px; color: #eaf4ff; margin-bottom: .4rem;
}
.pf-fb-thanks-note { font-size: .9rem; color: rgba(255,255,255,0.72); }

/* Scroll lock while the dialog is open. position:fixed rather than plain
   overflow:hidden, which iOS Safari does not honour on <body>; `top` is set
   in JS to the scroll offset so the page does not jump. See lockScroll. */
body.pf-locked {
  overflow: hidden;
  position: fixed;
  width: 100%;
}

.pf-foot {
  margin-top: 1.2rem; text-align: center;
  font-size: 0.75rem; color: rgba(255,255,255,0.62);
  max-width: calc(560px * var(--ui)); line-height: 1.7;
  animation: pf-up .9s .6s ease both;
}
/* Narrow-screen entry point for the feedback form; see the orbit note above. */
.pf-fb-foot {
  display: none;
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: inherit;
  color: rgba(60,158,191,0.95); text-decoration: underline;
  text-underline-offset: 2px;
}
.pf-fb-foot:hover { color: #8fdcf5; }

.pf-foot a { color: rgba(60,158,191,0.95); text-decoration: none; }
.pf-foot a:hover { color: #7fd4ef; text-decoration: underline; }

@keyframes pf-up { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }

/* ═══ DOMAIN VIEW ═══════════════════════════════ */
/* Spacing is set by each block's own margin now, so no container gap. */
/* The section centres its child, which suits the single-hero wheel view.
   A domain page is read top-down, so this auto margin absorbs the slack
   below and pins it to the top — otherwise a short Tips panel floats in
   the middle of the screen. */
#view-domain { width: min(100%, calc(780px * var(--ui))); gap: 0; margin-bottom: auto; }

.dv-head { width: 100%; }
.dv-back {
  display: inline-flex; align-items: center; gap: .45rem;
  background: none; border: none; cursor: pointer; padding: .3rem 0;
  font: inherit; font-size: .82rem; letter-spacing: .04em;
  color: rgba(255,255,255,0.72);
  transition: color .2s;
}
.dv-back:hover { color: rgba(255,255,255,0.85); }
.dv-title {
  font-family: 'pixer', Georgia, serif;
  font-weight: normal; letter-spacing: 2.5px;
  font-size: clamp(1.5rem, 3.6vw, 2.1rem);
  color: var(--dc); margin: .5rem 0 0;
  text-wrap: balance;
}
.dv-blurb {
  font-size: .93rem; line-height: 1.62;
  color: rgba(255,255,255,0.8); margin-top: .5rem; max-width: 62ch;
}

/* Title and score sit side by side; the ring drops under the text only
   when there genuinely isn't room for both. */
.dv-head-row {
  display: flex; align-items: center; gap: 1.4rem;
  margin-top: .55rem;
}
.dv-head-text { flex: 1; min-width: 0; }
.dv-ring-wrap { flex: 0 0 auto; }

/* The dial's tool rail, rebuilt above the domain's score ring so the to-do
   list and key dates stay reachable without navigating back. */
.dv-tools {
  display: flex; justify-content: center; gap: .4rem;
  margin-bottom: .5rem;
}
.dv-tool { width: 32px; height: 32px; }
@media (prefers-reduced-motion: no-preference) {
  .dv-ring-wrap { animation: dv-ring-in .55s cubic-bezier(.34,.78,.4,1) backwards; }
}
@keyframes dv-ring-in { from { opacity: 0; transform: scale(.88); } }

/* A miniature of the main dial, so the domain page and the wheel speak the
   same language rather than one using a ring and the other a bar. */
.dv-ring { width: calc(84px * var(--ui)); height: calc(84px * var(--ui)); display: block; overflow: visible; }
.dv-ring circle {
  fill: none;
  stroke-width: 6;
  transform: rotate(-90deg);          /* start the fill at 12 o'clock */
  transform-origin: 40px 40px;
}
.dv-ring-track { stroke: rgba(255,255,255,0.09); }
.dv-ring-fill {
  stroke: var(--dc);
  stroke-linecap: round;
  transition: stroke-dashoffset .45s cubic-bezier(.22,1,.36,1);
}
/* The intro drives the arc frame by frame, so the tween would lag behind
   each step and blur the sweep. */
.dv-ring.is-intro .dv-ring-fill { transition: none; }
.dv-ring.is-full .dv-ring-fill { filter: drop-shadow(0 0 5px var(--dc)); }
.dv-ring-val {
  font-family: 'pixer', Georgia, serif;
  font-size: 25px; fill: var(--dc);
  text-anchor: middle; letter-spacing: 1px;
}
.dv-ring-max {
  font-family: var(--mono);
  font-size: 11px; fill: rgba(255,255,255,0.62);
  text-anchor: middle;
}

/* ── Tabs ──────────────────────────────────────── */
.dv-tabs {
  display: flex; gap: .25rem;
  /* .pf-view centres its children, so without a width the strip would
     shrink-wrap and float to the middle instead of lining up with the cards. */
  width: 100%;
  margin-top: 1.3rem;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.dv-tab {
  appearance: none; background: none; border: none; cursor: pointer;
  font: inherit; font-size: .88rem; font-weight: 500;
  color: rgba(255,255,255,0.72);
  /* Share the line equally so the strip reads as a full-width control
     rather than two labels floating at one end. */
  flex: 1 1 0;
  text-align: center;
  padding: .55rem .95rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;               /* sits on the container's rule */
  transition: color .2s ease, border-color .2s ease;
}
.dv-tab:hover { color: rgba(255,255,255,0.88); }
.dv-tab.is-on { color: var(--dc); border-bottom-color: var(--dc); }
.dv-tab:focus-visible { outline: 2px solid var(--dc); outline-offset: -2px; border-radius: 4px; }

.dv-panels { width: 100%; }
/* display is set on .dv-panel below, so the attribute alone would lose. */
.dv-panel[hidden] { display: none; }
.dv-panel {
  display: flex; flex-direction: column; gap: .85rem;
  margin-top: .85rem;
}

/* Visible only to screen readers — the ring itself is aria-hidden. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --fill is the group's banked score as a 0-1 fraction of what the group can
   actually yield — its sub-cap where it has one, otherwise the sum of its
   items. Set per card in JS; the tint and the meter both read from it. */
.dv-group {
  --fill: 0;
  /* Pulled well off the domain hue toward a cool grey, so a finished group
     reads as "warmed" rather than as a coloured panel. */
  --ev-tint: color-mix(in srgb, var(--dc) 58%, #93a9ba);
  position: relative;
  width: 100%;
  background: color-mix(in srgb,
            var(--ev-tint) calc(var(--fill) * 12%), rgba(255,255,255,0.032));
  border: 1px solid color-mix(in srgb,
            var(--dc) calc(var(--fill) * 30%), rgba(255,255,255,0.07));
  border-radius: 14px;
  padding: .35rem 1.5rem .35rem 1.1rem;   /* right gutter clears the meter */
  /* No backdrop-filter here, deliberately. Every card was its own backdrop
     layer, and transitioning one card's tint forced a re-rasterise that left
     its neighbours sampling a stale buffer — the grey band that flickered
     across a card when a stepper was clicked. It was also buying nothing:
     behind these cards the page varies by about 12/255 per channel, so
     blurring it is indistinguishable from not. */
  transition: background-color .42s ease, border-color .42s ease;
}

/* Sits inside the card's right edge and fills downwards. scaleY off a top
   origin keeps it on the compositor; animating height would not. */
.ev-meter {
  position: absolute; top: .7rem; right: .62rem; bottom: .7rem;
  width: 3px; border-radius: 3px;
  background: rgba(255,255,255,0.07);
  overflow: hidden;
}
.ev-meter::after {
  content: ''; position: absolute; inset: 0;
  border-radius: 3px; background: var(--dc);
  transform: scaleY(var(--fill)); transform-origin: top;
  transition: transform .44s cubic-bezier(.3,.82,.35,1);
}
.dv-group.is-full .ev-meter { box-shadow: 0 0 7px color-mix(in srgb, var(--dc) 45%, transparent); }

/* ── Completion ──────────────────────────────
   Fires once, at the moment a group's last available point lands — not on
   arriving at an already-full group. The card's halo and the meter's swell
   run on the same curve and duration so they read as one gesture. */
.dv-group::after {
  /* display:none at rest rather than opacity:0 — otherwise all nine cards keep
     a shadowed, gradient-filled overlay alive for an effect that fires once. */
  content: ''; display: none;
  position: absolute; inset: -1px;
  border-radius: 15px; pointer-events: none; opacity: 0;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--dc) 65%, transparent),
              0 0 30px color-mix(in srgb, var(--dc) 38%, transparent);
  /* Kept low: this layer paints over the row text for the length of the
     run, and anything heavier turns the labels muddy. */
  background: radial-gradient(135% 150% at 100% 50%,
              color-mix(in srgb, var(--dc) 13%, transparent), transparent 64%);
}
.dv-group.is-cheer::after { display: block; }
@media (prefers-reduced-motion: no-preference) {
  .dv-group.is-cheer::after { animation: ev-cheer 1.05s cubic-bezier(.22,.8,.3,1); }
  .dv-group.is-cheer .ev-meter { animation: ev-cheer-meter 1.05s cubic-bezier(.22,.8,.3,1); }
}
@keyframes ev-cheer {
  0%   { opacity: 0; transform: scale(.988); }
  20%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.014); }
}
/* Ends on the same values as the .is-full rest state, so the meter settles
   instead of snapping back when the animation is removed. */
@keyframes ev-cheer-meter {
  0%   { transform: scaleX(1);   box-shadow: 0 0 7px color-mix(in srgb, var(--dc) 45%, transparent); }
  18%  { transform: scaleX(2.2); box-shadow: 0 0 18px color-mix(in srgb, var(--dc) 90%, transparent); }
  100% { transform: scaleX(1);   box-shadow: 0 0 7px color-mix(in srgb, var(--dc) 45%, transparent); }
}
.dv-group-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .8rem; padding-top: .95rem;
}
.dv-group-title {
  font-size: .92rem; font-weight: 600; letter-spacing: .02em;
  color: rgba(255,255,255,0.88);
}
.dv-cap {
  font-family: var(--mono); font-size: .66rem;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--dc); background: color-mix(in srgb, var(--dc) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--dc) 34%, transparent);
  padding: .2rem .5rem; border-radius: 999px; white-space: nowrap;
  margin-left: auto;   /* stays right even when the group has no title */
}
.dv-group-note {
  font-size: .81rem; line-height: 1.6;
  color: rgba(255,255,255,0.72); margin-top: .4rem;
}
/* The cap pill sits inline, immediately after the words it qualifies —
   whether those are a group's title or an achievement's own name. In flow, so
   it wraps with the text and never needs a line or a corner of its own, which
   is what made these cards taller than their contents.
   margin-left:auto is the old right-alignment and would push it to the far
   edge; zeroed, the gap is the explicit one below. vertical-align lifts the
   small uppercase face off the baseline to sit centred beside the text, and
   font-weight stops it inheriting the heading's 600. */
.dv-group-title .dv-cap,
.ev-label .dv-cap {
  margin-left: .55rem;
  vertical-align: 2px;
  font-size: .6rem;
  font-weight: 400;
  padding: .15rem .42rem;
}

/* ── Evidence rows ───────────────────────────── */
.ev-row {
  display: flex; align-items: flex-start; gap: 1.2rem;
  padding: .95rem 0;
}
/* The rule belongs BETWEEN two achievements, not on top of each one. It used
   to be a border on every row, cancelled for :first-child — but a group that
   carries a cap pill or a title has a header before its rows, so the first row
   was no longer the first child and kept its rule. The result was that whether
   an achievement's name sat above or below a line came down to whether its
   group happened to have a pill, which is an accident, not a design. */
.ev-row + .ev-row { border-top: 1px solid rgba(255,255,255,0.055); }
.ev-text { flex: 1; min-width: 0; }
.ev-label {
  font-size: .95rem; font-weight: 500; line-height: 1.42;
  color: rgba(255,255,255,0.9);
}
.ev-note {
  font-size: .82rem; line-height: 1.58;
  color: rgba(255,255,255,0.72); margin-top: .32rem; max-width: 56ch;
}
.ev-control { flex-shrink: 0; padding-top: .1rem; }

/* Checkbox */
.ev-check {
  display: inline-flex; align-items: center; gap: .55rem;
  background: none; border: none; cursor: pointer; font: inherit; padding: 0;
}
.ev-box {
  width: calc(26px * var(--ui)); height: calc(26px * var(--ui)); border-radius: 7px;
  border: 1.5px solid rgba(255,255,255,0.18);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .8rem; color: #05101c;
  transition: background .18s, border-color .18s, transform .18s;
}
.ev-check:hover .ev-box { border-color: var(--dc); }
.ev-check.is-on .ev-box { background: var(--dc); border-color: var(--dc); }
.ev-check:active .ev-box { transform: scale(.9); }
.ev-pts {
  font-family: var(--mono); font-size: .92rem; min-width: 1.6ch;
  color: rgba(255,255,255,0.62); transition: color .18s;
}
.ev-check.is-on .ev-pts { color: var(--dc); }

/* Stepper */
.ev-stepper {
  display: inline-flex; align-items: center;
  border: 1px solid rgba(255,255,255,0.12); border-radius: 9px;
  overflow: hidden; transition: border-color .2s;
}
.ev-stepper.is-on { border-color: color-mix(in srgb, var(--dc) 55%, transparent); }
.ev-step {
  width: calc(32px * var(--ui)); height: calc(32px * var(--ui)); border: none; cursor: pointer;
  background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.88);
  font-size: 1rem; line-height: 1; transition: background .16s, color .16s;
}
.ev-step:hover:not(:disabled) { background: color-mix(in srgb, var(--dc) 22%, transparent); color: #fff; }
.ev-step:disabled { opacity: .25; cursor: default; }
/* The + is disabled because the group is full, not because this item is — so
   the whole control dims to say "nothing more scores here" rather than leaving
   a lone greyed button that reads as broken. */
.ev-stepper.is-capped { opacity: .55; }
.ev-count {
  font-family: var(--mono); font-size: .98rem;
  min-width: calc(34px * var(--ui)); text-align: center;
  color: rgba(255,255,255,0.72); font-variant-numeric: tabular-nums;
  transition: color .18s;
}
.ev-stepper.is-on .ev-count { color: var(--dc); }

/* Options / bands */
.ev-opts { display: flex; flex-direction: column; gap: .3rem; min-width: calc(260px * var(--ui)); }
.ev-opt {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem;
  padding: .5rem .7rem; border-radius: 8px; cursor: pointer;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.07);
  font: inherit; font-size: .85rem; text-align: left;
  color: rgba(255,255,255,0.8);
  transition: background .16s, border-color .16s, color .16s;
}
.ev-opt:hover { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.9); }
.ev-opt.is-on {
  background: color-mix(in srgb, var(--dc) 16%, transparent);
  border-color: color-mix(in srgb, var(--dc) 55%, transparent);
  color: #fff;
}
.ev-opt-pts {
  font-family: var(--mono); font-size: .82rem;
  color: rgba(255,255,255,0.62); flex-shrink: 0;
}
.ev-opt.is-on .ev-opt-pts { color: var(--dc); }

/* Notes */
.dv-notes {
  width: 100%; padding: 1.1rem 1.2rem;
  border-radius: 14px;
  border: 1px solid rgba(255,255,255,0.055);
  background: rgba(255,255,255,0.018);
}
.dv-notes-title {
  font-family: var(--mono); font-size: .68rem;
  letter-spacing: .16em; text-transform: uppercase;
  color: rgba(255,255,255,0.62); margin-bottom: .6rem;
}
.dv-note-list { list-style: none; display: flex; flex-direction: column; gap: .45rem; }
.dv-note-list li {
  font-size: .84rem; line-height: 1.62;
  color: rgba(255,255,255,0.72); padding-left: 1rem; position: relative;
}
.dv-note-list li::before {
  content: ''; position: absolute; left: 0; top: .62em;
  width: 4px; height: 4px; border-radius: 50%;
  background: color-mix(in srgb, var(--dc) 70%, transparent);
}

@media (max-width: 620px) {
  /* Thumb-sized here; the bar itself is now the layout at every width. */
  .pf-tool { width: 44px; height: 44px; }
  .pf-stage { margin-top: .95rem; }
  /* The fifth button is on the bar, so the footer fallback stays down. */
  body.has-feedback .pf-fb-foot { display: none; }
}

/* ═══ PHONES ═══════════════════════════════════════
   The band that matters is 360-430px: Galaxy S22-S24 at 360, iPhone SE at
   375, iPhone 12 through 16 at 390-393, Pixel at 412, Pro Max at 430. The
   stylesheet used to do its narrow-screen work at 360 and below, which in
   practice meant almost no current handset ever reached it.
   =================================================== */
@media (max-width: 620px) {
  /* Side gutters come in so the cards get the width back, but never inside
     the rounded corner of a landscape-held phone. */
  .pf {
    /* The tool bar beneath the dial adds ~59px of chrome the dial must now
       make room for, or it would size itself off height it no longer has. */
    --chrome: 316px;
    padding-top: max(1.4rem, env(safe-area-inset-top));
    padding-bottom: calc(1.6rem + env(safe-area-inset-bottom));
    padding-left:  max(1rem, env(safe-area-inset-left));
    padding-right: max(1rem, env(safe-area-inset-right));
  }
  .pf-sub  { font-size: .9rem; }

  /* ── Breathing room ───────────────────────────
     Four blocks — masthead, dial, tool bar, footer — sat 15-18px apart while
     84px (iPhone 14) to 134px (Pro Max) of height pooled uselessly above and
     below them, because `justify-content: safe center` banks all the spare
     height in the outer margins. This hands that spare height to the gaps
     instead, split three ways.

     Auto margins are the mechanism precisely because they cannot cause a
     scroll: they only ever consume free space that already exists, so on a
     phone with none (an SE at 553px, where the dial is already sized by
     height) every one of them resolves to zero and the layout is exactly what
     it is today. Nothing is added — it is only moved. --chrome is untouched
     for the same reason: the base layout's height has not changed. */
  #view-wheel { flex: 1 1 auto; }
  /* Two empty flex items, one at each end. Auto margins always split the
     spare height into equal shares, so the only way to give the inner gaps a
     smaller portion is to raise the number of shares: each of these carries
     an auto margin on both sides, which takes the count from three to six.
     The two gaps therefore get a sixth of the spare height each instead of a
     third — half as much — and the other two thirds stay in the outer
     margins, keeping the whole block centred. */
  #view-wheel::before,
  #view-wheel::after { content: ''; margin: auto 0; }
  #view-wheel .pf-head  { margin-bottom: .95rem; }
  #view-wheel .pf-stage { margin-top: auto; }
  /* Minimum gap as padding, not margin — a margin here would be overridden by
     the auto that has to sit in its place. */
  #view-wheel .pf-foot  { margin-top: auto; padding-top: .9rem; }

  /* Fine print, and the one block that can be allowed below the fold. */
  .pf-foot { font-size: .7rem; line-height: 1.6; margin-top: .9rem; }

  /* ── The squeeze ──────────────────────────────
     An options list asks for 260px and .ev-control does not shrink, so on a
     390px screen the label was left an 8px column and set one character per
     line. Rows carrying one stack; a checkbox or stepper row has a narrow
     control and still reads fine side by side, so it is left alone. */
  .ev-row:has(.ev-opts) { flex-direction: column; gap: .7rem; }
  .ev-row:has(.ev-opts) .ev-control { padding-top: 0; width: 100%; }
  .ev-opts { min-width: 0; width: 100%; }
  /* Fallback for a browser without :has — the control may then shrink rather
     than starving the label, which is worse than stacking but not broken. */
  .ev-control { min-width: 0; }

  /* ── Domain masthead ──────────────────────────
     The tool rail is stacked above the score ring, so that column is as wide
     as four buttons — about 147px. Beside it the title was left wrapping in a
     190px gutter ("Ophthalmology / Specialty / Links" over three lines) while
     the rest of the row sat empty. Here the text takes the full width and the
     rail and the ring share the line underneath it. */
  .dv-head-row  { flex-wrap: wrap; gap: .9rem 1rem; }
  .dv-head-text { flex: 1 1 100%; }
  .dv-ring-wrap {
    flex: 1 1 100%;
    display: flex; align-items: center; justify-content: space-between;
  }
  .dv-tools { margin-bottom: 0; }

  /* Reclaimed from the card gutters: ~30px of line length per row, which at
     this width is the difference between three words and four. */
  .dv-group { padding: .35rem 1.15rem .35rem .9rem; }
  .pf-modal { padding-left: .75rem; padding-right: .75rem; }
  .pf-modal-head   { padding: 1.55rem 1.15rem .2rem; }
  .pf-modal-scroll { padding: 0 1.15rem 1.4rem; }
  .pf-modal-title  { font-size: 1.2rem; }
  .ev-row { gap: .8rem; }
}

@media (max-width: 360px) {
  /* The subheading wraps onto a further line or two down here, so the real
     chrome is nearer 330px than the 316px assumed above — and the dial, sized
     against the smaller figure, left the page 13px too tall to fit. */
  .pf { --chrome: 334px; }
  .pf-title { letter-spacing: 3px; }
  /* A stepper is ~128px once its buttons are thumb-sized, which leaves the
     label about 139px to wrap in — so those rows stack here too. A checkbox
     is only ~49px and still reads fine beside its label, so it stays put
     rather than leaving a lone tickbox adrift on a line of its own. */
  .ev-row:has(.ev-stepper) { flex-direction: column; gap: .7rem; }
  .ev-row:has(.ev-stepper) .ev-control { padding-top: 0; width: 100%; }
}

/* ── Landscape ────────────────────────────────────
   Held sideways there is no vertical room to spare, so the masthead and
   footer give theirs to the dial rather than pushing it off the screen. */
@media (orientation: landscape) and (max-height: 500px) {
  .pf {
    --chrome: 176px;
    padding-top: .9rem;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
  }
  /* The portrait floor of 228px is there to stop a narrow phone shrinking the
     hero to a token. Sideways the constraint is height, not width, and that
     floor is bigger than the room available — it forced the page to scroll to
     protect a size nothing was threatening. */
  .pf-stage {
    width: min(88vw, calc(100vh - var(--chrome)), 600px);
    /* 148px, not the portrait floor of 228: on the shortest landscape screen
       still in use (568x320, an SE-generation phone turned sideways) anything
       larger is more than the room left over and pushes the page into a
       scroll. It only binds below ~330px of height; every other phone is
       sized by the calc above and untouched by it. */
    width: max(148px, min(88vw, calc(100svh - var(--chrome)), 600px));
  }
  .pf-title { font-size: clamp(1.35rem, 7.5vh, 2rem); }
  .pf-sub   { font-size: .8rem; margin-top: .3rem; }
  /* The bar's reserved lane is undone — sideways the rail stands beside the
     dial, not beneath it, so nothing needs the space below. */
  .pf-stage { margin-top: .4rem; margin-bottom: .2rem; }
  /* A narrow landscape (a phone at 568x320, say) also matches the phone
     breakpoint and would inherit its 44px buttons, giving a rail taller than
     the dial it is meant to sit beside. */
  .pf-tool { width: 38px; height: 38px; }
  .pf-foot  { font-size: .68rem; line-height: 1.5; margin-top: .7rem; }
  /* The orbit does not survive here. Its radius is a fraction of the dial, and
     the dial is now ~214px, so neighbours a sensible 10deg apart land 23px
     apart — while the buttons are still 40px, and overlap each other by half.
     Widening the arc enough to fix that would fling the buttons so far from
     the rim they stop reading as attached to it.
     Sideways there is width to spare and no height, which is the opposite of
     the case the arc was designed for, so the rail simply stands up beside the
     dial instead: same buttons, same order, vertically centred on the rim. */
  .pf-tools {
    inset: 0 auto 0 100%;            /* the column just outside the stage */
    display: flex; flex-direction: column;
    justify-content: center; align-items: flex-start;
    gap: .45rem; padding-left: .7rem;
  }
  .pf-slot,
  .pf-tools.has-fb .pf-slot-4,
  .pf-tools.has-fb .pf-slot-5 { position: static; transform: none; display: block; }
  .pf-tools .pf-slot { animation-name: pf-tool-lift; }
  /* The footer link is the narrow-screen fallback for the orbit's fourth
     button; the column has room for all four, so it is not needed. */
  body.has-feedback .pf-fb-foot { display: none; }
}

/* Shortest landscape still in use — an SE-generation phone turned sideways.
   The dial is ~148px tall there and a single column of four buttons is 174px,
   so the rail overran the dial at both ends and clipped the footer. Folded
   into two columns of two it is 83px and sits comfortably beside the dial. */
@media (orientation: landscape) and (max-height: 340px) {
  .pf-tools {
    flex-wrap: wrap;
    /* padding-top is the bar layout's, and it counts against the column's
       height under border-box: left in, it took the space of the second
       button and every button wrapped into a column of its own, strung out
       in a long row off the side of the dial. */
    padding-top: 0;
    /* Big enough for two buttons (85px), smaller than three (132px). */
    max-height: 96px;
    align-content: center;
  }
}

/* ── Touch ────────────────────────────────────────
   Keyed on the pointer, not the screen: a 380px browser window on a desktop
   is still driven by a mouse and does not need 44px controls, and a tablet
   is far wider than any of the breakpoints above but is all thumb.
   Apple's HIG minimum is 44x44; WCAG 2.2 AA (2.5.8) sets a floor of 24x24,
   which several of these controls were under. */
@media (pointer: coarse) {

  /* Where there is room, the control genuinely grows. */
  .ev-step  { width: 44px; height: 44px; }
  .ev-count { min-width: 40px; }
  .ev-check { min-height: 44px; }
  .ev-opt   { min-height: 44px; }
  .pf-fb-dot   { height: 44px; }
  .pf-fb-grade { min-height: 44px; }
  /* The heading is the tap target now; the items below it are plain text. */
  .pf-todo-head { min-height: 44px; }
  .dv-back { min-height: 44px; }
  .dv-tab  { min-height: 44px; }
  .dv-tool { width: 40px; height: 40px; }

  /* Where growing it would wreck the composition — the 17px help dot in a
     sentence, the buttons spaced along the dial's arc — the target grows
     instead of the button: a transparent pseudo-element centred on it. The
     orbit's neighbours sit ~44px apart centre to centre, so these tile
     rather than overlapping and stealing each other's taps. */
  /* .pf-modal-close is deliberately absent: it is already absolutely
     positioned into the card's top-right corner, which is itself a containing
     block for the pseudo. Adding position:relative here overrode that and
     dropped the button back into normal flow at the card's left edge. */
  .pf-help, .pf-tool, .pf-fb-foot { position: relative; }
  .pf-help::after,
  .pf-tool::after,
  .pf-modal-close::after,
  .pf-fb-foot::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 44px; height: 44px;
    transform: translate(-50%, -50%);
  }
  /* A link inside a sentence: it needs the height, not a square. */
  .pf-fb-foot::after { width: calc(100% + 16px); }

  /* iOS zooms the whole page in when a focused field is under 16px, and does
     not zoom back out when the field is left. Every input here was 14.7px. */
  .pf-fb-input,
  .pf-fb-input.pf-fb-area { font-size: 16px; }
}

/* ── No hover ─────────────────────────────────────
   A touch browser fires :hover on tap and then leaves it stuck on the last
   thing touched. Anything that moves under :hover would stay moved. */
@media (hover: none) {
  .pf-tool:hover,
  .pf-help:hover,
  .pf-link:hover { transform: none; }
  .pf-link:hover::before { transform: scaleY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .pf-scan { animation: none !important; }
  .pf *, .pf *::before, .pf *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}


/* ── Account ─────────────────────────────────────
   Deliberately plain. The folder works without any of this, so the panel
   should read as an offer rather than a gate. */
.pf-acct-alt {
  width: 100%; margin-top: .5rem;
  padding: .7rem 1rem; border-radius: 10px; cursor: pointer;
  font: inherit; font-size: .92rem; font-weight: 500;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.8);
  transition: background .18s, border-color .18s, color .18s;
}
.pf-acct-alt:hover:not(:disabled) { background: rgba(255,255,255,0.08); color: #fff; }
.pf-acct-alt:disabled { opacity: .5; cursor: default; }

.pf-acct-link {
  display: block; width: 100%; margin-top: .6rem;
  background: none; border: none; cursor: pointer; padding: .2rem 0;
  font: inherit; font-size: .84rem; text-align: center;
  color: rgba(60,158,191,0.95); text-decoration: underline;
  text-underline-offset: 2px;
}
.pf-acct-link:hover { color: #7fd4ef; }

/* The one line that answers "is my work safe?" without opening anything. */
.pf-acct-state {
  font-family: var(--mono); font-size: .76rem;
  letter-spacing: .1em; text-transform: uppercase;
  color: rgba(143,227,194,0.9);
  display: flex; align-items: center; gap: .45rem;
  margin: .9rem 0 1.1rem;
}
.pf-acct-state::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; flex-shrink: 0;
}
.pf-acct-state.is-busy  { color: rgba(240,213,168,0.9); }
.pf-acct-state.is-error { color: #ffa9a9; }

/* Destructive, and dressed as such: separated by a rule, never the primary
   button, and it asks twice before it does anything. */
.pf-acct-danger {
  margin-top: 1.3rem; padding-top: 1.1rem;
  border-top: 1px solid rgba(255,255,255,0.07);
}
.pf-acct-del {
  width: 100%; padding: .65rem 1rem; border-radius: 10px; cursor: pointer;
  font: inherit; font-size: .88rem;
  background: rgba(255,120,120,0.06);
  border: 1px solid rgba(255,120,120,0.3);
  color: #ffa9a9;
  transition: background .18s, border-color .18s;
}
.pf-acct-del:hover { background: rgba(255,120,120,0.14); border-color: rgba(255,120,120,0.55); }
.pf-acct-del.is-armed { background: rgba(255,120,120,0.2); border-color: #ff8b8b; color: #fff; }
.pf-acct-note {
  display: block; margin-top: .5rem;
  font-size: .8rem; line-height: 1.5; color: rgba(255,255,255,0.62);
}

/* A quiet tick on the dial's account button once the folder is being kept. */
.pf-tool.is-on {
  color: #8fe3c2;
  border-color: rgba(143,227,194,0.45);
  background: rgba(143,227,194,0.1);
}

/* ── Icon micro-animations ───────────────────────
   Each icon animates the part of itself that carries the meaning, rather than
   the button doing something generic: the ticks on the checklist redraw, the
   calendar's posts lift, the chain flexes, the bubble speaks, the cloud's tick
   confirms. Small, fast and specific — the point is recognition, not display.

   Gated on a real pointer. A touch browser latches :hover on the last thing
   tapped, so on a phone these would fire once and then stay stuck mid-gesture
   on whichever button was last pressed. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {

  /* fill-box so a transform-origin of `center` means the centre of the shape
     itself, not of the 24x24 canvas it happens to sit in. */
  .pf-tool svg * {
    transform-box: fill-box;
    transform-origin: center;
    transition: transform .34s cubic-bezier(.22,1,.36,1),
                opacity   .22s ease;
  }

  /* ── Checklist ────────────────────────────────
     The rules slide in from the left in sequence and the ticks redraw over
     them, so the icon plays as a list being worked through. */
  .pf-tool:hover .i-tick1,
  .pf-tool:hover .i-tick2 { animation: ic-draw .5s cubic-bezier(.45,.85,.35,1) both; }
  .pf-tool:hover .i-tick2 { animation-delay: .1s; }
  .pf-tool:hover .i-rule1 { transform: translateX(1.6px); }
  .pf-tool:hover .i-rule2 { transform: translateX(1.6px); transition-delay: .06s; }
  .pf-tool:hover .i-rule3 { transform: translateX(1.6px); transition-delay: .12s; }
  .pf-tool:hover .i-dot   { transform: scale(1.7); transition-delay: .12s; }

  /* ── Calendar ─────────────────────────────────
     A day lands in the grid — which is what the panel is actually for. The
     posts give a small lift behind it, the way a page does when it is picked
     up, and the header rule is left alone: it spans the body exactly, so any
     scale on it pokes out through the rounded corners. */
  .pf-tool:hover .i-post1 { transform: translateY(-1.2px); }
  .pf-tool:hover .i-post2 { transform: translateY(-1.2px); transition-delay: .06s; }
  .pf-tool:hover .i-day   { opacity: 1; transform: none; transition-delay: .1s; }

  /* ── Chain ────────────────────────────────────
     Turned as one piece rather than pulled apart. Separating the halves read
     as a link *breaking*, which is precisely the wrong idea for a panel of
     links — and the gap was the most visible thing about it. Rotating about
     the icon's own centre keeps the chain whole and still gives the hover
     something to do.
     view-box, not the fill-box the other parts use: these two need to turn
     about the middle of the icon, not about the middle of each hook. */
  .pf-tool .i-linkA,
  .pf-tool .i-linkB { transform-box: view-box; transform-origin: 12px 12px; }
  .pf-tool:hover .i-linkA,
  .pf-tool:hover .i-linkB { transform: rotate(-14deg) scale(1.04); }

  /* ── Speech bubble ────────────────────────────
     Three dots land inside it in turn, the way a typing indicator does. Their
     resting state is declared outside this block — see below — because a
     touch device never enters it, and they must still start hidden there. */
  .pf-tool:hover .i-say1,
  .pf-tool:hover .i-say2,
  .pf-tool:hover .i-say3 { opacity: 1; transform: none; }
  .pf-tool:hover .i-say2 { transition-delay: .07s; }
  .pf-tool:hover .i-say3 { transition-delay: .14s; }
  .pf-tool:hover .i-bubble { transform: scale(1.04); }

  /* ── Cloud ────────────────────────────────────
     The tick draws itself, which is the same gesture the panel uses to say a
     folder has been saved. */
  .pf-tool:hover .i-check { animation: ic-draw .52s cubic-bezier(.45,.85,.35,1) both; }
  .pf-tool:hover .i-cloud { transform: translateY(-.7px); }
}

/* pathLength="1" on the path normalises its length, so one dash covers the
   whole stroke whatever its real geometry — no measuring, and it survives the
   path being redrawn. At rest the offset is 0, so the mark is simply there. */
@keyframes ic-draw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}
.i-tick1, .i-tick2, .i-check { stroke-dasharray: 1; stroke-dashoffset: 0; }

/* Unconditional, and it has to be. These three exist only to be revealed on
   hover, so on a phone — or for anyone who has asked for less motion, where
   the block above never applies — they would otherwise sit inside the bubble
   permanently, as three dots nobody asked for. */
.i-say1, .i-say2, .i-say3 { opacity: 0; }
.i-day { opacity: 0; transform-box: fill-box; transform-origin: center; transform: scale(.35); }

/* ── Storage warning ─────────────────────────────
   Shown only when this browser will not persist anything. Deliberately not
   dismissible: the condition does not go away, and hiding it would hide the
   fact that the work being done is about to be thrown away. Amber rather than
   red — nothing has broken yet, but it is about to. */
.pf-warn {
  display: flex; align-items: flex-start; gap: .6rem;
  width: min(100%, 560px);
  margin: 0 0 1.1rem;
  padding: .75rem .9rem;
  border-radius: 11px;
  font-size: .84rem; line-height: 1.55;
  color: rgba(255,255,255,0.82);
  background: rgba(212,145,58,0.1);
  border: 1px solid rgba(212,145,58,0.45);
}
.pf-warn[hidden] { display: none; }
.pf-warn svg { flex-shrink: 0; margin-top: .15rem; color: #f0d5a8; }
.pf-warn strong { color: #f0d5a8; font-weight: 600; }
/* The account is only an answer once there is a backend to sign in to. */
.pf-warn-acct { display: none; }
body.has-feedback .pf-warn-acct { display: inline; }

/* Sideways there is no vertical room to give away, and the warning is ~60px
   the --chrome budget knows nothing about — enough to push the dial off the
   bottom. It tightens, and the dial gives up the rest. The warning itself is
   at the top of the page, so it is seen either way; this is only about not
   burying the instrument beneath it. */
@media (orientation: landscape) and (max-height: 500px) {
  .pf-warn { font-size: .76rem; padding: .5rem .75rem; margin-bottom: .7rem; gap: .45rem; }
  body.pf-nostore .pf { --chrome: 244px; }
}


/* ── Large displays ──────────────────────────────
   A 27" panel was showing a laptop's 600px dial marooned in 890px of empty
   gutter each side, with 15px body text — the same layout at every size.

   Keyed on BOTH axes deliberately: a wide but short window, a browser at
   half height on a big monitor, must never be handed a dial taller than it
   can show. Raising --ui carries the whole interface with it, because the
   dial, the page widths, the chrome budget and every control are expressed
   against it; the root font moves in step so type and rem spacing keep pace.

   Percentages for the root font, never pixels: a px value would override
   whatever base size the reader has chosen in their browser. */
@media (min-width: 1500px) and (min-height: 880px) {
  :root { --ui: 1.1; }
  html { font-size: 110%; }
}
@media (min-width: 2000px) and (min-height: 1180px) {
  :root { --ui: 1.22; }
  html { font-size: 121%; }
}
/* 4K at 100% scaling. Most such panels run at 150%, which reports 2560x1440
   and lands in the tier above; this is for the ones that do not. */
@media (min-width: 2800px) and (min-height: 1600px) {
  :root { --ui: 1.42; }
  html { font-size: 140%; }
}

/* The field the form is waiting on. Amber rather than red — nothing is wrong,
   something is simply missing — and it clears the moment anything is typed or
   chosen, so the mark never outlives the gap it describes. */
.pf-fb-block.is-gap .pf-fb-legend,
.pf-fb-scale.is-gap .pf-fb-scale-label { color: #f0d5a8; }
.pf-fb-block.is-gap .pf-fb-input {
  border-color: rgba(240,213,168,0.55);
  background: rgba(240,213,168,0.05);
}
.pf-fb-block.is-gap .pf-fb-grade,
.pf-fb-scale.is-gap .pf-fb-dot { border-color: rgba(240,213,168,0.35); }

/* A tip that points somewhere. Same blue as the other links in the app, and
   it wraps rather than forcing the panel wider — a bare URL in a note is long
   enough to push a phone into horizontal scroll on its own. */
.dv-note-link {
  color: rgba(60,158,191,0.95);
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
}
.dv-note-link:hover { color: #7fd4ef; }


/* ── Wide screens ────────────────────────────────
   The footer disclaimer goes, leaving the dial and its bar alone on the page.
   Nothing is lost by it: the same caveat, in fuller form, is the closing
   paragraph of the About panel behind the ? in the subheading.
   Kept on a phone, where the page is a column you scroll anyway and the line
   costs nothing that was being used for something else. */
@media (min-width: 621px) {
  #view-wheel .pf-foot { display: none; }
  /* Chrome is the masthead, the bar's lane and the padding — no footer now,
     so the dial takes back most of the height that line was using.
     Sized for the worst case: the title is clamp(2.1rem, 5.2vw, 3.5rem), so it
     grows about 20px between a 700px window and a 1400px one, and a value fit
     for the narrow end lets the wide end overflow. */
  .pf { --chrome: calc(252px * var(--ui)); }
}
