/* ============================================================
   Profyl — component implementations
   Requires tokens.css. Brand v3 (Direction A, Lock-in) since 2026-10-13:
   every rule reads the v3 names (--pf-*, --type-*, --radius-*,
   --duration-*), except the document rules (.artifact__* from the title
   down, .pf-prose, .pf-standfirst, .typeface__*), which read the v2 names
   tokens.css part 3 pins so a resume renders as it did. docs/brand-v3/migration-plan.md, "Utility mapping".

   Every rule pairs a real pseudo-class with a forced-state class
   (:hover / .is-hover) so the library page can show every state at
   once without scripting. Ship both — the forced classes cost
   nothing and keep the reference honest.

   No selector in this file reads [data-mode]. Modes are token
   presets; if you need a mode branch here, the token is missing.
   ============================================================ */

/* ---------- utilities ---------- */

.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;
}

.skip-link{
  position:absolute; left:var(--space-4); top:-100px; z-index:100;
  padding:var(--space-3) var(--space-4); background:var(--pf-primary); color:var(--pf-on-primary);
  font:var(--type-label-lg-weight) var(--type-label-lg-size)/1 var(--font-sans); border-radius:var(--radius-md);
  transition:top var(--duration-sm) var(--ease-standard);
}
.skip-link:focus{ top:var(--space-4); }

.stack{ display:flex; flex-direction:column; gap:var(--stack); }
.stack--tight{ gap:var(--stack-tight); }
.stack--loose{ gap:var(--stack-loose); }
.cluster{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-3); }

/* Named .pf-overline, not .overline.
   `overline` is a built-in Tailwind utility (text-decoration-line: overline),
   and Tailwind's utilities layer sits above the layer this file is imported
   into — so the class drew a literal stroke above every section kicker in the
   app. It looked like a stray hyphen and it was in every page header, every
   price card and every "how to address it" label. `--*: initial` in @theme
   switches off Tailwind's *theme-derived* utilities; the static ones like
   overline, underline and truncate survive it.
   The pf- prefix is this file's existing convention for exactly this reason —
   .pf-prose, .pf-tabular, .pf-display, .pf-no-clip. Anything that could collide
   with a Tailwind utility name takes it. Enforced by scripts/lint-mode-utilities.js. */
/* v3's overline (§5): 12px caps at 108% width. At most one per section,
   and never stacked on a heading that already says the same thing. */
.pf-overline{
  margin:0;
  font:var(--type-overline-weight) var(--type-overline-size)/var(--type-overline-leading) var(--font-sans);
  font-stretch:var(--type-overline-width); letter-spacing:var(--type-overline-tracking);
  text-transform:uppercase; color:var(--pf-fg-muted);
}

/* The page head: kicker, title, lede. No rule: v3 separates by space and
   type, and the 2px rule is kept for the current item in navigation. */
.pagehead{
  display:flex; flex-direction:column; gap:var(--stack-tight);
}
/* type-h1, in full: the global h1 rule sets the family, this sets the rest
   so the title does not depend on which element carries it. */
.pagehead__title{
  margin:0; font-family:var(--font-display); font-weight:var(--type-h1-weight);
  font-size:var(--type-h1-size); line-height:var(--type-h1-leading);
  font-stretch:var(--type-h1-width); letter-spacing:var(--type-h1-tracking);
}
/* The display step (§5): ExtraBold at 112% width, for the moments that are
   an arrival — Home's greeting and the front door. */
.pagehead__title--display{
  font-weight:var(--type-display-lg-weight); font-size:var(--type-display-lg-size);
  line-height:var(--type-display-lg-leading); font-stretch:var(--type-display-lg-width);
  letter-spacing:var(--type-display-lg-tracking);
}
.pagehead__lede{ margin:0; color:var(--pf-fg-secondary); max-width:var(--measure-ui); font-size:var(--type-body-lg-size); line-height:var(--type-body-lg-leading); }

/* The kicker itself. Coloured, not muted grey — it is a label with a job:
   brand-coloured text (primary-text) in the label style. v3 has no
   per-module tint, so Applications and Presence share it. Flex so an icon
   can sit in the row without a second class. */
.eyebrow{
  display:flex; align-items:center; gap:var(--space-2);
  font:var(--type-label-weight) var(--type-label-size)/var(--type-label-leading) var(--font-sans);
  letter-spacing:var(--type-label-tracking); color:var(--pf-primary-text); margin:0;
}

/* Icons (design-system.md §3.8) are React elements, so this file styles none
   of them. The one thing CSS has to say is that a flex parent must not squash
   one: every icon sits next to text in a .btn, a .field__error, a status label
   or a chip, and all four are flex rows. */
.btn > svg,.field__error > svg,.status__label > svg,.chip__remove > svg,
.sidenav a > svg,.modeswitch a > svg,.bottomnav a > svg,.tabs a > svg,
.tabs button > svg,.masthead__nav a > svg,.segmented__option > span > svg,
.card__header > svg,.eyebrow > svg{ flex:none; }

/* ============================================================
   THE LOADING REGISTER
   design-system.md §1.7
   ============================================================ */

/* §1.5 bans skeleton shimmer, and the ban was read as "show nothing at all":
   seven surfaces returned an `sr-only` status line and rendered an empty
   viewport to everyone who could see. The app's own boot and every route
   change went through one of them, so the first thing a new user saw was a
   blank page — and on the 4G connection this product is actually used on,
   that blank is seconds long, not the flicker it looks like on localhost.

   The ban is on the *shimmer* — a fake preview of content that has not
   arrived, which in a product selling "we wrote this from your life" is a lie
   about the most important moment. It was never a ban on saying the app is
   working.

   Two properties keep this quiet enough to belong here:

   Delayed by --dur-reveal, so a response that arrives quickly never flashes
   it. Most do. This should rarely be seen at all.

   Opacity only — §1.5 permits opacity and <=8px translate, and there is no
   translate. Nothing pulses, sweeps, or counts.

   The element mounts immediately even though it is invisible for the first
   --dur-reveal, so role="status" announces on the same schedule it always
   did. The delay is a visual property, not a behavioural one, and a screen
   reader user loses nothing to it.

   Reduced motion: the global rule in tokens.css forces animation-duration to
   0.01ms and leaves animation-delay alone, so the line still waits out the
   threshold and then simply appears rather than fading. That is the correct
   behaviour, and it is why the keyframe ends at the visible state — `forwards`
   holds opacity:1 whether the fade ran or was collapsed. */
.loading{
  display:flex; align-items:center;
  min-height:var(--space-16);   /* reserved, so arriving content does not jump */
  color:var(--pf-fg-muted);
  font:400 var(--type-body-sm-size)/1.45 var(--font-sans);
  letter-spacing:var(--type-caption-tracking);
  opacity:0;
  animation:pf-loading-in var(--duration-sm) var(--ease-standard) var(--dur-reveal) forwards;
}
@keyframes pf-loading-in{ to{ opacity:1 } }

/* ============================================================
   BUTTONS
   ============================================================ */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--space-2);
  height:var(--control-md); padding:0 var(--space-4);
  font:var(--type-label-lg-weight) var(--type-label-lg-size)/1 var(--font-sans);
  letter-spacing:var(--type-label-lg-tracking); text-decoration:none; white-space:nowrap;
  /* 1.5px, the v3 control edge (brand-identity.md §9); transparent until a
     variant needs it, so every variant is the same size. */
  border:1.5px solid transparent; border-radius:var(--radius-md);
  background:transparent; color:var(--pf-fg); cursor:pointer;
  transition:background-color var(--duration-sm) var(--ease-standard),
             border-color var(--duration-sm) var(--ease-standard),
             color var(--duration-sm) var(--ease-standard);
}
.btn:focus-visible,.btn.is-focus{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
/* A button never grows past its column. And a button whose label carries the
   user's own words ("Responsibilities for Senior Engineer, Payments at
   Zeta", "Write it for Shorts / Reels and Blog post") may wrap, because a
   nowrap label of unknown length is a page that scrolls sideways on a phone
   (2026-10-04). Short fixed labels stay on one line. */
.btn{ max-width:100%; }
.btn.btn--wrap{
  white-space:normal; height:auto; min-height:var(--control-md);
  padding-block:var(--space-2); line-height:1.3; text-align:left; justify-content:flex-start;
}
.btn.btn--wrap.btn--sm{ height:auto; min-height:var(--control-sm); padding-block:var(--space-1); }
.btn.btn--wrap.btn--lg{ height:auto; min-height:var(--control-lg); }

.btn--primary{ background:var(--pf-primary); color:var(--pf-on-primary); }
.btn--primary:hover,.btn--primary.is-hover{ background:var(--pf-primary-hover); }
.btn--primary:active,.btn--primary.is-active{ background:var(--pf-primary-pressed); }

/* Outlined, as v3 draws it (compare.html, "Core components"): no fill, the
   3:1 input edge, the sunken ground on hover. It was tonal from 2026-09-27,
   because a grey 1px outline on white read as a text field. Weight now tells
   them apart: a button's label is 600 and centred, a field's text is 400. */
.btn--secondary{ background:transparent; border-color:var(--pf-border-input); color:var(--pf-fg); }
.btn--secondary:hover,.btn--secondary.is-hover{ background:var(--pf-surface-sunken); }
.btn--secondary:active,.btn--secondary.is-active{ background:var(--pf-border); }

/* The one button that actually deletes: the confirmation. v3's danger
   button, the error solid with its own ink (white in light, Ink in dark).
   v3 names no hover step, so it goes one shade deeper, as the primary does. */
.btn--destructive{ background:var(--pf-error-solid); color:var(--pf-on-error-solid); }
.btn--destructive:hover,.btn--destructive.is-hover{ background:color-mix(in srgb, var(--pf-error-solid) 88%, #000000); }
.btn--destructive:active,.btn--destructive.is-active{ background:color-mix(in srgb, var(--pf-error-solid) 78%, #000000); }

/* Any delete or remove: the secondary's shape in the error colours. The
   filled red above is kept for the confirmation, so red means "destructive"
   everywhere and "filled red" means "this is the point of no return". The
   hover is the error tint, a v3 role: the v2 mix (--btn-danger-hover) had no
   v3 token (migration-plan.md, "Found along the way" 4). No borderless
   variants: `ghost` and `quiet` were removed 2026-09-27. */
.btn--danger{ background:transparent; border-color:var(--pf-error); color:var(--pf-error); }
.btn--danger:hover,.btn--danger.is-hover{ background:var(--pf-error-bg); }
.btn--danger:active,.btn--danger.is-active{ background:var(--pf-error-bg); border-color:var(--pf-error-solid); }

/* v3 disables by opacity (--opacity-disabled, 0.45): the button keeps its
   own colours, so a disabled primary still reads as the primary. */
.btn[disabled],.btn[aria-disabled="true"],.btn.is-disabled{
  opacity:var(--opacity-disabled); cursor:not-allowed; pointer-events:none;
}

/* sm keeps a 44px hit area without growing its box */
/* An icon that closes or dismisses — IconButton. No box until hovered, 44px
   of target (the .btn--sm device). The one borderless control (§3.1), and
   only for a close: a bordered ✕ gave the least important control in a
   dialog or a banner the weight of an action. */
.iconbtn{
  position:relative; flex:none; display:inline-grid; place-items:center;
  width:var(--control-sm); height:var(--control-sm); padding:0; margin:0;
  border:0; border-radius:var(--radius-md); background:transparent;
  color:var(--pf-fg-muted); cursor:pointer;
  transition:background-color var(--duration-sm) var(--ease-standard), color var(--duration-sm) var(--ease-standard);
}
.iconbtn::after{ content:""; position:absolute; inset:-6px; }
.iconbtn:hover{ background:var(--pf-surface-sunken); color:var(--pf-fg); }
.iconbtn:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }

/* A disclosure in a line of text — "Job description ⌄" on the application's
   meta line. Text, underlined, with the chevron turning when open: the
   control that opens a thing is the control that closes it. */
.disclosure{
  position:relative; display:inline-flex; align-items:center; gap:var(--space-1);
  padding:0; border:0; background:none; cursor:pointer;
  font:inherit; color:var(--pf-fg-secondary);
  text-decoration:underline; text-decoration-color:var(--pf-border-input);
  text-underline-offset:3px;
}
.disclosure::after{ content:""; position:absolute; inset:-12px -4px; }
.disclosure:hover{ color:var(--pf-fg); text-decoration-color:currentColor; }
.disclosure:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }
.disclosure > svg{ transition:transform var(--duration-sm) var(--ease-standard); }
.disclosure[aria-expanded="true"] > svg{ transform:rotate(180deg); }
@media (prefers-reduced-motion: reduce){ .disclosure > svg{ transition:none; } }

/* The job description, opened: at reading size in a quiet sunken panel,
   capped and scrolling rather than pushing the documents off the page. */
.jdpanel{
  max-height:40vh; overflow:auto; white-space:pre-wrap;
  padding:var(--space-3) var(--space-4);
  background:var(--pf-surface-sunken); border-radius:var(--radius-md);
  font:var(--type-body-weight) var(--type-body-size)/var(--type-body-leading) var(--font-sans); color:var(--pf-fg-secondary);
}
.jdpanel:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }

.btn--sm{ height:var(--control-sm); padding:0 var(--space-3); font-size:var(--type-label-size); position:relative; }
.btn--sm::after{ content:""; position:absolute; inset:-6px; }
.btn--lg{ height:var(--control-lg); padding:0 var(--space-5); }

/* Label persists while loading; width is locked by the spinner slot */
.btn__spinner{
  width:14px; height:14px; border-radius:50%; flex:none;
  border:2px solid currentColor; border-top-color:transparent;
  animation:pf-spin 700ms linear infinite;
}
@keyframes pf-spin{ to{ transform:rotate(360deg); } }
/* The spinner, on its own: the same ring as a button's, for `Working`. */
.spin{
  display:inline-block; width:16px; height:16px; border-radius:50%; flex:none;
  border:2px solid var(--pf-primary); border-top-color:transparent;
  animation:pf-spin 800ms linear infinite;
}
/* Slower under reduced motion, not frozen: a ring that stops reads as a hang.
   !important because the global reduced-motion rule (tokens.css) is, and it
   was cutting both spinners to a single 0.01ms turn. */
@media (prefers-reduced-motion: reduce){
  .btn__spinner,.spin{ animation-duration:1800ms !important; animation-iteration-count:infinite !important; }
}

/* A wait on the model (2026-10-04): the ring, what is happening, how long. */
.working{ display:flex; align-items:flex-start; gap:var(--space-3); }
/* On the first line of a wrapped sentence, not its middle. */
.working > .spin{ margin-top:3px; }
.working__time{ margin-top:2px; }
.working__text{ margin:0; flex:1; min-width:0; font-size:var(--type-body-size); color:var(--pf-fg-secondary); }
.working__time{ flex:none; font:var(--type-data-weight) var(--type-data-size)/var(--type-data-leading) var(--font-mono); color:var(--pf-fg-muted); font-variant-numeric:tabular-nums; }

/* ============================================================
   FORMS
   ============================================================ */

.field{ display:flex; flex-direction:column; gap:var(--space-1); }
.field__label{
  display:var(--field-label-display,block);
  font:var(--type-label-weight) var(--type-label-size)/var(--type-label-leading) var(--font-sans);
  letter-spacing:var(--type-label-tracking); color:var(--pf-fg);
}
.field__req{ color:var(--pf-error); margin-left:2px; }
/* A label with its icon before it (2026-10-04, the account fields). */
.field__label--icon{ display:flex; align-items:center; gap:var(--space-1); }
.field__label--icon > svg{ flex:none; color:var(--pf-fg-muted); }
.field__help{ font:var(--type-body-sm-weight) var(--type-body-sm-size)/var(--type-body-sm-leading) var(--font-sans); color:var(--pf-fg-muted); }
.field__error{
  display:flex; align-items:flex-start; gap:var(--space-1);
  font:var(--font-weight-semibold) var(--type-body-sm-size)/var(--type-body-sm-leading) var(--font-sans); color:var(--pf-error);
}
/* §3.2: "Errors are never colour alone — icon plus text, always." The typed
   exclamation mark is the fallback for the hand-written marketing pages, which
   load this file with no React to render an icon into. Where a real icon is
   present it wins and the character stands down, so the two never double up. */
.field__error::before{ content:"!"; flex:none; font-weight:700; }
.field__error:has(> svg)::before{ content:none; }

.input,.textarea,.select{
  width:100%; height:var(--control-md); padding:0 var(--space-3);
  font:var(--type-body-weight) var(--type-body-size)/1.4 var(--font-sans); color:var(--pf-fg);
  background:var(--pf-surface);
  border:1.5px solid var(--pf-border-input);
  border-radius:var(--radius-md);
  transition:border-color var(--duration-sm) var(--ease-standard),
             background-color var(--duration-sm) var(--ease-standard);
}
.textarea{ height:auto; min-height:96px; padding:var(--space-3); line-height:var(--type-body-leading); resize:vertical; }
.select{ appearance:none; padding-right:var(--space-8); background-image:none; }
/* The small control height, for a select that sits in a row of text rather
   than in a form — the stage select beside an application's title. The text
   stays 16px: below that, iOS zooms the page on focus. */
.select--sm{ height:var(--control-sm); }

/* `.select` removes the native arrow and replaced it with nothing — a select
   with no affordance. The chevron is drawn with borders rather than an image,
   so it follows the text colour into dark mode for free and this file keeps
   its property of shipping no image assets. The wrapper exists only because a
   <select> cannot carry a pseudo-element of its own.

   Icons (design-system.md §3.8) do not change this. They are React elements
   rendered into markup, never url() or mask-image in a stylesheet, so this
   file and tokens.css both still ship no image assets and both still work
   standalone on the hand-written marketing pages. */
.select-wrap{ position:relative; display:block; }
.select-wrap::after{
  content:""; position:absolute; right:var(--space-4); top:50%;
  width:7px; height:7px; margin-top:-6px;
  border-right:1.5px solid var(--pf-fg-muted);
  border-bottom:1.5px solid var(--pf-fg-muted);
  transform:rotate(45deg);
  pointer-events:none;   /* the click still belongs to the select */
}

/* The `font` shorthand above resets font-variant-numeric to normal, and this
   file's layer beats the one tokens.css lives in — so the .pf-tabular utility
   silently loses on any control. Every other numeric surface in this file
   (.balance, .pack__price, .recorder__time) re-declares it for the same
   reason; a control needs the same courtesy. Matters for OTP codes, phone
   numbers, years and credential IDs, where digits sit in a column. */
.input.pf-tabular,.textarea.pf-tabular,.select.pf-tabular{ font-variant-numeric:tabular-nums; }

.input::placeholder,.textarea::placeholder{ color:var(--pf-fg-muted); }

.input:hover,.textarea:hover,.select:hover,
.input.is-hover,.textarea.is-hover,.select.is-hover{ border-color:var(--pf-fg-secondary); }

/* v3 rings a field 1px off its edge, not 2: the edge takes the focus
   colour too, so the two read as one thick line. */
.input:focus,.textarea:focus,.select:focus,
.input.is-focus,.textarea.is-focus,.select.is-focus{
  border-color:var(--pf-focus); outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:1px;
}

.input[aria-invalid="true"],.textarea[aria-invalid="true"],.is-error{ border-color:var(--pf-error); }

.input[disabled],.textarea[disabled],.select[disabled],.is-disabled-input{
  opacity:var(--opacity-disabled); cursor:not-allowed;
}
.input[readonly],.textarea[readonly],.is-readonly{
  background:var(--pf-surface-sunken); border-color:var(--pf-border-strong); color:var(--pf-fg-secondary);
}

/* Compose mode wants a lighter field. It cannot go borderless — a sunken
   fill on the page is barely 1.1:1 and fails WCAG 1.4.11 — so the 3:1 input
   edge stays, as a bottom rule. */
.input--bare{
  background:transparent; border:0;
  border-bottom:var(--hairline) solid var(--pf-border-input);
  border-radius:0; padding-left:2px; padding-right:2px;
}
.input--bare:hover,.input--bare.is-hover{ border-bottom-color:var(--pf-fg-secondary); }
/* The ring, like every other control. Without it this was the one focusable
   thing in the app whose *only* focus signal was a border colour — tolerable
   while it was unbuilt spec, a 2.4.7 failure the moment the applications
   search field started using it. */
.input--bare:focus,.input--bare.is-focus{
  border-bottom-color:var(--pf-focus);
  outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset);
}

/* A fixed prefix inside a field — "+91" before a mobile number (added
   2026-09-27). One box to the eye: the prefix takes the input's border and
   radius on its left, the input gives up its own there. The prefix is text,
   not a control, and is aria-hidden; the field's help says what it is. */
.input-affix{ display:flex; align-items:stretch; width:100%; }
.input-affix__prefix{
  display:inline-flex; align-items:center; padding:0 var(--space-3);
  font:var(--font-weight-medium) var(--type-body-size)/1 var(--font-sans); font-variant-numeric:tabular-nums; color:var(--pf-fg-muted);
  background:var(--pf-surface-sunken);
  border:1.5px solid var(--pf-border-input); border-right:0;
  border-radius:var(--radius-md) 0 0 var(--radius-md);
}
.input-affix > .input{ flex:1; min-width:0; border-top-left-radius:0; border-bottom-left-radius:0; }

/* choice controls */
.choice{ display:flex; align-items:flex-start; gap:var(--space-2); cursor:pointer; padding:var(--space-2) 0; }
.choice__box{
  appearance:none; flex:none; width:18px; height:18px; margin:1px 0 0;
  background:var(--pf-surface);
  border:1.5px solid var(--pf-border-input); border-radius:var(--radius-sm);
  display:grid; place-content:center; cursor:pointer;
  transition:background-color var(--duration-sm) var(--ease-standard), border-color var(--duration-sm) var(--ease-standard);
}
.choice__box--radio{ border-radius:50%; }
.choice__box:hover,.choice__box.is-hover{ border-color:var(--pf-fg-secondary); }
.choice__box:focus-visible,.choice__box.is-focus{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.choice__box:checked,.choice__box.is-checked{ background:var(--pf-primary); border-color:var(--pf-primary); }
.choice__box:checked::after,.choice__box.is-checked::after{
  content:""; width:10px; height:6px; border:2px solid var(--pf-on-primary);
  border-top:0; border-right:0; transform:rotate(-45deg) translate(1px,-2px);
}
.choice__box--radio:checked::after,.choice__box--radio.is-checked::after{
  content:""; width:6px; height:6px; border:0; border-radius:50%;
  background:var(--pf-on-primary); transform:none;
}
.choice__box:indeterminate::after{
  content:""; width:10px; height:2px; background:var(--pf-on-primary); border:0; transform:none;
}
.choice__box[disabled]{ opacity:var(--opacity-disabled); cursor:not-allowed; }
.choice__text{ font:var(--type-body-weight) var(--type-body-size)/var(--type-body-leading) var(--font-sans); color:var(--pf-fg-secondary); }

/* Switch — only for changes that take effect with no save step (§3.2).
   First rendered 2026-09-27, for the channel set. The off track is the 3:1
   input edge (border-input): a switch is a control, and 1.4.11 wants its
   resting state at 3:1 against the ground. The thumb is white in both
   themes, so it reads against the track whichever way it is set. */
.switch{
  appearance:none; position:relative; flex:none; width:36px; height:20px; margin:2px 0 0;
  background:var(--pf-border-input); border:0; border-radius:var(--radius-full); cursor:pointer;
  transition:background-color var(--duration-sm) var(--ease-standard);
}
.switch::after{
  content:""; position:absolute; top:2px; left:2px; width:16px; height:16px;
  background:var(--color-white); border-radius:50%;
  transition:transform var(--duration-sm) var(--ease-standard);
}
.switch:checked,.switch.is-checked{ background:var(--pf-primary); }
.switch:checked::after,.switch.is-checked::after{ transform:translateX(16px); }
.switch:focus-visible,.switch.is-focus{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.switch[disabled]{ opacity:var(--opacity-disabled); cursor:not-allowed; }
/* A switch that cannot move *right now* but still says what it is — the last
   channel left on. Keeps its colours, because greying an "on" switch makes it
   read as "off". The sentence beside it says why. */
.switch[aria-disabled="true"]{ cursor:not-allowed; }
@media (prefers-reduced-motion: reduce){ .switch,.switch::after{ transition:none; } }

/* A settings row: what it is on the left, the switch on the right, the whole
   row the hit area. The conventional shape, on purpose (§0.1: chrome is
   conventional). */
.switchrow{
  display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-4);
  padding:var(--space-3) 0; cursor:pointer;
  border-bottom:var(--hairline) solid var(--pf-border);
}
.switchrow:first-child{ border-top:var(--hairline) solid var(--pf-border); }
.switchrow__text{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.switchrow__label{ font:var(--type-label-lg-weight) var(--type-body-size)/1.35 var(--font-sans); color:var(--pf-fg); }
.switchrow__note{ font:var(--type-body-sm-weight) var(--type-body-sm-size)/var(--type-body-sm-leading) var(--font-sans); color:var(--pf-fg-muted); }

/* Segmented control — one of a few, all visible, for a setting that applies
   the moment it is picked. A radio group underneath: arrow keys move between
   options, and the selection is announced as one. Added 2026-09-27 for
   Appearance, whose button cycled through three states and showed only the
   current one, so nobody could see what the others were without pressing. */
.segmented{
  display:inline-flex; align-self:flex-start; gap:2px; padding:2px; margin:0;
  background:var(--pf-surface);
  border:var(--hairline) solid var(--pf-border-input); border-radius:var(--radius-md);
}
.segmented__option{ position:relative; display:inline-flex; cursor:pointer; }
.segmented__option input{ position:absolute; inset:0; opacity:0; margin:0; cursor:pointer; }
/* `>`: the option's own label, not every span inside it. A count nested in
   the label (the admin portal's "Spends 60") took the hover's light fill
   while inheriting the selected option's white text — unreadable
   (2026-09-29). Nested spans now just inherit. */
.segmented__option > span{
  display:inline-flex; align-items:center; gap:var(--space-2);
  height:calc(var(--control-md) - 6px); padding:0 var(--space-3);
  font:var(--type-label-weight) var(--type-label-size)/1 var(--font-sans); color:var(--pf-fg-secondary);
  border-radius:calc(var(--radius-md) - 2px);
  transition:background-color var(--duration-sm) var(--ease-standard), color var(--duration-sm) var(--ease-standard);
}
/* 44px target without growing the control — the .btn--sm device. */
.segmented__option::after{ content:""; position:absolute; inset:-5px 0; }
/* On a phone a choice of options fills the width in equal parts, so three
   options with icons cannot overflow a dialog (2026-10-04). */
@media (max-width:639px){
  .segmented--fill{ display:flex; align-self:stretch; width:100%; }
  .segmented--fill .segmented__option{ flex:1; min-width:0; }
  .segmented--fill .segmented__option > span{ flex:1; justify-content:center; padding:0 var(--space-2); }
}
.segmented__option:hover > span{ background:var(--pf-surface-sunken); }
/* The selected option in the primary fill — the same "on" as a checked
   switch or checkbox. A tinted fill was tried and was fainter than the hover,
   which is a selected state nobody can find (1.4.11). */
.segmented__option input:checked + span{ background:var(--pf-primary); color:var(--pf-on-primary); }
.segmented__option input:focus-visible + span{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
/* An option that is not available — Speak, once an idea has its one
   recording (2026-09-28). The words stay readable and say why nearby; the
   pointer says it cannot be picked. */
.segmented__option:has(input:disabled){ cursor:not-allowed; }
.segmented__option input:disabled{ cursor:not-allowed; }
.segmented__option input:disabled + span{ opacity:var(--opacity-disabled); background:none; }

/* Typeface picker (Settings → Documents). Added 2026-09-27. Each option is
   the top of the user's own resume set in that face — the sample reads
   --font-serif and --font-sans like the real sheet, so [data-typeface]
   styles it with no rule of its own. */
.typeface{ display:flex; flex-direction:column; gap:var(--space-3); }
.typeface__choice{ display:flex; align-items:center; gap:var(--space-2); }
.typeface__label{ font:600 var(--text-body-size)/1.3 var(--font-sans); color:var(--text); }
.typeface__sample{
  display:flex; flex-direction:column; gap:2px;
  padding:var(--space-3) var(--space-4);
  background:var(--paper); border:var(--hairline) solid var(--border-divider);
  border-radius:var(--radius-control);
}
.typeface__name{ font:400 22px/1.15 var(--font-serif); color:var(--text); }
.typeface__contact{ font:400 12px/1.4 var(--font-sans); color:var(--text-muted); margin-bottom:var(--space-2); }
.typeface__head{
  font:600 11px/1.4 var(--font-sans); letter-spacing:var(--tracking-overline); text-transform:uppercase;
  color:var(--text-muted); border-top:var(--hairline) solid var(--border-divider); padding-top:var(--space-1);
}
.typeface__role{ font:600 14px/1.35 var(--font-sans); color:var(--text); }
.typeface__note{ font:400 var(--text-label)/1.45 var(--font-sans); color:var(--text-muted); }

/* Chips — a tag list, for skills. v3's chip: a sunken pill with no edge,
   label weight. It is decorative, not a control boundary; the interactive
   part is the remove button inside it, which carries its own focus ring and
   its own 44px target. */
.chips{ display:flex; flex-wrap:wrap; gap:var(--space-2); }
.chip{
  display:inline-flex; align-items:center; gap:var(--space-2);
  padding:var(--space-1) var(--space-3);
  background:var(--pf-surface-sunken);
  border:var(--hairline) solid transparent;
  border-radius:var(--radius-full);
  font:var(--type-label-weight) var(--type-label-size)/1.5 var(--font-sans);
  color:var(--pf-fg-secondary);
}
/* A state, as a chip (compare.html): where an application stands, a pack
   that is recommended. The word is the signal; the tint only reinforces it
   (1.4.1). Every pair is a v3 role pair, checked by lint:contrast. Volt
   (--accent) is for good news and highlights only, and always carries its
   word: an offer, "Recommended". */
.chip--state{ padding:2px var(--space-3); line-height:1.4; white-space:nowrap; flex:none; }
.chip--neutral{ background:var(--pf-surface-sunken); color:var(--pf-fg-secondary); }
.chip--primary{ background:var(--pf-primary-soft); color:var(--pf-on-primary-soft); }
.chip--success{ background:var(--pf-success-bg); color:var(--pf-success); }
.chip--info{ background:var(--pf-info-bg); color:var(--pf-info); }
.chip--warning{ background:var(--pf-warning-bg); color:var(--pf-warning); }
.chip--error{ background:var(--pf-error-bg); color:var(--pf-error); }
.chip--accent{ background:var(--pf-accent); color:var(--pf-on-accent); }
.chip__remove{
  position:relative; flex:none;
  display:inline-flex; align-items:center; justify-content:center;
  width:var(--icon-sm); height:var(--icon-sm); padding:0;
  border:0; background:none; cursor:pointer;
  border-radius:var(--radius-full);
  font:400 13px/1 var(--font-sans); color:var(--pf-fg-muted);
}
/* 44px target without growing the chip — same device as .btn--sm. */
.chip__remove::after{ content:""; position:absolute; inset:-13px; }
.chip__remove:hover{ color:var(--pf-fg); }
.chip__remove:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }

/* ============================================================
   CARDS — the raised surface, a hairline and the resting shadow (v3 §6).
   In dark the lift is the lighter surface and the shadow's top highlight.
   ============================================================ */

.card{
  background:var(--pf-surface-raised);
  border:var(--hairline) solid var(--pf-border);
  border-radius:var(--radius-lg);
  box-shadow:var(--pf-shadow-sm);
  padding:var(--space-5);
  transition:border-color var(--duration-sm) var(--ease-standard),
             background-color var(--duration-sm) var(--ease-standard),
             box-shadow var(--duration-sm) var(--ease-standard);
}
.card--interactive{ cursor:pointer; display:block; width:100%; text-align:left; }
/* A card can be a <button> as well as an <a> — the profile's first-run doors
   act on the page they are on rather than going somewhere. `.card` already
   sets the border, ground, radius and padding; what a button brings of its
   own is type and ink, so it inherits both instead of the UA's. The ink
   matters most: `.card--offer` sets its colours on its children, but a stray
   text node would otherwise take `buttontext` against the dark door. */
button.card{ font:inherit; color:inherit; letter-spacing:inherit; line-height:inherit; }
/* Hover goes one elevation step up (§6). The lift itself is motion, 2b's. */
.card--interactive:hover,.card--interactive.is-hover{ border-color:var(--pf-border-strong); box-shadow:var(--pf-shadow-md); }
.card--interactive:focus-visible,.card--interactive.is-focus{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
/* Selected: the primary edge, and the selected tint behind it (§8's
   primary-soft "active" state). */
.card--selected{ border-color:var(--pf-primary); box-shadow:0 0 0 1px var(--pf-primary), var(--pf-shadow-sm); }
.card--disabled{ opacity:var(--opacity-disabled); box-shadow:none; cursor:not-allowed; }

/* A card inside a card — one role inside Experience, one degree inside
   Education. It takes the warm ground rather than the white page, which is the
   §0.1 inversion in miniature: the section is the sheet, the record sits on it.
   Two identical white boxes nested would read as a rendering mistake. */

/* ---------- profile sections (2026-10-03) ----------
   A section is a heading over a hairline, and a record is a row inside
   it. They were a card holding cards, six times down the page: three borders
   deep inside the form, which is the "everything boxed" look of a settings
   template rather than a document someone is writing about themselves. */
.profsection{ padding-top:var(--space-6); border-top:var(--hairline) solid var(--pf-border); }
.profsection:first-child{ border-top:0; padding-top:0; }
.profsection__title{ margin:0; font-size:var(--type-h2-size); line-height:var(--type-h2-leading); outline:none; }
.entry{ padding-top:var(--space-4); border-top:var(--hairline) solid var(--pf-border); }
.entry .card__title{ font-size:var(--type-h3-size); }

/* ---------- the open record is a sheet (2026-10-04) ----------
   Sections and closed records stay unboxed: a heading over a hairline, and
   rows. The one record being edited is the exception, because it is the one
   object being acted on, and an open role runs to a screen and a half. It sits
   on the raised surface with a hairline and the resting shadow.

   Never a card in a card. Inside an organisation with several roles, the open
   role is marked by a 2px brand rule on the organisation's rail, not by a
   second box. */
.entry[data-open="true"]{
  margin-block:var(--space-3);
  padding:var(--space-5);
  background:var(--pf-surface-raised);
  border:var(--hairline) solid var(--pf-border);
  border-radius:var(--radius-lg);
  box-shadow:var(--pf-shadow-sm);
}
/* Entries are each wrapped in a [data-entry] div, so the neighbour is found
   through the wrapper: the row after an open sheet needs no hairline above. */
[data-entry]:has(> .entry[data-open="true"]) + [data-entry] > .entry{ border-top-color:transparent; }
.entry__positions .entry[data-open="true"]{
  margin:var(--space-2) 0 var(--space-2) calc(-1 * (var(--space-4) + var(--hairline)));
  padding:var(--space-4) 0 var(--space-2) calc(var(--space-4) - var(--rule) + var(--hairline));
  background:none; border:0; border-radius:0; box-shadow:none;
  border-left:var(--rule) solid var(--pf-primary);
}

/* The open record's head stays in view while you work down a long role, so
   it still says which role and which company, with its Done beside it. Below
   lg it clears the sticky masthead; from lg the masthead scrolls away. */
.entry__head--sticky{
  position:sticky; top:var(--topbar-h); z-index:5;
  margin-inline:calc(-1 * var(--space-5)); padding:var(--space-2) var(--space-5);
  margin-top:calc(-1 * var(--space-2));
  background:var(--pf-surface-raised);
}
@media (min-width:1024px){ .entry__head--sticky{ top:0; } }
.entry__positions .entry__head--sticky{
  margin-inline:0 0; padding-inline:0;
}
.entry__context{ margin:0 0 2px; font:var(--font-weight-medium) var(--type-body-sm-size)/1.3 var(--font-sans); color:var(--pf-fg-muted); }
/* Pinned, the title keeps to one line so the head stays slim on a phone. */
.entry__head--sticky h3{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.entry__foot{ display:flex; justify-content:flex-start; padding-top:var(--space-3); border-top:var(--hairline) solid var(--pf-border); }

/* ---------- start cards ----------
   A way into something: the two on Home, the two on an empty profile. A
   raised v3 card (§6): the thing's icon on a primary-soft chip (§8's active
   state), an h4 title with an arrow, one sentence, and what it costs. Hover
   goes one elevation step up; the arrow's nudge and the lift are motion,
   session 2b's. */
.start{
  display:flex; flex-direction:column; align-items:flex-start; gap:var(--space-2);
  width:100%; padding:var(--space-5); margin:0;
  background:var(--pf-surface-raised); border:var(--hairline) solid var(--pf-border);
  border-radius:var(--radius-lg); box-shadow:var(--pf-shadow-sm);
  text-align:left; text-decoration:none; color:inherit; cursor:pointer;
  font:inherit;
  transition:box-shadow var(--duration-sm) var(--ease-standard), border-color var(--duration-sm) var(--ease-standard);
}
.start:hover{ box-shadow:var(--pf-shadow-md); border-color:var(--pf-border-strong); }
.start:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.start__icon{
  display:grid; place-items:center; width:40px; height:40px; margin-bottom:var(--space-2);
  border-radius:var(--radius-md); background:var(--pf-primary-soft); color:var(--pf-on-primary-soft);
}
.start__title{
  display:flex; align-items:center; gap:var(--space-2);
  margin:0; font:var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-leading) var(--font-display);
  letter-spacing:var(--type-h4-tracking); color:var(--pf-fg);
}
.start__arrow{ color:var(--pf-primary-text); transition:transform var(--duration-sm) var(--ease-standard); }
.start__body{ margin:0; color:var(--pf-fg-secondary); max-width:44ch; }
.start__note{
  margin:auto 0 0; padding-top:var(--space-2);
  font:var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-leading) var(--font-sans);
  letter-spacing:var(--type-caption-tracking); color:var(--pf-fg-muted);
}

/* ---------- empty states ----------
   v3's empty state (compare.html, "Core components"): a dashed edge, so it
   reads as a space waiting for something rather than a card that failed to
   fill; the thing's icon; a title that says what is true; one sentence; the
   action. Centred, because it is the whole of what the region holds. */
.empty{
  display:flex; flex-direction:column; align-items:center; gap:var(--space-3);
  padding:var(--space-8) var(--space-5); text-align:center;
  border:1.5px dashed var(--pf-border-strong); border-radius:var(--radius-lg);
}
.empty__icon{ color:var(--pf-fg-muted); }
.empty__icon > svg{ width:32px !important; height:32px !important; }
.empty__title{
  margin:0; font:var(--type-h3-weight) var(--type-h3-size)/var(--type-h3-leading) var(--font-display);
  letter-spacing:var(--type-h3-tracking); color:var(--pf-fg);
}
.empty__body{ max-width:var(--measure-ui); color:var(--pf-fg-secondary); }
.empty .cluster{ justify-content:center; }

/* ---------- work rows ----------
   Things the product has written — applications, ideas — as v3's app rows
   (compare.html, "In the app"): one raised card per item, the title and
   where it is from on the left, the state as a chip on the right, the facts
   on a line underneath with figures in mono. Replaced the hairline list and
   the applications table on 2026-10-13.

   The whole row is the target: the title's link stretches over the card,
   so it is one tab stop and the ring frames what you are choosing. */
.worklist{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--space-3); }
.workrow{
  position:relative; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:var(--space-1) var(--space-3);
  padding:var(--space-4);
  background:var(--pf-surface-raised); border:var(--hairline) solid var(--pf-border);
  border-radius:var(--radius-md); box-shadow:var(--pf-shadow-sm);
  transition:box-shadow var(--duration-sm) var(--ease-standard), border-color var(--duration-sm) var(--ease-standard);
}
.workrow:hover{ box-shadow:var(--pf-shadow-md); border-color:var(--pf-border-strong); }
.workrow__main{ min-width:0; }
.workrow__title{
  font:var(--font-weight-strong) var(--type-body-size)/1.35 var(--font-sans);
  color:var(--pf-fg); text-decoration:none; overflow-wrap:anywhere;
}
.workrow__title::after{ content:""; position:absolute; inset:0; border-radius:inherit; }
.workrow__title:focus-visible{ outline:none; }
.workrow:has(.workrow__title:focus-visible){ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.workrow__sub{ margin:2px 0 0; font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); color:var(--pf-fg-muted); }
.workrow__side{ display:flex; flex-direction:column; align-items:flex-end; gap:var(--space-1); }
.workrow__meta{
  grid-column:1 / -1; display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-1) var(--space-3);
  margin:var(--space-1) 0 0; font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); color:var(--pf-fg-secondary);
}
.workrow__date{ margin-left:auto; color:var(--pf-fg-muted); }
.workrow__chips{ display:flex; flex-wrap:wrap; justify-content:flex-end; gap:var(--space-2); }
.workrow__cells{ grid-column:1 / -1; }
.workrow__cells .cells--sm{ margin-top:0; }

/* v3's h4 (18px, 650): the step under a section's h2 and h3, so a card
   title never out-ranks the heading that introduces it. */
.card__title{
  font:var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-leading) var(--font-display);
  letter-spacing:var(--type-h4-tracking); color:var(--pf-fg); margin:0 0 var(--space-2);
}

/* A card that groups a whole settings concern needs a header its actions can
   sit in, and a rule that reaches the card's edges rather than stopping at the
   text. The negative inline margins pull the divider out through .card's
   padding; --space-5 is that padding, so the two move together.

   A divider at 1px, not a brand rule. */
.card__header{
  display:flex; align-items:baseline; justify-content:space-between;
  flex-wrap:wrap; gap:var(--space-3);
  margin:calc(var(--space-5) * -1) calc(var(--space-5) * -1) var(--space-4);
  padding:var(--space-4) var(--space-5);
  border-bottom:var(--hairline) solid var(--pf-border);
}
.card__header > .card__title{ margin:0; }

/* In a .stack the gap already separates the header from the body, and the
   margin would add to it rather than replace it — 32px where 16 was meant. */
.stack > .card__header{ margin-bottom:0; }
.stack > .card__footer{ margin-top:0; }

.card__footer{
  display:flex; align-items:center; flex-wrap:wrap; gap:var(--space-3);
  margin:var(--space-4) calc(var(--space-5) * -1) calc(var(--space-5) * -1);
  padding:var(--space-4) var(--space-5);
  border-top:var(--hairline) solid var(--pf-border);
}

/* Status cards are v3's alert (compare.html): a tinted fill, a 3px rule in
   the state's colour, the icon and title in that colour, the body in the
   ink colour. The title is what satisfies 1.4.1 — colour is never the only
   signal. */
.status{
  border-radius:var(--radius-md);
  border:var(--hairline) solid transparent;
  border-left:var(--marker) solid var(--status-rule);
  background:var(--status-bg);
  color:var(--status-body, var(--pf-fg));
  padding:var(--space-3) var(--space-4);
}
.status__label{
  display:flex; align-items:center; gap:var(--space-2);
  font:var(--font-weight-bold) var(--type-body-size)/1.4 var(--font-sans); margin:0 0 2px;
  color:var(--status-text);
}
.status__body{ margin:0; font:400 var(--type-body-size)/1.5 var(--font-sans); }
/* The same card on one row: label, then the sentence, then whatever acts on
   it. Added 2026-09-27 for the application page's "Draft ready", which as a
   two-line card was one of four blocks between the title and the documents.
   The text label stays — it is what carries the meaning (1.4.1). */
.status--inline{
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-1) var(--space-3);
  padding-block:var(--space-1);
}
.status--inline .status__label{ margin:0; }
.status--inline .status__body{ flex:1; min-width:0; }

/* An error summary is a status card holding links — §5.4 asks for one at the
   top of every long form, focused on submit, linking to each failing field.
   Links inherit the status colour so the tint stays coherent, and they keep
   their underline because colour is never the only signal. */
.status__body ul{ margin:var(--space-2) 0 0; padding-left:var(--space-4); }
.status__body li{ margin-bottom:var(--space-1); }
.status a:not(.btn){ color:inherit; text-underline-offset:2px; }
.status a:not(.btn):focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }
/* A secondary button inside a tinted card sits on the surface, so the
   tint does not show through it and it reads as a control (2026-10-04).
   Links styled as buttons keep the button's own colours: the rule above
   used to repaint them in the card's ink. */
.status .btn--secondary{ background:var(--pf-surface); }
.status .btn--secondary:hover,.status .btn--secondary.is-hover{ background:var(--pf-surface-sunken); }

/* The same three tokens, inline and compact — a state on one row of a list
   rather than a card of its own. It takes the `.status--*` modifiers without
   `.status`, because those modifiers set only the --status-* trio and `.status`
   is what consumes them; that is what makes it impossible for the label in a
   list and the card in a workspace to drift to different colours.

   Uppercase 11px with the 2px rule as a ::before, mirroring `.status__label`
   so the two read as one device at two sizes. The word is the signal and the
   colour is the reinforcement, never the other way round (§5.1 / 1.4.1). */
/* .pf-state, the 2px rule and a coloured word, was retired 2026-10-13 for
   v3's state chips (.chip--*, below with the chips). */

.status--gap{   --status-bg:var(--pf-partial-bg);     --status-rule:var(--pf-partial);     --status-text:var(--pf-partial); }
.status--notice{ --status-bg:var(--pf-info-bg); --status-rule:var(--pf-info); --status-text:var(--pf-info); }
.status--ready{ --status-bg:var(--pf-success-bg); --status-rule:var(--pf-success); --status-text:var(--pf-success); }
.status--error{ --status-bg:var(--pf-error-bg);   --status-rule:var(--pf-error);   --status-text:var(--pf-error); }
.status--recording{ --status-bg:var(--pf-primary-soft); --status-rule:var(--pf-primary-text); --status-text:var(--pf-on-primary-soft); }

/* "Still writing." Neutral on purpose: §1.5 spends the colour budget on gap /
   ready / error / recording, and this is none of those — it is the absence of
   a state, not a state. Neutral keeps the anatomy identical to .status--ready,
   which is the point. The workspaces currently render a Status card when the
   draft is ready and a bare <p> while it is still writing, so the block
   changes shape underneath the user at the exact moment the product delivers. */
.status--working{ --status-bg:var(--pf-surface-sunken); --status-rule:var(--pf-border-input); --status-text:var(--pf-fg); --status-body:var(--pf-fg-secondary); }

/* "Exported." Done, and asking for nothing — so the quietest of the states:
   muted text and a faint rule. Added 2026-09-27; exported rows borrowed
   .status--gap, the amber "worth knowing", which made the most finished
   state on the applications list the one that looked most in need of
   attention. Ready keeps the green because Ready is the one still to act on.
   The word carries the state; the rule only reinforces it (1.4.1). */
.status--done{ --status-bg:transparent; --status-rule:var(--pf-border-strong); --status-text:var(--pf-fg-muted); --status-body:var(--pf-fg-muted); }

/* The autosave state, pinned where you are — design-system §5.4 wants it
   "visible persistently", and on the profile builder the only indicator sat
   at the top of a page five screens long: while you typed into a role it was
   1,300px above you, and "Not saved — we'll keep trying" was shown nowhere
   else. This appears only when it matters (failed, or a save that is taking
   a while) and takes the status palette through the same trio as
   `.pf-state`, so it cannot drift from the cards.

   Top, not bottom. Below `lg` the bottom bar's height is content plus the
   safe-area inset and is no token; the top bar is `--topbar-h`, and the top
   is where the page's own indicator taught people to look. */
.pf-savenotice{
  position:fixed; z-index:30; right:var(--space-4);
  top:calc(var(--topbar-h) + var(--space-3));
  max-width:calc(100vw - 2 * var(--space-4));
  padding:var(--space-2) var(--space-3);
  background:var(--status-bg); color:var(--status-text);
  border:var(--hairline) solid var(--pf-border);
  border-left:var(--rule) solid var(--status-rule);
  border-radius:var(--radius-md);
  box-shadow:var(--pf-shadow-md);
  font:var(--font-weight-medium) var(--type-body-sm-size)/1.4 var(--font-sans);
}
@media (min-width:1024px){ .pf-savenotice{ top:var(--space-4); } }

/* ============================================================
   THE PROFILE, AS AN OVERVIEW (2026-09-27)
   The user's review: 5,400px on a desktop with two roles, every role always
   open at ~1,500px, eleven red Remove buttons and no way between six
   sections but scrolling. Records now close to one line, one open at a time;
   a line's remove is a small ✕; and a wide screen lists the sections beside
   the form, in the half that was empty.
   ============================================================ */

/* A record's head: the title and, closed, its one-line summary on the left;
   Remove (open only) and Edit / Done on the right. */
.entry__head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-2) var(--space-4); }
.entry__summary{
  margin:var(--space-1) 0 0;
  font:400 var(--type-body-sm-size)/1.45 var(--font-sans); color:var(--pf-fg-muted);
}
.entry__actions{ flex:none; gap:var(--space-2); }
/* Edit carries a pencil and Done a chevron that points up, the way the
   record closes (2026-10-04). It was one chevron turned half a circle, which
   with two different glyphs would turn the up one back down. */
/* A role that does not say what it covered, on its closed line (2026-10-04,
   `readiness`): words in the notice tone, never colour alone, and never the
   error colour, because nothing has gone wrong. */
.entry__flag{
  display:inline-block; margin-top:var(--space-1);
  padding:0 var(--space-2); border-radius:var(--radius-md);
  background:var(--pf-info-bg); color:var(--pf-info);
  font:500 var(--type-body-sm-size)/1.6 var(--font-sans);
}
/* The positions inside an organisation with several (2026-10-04): a nested
   list under the company, set in from the left on a hairline so the roles
   read as belonging to it. */
.entry__positions{
  margin-left:var(--space-2); padding-left:var(--space-4);
  border-left:var(--hairline) solid var(--pf-border);
}
.entry__positions .entry{ padding-top:var(--space-3); }
.entry__positions .entry .card__title{ font-size:var(--type-body-size); }
/* Held open by a field in error, after "Check my profile". Words, not a
   disabled button: a control that does nothing is a puzzle. */
.entry__locked{ font:500 var(--type-body-sm-size)/1.4 var(--font-sans); color:var(--pf-error); white-space:nowrap; }

/* One achievement: a textarea as tall as its text, from one line — the
   height of an input beside it — and its ✕. */
.pf-line{ display:flex; align-items:flex-start; gap:var(--space-2); }
/* A textarea that grows with its text from its usual height — a role's
   description. The same measuring as .pf-line__text, without the one-line
   start. */
.pf-grow{ resize:none; overflow:hidden; field-sizing:content; }
/* The new-idea box starts at about five lines and grows from there. */
.pf-grow--idea{ min-height:calc(5lh + 2 * var(--space-3) + 2 * var(--hairline)); }
.pf-line__text{
  flex:1; min-height:var(--control-md); padding-block:calc((var(--control-md) - 1lh) / 2 - 1px);
  resize:none; overflow:hidden; field-sizing:content;
}

/* The form in its measure, and the section list beside it once both fit
   beside the sidebar. Below that the list is not shown; the page is one
   column, as before. */
.pf-profilegrid{ display:block; }
/* The form keeps its measure at every width, list or no list: at 1024px it
   ran 729px, and a form wider than 640px reads as a mistake (section 1.6). */
.pf-profilegrid > :first-child{ max-width:var(--col-form); }
/* A jump lands with a little air above the section, not flush with the edge. */
.pf-profilegrid section{ scroll-margin-top:var(--space-6); }
.pf-sectionlist{ display:none; }
@media (min-width:1280px){
  .pf-profilegrid{
    display:grid; align-items:start; gap:var(--space-8);
    grid-template-columns:minmax(0, var(--col-form)) minmax(12rem, 16rem);
  }
  .pf-sectionlist{ display:block; position:sticky; top:var(--space-6); }
}
.pf-sectionlist__title{
  margin:0 0 var(--space-2);
  font:500 var(--type-body-sm-size)/1.4 var(--font-sans); color:var(--pf-fg-muted);
}
.pf-sectionlist ul{ list-style:none; margin:0; padding:0; border-left:var(--hairline) solid var(--pf-border); }
.pf-sectionlist a{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--space-3);
  padding:var(--space-2) var(--space-3);
  margin-left:calc(-1 * var(--hairline));
  border-left:var(--rule) solid transparent;
  text-decoration:none; color:var(--pf-fg);
  font:500 var(--type-body-sm-size)/1.4 var(--font-sans);
}
.pf-sectionlist a:hover{ background:var(--pf-surface-sunken); border-left-color:var(--pf-border-input); }
.pf-sectionlist a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:var(--radius-md); }
.pf-sectionlist__note{ color:var(--pf-fg-muted); font-weight:400; font-variant-numeric:tabular-nums; white-space:nowrap; }
.pf-sectionlist__note--error{ color:var(--pf-error); font-weight:500; }
.pf-sectionlist__note--notice{ color:var(--pf-info); font-weight:500; }
/* The quantities are data (brand-identity.md §5): the figure in mono, the
   word beside it in the sans. */
.pf-sectionlist__count{ font-family:var(--font-mono); font-weight:var(--type-data-weight); }
/* The save tick, motion pattern 5 ("lock-in", 2026-10-14): when a save lands,
   the sections it carried show the matched marker's Volt disc for a moment,
   sliding in and snapping, then fading. A confirmation of that save, never a
   lasting mark — a check that stayed would be a completeness score, which
   this list does not keep (lib/profile/outline.js). Decorative: the header's
   live region says the save. */
.pf-sectionlist__label{ display:inline-flex; align-items:center; gap:var(--space-2); }
.pf-savetick{
  display:grid; place-items:center; flex:none; width:16px; height:16px; border-radius:50%;
  background:var(--pf-match); color:var(--pf-on-match);
  animation:
    pf-lock-in var(--duration-xl) var(--ease-snap) both,
    pf-tick-out var(--duration-md) var(--ease-standard) 1.2s forwards;
}
.pf-savetick svg{ width:11px !important; height:11px !important; stroke-width:2.6 !important; }
@keyframes pf-tick-out{ to{ opacity:0; } }
.pf-sectionlist__save{
  margin:var(--space-4) 0 0; padding-left:var(--space-3);
  font:400 var(--type-body-sm-size)/1.4 var(--font-sans); color:var(--pf-fg-muted);
}

/* "Not written yet." Takes the module tint (§1.6 lists empty states as one of
   its three surfaces) and no fill, so it collides with neither the gap card in
   Applications nor the ready card anywhere. */
.status--empty{ --status-bg:transparent; --status-rule:var(--pf-gap-border); --status-text:var(--pf-fg); --status-body:var(--pf-fg-secondary); }

/* ============================================================
   MODAL
   ============================================================ */

.scrim{
  position:fixed; inset:0; background:var(--pf-scrim); z-index:50;
  display:grid; place-items:center; padding:var(--space-4);
  animation:pf-fade var(--duration-sm) var(--ease-standard);
}
/* A class-level `display` beats the UA rule for [hidden]. Without this
   every dialog renders on load — restate it at matching specificity. */
.scrim[hidden]{ display:none; }
.modal{
  /* Without this the panel cannot shrink below its content's min-content
     width: it is a grid item in .scrim, so `min-width` resolves to `auto`.
     The settings panel's pane list is four labels in a row on a phone, whose
     min-content is 473px — wider than a 375px viewport, so the panel hung off
     the right-hand edge with no way to scroll to it. */
  min-width:0;
  width:100%; max-width:560px; max-height:86vh; max-height:86dvh; display:flex; flex-direction:column;
  /* A sheet (§6): radius xl, shadow lg. In dark the raised surface is what
     lifts it off the page. */
  background:var(--pf-surface-raised); border-radius:var(--radius-xl);
  box-shadow:var(--pf-shadow-lg);
  animation:pf-rise var(--duration-sm) var(--ease-standard);
}
.modal--sm{ max-width:460px; } .modal--lg{ max-width:720px; }
/* The settings surface (§3.4a). A fixed height, not a fitted one: the panes
   are very different lengths — two fields in Personalization, a pack picker
   and a purchase ledger in Credits — and a dialog that resizes as you move
   down its own list is the list moving under the cursor that is clicking it. */
/* 960 wide (2026-09-27; it was 840) — the width of the content column, so
   the dialog is as wide as the page under it and no wider. The Documents
   pane's samples and the Credits pack picker were cramped at 840 once the
   200px pane list took its share. */
.modal--settings{ max-width:960px; height:min(86vh,620px); height:min(86dvh,620px); }
.modal__layout{ display:flex; align-items:stretch; min-height:0; min-width:0; flex:1; }
.modal__layout > .modal__body{ flex:1; min-width:0; padding-bottom:var(--space-6); }
.modal__nav{
  flex:none; min-width:0; overflow-y:auto;
  padding:0 var(--space-4) var(--space-6) var(--space-6);
  border-right:var(--hairline) solid var(--pf-border);
}
/* Below md the panel is most of a phone screen and 200px of it cannot be a
   column, so the list runs across the top — and wraps.

   *Revised 2026-09-26.* It was a horizontal scroller with its scrollbar
   hidden, the device the app uses for "several sections, not enough width".
   Measured at 320px: 517px of labels in a 240px strip, so "Credits", "Usage"
   and half of "Channels" showed, and Personalization and Account — the
   signed-in number and account deletion — started entirely out of sight,
   with a half-cut label as the only hint. A scroller suits a strip of many
   peers; five sections fit on two rows, and every one of them is then in
   view. Items still never shrink, so no label breaks across lines. */
@media (max-width:767px){
  .modal__layout{ flex-direction:column; }
  .modal__nav{
    padding:0 var(--space-6) var(--space-4);
    border-right:0; border-bottom:var(--hairline) solid var(--pf-border);
  }
  .modal__nav .sidenav{
    flex-direction:row; flex-wrap:wrap; column-gap:var(--space-1);
    max-width:none; width:100%;
  }
  .modal__nav .sidenav a{ flex:none; }
}
/* Six panes with icons ran to four rows at 320px (2026-10-04). On a phone the
   words carry the list on their own, so the glyphs go and it fits in two. */
@media (max-width:639px){
  .modal__nav .sidenav a > svg{ display:none; }
}

/* The account this menu belongs to, above its items. Not a menu item: there is
   nothing to activate, and making it one would put a no-op in the arrow-key
   cycle. */
.menu__identity{
  display:flex; align-items:center; gap:var(--space-3);
  padding:var(--space-3) var(--space-3) var(--space-4);
  font:400 var(--type-body-sm-size)/1.4 var(--font-sans);
}
.modal__header{
  display:flex; align-items:flex-start; gap:var(--space-4);
  padding:var(--space-6) var(--space-6) var(--space-4);
}
.modal__title{
  flex:1; margin:0; font:var(--type-h3-weight) var(--type-h3-size)/var(--type-h3-leading) var(--font-display);
  letter-spacing:var(--type-h3-tracking); color:var(--pf-fg);
}
.modal__title:focus{ outline:none; box-shadow:none; }
.modal__body{ padding:0 var(--space-6); overflow-y:auto; color:var(--pf-fg-secondary); }
.modal__footer{
  display:flex; flex-wrap:wrap; justify-content:flex-end; gap:var(--space-3);
  padding:var(--space-5) var(--space-6) var(--space-6);
}
/* On a phone the actions stack, full width, the primary on top: a row that
   could not wrap pushed "Try a different file" and "Keep my Profyl" off the
   left edge, out of reach (2026-10-04). Markup order is decline first, so
   column-reverse puts the primary first without reordering the DOM. */
@media (max-width:639px){
  .modal__footer{ flex-direction:column-reverse; align-items:stretch; }
  .modal__footer > .btn,.modal__footer > * > .btn{ width:100%; justify-content:center; }
  .modal__footer > .btn--wrap{ justify-content:center; text-align:center; }
}
@keyframes pf-fade{ from{ opacity:0 } }
@keyframes pf-rise{ from{ opacity:0; transform:translateY(6px) } }
@media (prefers-reduced-motion: reduce){ @keyframes pf-rise{ from{ opacity:0 } } }

/* ============================================================
   NAVIGATION
   ============================================================ */

/* ---------- the masthead ----------
   Replaced the full-height sidebar on 2026-10-03 (design-decisions.md). The
   sidebar was the ChatGPT shell (wordmark, collapse toggle, a "New" button,
   grouped rows, an avatar at the foot), and it was the single strongest
   reason the product read as a generic AI app. A masthead is the letterhead
   of the stationery the brand describes, and it gives the page back the 240px
   the rail took.

   A surface with a hairline under it since brand v3 (2026-10-13), as
   compare.html draws the app: it was a pine field in both themes, a token
   scope with constant ink. Light, it needs no scope: it reads the roles like
   any other surface, and the logo takes the theme's logo roles. The current
   destination is the ink colour with the 2px brand rule under it.

   Sticky below lg, where it is the only identity on screen and the bottom bar
   carries navigation. Static from lg, so a long document gets the whole
   viewport; --sticky-stack in tokens.css already assumes exactly that. */
.masthead{
  position:sticky; top:0; z-index:30;
  background:var(--pf-surface);
  border-bottom:var(--hairline) solid var(--pf-border);
  color:var(--pf-fg-secondary);
}
.masthead__inner{
  display:flex; align-items:center; gap:var(--space-8);
  height:var(--topbar-h); max-width:var(--shell-max); margin-inline:auto;
  padding-inline:var(--space-5);
}
@media (min-width:1024px){
  .masthead{ position:static; }
  .masthead__inner{ height:var(--masthead-h); padding-inline:var(--space-10); }
}

/* The logo: the fit P and the wordmark, as one inline SVG (Logo.jsx) so the
   wordmark is set in the page's own Archivo rather than a fallback, which is
   what an <img> of lockup.svg would get (brand-identity.md §3). Geometry is
   docs/brand-v3/logo/lockup.svg's, on its 146 × 48 box; the colours are the
   theme's logo roles, so dark mode is symbol-on-dark for free. A brand
   surface (.auth__brand) restates --logo-* for the reversed version.
   .wordmark is the link or block that holds it. */
.wordmark{
  position:relative; display:inline-flex; align-items:center; flex:none;
  color:var(--pf-fg); text-decoration:none; border-radius:var(--radius-sm);
}
.wordmark:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.logo{ display:block; height:var(--logo-h, 36px); width:auto; overflow:visible; }
.logo__stem{ fill:var(--logo-stem, var(--pf-logo-stem)); }
.logo__bowl{ fill:var(--logo-bowl, var(--pf-logo-bowl)); }
/* Set type, not lettering: Archivo ExtraBold at 112% width, −2% tracking.
   Sizes are the SVG's own units. */
.logo__word{
  fill:var(--logo-stem, var(--pf-logo-stem));
  font-family:var(--font-display); font-weight:var(--font-weight-extrabold);
  font-stretch:var(--font-width-display); font-size:34px; letter-spacing:-0.68px;
}
.logo__endorse{
  fill:var(--logo-endorse, var(--pf-fg-secondary));
  font-family:var(--font-sans); font-weight:var(--font-weight-medium);
  font-size:9.52px; letter-spacing:0.19px;
}

.masthead__nav{ display:none; align-self:stretch; gap:var(--space-8); }
@media (min-width:1024px){ .masthead__nav{ display:flex; } }
.masthead__nav a{
  display:flex; align-items:center; gap:var(--space-2);
  font:var(--font-weight-semibold) var(--type-body-size)/1 var(--font-sans); color:var(--pf-fg-secondary);
  text-decoration:none; box-shadow:inset 0 calc(var(--rule) * -1) 0 transparent;
  transition:color var(--duration-sm) var(--ease-standard),box-shadow var(--duration-sm) var(--ease-standard);
}
.masthead__nav a:hover{ color:var(--pf-fg); }
.masthead__nav a[aria-current]:not([aria-current="false"]){
  color:var(--pf-fg); box-shadow:inset 0 calc(var(--rule) * -1) 0 var(--pf-primary-text);
}
.masthead__nav a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }

.masthead__end{ margin-left:auto; display:flex; align-items:center; gap:var(--space-6); }
.masthead .balance{ color:var(--pf-fg-secondary); }
.masthead .balance:hover{ color:var(--pf-fg); }

/* The person, as a name rather than an avatar: it is a text link in a row of
   text links. Hidden below lg, where Account is in the bottom bar. */
.accountbtn{
  display:none; align-items:center; gap:var(--space-1);
  min-height:var(--hit-min); padding:0 var(--space-1);
  border:0; background:none; cursor:pointer;
  font:var(--font-weight-semibold) var(--type-body-size)/1 var(--font-sans); color:var(--pf-fg-secondary);
  border-radius:var(--radius-sm);
}
@media (min-width:1024px){ .accountbtn{ display:inline-flex; } }
.accountbtn:hover,.accountbtn[data-state="open"]{ color:var(--pf-fg); }
.accountbtn:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.accountbtn > svg{ flex:none; }

/* Initials, never a silhouette (brand §1). Now only inside the menu. */
.avatar{
  flex:none; display:grid; place-items:center;
  width:28px; height:28px; border-radius:var(--radius-full);
  background:var(--pf-primary-soft); color:var(--pf-on-primary-soft);
  font:var(--font-weight-bold) var(--type-caption-size)/1 var(--font-sans); letter-spacing:.02em;
}

/* ---------- the shell ---------- */
.shell{ min-height:100dvh; }
.shell__main{ padding-inline:var(--space-5); }
@media (min-width:1024px){ .shell__main{ padding-inline:var(--space-10); } }
/* The content column. Centred, and as wide as the widest thing it holds: a
   document with its margin notes. */
.shell__inner{ width:100%; max-width:var(--content-max); margin-inline:auto; }

/* The dropdown. Detached, so it takes the menu elevation (§6: shadow md). */
.menu{
  min-width:220px; padding:var(--space-2);
  background:var(--pf-surface-raised); color:var(--pf-fg-secondary);
  border:var(--hairline) solid var(--pf-border);
  border-radius:var(--radius-lg); box-shadow:var(--pf-shadow-md);
  z-index:50;
}
.menu__item{
  display:flex; align-items:center; gap:var(--space-2);
  min-height:36px; padding:var(--space-1) var(--space-3);
  border-radius:var(--radius-md); cursor:pointer;
  font:var(--font-weight-medium) var(--type-body-sm-size)/1.35 var(--font-sans); color:var(--pf-fg-secondary);
  user-select:none;
}
.menu__item[data-highlighted]{ outline:none; background:var(--pf-surface-sunken); color:var(--pf-fg); }
.menu__item[data-disabled]{ opacity:var(--opacity-disabled); pointer-events:none; }
.menu__item > svg{ flex:none; color:var(--pf-fg-muted); }
.menu__item[data-highlighted] > svg{ color:inherit; }
/* An item with a second line of explanation (Export's formats): the icon
   stays on the first line, centred on it, rather than on the pair. */
.menu__item--note{ align-items:flex-start; }
.menu__item--note > svg{ margin-top:calc((1.35em - var(--icon-sm)) / 2); }
.menu__sep{ height:var(--hairline); margin:var(--space-2) 0; background:var(--pf-border); }


/* What the document *is* — added when, exported, gaps flagged — as opposed to
   where you are in it. One muted line under the masthead. It lived in the
   rail, which meant it vanished below 1024px on the one screen where checking
   a draft against the posting is the entire task. */
.docmeta{
  margin:0; font:400 var(--type-body-sm-size)/1.4 var(--font-sans);
  color:var(--pf-fg-muted); font-variant-numeric:tabular-nums;
}

/* Module switch — navigation, so links + aria-current, not role=tablist */
.modeswitch{ display:flex; gap:var(--space-5); }
.modeswitch a{
  position:relative; display:flex; align-items:center; gap:var(--space-2);
  padding:var(--space-2) 0; text-decoration:none;
  font:var(--font-weight-semibold) var(--type-body-size)/1 var(--font-sans); color:var(--pf-fg-secondary);
}
.modeswitch a:hover{ color:var(--pf-fg); }
.modeswitch a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }
/* Keyed on the *presence* of aria-current, not its value. Two conventions are
   in the tree — `"page"` on route links and `"true"` on the rail — and three
   source comments apologise for it. Matching either lets the correct pair be
   adopted per device without a flag day: `page` for a link that navigates,
   `location` for an anchor marking position inside the current page, which is
   what a scroll-spy item actually is. `"page"` on a scroll-spy anchor tells a
   screen-reader user they are on a different page, and they are not.
   The rule that makes `:not(...)` safe: never render aria-current="false" —
   render the attribute or omit it. Every call site already does. */
.modeswitch a[aria-current]:not([aria-current="false"]){ color:var(--pf-fg); }
.modeswitch a[aria-current]:not([aria-current="false"])::after{
  content:""; position:absolute; left:0; right:0; bottom:-2px;
  height:var(--rule); background:var(--pf-primary-text);
}

/* Fluid up to --rail, never fixed at it. As a fixed 240px inside a 240px
   scrolling track this deadlocked: any vertical overflow raised a scrollbar,
   the scrollbar took 15px of width, the fixed 240px then overflowed
   horizontally, that scrollbar took 15px of height, and the two kept each
   other alive. Measured in the workspace rail at 900px tall — clientW 225
   against scrollW 240, clientH 254 against scrollH 260, both bars visible on a
   seven-item nav. A fluid width cannot enter the loop.
   The rail still reserves 240px; --rail is on the grid track (§1.6). */
.sidenav{ display:flex; flex-direction:column; gap:2px; width:100%; max-width:var(--rail); }
/* Flex rather than block: every item carries a 16px icon in front of a label
   that is always visible. Labels are never replaced by the glyph — §3.5's
   "icon-only navigation is a recall tax" applies to the rail as much as to the
   bottom bar. */
.sidenav a{
  position:relative; display:flex; align-items:center; gap:var(--space-2);
  min-height:36px; padding:var(--space-1) var(--space-3);
  font:var(--font-weight-medium) var(--type-body-sm-size)/1.35 var(--font-sans); color:var(--pf-fg-secondary); text-decoration:none;
  border-radius:var(--radius-md);
}
.sidenav a > svg{ color:var(--pf-fg-muted); }
.sidenav a:hover > svg,
.sidenav a[aria-current]:not([aria-current="false"]) > svg{ color:inherit; }
.sidenav a:hover{ background:var(--pf-surface-sunken); color:var(--pf-fg); }
.sidenav a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
/* The current item sits on primary-soft (§8: "the icon sits on a
   primary-soft chip"), with the 2px brand rule at its edge, so it is not
   colour alone. */
.sidenav a[aria-current]:not([aria-current="false"]){
  background:var(--pf-primary-soft); color:var(--pf-on-primary-soft);
}
.sidenav a[aria-current]:not([aria-current="false"])::before{
  content:""; position:absolute; left:0; top:6px; bottom:6px;
  width:var(--rule); background:var(--pf-primary-text); border-radius:1px;
}

/* .sidenav__sub is deliberately absent. It styled a nested set — the "On this
   page" block the sidebar used to carry — and is retired with it. Jumping
   within a page is the canvas's job (.tabs), not the chrome's.

   .sidenav__group went with it by accident and is back (2026-09-22). It had
   two consumers and only one of them was the block being removed: the other is
   the label over the profile group, which §1.6 has specified since the shell
   landed. Without it the four destinations read as one undifferentiated set,
   when two of them are what the product makes and two are what it reads from. */
/* Each group is its own column so `.sidenav`'s 2px row gap applies *inside* a
   group. The space *between* groups comes from the label's own top margin,
   which means an unlabelled group (the first one) simply runs on — which is
   what you want, since it starts the list. */
.sidenav__group{
  margin:var(--space-4) 0 var(--space-1); padding-inline:var(--space-3);
  font:var(--type-overline-weight) var(--type-overline-size)/var(--type-overline-leading) var(--font-sans);
  font-stretch:var(--type-overline-width); letter-spacing:var(--type-overline-tracking);
  text-transform:uppercase; color:var(--pf-fg-muted);
}
.sidenav a[aria-disabled="true"]{ opacity:var(--opacity-disabled); pointer-events:none; }

/* " · writing" / " · not written" beside a nav label. One idiom, visible, in
   both the rail and the tab strip. There were four — two visible and two
   sr-only — so a sighted user lost the cue below lg and a screen-reader user
   got different wording in each device. Quieter than the label it trails so it
   reads as an annotation rather than part of the name. */
.navstate{ color:var(--pf-fg-muted); font-weight:400; }

/* Four artifact tabs do not fit a 320px viewport. With `nowrap` and no
   scroller they simply ran off the edge — measured at 354px, clientWidth 280
   against scrollWidth 333, which put "Interview" past the viewport edge with
   no way to reach it. A cut-off destination is a 1.4.10 failure, and §5.6's
   "no horizontal scrolling at 320px" was being met by clipping rather than by
   reflow. They scroll now.

   Wrapping was the other option and it is wrong here: the 2px underline is the
   current tab's marker, and it reads as a rule under *a row of tabs*. Two rows
   with a hairline under only the last one is a different device.

   `overflow-x:auto` forces `overflow-y:auto` with it — the axes cannot
   disagree — so the scroll box would crop the 4px focus ring top and bottom.
   Clipping is at the *padding* edge, so --space-1 of vertical padding is
   exactly the room the ring needs. That moves the hairline off the border and
   onto a background, which paints across the padding box and stays put while
   the content scrolls; the current tab's underline reaches back down onto it. */
.tabs{
  display:flex; gap:var(--space-5);
  overflow-x:auto; overscroll-behavior-x:contain;
  padding-block:var(--space-1);
  scroll-padding-inline:var(--space-5);
  background:linear-gradient(var(--pf-border),var(--pf-border))
             left bottom / 100% var(--hairline) no-repeat;
  /* A classic scrollbar lands on the bottom edge — which is where the hairline
     and the current tab's underline already are, so the platform draws a grey bar
     straight through both. The cut-off tab at the edge is the affordance, and
     it is the one every tab strip on a phone already uses; nothing here is
     reachable only by dragging a scrollbar. Keyboard still scrolls tabs into
     view (scroll-padding-inline above), touch and trackpad are unaffected. */
  scrollbar-width:none;
}
.tabs::-webkit-scrollbar{ display:none; }

/* The way back out of a workspace. Added 2026-09-21: both workspaces were
   reachable only by a browser Back press, because the sidebar's Applications
   row does not light up on /applications/:id — `end` is on the route, and it
   would be wrong to light it anyway, since you are not on the list.

   Above the masthead rule rather than beside the title: it belongs to the
   page's position in a hierarchy, not to the thing on the page. Small, quiet,
   and the same in both modules. */
.backlink{
  display:inline-flex; align-items:center; gap:var(--space-1);
  margin-bottom:var(--stack-tight);
  font:500 var(--type-body-sm-size)/1.4 var(--font-sans);
  color:var(--pf-fg-muted); text-decoration:none;
}
.backlink:hover{ color:var(--pf-fg); }
.backlink:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }
.backlink > svg{ flex:none; }

/* A text link in running prose with its icon before the word (2026-10-04):
   "Read them before you send", the 404's destinations. The link stays inline
   so it wraps with the sentence; the glyph sits on the text rather than on
   the line box. Preflight makes every svg a block, hence inline-block. */
.iconlink{ white-space:nowrap; }
.iconlink > svg{
  display:inline-block; vertical-align:-0.15em;
  margin-inline-end:var(--space-1);
}

/* A workspace's strip, at every width. Added 2026-09-21.

   It used to be `lg:hidden` — the small-screen stand-in for a contents list in
   the sidebar. That list is gone (§1.6), and on a desktop that left four long
   artifacts stacked down one page with no way to reach the fourth except to
   scroll past three. The strip is the better device anyway: it sits in the
   canvas with the thing it indexes rather than 240px away in the chrome, and
   it is the pattern anyone would guess at.

   Sticky, so it is still there at the bottom of a resume. Below `lg` it parks
   under the top bar; from `lg` there is no top bar to clear. Both the artifact
   jump offset and the scroll-spy's rootMargin derive from `--sticky-stack`,
   which tracks that difference — this comment previously claimed `--rail-top`
   cleared both widths, and measurement said otherwise on every phone. */
.tabs--sticky{
  position:sticky; top:var(--topbar-h); z-index:20;
  /* --tabs-h is what --sticky-stack is built from, so it has to be the real
     height rather than a note of what the padding happened to produce.
     min-height makes the token authoritative: change the padding and the
     strip still occupies what the jump offset reserves for it. */
  min-height:var(--tabs-h);
  background:var(--pf-bg);
  /* Content passing underneath would otherwise show through the scroller's
     own padding-block, above and below the labels. */
  padding-block:var(--space-3) var(--space-1);
}
@media (min-width:1024px){ .tabs--sticky{ top:0; } }

/* The workspace toolbar (2026-09-27): the strip on the left, the package's
   actions on the right, one sticky row. It takes over .tabs--sticky's job and
   its numbers — --tabs-h, the same padding, the same `top` either side of lg
   — so the jump offset and the scroll-spy margin, both built from
   --sticky-stack, stay right. The hairline moves from the strip to the whole
   row, so it runs under the actions too; the current tab's underline still
   lands on it (the strip's bottom plus --space-1 is the row's bottom edge).
   Below `sm` the actions leave for their own non-sticky row above
   (WorkspaceLayout): a two-row sticky bar is a sixth of a phone screen. */
.worktoolbar{
  position:sticky; top:var(--topbar-h); z-index:20;
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-4);
  min-height:var(--tabs-h);
  padding-block:var(--space-3) var(--space-1);
  background:var(--pf-bg) linear-gradient(var(--pf-border),var(--pf-border))
             left bottom / 100% var(--hairline) no-repeat;
}
@media (min-width:1024px){ .worktoolbar{ top:0; } }
.worktoolbar > .tabs{ flex:1; min-width:0; background:none; }
.worktoolbar__actions{ flex:none; align-items:center; gap:var(--space-2); padding-bottom:var(--space-2); }

/* A figure beside a tab's label: the gap count, in mono. Plain text since 2026-10-03.
   A pill read as a notification badge, and a count of gaps is not a thing
   waiting for you to clear it. */
.tabs__count{ font:inherit; font-family:var(--font-mono); font-weight:var(--type-data-weight); font-variant-numeric:tabular-nums; color:var(--pf-fg-muted); }
.tabs [aria-selected="true"] .tabs__count{ color:inherit; }
/* `.tabs` began as jump links into one long page and is now also a real
   tablist (the voice screen swaps its panel). Both are "several sections, not
   enough width", so they get one device rather than a second one invented to
   look the same — the selectors take `button` alongside `a`, and the current
   marker keys on `aria-selected` as well as `aria-current`. */
.tabs a,.tabs button{
  position:relative; display:flex; align-items:center; gap:var(--space-2);
  flex:none;   /* a tab is as wide as its label — never squeezed to fit */
  padding:var(--space-2) 0 var(--space-3); text-decoration:none;
  font:var(--type-label-weight) var(--type-label-size)/1 var(--font-sans);
  letter-spacing:var(--type-label-tracking); color:var(--pf-fg-secondary);
}
.tabs button{ border:0; background:none; cursor:pointer; }
.tabs a:hover,.tabs button:hover{ color:var(--pf-fg); }
.tabs a:focus-visible,.tabs button:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }
.tabs a[aria-current]:not([aria-current="false"]),
.tabs [aria-selected="true"]{ color:var(--pf-fg); }
/* Reaches down through the scroller's padding so the current tab's rule still
   lands on the hairline rather than floating --space-1 above it. */
.tabs a[aria-current]:not([aria-current="false"])::after,
.tabs [aria-selected="true"]::after{
  content:""; position:absolute; left:0; right:0;
  bottom:calc(var(--space-1) * -1); height:var(--rule); background:var(--pf-primary-text);
}

/* Mobile bottom bar — design-system.md §3.5. Max three destinations, 44px
   minimum targets, and labels always visible: icon-only navigation is a
   recall tax on infrequent users, and this app is used in bursts weeks
   apart. A surface with a hairline, like the masthead. The current item is
   the link colour with the 2px brand rule on top, so it is not colour alone. */
.bottomnav{
  position:fixed; left:0; right:0; bottom:0; z-index:40;
  display:flex; align-items:stretch;
  background:var(--pf-surface); border-top:var(--hairline) solid var(--pf-border);
  padding-bottom:env(safe-area-inset-bottom);
}
.bottomnav a{
  position:relative; flex:1;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
  min-height:var(--hit-min); padding:var(--space-2) var(--space-1);
  text-decoration:none; text-align:center;
  font:var(--font-weight-semibold) var(--type-caption-size)/var(--type-caption-leading) var(--font-sans);
  letter-spacing:var(--type-caption-tracking);
  color:var(--pf-fg-muted);
}
.bottomnav a:hover{ color:var(--pf-fg); }
.bottomnav a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); }
.bottomnav a[aria-current]:not([aria-current="false"]){ color:var(--pf-link); }
.bottomnav a[aria-current]:not([aria-current="false"])::before{
  content:""; position:absolute; left:0; right:0; top:0; height:var(--rule); background:var(--pf-primary-text);
}

/* ============================================================
   DOCUMENT SURFACE
   The manuscript margin is decorative (1.53:1) and must never be
   the sole carrier of meaning. Absent in compose mode via tokens.
   ============================================================ */

/* The rule sits at --page-margin (56px, §0.3) and the text clears it by one
   --space-4. Both were hard-coded off the --space ramp before — left:32px with
   48px of padding — so the margin shipped 24px shallower than the spec it is
   named in. Deriving the padding from the token keeps the gutter fixed if the
   inset is ever retuned. */
.page{
  position:relative;
  background:var(--page-bg,var(--pf-surface));
  border:var(--page-edge,var(--hairline) solid var(--pf-border-strong));
  border-radius:var(--radius-lg);
  padding:var(--space-5) var(--space-5) var(--space-5)
          calc(var(--page-margin) + var(--space-4));
  transition:padding var(--duration-md) var(--ease-standard);
}
/* The manuscript margin rule (v2's .page::before) was retired 2026-10-13: a
   v2 stationery device. The sheet keeps its margin, so a document's lines
   break where they always did. */
[data-mode="compose"] .page{ padding-left:var(--space-2); padding-right:var(--space-2); }

@media (max-width:767px){
  .page{ padding-left:var(--space-5); }
}

/* ============================================================
   THE ARTIFACT FRAME
   design-system.md §3.3 — "a page is a thing the user will read,
   export or send." This is what identifies one.

   The system specified cards, buttons, forms, modals, status and
   navigation, and never specified the only object this product
   actually makes. So a generated resume rendered into a bare
   <div id={active}> with no heading, no rule and no metadata, and
   the only thing naming it was a highlighted rail item 240px away.
   That is the single largest reason the workspace read as empty.

   It composes .page, so it is a bounded white sheet in document
   mode and nothing at all in compose — no component branch.
   ============================================================ */

.artifact{
  /* Clear the whole sticky stack, not just the top bar. This was --rail-top
     (topbar + a breath), which happens to clear the desktop stack and misses
     the phone one by 28px — the strip is 52px tall and sits *under* the 56px
     top bar below lg, so there the chrome is 108px, not 56. --jump-top is
     responsive for the same reason the strip's own `top` is. */
  scroll-margin-top:var(--jump-top);
}
.artifact:focus{ outline:none; }   /* :focus-visible still rings it */

.artifact__head{
  display:flex; flex-direction:column; gap:var(--space-1);
  border-top:var(--artifact-rule,none);
  padding-top:var(--artifact-rule-gap,0);
}
/* The eyebrow, and — when a document has them — its own actions on the same
   line (2026-09-27), so Copy is at the top of the post rather than a scroll
   below it. Wraps under the eyebrow on a narrow screen. */
.artifact__topline{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:var(--space-2) var(--space-3);
}
.artifact__headactions{ gap:var(--space-2); margin-left:auto; }  /* right-aligned with or without an eyebrow */
/* The title and the document's actions on one row — the letterhead name on
   the left, Copy and Rewrite this on the right — so the actions cost no line
   of their own. Wraps under the title when there is no room. */
.artifact__titlerow{
  display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:space-between;
  gap:var(--space-2) var(--space-3);
}
/* No visible title to share a row with (a Presence post): the actions float
   at the top right of the text, which flows beside them — and, where the
   text's measure leaves room, never reaches them at all. On a phone the
   text needs the whole width, so there they take a row above it. */
.artifact__floatactions{ gap:var(--space-2); justify-content:flex-end; margin-bottom:var(--space-2); }
@media (min-width:640px){
  .artifact__floatactions{ float:right; margin:0 0 var(--space-2) var(--space-4); }
}
/* Inside a tab panel there is one artifact, directly under the toolbar's
   own hairline, so the rule-and-gap that separates stacked artifacts in
   compose mode (--artifact-rule, --artifact-rule-gap) only drew a second line
   37px below the first. The stack still gets it. */
[role="tabpanel"] > .artifact > .artifact__head{ border-top:0; padding-top:0; }
/* A header with nothing visible in it — only a screen-reader title — lays
   out as if absent: its children join the frame directly, and the sr-only
   title is absolutely positioned, so it takes no gap in the stack. */
.artifact__head--bare{ display:contents; }

/* The user's idea as a page title (2026-09-27): the h3 step, and three
   lines at most, with "Show all" beside the meta line when
   it runs longer. At 28px a long idea was the whole first screen. */
.pf-idea-title{
  font-size:var(--type-h3-size); line-height:1.45;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; overflow:hidden;
}
.pf-idea-title--open{ display:block; -webkit-line-clamp:unset; overflow:visible; }

/* Where LinkedIn cuts a post to "see more" — a dashed hairline across the
   text with a small caption, so the hook can be judged. Not an imitation of
   LinkedIn's interface. The text either side is unchanged, and copying is
   unaffected: the marker is not in the copied text. */
.pf-fold{
  display:flex; align-items:center; gap:var(--space-2);
  margin:var(--space-2) 0;
  font:500 var(--type-caption-size)/1.3 var(--font-sans); color:var(--pf-fg-muted);
}
.pf-fold::before,.pf-fold::after{
  content:""; flex:1; border-top:var(--hairline) dashed var(--pf-border-input);
}

/* 28px serif 400 — the register the scale owned and nothing used. 38 is a page
   title the product wrote; 28 is a document the user owns. Weight comes from
   the global h1,h2 rule at 400: a bold serif reads as a tabloid (§2). */
.artifact__title{ margin:0; font-size:var(--text-h2); line-height:var(--lh-heading); }

.artifact__subtitle{
  margin:0; font:500 var(--text-caption)/1.45 var(--font-sans);
  letter-spacing:var(--tracking-label); color:var(--text-muted);
  font-variant-numeric:tabular-nums;   /* the `font` shorthand resets it */
}

/* A divider, not the signature stroke. §0.2 gives the 2px pine rule four jobs
   and a letterhead is not a fifth — nine artifacts each drawing one is exactly
   the "a fifth use makes it a texture" failure. */
.artifact__rule{
  height:var(--hairline); background:var(--border-divider);
  margin-top:var(--stack-tight); max-width:var(--measure-prose);
}

/* A section head INSIDE a sheet: quiet furniture, not a headline. Today
   "Experience" renders at 28px serif — the least informative word on the page
   is the biggest thing on it, which is the opposite of how a resume is set.
   Letterspaced caps at label size with a hairline running out to the measure
   frees 28 for the name block. */
.artifact__section{
  display:flex; align-items:center; gap:var(--space-3);
  margin:var(--stack-loose) 0 var(--stack);
  font:600 var(--text-caption)/1.4 var(--font-sans);
  letter-spacing:var(--tracking-overline); text-transform:uppercase;
  color:var(--text); max-width:var(--measure-prose);
}
.artifact__section::after{
  content:""; flex:1; height:var(--hairline); background:var(--border-divider);
}

/* Role left, dates right, in a tabular column down the page — the single most
   "printed document" move available, and it is one flex row. Wraps rather than
   squeezing on a narrow screen, so the dates fall under the role. */
.artifact__entry{
  display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between;
  gap:var(--space-2) var(--space-4); max-width:var(--measure-prose);
}
.artifact__role{ margin:0; font:600 var(--text-body-size)/var(--lh-tight) var(--font-sans); }
.artifact__dates{
  flex:none; font:500 var(--text-label)/1.4 var(--font-sans);
  letter-spacing:var(--tracking-label); color:var(--text-muted);
  font-variant-numeric:tabular-nums;
}

/* Hanging figures in the margin — numbered interview questions, script beats,
   thread positions. The same register §2.1a already defines for script
   direction, so one rule serves three surfaces. decimal-leading-zero puts 01
   in the same optical column as 11. */
.artifact__numbered{ list-style:none; margin:0; padding:0; counter-reset:pf-n; }
.artifact__numbered > li{ position:relative; padding-left:var(--space-8); }
.artifact__numbered > li::before{
  counter-increment:pf-n; content:counter(pf-n,decimal-leading-zero);
  position:absolute; left:0; top:.35em;
  font:var(--type-data-weight) var(--type-data-size)/1 var(--font-mono);
  color:var(--pf-primary-text);
  font-variant-numeric:tabular-nums;
}

/* An en dash, not a browser disc. Older Safari ignores a string
   list-style-type and falls back to a disc, which is an acceptable floor. */
/* A role's responsibilities, between its company and its achievements
   (2026-09-27) — a paragraph or a list, as written. The list takes the
   bullets' look; the space after it keeps it from running into theirs. */
.artifact__roledesc{ margin:var(--stack-tight) 0 0; }
ul.artifact__roledesc{ margin-bottom:var(--stack); }
.artifact__bullets{ list-style-type:"–"; padding-left:var(--space-5); margin:0; }
.artifact__bullets li::marker{ color:var(--text-muted); }
.artifact__bullets li{ margin-bottom:var(--stack-tight); }

/* A resume entry's pieces, named so the preview and the printed document
   (lib/documents) use one set of rules rather than utilities in JSX, which
   the print path cannot read (tech-stack.md §2.1). */
.artifact__item{ margin-top:var(--stack); }
/* Several positions at one employer (2026-10-04): the employer and its whole
   span on the first line, in the role's weight, and each position under it,
   set in on a hairline with its own title and dates. One position prints as
   an entry always did, so none of this applies to it. */
.artifact__company{ margin:0; font:600 var(--text-body-size)/var(--lh-tight) var(--font-sans); }
.artifact__position{
  margin-top:var(--stack-tight); padding-left:var(--space-4);
  border-left:var(--hairline) solid var(--border-divider);
}
.artifact__position .artifact__role{ font-weight:500; }
.artifact__org{ margin:0; font:400 var(--text-label)/1.4 var(--font-sans); color:var(--text-muted); }
.artifact__achievements{ margin-top:var(--stack-tight); }
.artifact__para{ margin:0 0 var(--stack); }

.artifact__more{ margin-top:var(--stack); }

.artifact__foot{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:var(--space-3); margin-top:var(--stack-loose); padding-top:var(--space-4);
  border-top:var(--hairline) solid var(--pf-border);
}
/* The actions sit right whether or not there is a colophon to push them there.
   space-between alone put them left the moment the colophon was absent, which
   is why an empty <p> survived in the frame for as long as it did — it was
   holding the layout up by accident. */
.artifact__foot > .cluster{ margin-left:auto; }
.artifact__colophon{
  margin:0; font:var(--type-data-weight) var(--type-caption-size)/1.45 var(--font-mono);
  color:var(--pf-fg-muted); font-variant-numeric:tabular-nums;
}

/* ============================================================
   PROSE MODIFIERS
   ============================================================ */

/* Newsreader is loaded as a variable font WITH italics, and the app uses
   little of it. A standfirst is the one editorial device that wants it: the
   premise, in the user's own voice, above the thing derived from it. On a
   resume this is the summary, which currently renders as an indistinguishable
   first paragraph. */
.pf-standfirst{
  margin:0; font-family:var(--font-serif); font-style:italic;
  font-size:var(--text-prose-lg); line-height:1.5;
  color:var(--text-body); max-width:var(--measure-prose);
}

/* Compose mode sets --measure-prose:52ch, which is right for a post read
   in a feed and wrong for a blog post or a video script — a 1,500-word
   article at 52ch is a thin ribbon eight screens long. Measure is
   channel-bound, not mode-bound (design-system.md §2). */
.pf-prose--long{ --measure-prose:68ch; --prose-size:var(--text-prose); }

/* A script carries two registers at once: voice — the words the user will
   actually say, set as prose — and direction, the scaffolding around them,
   set small and muted, because direction is the product talking.
   Also governs thread position markers ("1/", "2/"). */
.script{ display:flex; flex-direction:column; gap:var(--stack-tight); }
.script__direction{
  font:500 var(--type-body-sm-size)/1.4 var(--font-sans);
  letter-spacing:var(--type-caption-tracking);
  color:var(--pf-fg-muted);
  font-variant-numeric:tabular-nums;
}
/* Spoken lines take .pf-prose — they are generated from the profile like
   anything else. No separate class; a second one would invite divergence. */

/* ============================================================
   VOICE CAPTURE
   The microphone is a first-class surface, not an accessory.
   design-system.md §3.6 · brand-guidelines.md §1.5 Rule 3
   ============================================================ */

.recorder{ display:flex; flex-direction:column; gap:var(--stack-tight); }
.recorder__controls{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-3); }

/* The brand colour, never red. Red in this product means something has gone wrong, and
   a microphone that is working correctly must not borrow that meaning. */
.recorder__dot{
  flex:none; width:10px; height:10px; border-radius:50%;
  background:var(--pf-primary-text);
}

/* The keyframes start AND end at full opacity on purpose. The global
   reduced-motion rule in tokens.css forces animation-duration to 0.01ms and
   iteration-count to 1, which snaps any animation to its end state — so the
   end state has to be the solid, visible dot, not the faded one. */
.recorder__dot--live{ animation:pf-pulse 1600ms var(--ease-standard) infinite; }
@keyframes pf-pulse{ 0%,100%{ opacity:1 } 50%{ opacity:.35 } }

/* Counts up, never down. A countdown is banned urgency, and cutting someone
   off mid-sentence about their own career is the worst moment to enforce a
   cap. Two minutes is a suggestion in the copy, not a limit in the control. */
.recorder__time{
  font:var(--type-data-weight) var(--type-data-size)/var(--type-data-leading) var(--font-mono);
  font-variant-numeric:tabular-nums;
  color:var(--pf-fg-muted);
}

/* Provisional. A waveform is the expected affordance and also the most
   machine-looking element we could ship, which cuts against Pillar 2 —
   open question in brand-guidelines.md §7. Decorative: aria-hidden, and
   never the only indication that recording is live. */
.recorder__level{ display:flex; align-items:flex-end; gap:3px; height:20px; }
.recorder__level span{
  display:block; width:3px; border-radius:2px;
  background:var(--pf-primary-text); opacity:.4;
}
.recorder__level span[data-on]{ opacity:1; }

/* ============================================================
   CREDITS AND PACKS
   pricing.md §9 · design-system.md §3.7
   ============================================================ */

/* Balance is a line of text. Never a meter, gauge, ring or progress bar —
   a depleting bar is a countdown with extra steps, and a visible meter
   while the user works is exactly what suppresses the iteration the
   product depends on. Phrase it as the finished thing, not the unit:
   "Two applications left", never "2 credits". */
.balance{
  /* Flex, for the wallet glyph in front (added 2026-09-27). Its colour is
     currentColor, so it follows the muted text and the hover with it. */
  display:inline-flex; align-items:flex-start; gap:var(--space-2); margin:0;
  font:500 var(--type-body-sm-size)/1.4 var(--font-sans);
  font-variant-numeric:tabular-nums;
  color:var(--pf-fg-muted);
}
/* It is a link to the credits pane wherever a balance is shown in the app —
   the demo pages render it as a plain paragraph and are untouched by these. */
/* The glyph sits on the first line, and the free-run note is a line of its
   own under the count rather than wrapping out of it. */
.balance > svg{ margin-top:1px; }
.balance__count,.balance__note{ display:block; }
a.balance:hover{ color:var(--pf-fg); }
a.balance:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }

/* Packs reuse .card. The selected pack takes .card--selected; the
   recommended one carries v3's accent chip, "Recommended". */
/* Inside the settings panel the ladder is a control, not a pricing page:
   same three options, same words, less air. The price drops one step on the
   scale because at --text-h2 it is the largest thing in the dialog, which is
   the wrong emphasis for a pane you opened to check a balance. */
.packs--compact .card{ padding:var(--space-3); }
.packs--compact .pack__price{ font-size:var(--type-h3-size); }

.pack{ display:flex; flex-direction:column; align-items:flex-start; gap:var(--space-1); }
.pack__name{
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-2);
  font:var(--type-label-weight) var(--type-label-size)/var(--type-label-leading) var(--font-sans); color:var(--pf-fg-secondary);
}
/* A figure that lines up: mono data style (§5), Archivo for the ₹ the mono
   face lacks (the stack lists it second). */
.pack__price{
  font:var(--type-data-lg-weight) var(--type-h2-size)/var(--type-data-lg-leading) var(--font-mono);
  letter-spacing:var(--type-data-lg-tracking); color:var(--pf-fg); font-variant-numeric:tabular-nums;
}
.pack__unit{
  font:var(--type-data-weight) var(--type-caption-size)/1.4 var(--font-mono); color:var(--pf-fg-muted);
  font-variant-numeric:tabular-nums;
}
/* On a phone each pack is a row — name and credits on the left, the price on
   the right — about 56px rather than a 129px card (2026-09-28). Three cards
   stacked made the paywall scroll: 653px of it in a 461px body. */
@media (max-width:639.98px){
  .packcard{ padding:var(--space-3) var(--space-4); }
  .pack{
    display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center;
    grid-template-areas:"name price" "unit price"; gap:2px var(--space-3); text-align:left;
  }
  .pack__name{ grid-area:name; }
  .pack__unit{ grid-area:unit; }
  .pack__price{ grid-area:price; font-size:var(--type-h3-size); }
}

/* ============================================================
   PRINT — the resume and cover letter as PDF
   Decided 2026-09-30: PDFs are made on the device. The browser prints
   lib/documents' HTML in a hidden frame, the Expo app hands the same HTML
   to expo-print, and both read this. There is no server renderer, so
   these rules are the whole of what a PDF looks like.

   The page box has no margin and the sheet's padding is the margin:
   with none, Chrome has nowhere to print its URL and date. A4, because
   that is the paper an Indian recruiter's printer holds.
   ============================================================ */
@page{ size:A4; margin:0; }

@media print{
  .pf-print{ margin:0; background:#fff; }
  .pf-print .page{
    border:0; border-radius:0; background:#fff;
    padding:18mm 18mm 18mm 20mm; max-width:none;
  }
  .pf-print .page::before{ display:none; }   /* the margin rule is a screen flourish */
  .pf-print .artifact__item,
  .pf-print .artifact__position,
  .pf-print .artifact__bullets li,
  .pf-print .artifact__para{ break-inside:avoid; }
  .pf-print .artifact__section{ break-after:avoid; }
  .pf-print .artifact__entry{ break-after:avoid; }
  /* An employer with several positions may run onto the next page between
     them; each position holds together. */
  .pf-print .artifact__item--grouped{ break-inside:auto; }
}

/* ============================================================
   THE FRONT DOOR (2026-10-03)
   Sign-in, the phone check, the waitlist. A brand field with the logo and a
   specimen of what Profyl writes, and the form on the ground beside it.
   The field is Ink 900 in both themes (v3's dark brand surface, §4), so it
   takes constant ink and the reversed endorsed lockup (§3: the sign-in
   screen carries the endorsement).
   ============================================================ */
.auth{ min-height:100dvh; display:grid; grid-template-columns:minmax(0,1fr); }
@media (min-width:1024px){ .auth{ grid-template-columns:minmax(0,6fr) minmax(0,5fr); } }

.auth__brand{
  display:flex; flex-direction:column; justify-content:flex-start; gap:var(--space-6);
  padding:var(--space-5); color:var(--color-white);
  background:var(--color-ink-900);
  --logo-stem:var(--color-white);
  --logo-bowl:var(--color-volt-400);
  --logo-endorse:var(--color-steel-300);
  /* The endorsement stays at least 11px tall on screen (§3): 9.52 of 48 units. */
  --logo-h:56px;
}
.auth__brand .wordmark:focus-visible{ outline-color:var(--color-white); }
/* From lg, one column on one left edge (2026-10-14): the logo at the top,
   then the promise and the specimen together, centred in the height that is
   left. The promise and both cards share the column's width, so the edges
   run straight down the field. The column sits in the middle of the field,
   as the form does in its half: on the left, a wide screen left half the
   field empty beside it. */
@media (min-width:1024px){
  .auth__brand{
    display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(0,1fr); gap:var(--space-8);
    padding:var(--space-10) var(--space-12); overflow:hidden;
    --hero-col:30rem;
  }
  .auth__brand > *{ width:min(100%, var(--hero-col)); justify-self:center; }
}
/* A wide screen takes the column up a step, and a tall one the promise
   too (below). */
@media (min-width:1600px){
  .auth__brand{ --hero-col:36rem; }
}
.auth__hero{ display:flex; flex-direction:column; gap:var(--space-6); }
@media (min-width:1024px){
  .auth__hero{ align-self:center; gap:var(--space-8); }
}
/* The promise (brand-identity.md §1), in the display style on the brand
   field. Volt under the one word that carries it, as compare.html's hero. */
.auth__promise{
  margin:0; max-width:18ch; color:var(--color-white);
  font:var(--type-h2-weight) var(--type-h2-size)/var(--type-h2-leading) var(--font-display);
  letter-spacing:var(--type-h2-tracking);
}
@media (min-width:1024px){
  .auth__promise{
    font-weight:var(--type-display-md-weight); font-size:var(--type-display-md-size);
    line-height:var(--type-display-md-leading); font-stretch:var(--type-display-md-width);
    letter-spacing:var(--type-display-md-tracking);
  }
}
@media (min-width:1600px) and (min-height:1000px){
  .auth__promise{
    max-width:none;
    font-weight:var(--type-display-lg-weight); font-size:var(--type-display-lg-size);
    line-height:var(--type-display-lg-leading); font-stretch:var(--type-display-lg-width);
    letter-spacing:var(--type-display-lg-tracking);
  }
}

.auth__main{ display:flex; align-items:center; padding:var(--space-10) var(--space-5) var(--space-16); }
.auth__inner{ width:100%; max-width:440px; margin-inline:auto; }
/* An entry moment, so the title takes the display style (§5). */
.auth .pagehead__title{
  font-weight:var(--type-display-lg-weight); font-size:var(--type-display-lg-size);
  line-height:var(--type-display-lg-leading); font-stretch:var(--type-display-lg-width);
  letter-spacing:var(--type-display-lg-tracking);
}
/* A title that takes focus on arrival is a reading position, not a control:
   no ring, as the dialog title (.modal__title). */
.pagehead__title[tabindex="-1"]:focus{ outline:none; box-shadow:none; }
/* The consent page's summary (2026-10-06): four plain statements, set as a
   list the eye can count rather than a paragraph to wade through. */
.consent__points{
  margin:0; padding-left:var(--space-5); list-style:disc;
  display:flex; flex-direction:column; gap:var(--space-2);
  color:var(--pf-fg-secondary); max-width:var(--measure-ui);
}
.consent__points li::marker{ color:var(--pf-fg-muted); }
.consent__points li{ text-wrap:pretty; }
/* A link inside a choice's words, the terms beside the consent box: it has
   to read as a link, underlined, not by colour alone. Opens a new tab, so
   it never ticks the box it sits in. */
.choice__text a{
  color:var(--pf-fg); font-weight:500;
  text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px;
}
.choice__text a:hover{ text-decoration-thickness:2px; }
.choice__text a:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }

/* The specimen: the product as the image (§8), compare.html's hero. A job
   description with its requirements in the three gap states, the line that
   counts them, and the tailored resume it produced. Real UI parts (.req,
   .fitbar, chips) on the theme's surfaces, set on the Ink field. */
.auth__specimen{ display:none; }
@media (min-width:1024px){
  .auth__specimen{ display:flex; flex-direction:column; gap:var(--space-3); }
}
/* A shorter screen (1366×768 is common) loses a matched requirement and the
   second resume line, rather than scaling the column, which would pull its
   edges off the promise's. All three states stay. */
@media (min-width:1024px) and (max-height:899px){
  .auth__brand{ padding-block:var(--space-6); gap:var(--space-6); }
  .auth__hero{ gap:var(--space-6); }
  .specimen__reqs li:nth-child(3),
  .specimen__lines li + li{ display:none; }
}
.specimen{
  padding:var(--space-5);
  background:var(--pf-surface-raised); color:var(--pf-fg);
  border:var(--hairline) solid var(--pf-border); border-radius:var(--radius-lg);
  box-shadow:var(--pf-shadow-xl);
}
.specimen__top{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-3); }
.specimen__role,.specimen__name{
  margin:var(--space-2) 0 var(--space-1);
  font:var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-leading) var(--font-display);
  letter-spacing:var(--type-h4-tracking);
}
.specimen__name{ margin:0; }
.specimen__sub{ margin:0; font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); color:var(--pf-fg-muted); }
.specimen__reqs{ list-style:none; margin:0; padding:0; }
.specimen__lines{
  margin:var(--space-3) 0 0; padding:0; list-style:none;
  display:flex; flex-direction:column; gap:var(--space-2);
  font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading);
}
.specimen__lines li{ position:relative; padding-left:var(--space-4); }
.specimen__lines li::before{
  content:""; position:absolute; left:0; top:.6em; width:6px; height:6px;
  border-radius:50%; background:var(--pf-primary-text);
}
.specimen__fit{
  display:flex; align-items:center; gap:var(--space-3);
  margin-top:var(--space-4); padding-top:var(--space-3); border-top:var(--hairline) solid var(--pf-border);
}

/* ============================================================
   GAP STATES, FIT, HIGHLIGHTS (brand-identity.md §4, §5)
   Shared by the sign-in specimen now and the gap analysis in session 2d.
   ============================================================ */

/* One requirement: its state icon, the requirement, the state's word. The
   icon's shape and the word carry the state, so colour is never alone:
   matched is the Volt marker with an Ink check, close is amber and
   half-filled, to address is a dashed neutral outline. Never red. */
.req{
  display:grid; grid-template-columns:22px minmax(0,1fr) auto; align-items:center; gap:var(--space-3);
  padding:var(--space-2) 0; border-top:var(--hairline) solid var(--pf-border);
  font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading);
}
.req__mark{ display:grid; place-items:center; width:22px; height:22px; border-radius:50%; }
.req__word{ font:var(--type-label-weight) var(--type-caption-size)/1.3 var(--font-sans); color:var(--pf-fg-secondary); }
.req--matched .req__mark{ background:var(--pf-match); color:var(--pf-on-match); }
.req--matched .req__mark svg{ stroke-width:2.6 !important; }
.req--close .req__mark,.req--close .req__word{ color:var(--pf-partial); }
.req--to-address .req__mark{ color:var(--pf-gap-border); }

/* A fit bar: how much of something is matched, always beside the words
   that say so ("9 of 13 matched"), never on its own. One colour. */
.fitbar{ flex:1; display:block; height:8px; overflow:hidden; border-radius:var(--radius-full); background:var(--pf-surface-sunken); }
.fitbar > span{ display:block; height:100%; background:var(--pf-primary); }
.fitbar--mini{ flex:none; width:64px; height:5px; }
/* The fit on a row: the matched marker, "9/13" in mono and the mini bar
   (Fit.jsx). The marker is the Gaps tab's own Volt check, so the meaning
   carries from one to the other; the figure beside it says it in words. */
.fit{ display:inline-flex; align-items:center; gap:var(--space-2); }
.fit .req__mark{ width:18px; height:18px; }
.fit .req__mark svg{ width:12px !important; height:12px !important; }

/* ---------- the gap analysis (session 2d, artifacts.jsx) ----------
   compare.html's "careful moment": a summary (what is close, what to
   address, one cell per requirement, the legend in words), then the groups
   strengths first. Matched rows are plain and quick; close rows sit on the
   amber tint; to-address rows have a dashed neutral edge. Never red. */
.fitview{ display:flex; flex-direction:column; gap:var(--space-8); max-width:var(--measure-prose); }
.fitsum{ display:flex; flex-direction:column; gap:var(--space-3); }
.fitsum__sub{ margin:0; font-size:var(--type-body-lg-size); line-height:var(--type-body-lg-leading); color:var(--pf-fg-secondary); }
.cells{ display:grid; grid-template-columns:repeat(auto-fill, minmax(20px, 1fr)); gap:4px; max-width:440px; }
.cell{ display:block; height:24px; border-radius:var(--radius-sm); }
.cell--matched{ background:var(--pf-match); }
.cell--close{ background:var(--pf-partial-bg); box-shadow:inset 0 0 0 2px var(--pf-partial); }
.cell--to_address{ border:2px dashed var(--pf-gap-border); }
/* The strip on a line of text: the workspace header, Home's latest row. */
.cells--sm{ grid-template-columns:repeat(auto-fill, 14px); gap:3px; margin-top:var(--space-2); }
.cells--sm .cell{ height:14px; border-radius:var(--radius-xs); }
.cells--sm .cell--close{ box-shadow:inset 0 0 0 1.5px var(--pf-partial); }
.cells--sm .cell--to_address{ border-width:1.5px; }
.legend{ display:flex; flex-wrap:wrap; gap:var(--space-2) var(--space-5); margin:0; font-size:var(--type-body-sm-size); color:var(--pf-fg-secondary); }
.legend__item{ display:inline-flex; align-items:center; gap:var(--space-2); }
.legend .req__mark{ width:20px; height:20px; }

.fitgroup{ display:flex; flex-direction:column; gap:var(--space-3); }
.fitgroup__title{
  display:flex; align-items:baseline; gap:var(--space-2); margin:0;
  font:var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-leading) var(--font-display);
  letter-spacing:var(--type-h4-tracking); color:var(--pf-fg);
}
.reqlist{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--space-2); }
.gaprow{
  display:grid; grid-template-columns:22px minmax(0,1fr) auto; align-items:start; gap:var(--space-3);
  padding:var(--space-3) var(--space-4); border-radius:var(--radius-md);
  background:var(--pf-surface-raised); border:var(--hairline) solid var(--pf-border);
}
.gaprow .req__mark{ margin-top:1px; }
.gaprow--close{ background:var(--pf-partial-bg); border-color:transparent; }
.gaprow--to-address{ background:transparent; border:1.5px dashed var(--pf-gap-border); }
.gaprow__body{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.gaprow__title{ margin:0; font:var(--font-weight-semibold) var(--type-body-size)/1.4 var(--font-sans); color:var(--pf-fg); }
.gaprow__weight{ font-weight:var(--font-weight-regular); color:var(--pf-fg-muted); }
.gaprow__evidence{ margin:0; font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); color:var(--pf-fg-secondary); }
.gaprow--close .gaprow__evidence,.gaprow--close .gaprow__move{ color:var(--pf-fg); }
.gaprow__move{ margin:var(--space-1) 0 0; font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); color:var(--pf-fg); }
.gaprow .req__word{ white-space:nowrap; padding-top:2px; }
/* On a phone the state's word sits over the requirement, like a label,
   so the text keeps the row's width. */
@media (max-width:639px){
  .gaprow{ grid-template-columns:22px minmax(0,1fr); row-gap:2px; }
  .gaprow .req__mark{ grid-row:1 / span 2; }
  .gaprow .req__word{ grid-column:2; grid-row:1; justify-self:start; padding-top:3px; }
  .gaprow__body{ grid-column:2; grid-row:2; }
}
@media print{
  .gaprow,.cell{ break-inside:avoid; }
}

/* The highlighter's v3 forms: a Volt underline under the word that matters
   (the hero's "sharper"), and a highlight that is the accent's soft tint
   with the ink colour on it. Never Volt as text on light. */
.hl{
  text-decoration:underline; text-decoration-color:var(--pf-accent);
  text-decoration-thickness:.14em; text-underline-offset:.12em; text-decoration-skip-ink:none;
}
mark{ background:var(--pf-accent-soft); color:var(--pf-fg); padding:0 1px; border-radius:var(--radius-xs); }
/* In dark the soft tint is a raised grey, so the highlight carries a Volt
   underline as well, as compare.html draws it (its --mark-deco). */
[data-theme="dark"] mark{ text-decoration:underline; text-decoration-color:var(--pf-accent); text-decoration-thickness:2px; text-underline-offset:3px; }
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) mark{ text-decoration:underline; text-decoration-color:var(--pf-accent); text-decoration-thickness:2px; text-underline-offset:3px; }
}
/* Never under a gap: an old analysis's highlighted title is amber. */
.gapnote__title mark{ text-decoration:none !important; }


/* ============================================================
   THE GAP MEMO (2026-10-03)
   Numbered notes, each gap marked in the "close" colours (amber, §4: gaps
   are never red, and never Volt, which means matched). Replaced a stack of
   tinted alert boxes each stamped "GAP".
   ============================================================ */
.gapmemo{ list-style:none; margin:0; padding:0; }
.gapnote{ display:grid; grid-template-columns:28px minmax(0,1fr); gap:var(--space-3); padding:var(--space-5) 0; }
.gapnote:first-child{ padding-top:0; }
.gapnote + .gapnote{ border-top:var(--hairline) solid var(--pf-border); }
.gapnote__n{
  display:grid; place-items:center; width:24px; height:24px; margin-top:2px;
  background:var(--pf-partial-bg); color:var(--pf-partial); border:var(--hairline) solid var(--pf-partial);
  border-radius:var(--radius-sm); font:var(--font-weight-semibold) var(--type-caption-size)/1 var(--font-mono); font-variant-numeric:tabular-nums;
}
.gapnote__title{
  margin:0; font:var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-leading) var(--font-display);
  letter-spacing:var(--type-h4-tracking); color:var(--pf-fg);
}
.gapnote__title mark{
  background:var(--pf-partial-bg); color:var(--pf-partial); padding:1px 4px; border-radius:var(--radius-xs);
  -webkit-box-decoration-break:clone; box-decoration-break:clone;
}
.gapnote__fix{ margin:0; max-width:var(--measure-prose); font-size:var(--type-body-size); color:var(--pf-fg-secondary); }
.gapnote__fix strong{ color:var(--pf-fg); }

/* Where a document stands, one line under its title: not a banner. */
.docstatus{ margin:0; font-size:var(--type-body-size); color:var(--pf-fg-secondary); }
.docstatus strong{ color:var(--pf-fg); }

/* A thread, numbered post by post. The number is direction, so it is sans. */
.thread{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--stack); }
.thread > li{ display:grid; grid-template-columns:2.25rem minmax(0,1fr); align-items:baseline; }
.thread__n{ font:var(--type-data-weight) var(--type-data-size)/1 var(--font-mono); color:var(--pf-primary-text); font-variant-numeric:tabular-nums; }

/* A button that reads as a word in a sentence: rewrite starting points. */
.refine__presets{ display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--space-1) var(--space-3); margin:0; font-size:var(--type-body-size); color:var(--pf-fg-muted); }
.textbtn{
  padding:0; border:0; background:none; cursor:pointer;
  font:var(--font-weight-medium) var(--type-body-size)/1.4 var(--font-sans); color:var(--pf-fg);
  text-decoration:underline; text-decoration-color:var(--pf-border-input); text-underline-offset:4px;
}
.textbtn:hover{ text-decoration-color:currentColor; }
.textbtn:focus-visible{ outline:var(--focus-ring-width) solid var(--pf-focus); outline-offset:var(--focus-ring-offset); border-radius:2px; }

/* Home's "still writing" card carries the ring at its top right. */
.status:has(.home__writingspin){ position:relative; padding-right:calc(var(--space-4) + 24px); }
.home__writingspin{ position:absolute; top:var(--space-3); right:var(--space-4); }


/* ============================================================
   PHONES AND THE TABLET BAND (2026-10-04 mobile pass)
   ============================================================ */

/* A long word in a large heading wraps rather than pushing the page wider. */
h1,h2,.pagehead__title{ overflow-wrap:break-word; }

/* The sign-in grid: the brand strip takes its content's height, the form the
   rest. Without rows the two auto rows shared the spare height and the brand
   strip grew to a third of a tablet screen. */
@media (max-width:1023px){
  .auth{ grid-template-rows:auto 1fr; }
}

/* Below lg the save notice sits bottom-left above the bottom bar, clear of
   the masthead and of an open record's pinned head and its Done. */
@media (max-width:1023px){
  .pf-savenotice{
    top:auto; right:auto; left:var(--space-4);
    bottom:calc(64px + env(safe-area-inset-bottom) + var(--space-3));
  }
}

/* Focus never lands under the fixed bars: the masthead plus an open record's
   pinned head above, the bottom bar below. */
@media (max-width:1023px){
  html{ scroll-padding-top:calc(var(--topbar-h) + 64px); scroll-padding-bottom:80px; }
}

/* Nested roles on a phone: less indent, so the fields keep their width. */
@media (max-width:639px){
  .entry__positions{ margin-left:0; padding-left:var(--space-3); }
  .entry__positions .entry[data-open="true"]{
    margin-left:calc(-1 * (var(--space-3) + var(--hairline)));
    padding-left:calc(var(--space-3) - var(--rule) + var(--hairline));
  }
}

/* 44px targets for small controls, without growing them. */
.tabs button,.tabs a,.textbtn,.iconlink,a.balance,.select--sm,.wordmark{ position:relative; }
/* ::before, because a tab's ::after is its current-tab marker. */
.tabs button::before,.tabs a::before{ content:""; position:absolute; inset:-4px 0; }
.textbtn::after,.iconlink::after,a.balance::after,.wordmark::before{ content:""; position:absolute; inset:-12px -4px; }
@media (max-width:1023px){
  .menu__item,.sidenav a{ min-height:var(--hit-min); }
}

/* The md row: a long company and status cannot squeeze the title. */
@media (min-width:768px){
  .workrow{ grid-template-columns:minmax(0,1fr) minmax(0,auto); }
  .workrow__meta{ max-width:45%; justify-self:end; }
}

/* ============================================================
   MOTION (brand-identity.md §7; session 2b, 2026-10-13)
   Motion explains a change of state. Things arrive and settle flush; good
   news snaps, hard news settles. Every distance is a --motion-distance-*
   token and every duration a --duration-*, so the reduced-motion block in
   tokens.css (the device's setting, or data-motion="reduced") turns
   movement into short fades and stops loops without a rule here. The
   keyframes are this file's own, so the plain-CSS consumers (admin, the
   website) get them too.
   ============================================================ */
@keyframes pf-align-in{ from{ opacity:0; transform:translateY(var(--motion-distance-md)); } }
@keyframes pf-slide-snap{ from{ opacity:0; transform:translateX(calc(var(--motion-distance-lg) * -1)); } }
@keyframes pf-settle{ from{ opacity:0; transform:translateY(var(--motion-distance-sm)); } }
@keyframes pf-marker{ from{ opacity:0; transform:scaleX(.4); } }
@keyframes pf-scan{ from{ top:0; } to{ top:calc(100% - 2px); } }
/* The logo's bowl: 12 of the logo's 48 units right, on the icon's 24-unit grid. */
@keyframes pf-lock-in{ from{ opacity:0; transform:translateX(calc(var(--motion-distance-logo) / 2)); } }

/* 1 · Page entry, "align in": a page's blocks rise and fade in, 40ms
   apart, for at most six (ModuleLayout puts .pf-enter on every page). */
.pf-enter > *{ animation:pf-align-in var(--duration-md) var(--ease-enter) both; }
.pf-enter > :nth-child(2){ animation-delay:calc(1 * var(--duration-stagger)); }
.pf-enter > :nth-child(3){ animation-delay:calc(2 * var(--duration-stagger)); }
.pf-enter > :nth-child(4){ animation-delay:calc(3 * var(--duration-stagger)); }
.pf-enter > :nth-child(5){ animation-delay:calc(4 * var(--duration-stagger)); }
.pf-enter > :nth-child(n+6){ animation-delay:calc(5 * var(--duration-stagger)); }

/* 4 · Hover and press, "lean and lock": one shade deeper (the variants
   above), the arrow nudges 2px, a press scales to .98, a card lifts 2px
   and one elevation step. Focus rings appear at once, never animated.
   Only where motion is welcome: a reduced setting keeps everything still. */
@media (prefers-reduced-motion: no-preference){
  :root:not([data-motion="reduced"]) .btn{ transition-property:background-color, border-color, color, transform; }
  :root:not([data-motion="reduced"]) .btn:active{ transform:scale(.98); transition-duration:var(--duration-xs); }
  :root:not([data-motion="reduced"]) .start,
  :root:not([data-motion="reduced"]) .workrow{ transition-property:box-shadow, border-color, transform; }
  :root:not([data-motion="reduced"]) .start:hover,
  :root:not([data-motion="reduced"]) .workrow:hover{ transform:translateY(-2px); }
  :root:not([data-motion="reduced"]) .start:hover .start__arrow{ transform:translateX(2px); }
}

/* 2 · Generating a document, "assembly": no spinner. A skeleton of the
   document with a 2px scan line travelling down it, and the real stage
   named above it; the document slides in and snaps flush when it lands
   (Artifact.jsx). The scan is a loop, so a reduced setting removes it. */
.assembly{ display:flex; flex-direction:column; gap:var(--space-3); max-width:var(--measure-prose); }
.assembly__stage{ display:flex; align-items:center; gap:var(--space-2); margin:0; color:var(--pf-fg); }
.assembly__dot{ flex:none; width:10px; height:10px; border-radius:50%; background:var(--pf-primary-text); }
.assembly__sheet{
  position:relative; overflow:hidden; display:flex; flex-direction:column; gap:var(--space-5);
  padding:var(--space-2) 0;
}
.assembly__block{ display:flex; flex-direction:column; gap:var(--space-2); animation:pf-slide-snap var(--duration-lg) var(--ease-snap) both; }
.assembly__block:nth-child(2){ animation-delay:calc(2 * var(--duration-stagger)); }
.assembly__block:nth-child(3){ animation-delay:calc(4 * var(--duration-stagger)); }
.assembly__line{ display:block; height:9px; border-radius:var(--radius-sm); background:var(--pf-surface-sunken); }
.assembly__line--head{ width:32%; height:11px; background:var(--pf-border); }
.assembly__line:nth-child(3){ width:88%; }
.assembly__line:nth-child(4){ width:64%; }
.assembly__scan{
  position:absolute; left:0; right:0; top:0; height:2px; background:var(--pf-scan);
  animation:pf-scan 1.6s linear infinite;
}
@media (prefers-reduced-motion: reduce){ .assembly__scan{ display:none; } }
[data-motion="reduced"] .assembly__scan{ display:none; }
/* A document that lands while you watch slides in from the left and snaps. */
.artifact__arrive{ animation:pf-slide-snap var(--duration-lg) var(--ease-snap) both; }

/* The package's own progress, on the workspace: the stage it is really at,
   the parts finished in mono, and a bar that grows as each one lands.
   Progress shown is real (§7): it moves only when a document is written. */
.progress{ display:flex; flex-direction:column; gap:var(--space-2); max-width:var(--measure-ui); }
.progress__line{ display:flex; align-items:center; gap:var(--space-3); margin:0; }
.progress__count{ margin-left:auto; color:var(--pf-fg-muted); }
.progress__bar{ display:block; height:4px; border-radius:var(--radius-full); background:var(--pf-surface-sunken); overflow:hidden; }
.progress__bar > span{
  display:block; height:100%; background:var(--pf-primary-text);
  transition:width var(--duration-xl) var(--ease-enter);
}

/* 3 · The gap results, "strengths first": matched cells and rows snap in,
   then, after a 200ms pause, the close and to-address ones settle in
   slower and softer, with no overshoot (artifacts.jsx sets the order). */
.reveal-snap{ animation:pf-slide-snap var(--duration-lg) var(--ease-snap) both; }
.reveal-marker{ animation:pf-marker var(--duration-lg) var(--ease-snap) both; transform-origin:left center; }
.reveal-settle{ animation:pf-settle var(--duration-lg) var(--ease-standard) both; }

/* 5 · Confirm, "lock-in": the toast rises from the bottom with the mini
   symbol, whose bowl snaps onto its stem (lib/toast.jsx, Icon.jsx). */
.toast-region{
  position:fixed; z-index:var(--z-toast); left:50%; transform:translateX(-50%);
  bottom:calc(var(--space-6) + env(safe-area-inset-bottom));
  width:max-content; max-width:calc(100vw - 2 * var(--space-4));
}
@media (max-width:1023px){ .toast-region{ bottom:calc(64px + var(--space-3) + env(safe-area-inset-bottom)); } }
.toast{
  display:flex; align-items:center; gap:var(--space-3);
  padding:var(--space-3) var(--space-4); border-radius:var(--radius-md);
  background:var(--pf-inverse-bg); color:var(--pf-inverse-fg); box-shadow:var(--pf-shadow-xl);
  font-size:var(--type-body-sm-size); line-height:var(--type-body-sm-leading); font-weight:var(--font-weight-medium);
  animation:pf-align-in var(--duration-md) var(--ease-enter) both;
  --lock-bowl:var(--color-volt-400);
}
[data-theme="dark"] .toast{ --lock-bowl:var(--color-cobalt-600); }
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]) .toast{ --lock-bowl:var(--color-cobalt-600); } }
.lockin{ overflow:visible; }
.lockin__bowl{ fill:var(--lock-bowl, var(--pf-primary-text)); animation:pf-lock-in var(--duration-xl) var(--ease-snap) both; }
.btn--primary .lockin__bowl{ fill:var(--pf-accent); }
