/* Profile — native module styles, built against apps/shared/design-tokens.css */

/* Phase 3 retrofit (2026-07-13): local ink-pill override for Profile's
 * primary actions — every .yeja-pill-btn usage here (14 call sites: Save
 * Changes/Address/Email, View Portfolio, Save Story, Confirm, Copy Link,
 * Add Entry, Send Invite/Inquiry, Save Style, Export/Generate CV PDF) is
 * a genuine primary action, so a blanket override is appropriate — same
 * pattern as Auctions'/Social's .yeja-pill-btn overrides. Scoped to
 * .prof-page since that's the confirmed real root across all 3 render
 * paths (private command center, public summary, public/preview
 * Portfolio) — see mountProfile()/renderHTML()/renderPublicHTML()/
 * renderPublicPortfolioHTML(). No bare .button class combined with
 * .yeja-pill-btn anywhere in this app (checked — the trap that bit
 * Social), so no html[data-theme="dark"] .button specificity fight to
 * guard against here.
 *
 * Bug fixed 2026-07-22: this rule's own ".prof-page .yeja-pill-btn"
 * selector (specificity 0,2,0) unconditionally beat the shared
 * .yeja-pill-btn-ghost/.yeja-pill-btn-danger modifiers (0,1,0 each),
 * re-flattening every ghost/danger button in this app back to solid
 * fill/no-border — confirmed live via CDP on the public portfolio's
 * "Just send a quick inquiry" button (computed background was solid
 * black, not transparent, even with both classes present in the
 * markup). The "every usage here is primary" assumption predates this
 * app's own ghost/danger buttons (Sign Out Everywhere, Delete My
 * Account, Download Summary (PDF), the quick-inquiry gate). Excluding
 * those modifiers here rather than raising their specificity keeps the
 * shared component the single source of truth for what "ghost"/
 * "danger" actually look like.
 *
 * THE :not() EXCLUSIONS BELOW ARE LOAD-BEARING (backlog f7a0fa25 item 3).
 * They are not defensive noise and must not be "simplified" away: removing
 * either one silently re-flattens every ghost and danger button in this app
 * — including Delete My Account — back to a solid primary fill, which is
 * the specific regression documented above. Reviewed and deliberately left
 * in place; no behavioural change was made here. */
.prof-page .yeja-pill-btn:not(.yeja-pill-btn-ghost):not(.yeja-pill-btn-danger) {
  background: var(--yeja-text-primary);
  border: none;
  color: var(--yeja-canvas);
  text-transform: none;
  font-weight: 500;
  transition: opacity 0.15s ease-in-out;
}
.prof-page .yeja-pill-btn:not(.yeja-pill-btn-ghost):not(.yeja-pill-btn-danger):hover {
  background: var(--yeja-text-primary);
  opacity: 0.85;
}

/* Elastic width (backlog ff1dd8a8, 2026-07-26: "expand the width of the
 * profile canvas, it must be elastic abiding the browser size") — was a
 * hard 640px cap, which is why .prof-hub-grid's auto-fill(300px) below
 * never got room to lay out more than 1 column even on a wide desktop
 * viewport. Fluid width capped at 1400px so text measure doesn't run
 * unreadably wide on ultra-wide monitors. .pf-page (public portfolio,
 * below) keeps its own separate 900px override untouched. */
.prof-page {
  width: 100%;
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--space-4, 32px) var(--space-3, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-4, 32px);
  box-sizing: border-box;
}

/* Real bug fixed 2026-07-17: the shell's global apps/platform/styles.css
 * sets `body { font-family: var(--font-family-base) }` once, and every
 * module-federation-mounted app just inherits it — but
 * apps/portfolio-public/index.html is a standalone page with no shell,
 * so nothing ever set a base font at all and the browser fell back to
 * its default serif. Scoped to body (not .prof-page) so it also covers
 * the lightbox/CV-export modals, which are appended straight to
 * document.body outside .prof-page, not just the main page content. */
body {
  font-family: var(--font-family-base, "Inter", system-ui, sans-serif);
  color: var(--ink);
}

.prof-header {
  display: flex;
  align-items: center;
  gap: var(--space-3, 24px);
  /* MUST wrap (2026-08-05). .prof-verification-action is `flex: 0 0 auto`
   * so it refuses to shrink; in a nowrap row at 390px the "Request
   * Verification" button was pushed to x=411 in a 390px viewport — 21px
   * of a primary action unreachable. It does NOT show up as horizontal
   * scroll (an ancestor clips it, so scrollWidth stays 390), which is why
   * the usual no-h-scroll check never caught it.
   *
   * The public portfolio already had this fix at .pf-page .prof-header;
   * the DASHBOARD header never did. Fixing it here covers every Profile
   * tab, not just the Portfolio one. */
  flex-wrap: wrap;
  row-gap: var(--space-2, 16px);
}

/*
 * Avatar upload added 2026-07-08: .prof-avatar-wrap positions the new
 * edit-photo button in the bottom-right corner of the circular avatar,
 * matching the small-badge-on-avatar pattern used elsewhere on the
 * platform (e.g. verification badges) rather than a separate detached
 * "change photo" link.
 */
.prof-avatar-wrap {
  position: relative;
  flex: 0 0 64px;
}

.prof-avatar {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-circle, 50%);
  background: var(--surface-alt);
  color: var(--muted);
  display: inline-grid;
  place-items: center;
  font-size: var(--text-h2-size, 24px);
  font-weight: var(--text-h2-weight, 600);
  overflow: hidden;
}

.prof-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.prof-avatar-edit-btn {
  position: absolute;
  bottom: -2px;
  inset-inline-end: -2px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--yeja-cobalt-ink);
  color: var(--yeja-gray-0, #ffffff);
  display: inline-grid;
  place-items: center;
  cursor: pointer;
  /* padding/font reset — a <button> since 2d9bc115, so it no longer
   * inherits the <label>'s zero-padding default. The 2px border is the
   * intended paper-coloured ring, not a UA border. */
  padding: 0;
  font-family: inherit;
  border: 2px solid var(--paper);
  transition: transform 0.15s ease-in-out;
}
.prof-avatar-edit-btn:hover { transform: scale(1.08); }
@media (prefers-reduced-motion: reduce) {
  .prof-avatar-edit-btn { transition: none; }
  .prof-avatar-edit-btn:hover { transform: none; }
}
.prof-avatar-edit-btn svg { width: 12px; height: 12px; }
.prof-avatar-edit-btn .yeja-icon { font-size: 12px; }

/* Hover overlay + drag-and-drop (backlog 06c5c561, 2026-08-02) — a
 * full-circle overlay over the avatar image itself, fading in on
 * hover/focus/drag-over, on top of (not replacing) the always-visible
 * corner edit badge above — the badge stays as the only affordance a
 * touch/no-hover device has, the overlay is purely an enhancement for
 * pointer devices. */
.prof-avatar-hover-overlay {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-circle, 50%);
  /* Scrim over imagery — color-mix on the gray-1000 token, per design
   * guide §1. Was rgba(0,0,0,.55), a raw literal the guide forbids even
   * for a colour that deliberately doesn't theme. */
  background: color-mix(in oklch, var(--yeja-gray-1000) 55%, transparent);
  color: var(--yeja-gray-0, #ffffff);
  /* button reset — this is a <button> now, not a <label> (WCAG 2.1.1,
   * backlog 2d9bc115), so it arrives with UA border/padding/font. */
  border: 0;
  padding: 0;
  font-family: inherit;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-size: 10px;
  font-weight: 600;
  opacity: 0;
  cursor: pointer;
  transition: opacity 0.15s ease-in-out;
}
.prof-avatar-hover-overlay .yeja-icon { font-size: 18px; }
.prof-avatar-wrap:hover .prof-avatar-hover-overlay,
.prof-avatar-hover-overlay:focus-visible,
.prof-avatar-wrap.is-drag-over .prof-avatar-hover-overlay {
  opacity: 1;
}
/* Cobalt focus ring, 2px / 2px offset (guide §3). Before 2d9bc115 the
 * :focus-visible rule above could never match — a <label> isn't
 * focusable — so keyboard users got no visible focus state either. */
.prof-avatar-hover-overlay:focus-visible,
.prof-avatar-edit-btn:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .prof-avatar-hover-overlay { transition: none; }
}
.prof-avatar-wrap.is-drag-over .prof-avatar {
  outline: 2px dashed var(--yeja-cobalt, #1E88E5);
  outline-offset: 3px;
}

.prof-avatar-status {
  margin: 4px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  min-height: 16px;
}

/* Crop/zoom/rotate editor modal (backlog 06c5c561, 2026-08-02) — a
 * dedicated .yeja-dialog-card, not the standard 480px width (needs room
 * for the square canvas), circular crop mask applied via clip-path on the
 * canvas element itself so what previews as masked-out on screen is
 * exactly what the Save step excludes from the real output. */
.prof-avatar-editor-card { max-width: 400px; }
.prof-avatar-editor-canvas-wrap {
  display: flex;
  justify-content: center;
}
.prof-avatar-editor-canvas-wrap canvas {
  clip-path: circle(50% at 50% 50%);
  background: var(--surface-alt);
  touch-action: none; /* pointer drag-to-pan must not also scroll the page on touch */
  cursor: grab;
}
.prof-avatar-editor-canvas-wrap canvas:active { cursor: grabbing; }
.prof-avatar-editor-control {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: var(--text-body-size, 14px);
}
.prof-avatar-editor-control span:first-child {
  flex: 0 0 52px;
  color: var(--muted);
}
.prof-avatar-editor-control input[type="range"] { flex: 1; }
.prof-avatar-editor-value {
  flex: 0 0 36px;
  text-align: end;
  font-family: var(--font-family-mono);
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}
.prof-avatar-editor-progress {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Track — the outer bar's own background/rounding/clip. */
.prof-avatar-editor-progress-track {
  flex: 1;
  height: 6px;
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-alt);
  overflow: hidden;
}
/* Fill — always full width, scaled via transform (not width) so updating
 * progress every tick doesn't trigger layout, only compositing. Anchored
 * left via transform-origin so scaleX shrinks/grows from the start edge,
 * matching how a real progress bar reads. */
.prof-avatar-editor-progress-bar {
  height: 100%;
  width: 100%;
  border-radius: var(--radius-pill, 999px);
  background: var(--yeja-cobalt, #1E88E5);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.15s ease-out;
}
.prof-avatar-editor-progress span {
  flex: 0 0 auto;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  font-family: var(--font-family-mono);
}
.prof-avatar-editor-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.prof-header-text {
  flex: 1;
  /* 160px, not 0 (2026-08-06): at 0 the flex algorithm keeps shrinking
   * this column instead of wrapping .prof-header-actions onto its own
   * row, so the name collapsed to ~14px wide and wrapped internally
   * behind the actions group rather than the row breaking cleanly. */
  min-width: 160px;
}

.prof-header-text h1 {
  margin: 0;
  font-size: var(--text-h2-size, 24px);
  font-weight: var(--text-h2-weight, 600);
  line-height: var(--text-h2-line, 32px);
  color: var(--ink);
}

.prof-email {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}

.prof-badge {
  flex: 0 0 auto;
  padding: 4px 12px;
  border-radius: var(--radius-pill, 24px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
}

/* Moved into .prof-header, left of the Verified badge (2026-08-06) — was
 * previously a block-level sibling of .prof-page, which stretched it to
 * the full page width instead of hugging its own text. Grouped with the
 * badge in .prof-header-actions (below) so the pair wraps onto its own
 * row as a unit at narrow widths, rather than colliding with
 * .prof-header-text's own internal wrap (flex: 1 there means it still
 * claims the whole first row's remaining width on its own). */
.prof-header-atelier-link {
  flex: 0 0 auto;
  white-space: nowrap;
}

.prof-header-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
}

.prof-badge-verified { background: var(--yeja-success-bg); color: var(--yeja-mint); }
.prof-badge-pending  { background: var(--yeja-info-bg); color: var(--yeja-cobalt); }
.prof-badge-basic    { background: var(--surface-alt); color: var(--muted); }

.prof-verification-action {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}
.prof-verification-hint {
  margin: 0;
  max-width: 220px;
}

/*
 * Self-view discoverability banner (2026-08-06) — sits above .prof-hub-grid
 * on the Account Hub landing view only (see renderHubHTML()/activateHubCard()
 * in app.js), never on any other tab, so it doesn't become repeated chrome
 * once the user has already navigated into the destination it points at.
 */
.prof-self-view-banner {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px);
  margin-bottom: var(--space-3, 24px);
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  background: var(--surface-alt);
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}

.prof-self-view-banner .yeja-icon {
  flex: 0 0 auto;
  color: var(--yeja-cobalt);
}

.prof-self-view-banner span:not(.yeja-icon) {
  flex: 1;
}

.prof-self-view-banner .yeja-pill-btn {
  flex: 0 0 auto;
}

@media (max-width: 560px) {
  .prof-self-view-banner {
    flex-wrap: wrap;
  }
}

/* .prof-card -> apps/shared/components.css's .yeja-card (Phase 4,
 * 2026-07-19) — canonical extraction, byte-identical to what was here. */

.prof-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2, 16px);
}

.prof-form label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-form input,
.prof-form select {
  height: 38px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

.prof-form input:focus-visible,
.prof-form select:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 1px;
}

/* Multi-role checkboxes (2026-07-26) — replaces the old single <select
 * name="role">; spans both grid columns of .prof-form since it holds 4
 * checkboxes, not one control. */
.prof-roles-fieldset {
  grid-column: 1 / -1;
  border: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.prof-roles-fieldset legend {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  padding: 0;
}
.prof-roles-hint {
  font-weight: 400;
  opacity: 0.75;
}
.prof-roles-checkboxes {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.prof-role-checkbox {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
  cursor: pointer;
}
.prof-role-checkbox input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--yeja-cobalt, #1E88E5);
}

.prof-form-actions {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  margin-top: var(--space-1, 8px);
}

.prof-form-error {
  grid-column: 1 / -1;
  font-size: var(--text-label-size);
  line-height: var(--text-label-line);
  color: var(--yeja-crimson);
  margin: 0;
}

.prof-save-status {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* Username live-availability check (Username Addendum, 2026-07-26) —
 * grid-column spans both .prof-form columns since it sits directly below
 * the username input, same layout treatment as .prof-form-error. */
.prof-username-status {
  grid-column: 1 / -1;
  font-size: var(--text-label-size, 12px);
  margin-top: -8px;
  color: var(--muted);
}
.prof-username-status.is-error {
  color: var(--yeja-crimson);
}
.prof-username-status.is-success {
  color: var(--yeja-mint);
}

/* Scoped utilities replacing inline style= attributes (backlog 8fdcd3ef
 * item 5, 2026-08-02). Each of these was repeated verbatim across several
 * call sites in app.js, so they are named rules rather than one-offs —
 * the same layout treatment .prof-form-error / .prof-roles-fieldset above
 * already use, just reusable.
 *
 * The one inline style deliberately KEPT is the palette swatch's
 * `background:${hex}` — that value is the artist's own chosen colour,
 * computed per element at render time, so it cannot become a static
 * class. */

/* Spans both columns of .prof-form's grid. */
.prof-field-full {
  grid-column: 1 / -1;
}

/* Read-only share/copy input that should take the remaining row width
 * next to its Copy button, without collapsing on narrow viewports. */
.prof-copy-input {
  flex: 1;
  min-width: 200px;
}

/* A .prof-address-delete used as a normal-width text button (Cancel in
 * the delete-account and CV-export modals) rather than a square icon.
 *
 * Fixed 2026-08-04: this rule sits at line ~457 while .prof-address-delete
 * is declared at ~844, so at EQUAL specificity (both single classes) the
 * LATER rule won and `width: 32px` was still in force. The label needed
 * ~59px, so "Cancel" overflowed its own 34px box and the Generate PDF
 * pill beside it painted over the spill — measured live:
 * scrollWidth > clientWidth on the button.
 *
 * min-width + auto height rather than a bare `width: auto`, because
 * .prof-address-delete also pins height: 32px and display: inline-grid,
 * which cramp a text label. Not moved below the other rule: co-locating
 * it with the delete-button block would separate it from the other
 * button-shape modifiers here, and an explicit min-width documents the
 * intent better than relying on source order. */
.prof-btn-text-cancel {
  width: auto;
  min-width: max-content;
  height: auto;
  min-height: 32px;
  padding: 6px 16px;
  white-space: nowrap;
}

/* A .prof-field-hint carrying an error message. Was an inline
 * `color: var(--danger, var(--yeja-crimson))` — and **--danger is not
 * defined anywhere in the repo**, so that fallback was doing all the work
 * while looking like a token reference (the `var(--x, y)` hides-an-
 * undefined-token class). Uses --yeja-crimson directly, matching
 * .prof-form-error, which is this file's canonical error colour.
 * .prof-field-hint sets no `display`, so the `hidden` attribute on these
 * elements still works without a [hidden] companion rule. */
.prof-field-hint.is-error {
  color: var(--yeja-crimson);
}

/* Extra breathing room above a hint that follows a block of controls
 * (the delete-account explainer under Account Security). */
.prof-field-hint.has-top-gap {
  margin-top: var(--space-2, 16px);
}

.prof-referral-row {
  display: flex;
  gap: 8px;
}
.prof-referral-row input {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.04em;
}
/* The invite URL is a long string, not a short code: the wide tracking
 * that makes an 8-character code readable just makes a URL harder to scan
 * and pushes it out of the field sooner. min-width:0 above is what lets
 * either input actually shrink inside the flex row instead of overflowing
 * it — a flex item's default min-width:auto refuses to go below its
 * content width, which is how a long URL blows out a row like this. */
.prof-referral-row input.prof-copy-input {
  letter-spacing: normal;
}

/* Code and link are one labelled group; the hint belongs to both. */
.prof-referral-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}
.prof-referral-link-label[hidden] {
  /* Belt and braces: [hidden] is display:none by default, but a
   * display rule on a label elsewhere in this file could out-specify
   * the UA sheet and paint an invite box with an empty URL in it. */
  display: none;
}

.prof-export-data-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 16px);
  margin-top: var(--space-1, 8px);
}

/* TOTP MFA (B2, 2026-07-26) — enroll → verify wizard inside the Security
 * section's own card (renderTotpSectionHTML() in app.js). .prof-mono
 * (defined further down this file) supplies the monospace/tabular-nums
 * treatment for the manual-entry secret; everything below is layout only. */
.prof-totp-setup {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1, 8px);
}

.prof-totp-qr {
  display: block;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  padding: var(--space-1, 8px);
  /* QR codes need a guaranteed light backing regardless of theme — dark
     mode would otherwise invert the SVG's own black-on-white modules into
     an unscannable white-on-transparent code. Deliberately NOT themable,
     but per guide §1 a literal that must not theme is still not a raw
     hex: --yeja-gray-0 is that white, and it is ramp-fixed (it does not
     flip between themes), so scannability is preserved. */
  background: var(--yeja-gray-0, #fff);
}

.prof-totp-secret {
  display: inline-block;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--surface-alt);
  color: var(--ink);
  font-size: var(--text-label-size, 12px);
  letter-spacing: 0.04em;
  word-break: break-all;
}

.prof-totp-recovery-notice {
  margin-top: var(--space-2, 16px);
  padding: var(--space-2, 16px);
  border: 1px solid var(--yeja-amber, #FFB300);
  border-radius: var(--radius-control, 8px);
  background: var(--yeja-warning-bg);
}

.prof-totp-recovery-title {
  margin: 0 0 var(--space-1, 8px);
  font-weight: 600;
  color: var(--yeja-mint);
}

.prof-totp-recovery-heading {
  margin: 0 0 4px;
  font-weight: 600;
  color: var(--ink);
}

/* Sign-in Activity (B3, 2026-07-26) — same list/row shape as
 * .prof-kyc-list/.prof-kyc-row directly below, kept as its own class pair
 * rather than reused directly since this list has no status dot and its
 * items are plain read-only timestamps. */
.prof-login-activity-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
  margin: var(--space-1, 8px) 0;
  padding: 0;
  list-style: none;
}

.prof-login-activity-row {
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.prof-login-activity-row:last-child {
  border-bottom: none;
}

.prof-login-activity-note {
  margin-top: var(--space-1, 8px);
}

.prof-kyc-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.prof-kyc-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.prof-kyc-row:last-child {
  border-bottom: none;
}

.prof-kyc-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  /* was var(--yeja-n500, ...) — that token was deleted in Phase 1's
   * warm-ramp retokening; the fallback masked this from being visually
   * broken, but it was silently never resolving via the live token
   * system (always the stale hardcoded fallback). */
  background: var(--yeja-text-muted, #5A5A5A);
  flex: 0 0 auto;
}

.prof-kyc-dot.done {
  background: var(--yeja-mint);
}

.prof-kyc-label {
  flex: 1;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

.prof-kyc-state {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

@media (max-width: 560px) {
  .prof-form {
    grid-template-columns: 1fr;
  }
}

/*
 * Public profile view added 2026-07-08: read-only card shown when this
 * module is deep-linked to someone else's profile (see viewingArtistId
 * in app.js). Reuses .prof-page/.prof-header/.prof-avatar/.prof-card
 * above; only the activity grid below is new.
 */
.prof-activity-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2, 16px);
}

.prof-activity-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: var(--space-2, 16px) var(--space-1, 8px);
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  text-decoration: none;
  transition: background 0.15s ease-in-out;
}

.prof-activity-stat:hover {
  background: var(--surface-alt);
}

.prof-activity-stat:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

.prof-activity-count {
  font-size: var(--text-h2-size, 24px);
  font-weight: var(--text-h2-weight, 600);
  color: var(--ink);
}

.prof-activity-label {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  text-align: center;
}

@media (max-width: 560px) {
  .prof-activity-grid {
    grid-template-columns: 1fr;
  }
}

/*
 * Other-user profile "About" card added 2026-08-06 — bio excerpt + skills
 * tags, a simplified read of the same data the full Atelier's
 * skillsHTML()/artist-statement section render (apps/portfolio-public/
 * atelier.js). Reuses .yeja-card/.yeja-tag-chip/.yeja-row-list-item from
 * apps/shared/components.css; only the excerpt typography and the tag-row
 * wrap need a local rule.
 */
.prof-bio-excerpt {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-body-size, 14px);
  color: var(--muted);
  line-height: 1.5;
}

.prof-skills-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1, 8px);
}

.prof-timeline-highlight-when {
  font-size: var(--text-label-size, 12px);
  font-weight: 400;
  color: var(--muted);
  white-space: nowrap;
}

/*
 * Internal command-center tab strip added 2026-07-15 (Shipping, Orders,
 * Bids, RFPs, NFTs, Activity, Security, Portfolio-editing, alongside the
 * original Overview), extracted to apps/shared/tabs.js + .yeja-tabs/
 * .yeja-tab in apps/shared/design-tokens.css (Phase 3, 2026-07-19).
 * REMOVED (backlog d85b90d8, 2026-08-02) — replaced by .prof-breadcrumbs
 * below; the Account hub's own cards are the real navigation surface now
 * (see PROF_TABS' header comment in app.js). Only the loading-state class
 * below survives from that era, since it isn't part of the removed strip.
 */
.prof-tab-loading {
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
  padding: var(--space-3, 24px) 0;
}

/* Breadcrumbs — every style now lives in apps/shared/components.css's
 * .yeja-crumbs block (backlog 5f81ff03, 2026-08-02). The local copy that
 * used to sit here existed only because My Studio's classes lived in a
 * stylesheet Profile never loads; its own comment said so. That is
 * exactly the duplication the shared component removes, so all that
 * remains here is Profile's own spacing around the trail. */
/* Gap to the cards below cut by 80% (2026-08-02, backlog bd5a6a24,
 * user instruction: "reduce the padding between breadcrumb and the cards
 * by 80%"). Was var(--space-2, 16px) → 3px.
 *
 * A literal, deliberately: the 8pt scale in apps/shared/design-tokens.css
 * starts at --space-1 (8px) and has no smaller step, so there is no token
 * that expresses 80%-off-16px. Writing var(--space-0, 3px) would look
 * tokenised while referencing a token that does not exist — the same
 * undefined-token trap that already shipped in this repo as a var(--border,
 * ...) fallback pointing at a token nobody ever defined. A spacing literal
 * is not a themable value, so it carries none of the risk a raw colour
 * would. */
.prof-page .prof-breadcrumbs {
  padding: 0 0 3px;
}

#prof-tab-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-4, 32px);
}

/* Clearance for the floating Style dock (2026-08-30). The dock is
 * position:fixed, so it takes no space in flow and the last card scrolls
 * underneath it — on a short viewport the final control ends up permanently
 * behind the bar.
 *
 * Scoped by :has() on the DOCK'S OWN PARENT, not on #prof-tab-content:
 * mountPortfolioStyleDock appends the dock to `.prof-page`
 * (host.parentElement), so it is a SIBLING of #prof-tab-content, not a
 * descendant — `#prof-tab-content:has(#pf-style-dock)` matched nothing and
 * silently applied no padding (confirmed live: paddingBottom stayed 0px).
 * The dock is removed on every tab switch, so no other tab pays for the gap.
 * ~62px dock + 20px bottom offset + breathing room. */
.prof-page:has(#pf-style-dock) #prof-tab-content {
  padding-bottom: 104px;
}

/* Ship To/From wrapper divs are one level nested inside #prof-tab-content
 * (backlog d85b90d8, 2026-08-02), so its gap rule above never reached the
 * two .yeja-card sections (address list + "Add Address" form) stacked
 * inside each wrapper — they sat flush together with no gap at all. */
#prof-shipping-section,
#prof-ship-from-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-4, 32px);
}

/* .prof-empty-state -> apps/shared/components.css's .yeja-empty-state
 * (Phase 4, 2026-07-19) — canonical extraction, byte-identical. */

.prof-field-hint {
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
  margin: var(--space-1, 8px) 0 0;
}

.prof-checkbox-label {
  flex-direction: row !important;
  align-items: center;
  gap: 8px !important;
}

.prof-notification-toggles {
  display: flex;
  flex-direction: column;
  gap: var(--space-2, 16px);
}

.prof-notification-toggle-hint {
  display: block;
  font-size: var(--text-label-size, 12px);
  font-weight: 400;
  color: var(--muted);
}

/* .status-chip -> apps/shared/components.css's .yeja-status-chip (Phase
 * 4, 2026-07-19) — bucket-based now (neutral/info/success/warning/
 * danger), not word-based; see statusChipHTML()/PROF_STATUS_BUCKETS in
 * app.js and components.css's own comment for why. This was previously
 * a bare, unscoped .status-chip that collided with My Studio's and
 * Social's own differently-styled same-named classes whenever more than
 * one app's stylesheet was loaded in the same shell session (inject-
 * stylesheet.js never unloads a previous app's CSS) — the bucket
 * migration removes the shared class name collision risk entirely, not
 * just Profile's copy of it. */

/* Shipping addresses */
.prof-address-list,
.prof-order-list,
.prof-bid-list,
.prof-rfp-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

/* Shared plain-text hint under a form section or field (2026-07-28,
 * Art Board cart checkout — used by the Ship From address prompt and
 * My Studio's price/shipping-dimensions field). */
.field-hint {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
}

.prof-address-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px) 0;
  border-bottom: 1px solid var(--line);
}

.prof-address-row:last-child { border-bottom: none; }

.prof-address-label {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

.prof-address-recipient,
.prof-address-lines,
.prof-address-phone {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
}

.prof-address-delete {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: inline-grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.prof-address-delete:hover {
  color: var(--yeja-crimson, #E53935);
  border-color: var(--yeja-crimson, #E53935);
}

.prof-address-delete svg { width: 16px; height: 16px; }
.prof-address-delete .yeja-icon { font-size: 16px; }

/* Orders / Bids / RFPs shared row shape -> apps/shared/components.css's
 * .yeja-row-list-* (Phase 4, 2026-07-19) — canonical extraction,
 * byte-identical to what was here (this was already unified internally
 * via one comma-selector rule before the extraction). */

/* NFTs owned */
.prof-nft-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-2, 16px);
}

.prof-nft-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  padding: var(--space-1, 8px);
}

.prof-nft-thumb {
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-media, 3px);
  background: var(--surface-alt);
  overflow: hidden;
  margin-bottom: var(--space-1, 8px);
}

.prof-nft-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.prof-nft-title {
  margin: 0;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.prof-nft-meta {
  margin: 2px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* Activity feed — reuses .prof-kyc-dot's dot+label+state row shape,
 * extended with a trailing date column. */
.prof-activity-feed {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.prof-activity-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.prof-activity-row:last-child { border-bottom: none; }

.prof-activity-text {
  flex: 1;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

.prof-activity-date {
  flex: 0 0 auto;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* Maker tier row (Your Activity v1, A6, 2026-07-26) — read-only status
 * chip + hint text. Chip colors match Art Board's own MAKER_TIER_BADGES
 * palette (.masonry__tier-badge in apps/art_board/styles.css) for
 * trusted/signature so the same tier reads as the same color everywhere
 * on the platform; new_maker/established get their own neutral/info
 * treatment since Art Board deliberately renders no badge at all for
 * those two (this is the owner's own status row, not a public badge, so
 * it shows all 4 tiers rather than staying silent for the two that
 * aren't "achievement" tiers). */
.prof-tier-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  flex-wrap: wrap;
}

.prof-tier-chip {
  flex: 0 0 auto;
  padding: 4px 12px;
  border-radius: var(--radius-pill, 999px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  white-space: nowrap;
}

.prof-tier-chip--new_maker {
  background: var(--surface-alt);
  color: var(--muted);
}

.prof-tier-chip--established {
  background: var(--yeja-info-bg);
  color: var(--yeja-cobalt, #1E88E5);
}

.prof-tier-chip--trusted {
  background: var(--yeja-info-bg);
  color: var(--yeja-cobalt, #1E88E5);
}

.prof-tier-chip--signature {
  background: color-mix(in srgb, var(--yeja-amber, #FFB300) 18%, transparent);
  color: var(--yeja-amber, #FFB300);
}

.prof-tier-hint {
  flex: 1 1 220px;
  margin: 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* A12 (2026-08-03) — "Your Plan" hub pane. A DISTINCT class from
 * .prof-tier-chip above, not a second bare `.prof-tier-chip {}` rule —
 * that class already has four --<tier> modifiers
 * (--new_maker/--established/--trusted/--signature) applied alongside the
 * base class on the SAME element (see renderMakerTierHTML()'s
 * `class="prof-tier-chip prof-tier-chip--${tier}"`), so a second base-class
 * rule declared later in this file would win the cascade on source order
 * alone (equal specificity, same class) and silently repaint every maker-
 * tier badge platform-wide with this rule's colors instead of its own.
 * Professional/Business aren't part of that vocabulary at all, so this
 * gets its own name and reuses only the shared layout (padding/radius/
 * font) via .prof-tier-chip, plus its own fill — a plain, calm treatment
 * (same bucket-neutral spirit as .yeja-status-chip.info) since the REAL
 * status signal for a plan is the adjacent .yeja-status-chip
 * (active/trialing/past_due/canceled), not this label. */
.prof-plan-tier-chip {
  background: var(--yeja-info-bg);
  color: var(--yeja-cobalt, #1E88E5);
}

.prof-plan-invoices {
  margin-top: var(--space-3, 20px);
  padding-top: var(--space-3, 20px);
  /* --line, not the old var(--border, <grey literal>) (backlog 8fdcd3ef).
   * `--border` is NOT a token — it is defined nowhere in
   * design-tokens.css, so this rule silently fell through to its
   * hardcoded light-grey fallback in BOTH themes,
   * leaving a light-grey hairline sitting on a near-black dark-mode
   * surface. --line is the real themed hairline token, used 53x in this
   * file already. This was the only var(--border, …) left in the repo. */
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.prof-plan-invoices h3 {
  margin: 0 0 var(--space-1, 8px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/*
 * Portfolio owner-side editing (2026-07-15) — pin management, timeline
 * CRUD, collaborations, capability tags. Lives inside the same
 * .prof-card/.prof-form scaffolding as the rest of the private view.
 */
.prof-pin-edit-grid {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a `1fr` track carries an implicit `auto`
   * MINIMUM, so it can never shrink below its widest item's intrinsic
   * width — one wide child silently widens the whole column and overflows
   * the card. Measured at 390px: a 448px track in a 292px body. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2, 16px);
}

.prof-pin-edit-card {
  display: flex;
  gap: var(--space-2, 16px);
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  padding: var(--space-2, 16px);
}
.prof-pin-edit-card[draggable="true"] {
  cursor: grab;
}

/* Drag handle + keyboard Up/Down buttons (backlog d85b90d8, 2026-08-02) —
 * same drag-plus-arrow-buttons a11y pattern as Page Layout's section
 * reorder in apps/admin/admin.js. */
.prof-pin-edit-reorder {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.prof-pin-drag-handle {
  cursor: grab;
}
.prof-pin-move-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.prof-pin-move-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Enlarged from a 96px fixed thumb (backlog d85b90d8, 2026-08-02: "increase
 * image preview windows larger") and turned into a real <button> so owners
 * can click into the same lightbox (openLightbox/portfolioLightboxHTML)
 * the public portfolio page's pin grid already opens on click — border/
 * background/font reset since the element is no longer a plain <div>. */
.prof-pin-edit-thumb {
  /* 160px -> 288px, +80% (2026-08-04 user instruction). The preview is
   * how an artist judges which work is pinned where, and at 160px a
   * detailed painting was unreadable. min() keeps it from crowding the
   * card's text column on a narrow window. */
  flex: 0 0 min(288px, 42vw);
  aspect-ratio: 1 / 1;
  border: none;
  border-radius: var(--radius-control, 8px);
  background: var(--surface-alt);
  overflow: hidden;
  padding: 0;
  font: inherit;
  cursor: pointer;
}
.prof-pin-edit-thumb:disabled {
  cursor: default;
}
.prof-pin-edit-thumb:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

.prof-pin-edit-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.prof-pin-edit-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.prof-pin-edit-title {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

.prof-pin-edit-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-pin-edit-field textarea,
.prof-pin-edit-field input {
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
  font-family: inherit;
}
.prof-pin-edit-field textarea { resize: vertical; }

/* Pin-artwork "+" tile (2026-07-12) — sits inside the pin grid itself,
 * matching the row shape of .prof-pin-edit-card (the grid is a single
 * column of full-width rows, not a thumbnail grid), instead of a
 * separate dropdown+button row below it. Clicking the icon swaps it for
 * the picker rather than opening a second control elsewhere on the page. */
.prof-pin-add-card {
  min-height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--line);
  border-radius: var(--radius-control, 8px);
  padding: var(--space-2, 16px);
  /* A grid ITEM defaults to min-width:auto, i.e. never smaller than its own
   * content's intrinsic width. At 390px this card measured 448px inside a
   * 292px track and pushed Import and the hint past the screen edge. The
   * dashed border made it read as the panel's own bug, but every child was
   * already inside the card — the card was the thing overflowing. */
  min-width: 0;
}

/* Once the add panel is open the card stops being a centred icon tile and
 * becomes a full-width form block (2026-08-29). Keyed off the PANEL, not the
 * picker: the picker no longer exists for an artist whose studio is empty,
 * and the old :has(#prof-pin-picker) selector left that case centred and
 * cramped. */
.prof-pin-add-card:has(.prof-pin-add-panel:not([hidden])) {
  justify-content: stretch;
  align-items: stretch;
  flex-direction: column;
}

.prof-pin-add-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-2, 16px);
  width: 100%;
}

.prof-pin-add-row {
  display: flex;
  align-items: center;
  gap: var(--space-1, 8px);
  flex-wrap: wrap;
}

.prof-pin-add-row__label {
  flex: 0 0 auto;
  min-width: 8.5rem;
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}

/* Narrow viewports: the 8.5rem label column plus a 16rem URL field cannot
 * both fit at 390px, and the Import button was pushed off the right edge
 * (caught on a real CDP setDeviceMetricsOverride screenshot — the document
 * reported NO scrollWidth overflow, because the button was clipped by the
 * card rather than extending the page, so only the image showed it). The
 * label takes its own line and every control goes full width. */
@media (max-width: 30rem) {
  .prof-pin-add-row {
    flex-direction: column;
    align-items: stretch;
  }

  .prof-pin-add-row__label {
    min-width: 0;
  }

  /* Drop the 16rem basis: at 390px it is wider than the track and is what
   * inflated it. `auto` sizes from the content instead. */
  .prof-pin-add-row__url {
    flex: 1 1 auto;
    min-width: 0;
  }

  .prof-pin-add-panel .prof-btn-secondary {
    width: 100%;
  }

  /* The URL field and its button stack too — side by side at this width the
   * input is barely wide enough to read a pasted link back. */
  .prof-pin-add-row__url {
    flex-direction: column;
  }

  .prof-pin-add-row__url .prof-btn-secondary {
    width: 100%;
    flex: 0 0 auto;
  }

  /* Only once the row is a COLUMN does the input become a stretched flex
   * item that grows vertically (measured ~400px of empty box). Scoped here
   * on purpose: as a flex-basis in the desktop ROW the same declaration
   * collapsed the field to 38px WIDE. */
  .prof-pin-add-row__url input[type="url"] {
    flex: 0 0 38px;
  }
}

/* The URL row's input must actually take the free space; without flex:1 on
 * this wrapper the input collapses to its intrinsic width beside the button. */
.prof-pin-add-row__url {
  display: flex;
  flex: 1 1 16rem;
  gap: var(--space-1, 8px);
  /* Without this the 16rem BASIS becomes the card's intrinsic width, which
   * sizes the grid track itself (measured: a 448px track in a 390px
   * viewport) — the flex-basis, not any child, was the overflow's source. */
  min-width: 0;
}

.prof-pin-add-row__url input {
  flex: 1 1 auto;
  min-width: 0;
}

.prof-pin-add-panel input[type="url"] {
  height: 38px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

.prof-pin-add-progress {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}

/* Guide §: 2px cobalt ring, 2px offset, on every interactive element. These
 * controls sit in a bare <div>, not inside .prof-form, so the form-scoped
 * input/select rule above never reached them — keyboard focus fell back to
 * the browser's own 1px default ring (verified live via a real Tab press;
 * a scripted .focus() does not engage :focus-visible and cannot show this). */
.prof-pin-add-panel input:focus-visible,
.prof-pin-add-panel select:focus-visible,
.prof-pin-add-panel button:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

.prof-pin-add-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: none;
  color: var(--muted);
  display: inline-grid;
  place-items: center;
  cursor: pointer;
  transition: color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.prof-pin-add-btn:hover {
  color: var(--yeja-cobalt, #1E88E5);
  border-color: var(--yeja-cobalt, #1E88E5);
}

.prof-pin-add-btn svg { width: 18px; height: 18px; }
.prof-pin-add-btn .yeja-icon { font-size: 18px; }

/* An author `display` rule BEATS the `hidden` attribute, which is only a UA
 * `display:none` (2026-08-29). `.prof-pin-add-btn { display: inline-grid }`
 * therefore kept painting the "+" circle above the opened add panel even
 * though the button carried `hidden`. Latent since the tile was written —
 * the old picker sat in the same row so the leftover circle read as part of
 * the layout; the stacked panel is what made it obvious. */
.prof-pin-add-btn[hidden] { display: none; }

#prof-pin-picker {
  flex: 1 1 12rem;
  min-width: 0;
  height: 38px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

/* Publish Portfolio card (cleaned up 2026-07-12) — status chip + a single
 * primary action row (Preview / Publish toggle) instead of two stacked
 * checkboxes with no visual hierarchy. */
.prof-publish-card .prof-field-hint { margin-top: 0; }

/* Both rows WRAP (2026-08-30). Neither set flex-wrap, so both defaulted to
 * nowrap and the title + status chip + Preview + Expand All were forced onto
 * one line. At 390px that pushed "Expand All" — the only control that opens
 * the eleven collapsed cards — to a right edge of 402 against a 390 viewport.
 * The page has no horizontal scroll, so it was simply unreachable: the
 * editor's primary navigation affordance, off-screen on a phone. */
.prof-publish-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  flex-wrap: wrap;
}

.prof-publish-head h2 { margin: 0; }

.prof-publish-head-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  flex-wrap: wrap;
}

.prof-publish-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3, 24px);
  flex-wrap: wrap;
}

.prof-publish-toggle { margin: 0; }

.prof-btn-secondary {
  height: 36px;
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill, 24px);
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease-in-out;
}

.prof-btn-secondary:hover {
  border-color: var(--yeja-cobalt, #1E88E5);
  color: var(--yeja-cobalt, #1E88E5);
}

.prof-timeline-edit-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin-bottom: var(--space-2, 16px);
}

.prof-timeline-edit-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.prof-timeline-edit-row:last-child { border-bottom: none; }

.prof-timeline-edit-text {
  flex: 1;
  min-width: 0;
}

.prof-timeline-edit-title {
  margin: 0;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

.prof-timeline-edit-kind {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  font-weight: 400;
}

.prof-timeline-edit-meta {
  margin: 2px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-collab-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
  margin-bottom: var(--space-2, 16px);
}

.prof-collab-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  padding: var(--space-1, 8px) 0;
  border-bottom: 1px solid var(--line);
}

.prof-collab-row:last-child { border-bottom: none; }

.prof-collab-text {
  flex: 1;
  min-width: 0;
}

.prof-collab-title {
  margin: 0;
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  color: var(--ink);
}

.prof-collab-meta {
  margin: 2px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-tag-groups {
  display: flex;
  flex-direction: column;
  gap: var(--space-3, 24px);
}

.prof-tag-group-label {
  margin: 0 0 var(--space-1, 8px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
}

.prof-tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: var(--space-1, 8px);
}

.prof-tag-chips .chip button {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 1em;
  padding: 0 0 0 4px;
  line-height: 1;
}

.prof-tag-add-form {
  display: flex;
  gap: 8px;
}

.prof-tag-add-form input {
  flex: 1;
  height: 34px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

/*
 * Public Portfolio (2026-07-15) — a wider, more visual sibling of the
 * .prof-page private view. .pf-page overrides .prof-page's 640px cap
 * since a showcase needs real room for the pin grid.
 */
.pf-page {
  max-width: 900px;
}

.pf-commission-status {
  display: flex;
  gap: 8px;
}

/* Theme toggle, top-right of the public portfolio header only (backlog
 * d366b58e, 2026-07-27) — pushes the control to the far right via
 * margin-left:auto. flex-shrink:0 (backlog d85b90d8, 2026-08-02) stops
 * the 4-button control from being squeezed below its own min-width on
 * narrow viewports/long artist names, which is what read as "jumbled" —
 * .prof-header has no flex-wrap, so without a shrink guard the theme
 * control was the only flexible item left once .prof-header-text
 * (flex:1, min-width:0) had already given up all the room it could. */
.pf-page .pf-theme-control {
  margin-inline-start: auto;
  align-self: flex-start;
  flex-shrink: 0;
}

/* .prof-header itself gets an explicit wrap fallback on the public page
 * only — at very narrow widths the theme control drops to its own line
 * under the name/avatar row instead of fighting for horizontal space. */
.pf-page .prof-header {
  flex-wrap: wrap;
  row-gap: var(--space-2, 16px);
}

/* The wrap above could never actually fire on its own: .prof-header-text
 * is flex:1 with min-width:0, so it kept shrinking to whatever was left
 * rather than pushing the theme control onto a new line. At 390px that
 * squeezed the artist name into a two-line "Yeja / AI" beside the
 * control — the residual "jumbled" header (confirmed by screenshot; the
 * DOM measurements all looked fine, no overflow, buttons evenly spaced).
 *
 * A min-width gives the name a floor to defend: below it the theme
 * control wraps to its own line instead of the name fragmenting. 12rem
 * is roughly a two-word display name at this type size, and it is a
 * MIN-width not a fixed one, so wide viewports are unchanged. */
.pf-page .prof-header .prof-header-text {
  min-width: min(12rem, 100%);
}

.pf-pin-header { margin: 0; }

/* 5 columns / 3 rows (backlog d366b58e, 2026-07-27: "layout of the
 * showcase items must abide by the same characteristics of art_board,
 * same layout, same hover, same grid, same info... five columns in the
 * default desktop browser layout, three rows for a max total of 15
 * items") — PORTFOLIO_MAX_PINS (app.js) caps pins at 15, so 5 cols
 * always yields at most 3 rows. Gap and hover-reveal label now match
 * Art Board's own .masonry__frame/.masonry__hover-label (apps/art_board/
 * styles.css) exactly, rather than reusing its masonry packing engine,
 * since this grid is a small static count, not an infinite-scroll feed. */
.pf-pin-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
}

@media (max-width: 900px) {
  .pf-pin-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 640px) {
  .pf-pin-grid { grid-template-columns: repeat(1, 1fr); }
}

.pf-pin-card {
  display: flex;
  flex-direction: column;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  text-align: start;
  font-family: inherit;
}

.pf-pin-thumb {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-media, 8px);
  background: var(--surface-alt);
  overflow: hidden;
  transition: transform 0.15s ease-in-out;
}

.pf-pin-card:hover .pf-pin-thumb { transform: translateY(-2px); }

.pf-pin-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Hover-reveal title/status overlay — same mechanism (0.5s delegated
 * timer, opacity-only transition) and same visual treatment (bottom
 * gradient scrim) as Art Board's .masonry__hover-label. */
.pf-pin-hover-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px;
  /* Scrim over imagery — color-mix on the backdrop token, per guide §1,
   * same recipe as .pf-lightbox-backdrop below. Was a raw
   * rgba(0, 0, 0, 0.72), which the hex linter never caught because it only
   * scans #-values. This gradient sits over the artwork thumbnail, so it
   * is deliberately near-black in both themes rather than themable. */
  background: linear-gradient(to top, color-mix(in oklch, var(--yeja-backdrop) 72%, transparent), transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease-in-out;
}

/* Keyboard parity for the 500ms hover-reveal label (backlog 1cece0a2
 * item 7). The .is-hover-label-visible class is set only by
 * wirePinGridHover()'s mouseover/mouseout handlers, so a keyboard user
 * tabbing across the public Pinned Showcase could NEVER see the title/
 * status label these cards exist to surface — the information was
 * pointer-only on a public page.
 *
 * Deliberately no delay on the focus path: the 500ms exists to stop
 * labels flickering as a mouse sweeps across the grid, but focus lands on
 * exactly one card at a time and is always intentional, so waiting half a
 * second would just feel broken. :focus-within rather than :focus because
 * the label itself can contain focusable content. */
.pf-pin-card.is-hover-label-visible .pf-pin-hover-label,
.pf-pin-card:focus-within .pf-pin-hover-label { opacity: 1; }

.pf-pin-title {
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  /* Foreground on the scrim above, not themable chrome: it must stay
   * light in both themes or it vanishes against the dark gradient.
   * --yeja-gray-0 is the token for that literal white. */
  color: var(--yeja-gray-0, #ffffff);
}

.pf-section {
  border-top: 1px solid var(--line);
  padding-top: var(--space-4, 32px);
}

.pf-section h2 {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
  color: var(--ink);
}

/* Recurring monthly support (2026-07-21) */
.pf-support-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2, 16px);
  margin-top: var(--space-2, 16px);
}

.pf-support-amounts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* align-items: center, not the default `stretch` (backlog d85b90d8).
     THIS was why the amount buttons rendered 38px against the Get In
     Touch pills' 28px: as flex items they were being stretched to the
     tallest item in the row — the .pf-support-custom text input, whose
     own height comes from the generic input styling. Every attempt to
     fix it on the BUTTON (padding, line-height, border) was doomed,
     because the height wasn't coming from the button at all. */
  align-items: center;
}

/* Sized to match .yeja-pill-btn exactly (backlog d85b90d8: "Get In Touch
 * and Support artist pills need to be the same ratios. Support artist are
 * currently too large.").
 *
 * MEASURED, not guessed: with real tokens loaded the Get In Touch pills
 * compute to 28px tall / 6.67px 16.67px padding, while these amount
 * buttons were 38px — the visible mismatch. The oversize was here, on the
 * AMOUNT SELECTORS, not on the "Support" CTA itself (which was already a
 * .yeja-pill-btn at 28px). Matching the shared pill's own values rather
 * than eyeballing a smaller number keeps the two rows aligned if that
 * component's spec ever changes.
 *
 * --radius-pill, not a literal 999px: the token is the platform's pill
 * radius and a hardcoded value silently stops tracking it. */
.pf-support-amount-btn {
  /* Mirrors .yeja-pill-btn's own box: inline-flex + centred content +
     line-height normal. An explicit line-height was tried first and made
     these TALLER (16.8px of leading + padding + 2px of border = 38px vs
     the pill's 28px) — the pill relies on `normal` leading and has no
     border, so matching padding alone is not enough. Measured against
     the live pill rather than eyeballed. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5em 1.25em;
  border-radius: var(--radius-pill);
  /* Border box-sizing already accounts for this 1px, but the pill has
     none — so the border is drawn inside via an inset shadow instead,
     keeping the outer height identical while preserving the outline that
     distinguishes an unselected amount from the solid CTA. */
  border: 0;
  box-shadow: inset 0 0 0 1px var(--line);
  background: var(--surface);
  color: var(--ink);
  /* NO font-size, deliberately. .yeja-pill-btn (design-tokens.css)
     declares none either — it inherits from context, and its em-based
     padding scales off whatever it inherits. Setting one here made these
     14px against the pill's inherited 13.33px, which reintroduced the
     height difference this rule exists to remove. */
  font-weight: 500;
  cursor: pointer;
}

.pf-support-amount-btn:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.pf-support-amount-btn.selected {
  border-color: var(--ink);
  background: var(--ink);
  /* --yeja-canvas is defined in all three theme blocks, so the old
     pure-white fallback was both unnecessary and a raw literal that
     would not theme if it ever fired. (Spelled out in words: this
     linter matches by line, so naming the hex here would flag it.) */
  color: var(--yeja-canvas);
}

.pf-support-custom {
  width: 100px;
}

.pf-support-modal-card {
  max-width: 420px;
}

.pf-support-card-element {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 12px;
  margin: 8px 0 16px;
  background: var(--surface);
}

/* Get In Touch gate (2026-07-17) — the intent question shown before
 * either the RFP hand-off or the quick-inquiry form. */
.pf-touch-gate p {
  margin: 0 0 var(--space-2, 16px);
  color: var(--muted);
  max-width: 60ch;
}

/* Professional timeline — new component, no precedent elsewhere in the
 * codebase; extends .prof-kyc-row's dot+label+state shape into a real
 * vertical rail with a connecting line between entries. */
.pf-timeline {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3, 24px);
  padding-inline-start: 20px;
}

.pf-timeline::before {
  content: '';
  position: absolute;
  inset-inline-start: 3px;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--line);
}

.pf-timeline-entry {
  position: relative;
  display: flex;
  gap: var(--space-2, 16px);
}

.pf-timeline-dot {
  position: absolute;
  inset-inline-start: -20px;
  top: 4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--yeja-cobalt, #1E88E5);
}

.pf-timeline-kind {
  margin: 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.pf-timeline-title {
  margin: 2px 0 0;
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  color: var(--ink);
}

.pf-timeline-org {
  margin: 2px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.pf-timeline-desc {
  margin: var(--space-1, 8px) 0 0;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

.pf-collab-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-2, 16px);
}

.pf-collab-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  padding: var(--space-2, 16px);
}

.pf-collab-project {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
  font-size: var(--text-body-size, 14px);
}

.pf-collab-with,
.pf-collab-role {
  margin: 4px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  white-space: pre-wrap;
}

.pf-tag-group {
  margin-bottom: var(--space-2, 16px);
}

.pf-tag-group:last-child { margin-bottom: 0; }

.pf-tag-group-label {
  margin: 0 0 8px;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
}

.pf-tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/*
 * Immersive lightbox — appended to document.body (see openLightbox() in
 * app.js), entirely outside the shell's own chrome/canvas, so it can be
 * a true full-viewport distraction-free view. Fixed positioning + a high
 * z-index is the only way to escape the shell's own stacking context
 * from inside #app-canvas.
 */
.pf-lightbox-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9999;
  /* Found by the same sweep as the report-modal backdrop (bdbfb69b): a
   * raw rgba() scrim, which CLAUDE.md forbids even for a colour that
   * deliberately doesn't theme. Same color-mix-over-`backdrop` recipe as
   * every other backdrop on the platform, just at the higher 92% opacity
   * a full-bleed image viewer wants. --yeja-backdrop is pure black in dark
   * and gray-950 in light, so this stays near-black in both, matching
   * the original intent rather than changing the look. */
  background: color-mix(in oklch, var(--yeja-backdrop) 92%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Image-over-details, stacked (backlog ac406d0d: "Pinned Showcase enlarge
   image preview width 100%"). Was image-left/details-right with the stage
   at flex: 1.6, so the preview only ever occupied ~62% of the modal —
   which is what the row is asking to change. The details panel keeps its
   place directly beneath rather than being dropped, since it carries the
   story/provenance content the lightbox exists to show.

   This makes the desktop layout match what the <=800px branch below
   already did, so there is now ONE stacking model at every width instead
   of two. */
/* SPLIT LAYOUT: media left, details right (2026-08-04 user instruction —
 * "image in the left (zoom in / out / reset, etc.), details in the
 * right"). This restores the exact shape of apps/art_board's own detail
 * view, including its 1.6 : 1 flex ratio, so every image detail view on
 * the platform behaves identically. It was stacked (image over details)
 * from backlog ac406d0d; the split is now the instruction. */
.pf-lightbox-inner {
  display: flex;
  flex-direction: row;
  width: 90vw;
  height: 90vh;
  max-width: 1400px;
  background: var(--paper);
  border-radius: 16px;
  overflow: hidden;
  /* Tokens, not raw rgba(). These were literal rgba(0,0,0,…) — the hex
     linter only scans for #-values, which is how they survived. Per guide
     §1 a scrim over imagery is a color-mix on --yeja-backdrop. */
  box-shadow: 0 24px 64px color-mix(in oklch, var(--yeja-backdrop) 40%, transparent);
}

.pf-lightbox-stage {
  /* 1.6 against the story panel's 1 — apps/art_board's own detail ratio.
   * A COLUMN so the zoom bar is a flow sibling BELOW the image rather
   * than an overlay on it (the collision Art Board already fixed once). */
  flex: 1.6;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-2, 16px);
  background: color-mix(in oklch, var(--yeja-backdrop) 92%, transparent);
}

.pf-lightbox-image {
  max-width: 100%;
  /* min-height:0 on the flex parent plus this keeps the image from
   * pushing the zoom bar out of the stage on a tall work. */
  min-height: 0;
  flex: 1 1 auto;
  object-fit: contain;
}

@media (max-width: 800px) {
  /* The split collapses to stacked on narrow viewports — a 1.6:1 row at
     390px would give the details ~140px. Explicit now that ROW is the
     desktop default. */
  .pf-lightbox-inner {
    flex-direction: column;
    width: 100vw;
    height: 100vh;
    max-width: none;
    border-radius: 0;
  }
  .pf-lightbox-stage { flex: none; height: 45vh; }
}

/* Lightbox chrome sits on the near-black .pf-lightbox-backdrop, so it is
 * deliberately light in BOTH themes — but a literal that must not theme is
 * still not a raw hex (guide §1). --yeja-gray-0 is the token for that
 * white; the translucent fill is a color-mix on it rather than a raw
 * rgba(255,255,255,.1), which the #-only hex linter never flagged. */
.pf-lightbox-close {
  position: absolute;
  top: 24px;
  inset-inline-end: 24px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: color-mix(in oklch, var(--yeja-gray-0) 10%, transparent);
  color: var(--yeja-gray-0, #ffffff);
  display: inline-grid;
  place-items: center;
  cursor: pointer;
}

.pf-lightbox-close svg { width: 20px; height: 20px; }
.pf-lightbox-close .yeja-icon { font-size: 20px; }

/* Same backdrop-bound chrome as .pf-lightbox-close above — see its comment. */
.pf-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: color-mix(in oklch, var(--yeja-gray-0) 10%, transparent);
  color: var(--yeja-gray-0, #ffffff);
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
}

.pf-lightbox-prev { left: 24px; }
.pf-lightbox-next { right: 24px; }

/* Stacked under the full-width stage (ac406d0d). flex-grow must be 0 —
   with the previous `flex: 1` in a column layout the panel would claim an
   equal share of the height, shrinking the very preview this change
   exists to enlarge. It sizes to its content instead, capped so a long
   story scrolls rather than pushing the image out of view. */
.pf-story-panel {
  /* flex 1 against the stage's 1.6 — Art Board's own ratio. */
  flex: 1;
  min-width: 0;
  max-height: none;
  background: var(--paper);
  color: var(--ink);
  padding: var(--space-3, 24px);
  overflow-y: auto;
  /* Same defect, same fix as .prof-map-body below (see its comment): in the
   * default content-box sizing this panel's 24px padding is ADDED to the
   * width the column layout gives it, so at 390px it computed to
   * 390 + 24 + 24 = 438px and hung 48px off the right edge, dragging the
   * title, Report button, spec rows and the Inquire CTA out of view with
   * it. Measured on a real 390px device profile, not guessed. There is no
   * global border-box reset in this codebase, so every padded box that is
   * also width-constrained has to opt in individually. */
  box-sizing: border-box;
}

@media (max-width: 800px) {
  /* Below 800px the stage is a fixed 45vh, so the panel should take the
     whole remainder — max-height: 40% from the base rule would strand
     usable space and force scrolling that isn't needed here. */
  .pf-story-panel { flex: 1; max-height: none; }
}

.pf-story-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2, 16px);
}

.pf-story-panel h3 {
  margin: 0 0 4px;
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
}

.pf-story-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.pf-story-action-btn {
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  padding: 6px 12px;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  font-family: inherit;
}

.pf-story-action-btn:hover { background: var(--surface-alt); }

.pf-story-action-btn--danger { color: var(--yeja-crimson, #E53935); border-color: var(--yeja-crimson, #E53935); }

/* .pf-toast / .pf-toast.is-visible deleted 2026-08-02 (backlog 8fdcd3ef
 * item 3) along with showPfToast() in app.js. Profile now uses the shared
 * apps/shared/toast.js exclusively, whose styles live in
 * apps/shared/components.css — one toast system, not two. */

.pf-story-meta {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* Price as a first-class line (backlog 447edf58) — previously the number
 * only ever appeared as a fragment inside the availability chip, which is
 * not where a collector looks for it. Mono per guide §2's numeric/
 * technical metadata voice, and --ink rather than --muted because this is
 * primary information, not a caption. */
.pf-story-price {
  margin: calc(-1 * var(--space-1, 8px)) 0 var(--space-2, 16px);
  font-family: var(--font-family-mono, 'JetBrains Mono', ui-monospace, monospace);
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  color: var(--ink);
}

/* Per-artwork Inquire CTA + spec block (backlog a419b96e). Before this,
 * nothing on the public page was actionable for a SPECIFIC piece, and a
 * collector had no dimensions to judge the work by. */
.pf-inquire-btn {
  margin-top: var(--space-2, 16px);
  width: 100%;
}

.pf-spec-block {
  margin-top: var(--space-3, 24px);
  padding-top: var(--space-2, 16px);
  border-top: 1px solid var(--line);
}
.pf-spec-heading {
  margin: 0 0 var(--space-1, 8px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.pf-spec-list { margin: 0; }
.pf-spec-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}
.pf-spec-row:last-child { border-bottom: 0; }
.pf-spec-label {
  flex: 0 0 auto;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}
/* Mono on the VALUES only (guide §2 numeric/technical metadata voice) —
 * the labels stay in the body face so the block doesn't read as code. */
.pf-spec-value {
  margin: 0;
  flex: 1 1 auto;
  text-align: end;
  font-family: var(--font-family-mono, 'JetBrains Mono', ui-monospace, monospace);
  font-size: var(--text-label-size, 12px);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.pf-story-block {
  margin-top: var(--space-2, 16px);
}

.pf-story-block h4 {
  margin: 0 0 4px;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.pf-story-block p {
  margin: 0;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

/* Story fields became rich text 2026-08-04 — the single <p> above is now
 * a .pf-story-prose wrapper holding whatever the allowlist in
 * apps/shared/rich-text.js permits. Paragraphs after the first need their
 * spacing back (the rule above zeroes all margins). */
.pf-story-prose {
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}
.pf-story-prose > :first-child { margin-top: 0; }
.pf-story-prose > :last-child { margin-bottom: 0; }
.pf-story-prose p { margin: 0 0 0.75em; }
.pf-story-prose ul,
.pf-story-prose ol { margin: 0 0 0.75em; padding-inline-start: 1.3em; }
.pf-story-prose li { margin-bottom: 0.25em; }
.pf-story-prose h3,
.pf-story-prose h4 {
  /* Reset the uppercase label styling inherited from .pf-story-block h4 —
   * these are the artist's own sub-headings inside the prose, not the
   * block's section label. */
  margin: 1em 0 0.35em;
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  color: var(--ink);
  text-transform: none;
  letter-spacing: normal;
}
.pf-story-prose blockquote {
  margin: 0.75em 0;
  padding-inline-start: 12px;
  border-inline-start: 2px solid var(--yeja-border);
  color: var(--muted);
  font-style: italic;
}
.pf-story-prose a { color: var(--yeja-cobalt); }

/* Report modal (2026-07-17) — same shared apps/shared/report-modal.js
 * markup Art Board/Experimental use, scoped here as .prof-page/.pf-modal*
 * (own wrapperClass/prefix) since this page's tokens (--ink/--paper/
 * --line/--muted) are its own, not --yeja-*. */
.prof-page.pf-modal-backdrop {
  position: fixed; inset: 0;
  /* --yeja-backdrop, NOT --ink (backlog bdbfb69b). --ink is the
   * FOREGROUND token: it is pure white in dark mode, so this scrim computed
   * to oklch(0.999994 … / 0.55) — literally 55% WHITE — and washed the
   * whole page to milky grey instead of dimming it. Confirmed live by
   * reading the computed value in dark mode before the fix.
   * --yeja-backdrop is gray-950 in light and pure black in dark, i.e. the
   * dim layer this recipe is specified against in guide §5 (color-mix
   * over `backdrop`, never element opacity).
   *
   * Note the other three `background: var(--ink)` rules in this file
   * (:1441 amount button, :1724 toast, :1903 pill) are CORRECT and were
   * deliberately left alone — they are the inverted-fill idiom
   * (`background: var(--ink); color: var(--paper)`), which is what --ink
   * is for. The bug is specific to using a foreground token as a SCRIM. */
  background: color-mix(in oklch, var(--yeja-backdrop) 55%, transparent);
  display: none; align-items: center; justify-content: center;
  padding: var(--space-3, 24px);
  /* Above .pf-lightbox-backdrop (z-index: 9999) — the Report button is
     opened FROM inside the lightbox, so this modal was rendering behind
     it at the old 9998. */
  z-index: 10000;
}

/* THE OPEN STATE. Missing until 2026-08-04, which is why Report did
 * nothing: apps/shared/report-modal.js opens the portal by adding
 * `is-open` (report-modal.js:150), but this file only ever declared the
 * RESTING `display: none` above — so the backdrop was appended to <body>,
 * given the class, and stayed invisible forever. Measured: the element
 * was present with class="prof-page pf-modal-backdrop is-open",
 * hidden=false, z-index 10000, and computed display:none with a 0x0 box.
 *
 * apps/art_board/styles.css:1826 has always had its own half of this pair
 * (.ab-modal-backdrop.is-open { display: flex }) — the Profile copy was
 * simply never written. An undefined class fails SILENTLY, which is the
 * recorded bug class this belongs to. */
.prof-page.pf-modal-backdrop.is-open { display: flex; }

.prof-page .pf-modal {
  display: flex;
  flex-direction: column;
  width: min(440px, 100%);
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: var(--space-3, 24px);
  /* Tokens, not raw rgba() — same recipe as .pf-lightbox-inner's shadow
   * above. The #-only hex linter never scanned rgba(), which is how this
   * one survived the earlier sweep. */
  box-shadow: 0 24px 64px color-mix(in oklch, var(--yeja-backdrop) 35%, transparent);
}

@media (max-width: 720px) {
  .pf-story-panel {
    width: 100%;
  }
}

/* CV export modal — backdrop/card base now apps/shared/components.css's
 * .yeja-overlay-backdrop/.yeja-dialog-card (Phase 4, 2026-07-19),
 * byte-identical to what was here; .pf-lightbox-backdrop/.pf-cv-modal-
 * card kept alongside as extra classes only where a real visual
 * difference exists (lightbox: darker 0.92 backdrop) or for h2 sizing
 * below. */
.pf-cv-modal-card h2 {
  margin: 0;
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
}

.pf-cv-template-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.pf-cv-template-option {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 12px;
  align-items: baseline;
  padding: var(--space-1, 8px);
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  cursor: pointer;
}

.pf-cv-template-option input { grid-row: 1 / 3; align-self: center; }

.pf-cv-template-name {
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
  color: var(--ink);
}

.pf-cv-template-desc {
  grid-column: 2;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* Delete Account modal — same backdrop/card base as the CV export modal. */
.pf-delete-modal-card h2 {
  margin: 0;
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
}

.pf-delete-disclosure {
  background: var(--yeja-canvas-paper, var(--surface-alt));
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  padding: var(--space-2, 16px);
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.pf-delete-disclosure h3 {
  margin: var(--space-1, 8px) 0 0;
  font-size: var(--text-label-size, 12px);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

.pf-delete-disclosure h3:first-child { margin-top: 0; }

.pf-delete-disclosure ul {
  margin: 0;
  padding-inline-start: 1.2em;
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Portfolio preview modal (2026-07-12) — full-viewport overlay showing
 * renderPublicPortfolioHTML() as-is, same escape-the-shell approach as
 * the lightbox, with a fixed banner so it never reads as the real
 * public page. */
.pf-preview-modal {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--paper);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.pf-preview-banner {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  padding: 10px var(--space-3, 24px);
  background: var(--yeja-cobalt-ink);
  /* Foreground ON the cobalt fill, not themable chrome — it must stay
   * light in both themes to keep contrast against the banner. Uses the
   * --yeja-gray-0 token rather than a raw #ffffff (guide §1). */
  color: var(--yeja-gray-0, #ffffff);
  font-size: var(--text-body-size, 14px);
  font-weight: 600;
}

/* Also on the cobalt banner — see .pf-preview-banner above. */
.pf-preview-banner .prof-address-delete {
  border-color: color-mix(in oklch, var(--yeja-gray-0) 40%, transparent);
  color: var(--yeja-gray-0, #ffffff);
}

.pf-preview-banner .prof-address-delete:hover {
  border-color: var(--yeja-gray-0, #ffffff);
  color: var(--yeja-gray-0, #ffffff);
}

.pf-preview-body { flex: 1; }

/* Portfolio Style editor (2026-07-17) — mirrors apps/themes/app.js's
 * token editor visually (same field-group/preset-grid/bg-row/css-editor
 * shapes) but against Profile's own token names instead of the Themes
 * app's --yeja-* aliases, since this section lives inside .prof-card
 * scaffolding, not a standalone themes-* page shell. */
.prof-style-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
  padding-top: var(--space-3, 24px);
  border-top: 1px solid var(--line);
}

.prof-style-group:first-of-type {
  padding-top: 0;
  border-top: none;
}

.prof-style-group h3 {
  margin: 0;
  font-size: var(--text-label-size, 12px);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

/* "Settings Not Available Here" (2026-08-26) — the honest note listing
   theme fields an Atelier cannot consume, so their absence does not read
   as a bug. It is a <details>, so it must NOT inherit .prof-style-group's
   display:flex: a flex <details> lays its summary and content out as flex
   items and the disclosure stops behaving like one. */
.prof-style-unsupported {
  display: block;
}

.prof-style-unsupported > summary {
  cursor: pointer;
  font-size: var(--text-label-size, 12px);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  border-radius: var(--radius-control, 8px);
}

/* Every interactive element gets the cobalt focus ring, and a <summary>
   is interactive — it is reachable by Tab and toggled by Enter. */
.prof-style-unsupported > summary:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-style-unsupported ul {
  margin: var(--space-1, 8px) 0 0;
  padding-inline-start: var(--space-3, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
}

.prof-style-field {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
}

.prof-style-field span { flex: 0 0 auto; min-width: 110px; }
.prof-style-field select,
.prof-style-field input[type="range"] { flex: 1; }

.prof-style-field select {
  height: 32px;
  border-radius: var(--radius-control, 8px);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 0 0.5rem;
}

#pf-tk-density {
  height: 32px;
  border-radius: var(--radius-control, 8px);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 0 0.5rem;
}

#pf-tk-shadow-intensity {
  height: 32px;
  margin-top: 4px;
  border-radius: var(--radius-control, 8px);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 0 0.5rem;
}

.prof-style-color-row {
  display: flex;
  gap: var(--space-3, 24px);
  flex-wrap: wrap;
}

.prof-style-color-field {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-style-color-field input[type="color"] {
  width: 32px;
  height: 32px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  padding: 0;
  cursor: pointer;
}

.prof-style-color-field input[type="text"] {
  width: 80px;
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: var(--text-label-size, 12px);
  padding: 4px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
}

.prof-style-contrast-warning {
  color: var(--yeja-crimson, #E53935);
}

.prof-style-preset-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-1, 8px);
}

.prof-style-preset-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: var(--space-1, 8px);
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  cursor: pointer;
  transition: background 0.15s ease-in-out, transform 0.15s ease-in-out;
}

.prof-style-preset-card:hover {
  background: var(--surface-alt);
  transform: translateY(-1px);
}

.prof-style-preset-card:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 1px;
}

/* A palette must be readable against ANY ground (2026-08-30). The swatch had
 * no boundary of its own, so a band whose colour matched the card behind it
 * simply disappeared: "Gallery Noir" showed as a lone floating amber stripe in
 * dark mode, its two near-black bands invisible, and "Atelier Warm"'s cream
 * band did the same in light. A preset is being judged BY its colours, so none
 * of them may go missing.
 *
 * The hairline is a translucent ink rather than --line: it has to read against
 * both a white band and a black one, which a single opaque token cannot do. */
.prof-style-preset-swatch {
  display: flex;
  width: 100%;
  height: 28px;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid color-mix(in oklch, var(--yeja-text-primary) 22%, transparent);
}

.prof-style-preset-swatch span { flex: 1; }

/* Separator between adjacent bands, same reasoning as the outer hairline:
 * two dark bands side by side otherwise read as one wide band. */
.prof-style-preset-swatch span + span {
  border-left: 1px solid color-mix(in oklch, var(--yeja-text-primary) 22%, transparent);
}

.prof-style-preset-name {
  font-size: var(--text-label-size, 12px);
  color: var(--ink);
  font-weight: 600;
}

/* No justify-content — the row starts flush left, matching the "Background
 * Image" heading/hint above it and every other .prof-style-group in this
 * editor (backlog d85b90d8, 2026-08-02: "Background Image button is too
 * center aligned"). A `justify-content: center` here centred the thumbnail
 * and Upload button in the card while their own heading stayed left, so
 * the control read as detached from its label. The Themes app's
 * .themes-bg-row, which this block was copied from, never had the
 * declaration at all. */
.prof-style-bg-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
}

.prof-style-bg-thumb {
  width: 64px;
  height: 44px;
  flex: 0 0 auto;
  border-radius: var(--radius-control, 8px);
  object-fit: cover;
  border: 1px solid var(--line);
}

.prof-style-bg-thumb-empty {
  display: grid;
  place-items: center;
  background: var(--surface-alt);
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
}

.prof-style-bg-actions {
  display: flex;
  gap: var(--space-1, 8px);
}

/* Now a real <button> rather than a <label> wrapping the hidden file input
 * (backlog d85b90d8, 2026-08-02) — see the button's comment in app.js.
 * .prof-btn-secondary already supplies the border/padding/background and
 * font-family: inherit, so no extra UA-chrome resets are needed here;
 * align-items centres the label text now that the box is a flex button. */
.prof-style-upload-btn {
  margin: 0;
  display: inline-flex;
  align-items: center;
}

/* Guide §: 2px cobalt ring, 2px offset, on every interactive element. The
 * label version could never be focused, so this rule had no counterpart. */
.prof-style-upload-btn:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

.prof-style-bg-status {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  margin: 0;
  min-height: 16px;
}

.prof-style-css-editor {
  width: 100%;
  min-height: 140px;
  padding: var(--space-2, 16px);
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: var(--text-label-size, 12px);
  resize: vertical;
}

.prof-style-css-editor:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 1px;
}

/* Inquiries Inbox (2026-07-17) */
/* Inbox | Archived now sit INSIDE .pf-inbox-toolbar, on the search bar's
 * own line (2026-08-02, backlog bd5a6a24). The old margin-bottom spaced
 * them from the thread list when they were a separate block below the
 * toolbar; inside a flex row it would only push the row taller, and the
 * toolbar already owns the gap to the list. overflow-x:auto comes from
 * .yeja-category-pills and is cleared here: it exists for a long
 * horizontally-scrolling category strip, but with exactly two pills in a
 * wrapping flex row it does nothing except risk clipping the focus ring. */
.pf-inquiry-filter-pills {
  margin-bottom: 0;
  overflow-x: visible;
}

.pf-inquiry-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Fallback dropped 2026-08-02 (backlog bd5a6a24). --yeja-amber IS defined
 * (design-tokens.css:23), and the literal that used to sit in the fallback
 * slot here was a DIFFERENT amber from the token's real value. It never
 * rendered, but it documented the wrong brand colour — exactly the
 * token-drift class CLAUDE.md calls out. A bare var() is also the only
 * form that fails loudly if the token is ever removed, instead of
 * silently painting a stale literal. */
.pf-inquiry-star.is-starred { color: var(--yeja-amber); }

.pf-inquiry-detail {
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 16px);
  padding: var(--space-3, 24px);
  background: var(--paper);
}

.pf-inquiry-forward-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: var(--space-2, 16px);
  padding-top: var(--space-2, 16px);
  border-top: 1px solid var(--line);
}

.pf-inquiry-forward-form label {
  width: 100%;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.pf-inquiry-forward-form input {
  flex: 1;
  min-width: 200px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-body-size, 14px);
}

.pf-inquiry-forward-status {
  width: 100%;
  margin: 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* ── Inquiries Inbox retrofit: two-pane thread view (Plan v2 Phase 2,
 * tracker 627414ac) ──
 * Replaces the single-pane list+detail-panel above (.pf-inquiry-list /
 * .pf-inquiry-detail, kept in place — nothing deletes them — they still
 * back inquiryDetailHTML()'s forward/archive/spam actions, now rendered
 * INSIDE the thread view's header instead of a separate stacked block).
 * Two real panes side by side above 900px, single-pane stack below it,
 * per this item's own spec. */
.pf-thread-shell {
  display: flex;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 16px);
  overflow: hidden;
  min-height: 480px;
  background: var(--paper);
}

.pf-thread-list-pane {
  width: 360px;
  min-width: 320px;
  flex-shrink: 0;
  border-inline-end: 1px solid var(--line);
  overflow-y: auto;
  max-height: 720px;
}

.pf-thread-view-pane {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

@media (max-width: 900px) {
  .pf-thread-shell {
    flex-direction: column;
    min-height: 0;
  }
  .pf-thread-list-pane {
    width: 100%;
    min-width: 0;
    border-inline-end: none;
    border-bottom: 1px solid var(--line);
    max-height: 320px;
  }
  /* On the stacked layout, showing the thread view pushes the list pane
   * out of the way instead of both competing for vertical space — same
   * "one view at a time on narrow screens" pattern as every other
   * master-detail surface on this platform (e.g. the shell's own mobile
   * nav). data-thread-mobile-view toggles which pane is present. */
  .pf-thread-shell[data-thread-mobile-view="thread"] .pf-thread-list-pane { display: none; }
  .pf-thread-shell:not([data-thread-mobile-view="thread"]) .pf-thread-view-pane { display: none; }
}

.pf-thread-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 12px 16px;
  border: none;
  border-bottom: 1px solid var(--line);
  background: transparent;
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  color: var(--ink);
}

.pf-thread-row:hover { background: var(--surface-alt); }
.pf-thread-row.is-selected { background: var(--surface-alt); }
.pf-thread-row.is-selected::before {
  content: '';
  position: absolute;
}

.pf-thread-row-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--surface-alt);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  overflow: hidden;
}

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

.pf-thread-row-body {
  flex: 1;
  min-width: 0;
}

.pf-thread-row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.pf-thread-row-name {
  font-size: 15.5px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pf-thread-row.is-unread .pf-thread-row-name { font-weight: 700; }

.pf-thread-row-time {
  font-size: 12.5px;
  font-family: "SF Mono", "Fira Code", monospace;
  color: var(--muted);
  flex-shrink: 0;
}

.pf-thread-row-preview-line {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
}

.pf-thread-row-preview {
  font-size: 14px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}

.pf-thread-row-unread-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  /* Fallback dropped 2026-08-02 (backlog bd5a6a24) — --yeja-crimson is
   * defined (design-tokens.css:21). This one matched, but a literal that
   * agrees today drifts silently the day the token is retuned. */
  background: var(--yeja-crimson);
  flex-shrink: 0;
}

.pf-thread-row-intent {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border: 1px solid var(--muted);
  border-radius: var(--radius-pill, 999px);
  font-size: 10px;
  font-family: "SF Mono", "Fira Code", monospace;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  margin-top: 6px;
}

.pf-thread-list-empty {
  padding: var(--space-4, 32px) var(--space-2, 16px);
  text-align: center;
  color: var(--muted);
  font-size: 15.5px;
}

/* Thread view */
.pf-thread-view-header {
  padding: var(--space-3, 24px);
  border-bottom: 1px solid var(--line);
}

.pf-thread-view-back {
  display: none;
  margin-bottom: var(--space-2, 16px);
}

@media (max-width: 900px) {
  .pf-thread-view-back { display: inline-flex; }
}

.pf-thread-view-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  flex-wrap: wrap;
}

.pf-thread-view-title-text {
  min-width: 0;
  flex: 1;
}

.pf-thread-view-convert-btn {
  flex-shrink: 0;
}

.pf-thread-view-name {
  font-size: 20px;
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pf-thread-view-source {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  margin: 4px 0 0;
}

.pf-thread-view-source a { color: var(--muted); text-decoration: underline; }
.pf-thread-view-source a:hover { color: var(--ink); }

.pf-thread-artwork-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--space-2, 16px);
  padding: 6px 12px 6px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill, 999px);
  color: var(--ink);
  text-decoration: none;
  font-size: 13.5px;
}

.pf-thread-artwork-chip img {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
}

.pf-thread-view-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: var(--space-2, 16px);
}

.pf-thread-messages {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-3, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-3, 24px);
}

/* Flat, quiet rendering per this item's own spec ("no chat bubbles") —
 * a sender label + timestamp line above plain body text, same visual
 * register as .pf-inquiry-message above, not a messaging-app bubble
 * treatment. */
.pf-thread-message {
  max-width: 640px;
}

.pf-thread-message--collector { align-self: flex-start; }
.pf-thread-message--artist { align-self: flex-end; text-align: right; }

.pf-thread-message-meta {
  font-size: 12.5px;
  color: var(--muted);
  margin: 0 0 4px;
}

.pf-thread-message-body {
  font-size: var(--text-body-size, 14px);
  color: var(--ink);
  white-space: pre-wrap;
  margin: 0;
}

.pf-thread-suggested {
  padding: 0 var(--space-3, 24px);
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.pf-thread-suggested-label {
  font-size: 11px;
  font-family: "SF Mono", "Fira Code", monospace;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  width: 100%;
}

.pf-thread-composer {
  display: flex;
  gap: 8px;
  align-items: flex-end;
  padding: var(--space-3, 24px);
  border-top: 1px solid var(--line);
}

.pf-thread-composer textarea {
  flex: 1;
  min-height: 44px;
  max-height: 160px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--paper);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-body-size, 14px);
  resize: vertical;
}

.pf-thread-empty-state {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 15.5px;
  padding: var(--space-4, 32px);
}

/* ── Account hub (Profile 2.0 Phase A, item A1, 2026-07-26) ──
 * New "Account" tab landing content — a grid of destination cards, most
 * of which link out to other tabs/apps that don't fully exist yet (Your
 * Orders/Activity/Addresses/etc are separate A5-A13 backlog items). This
 * is shell scaffolding only: the grid + card component, not the
 * destinations behind each card. Renders inside the same #prof-tab-content
 * region every other tab uses (see renderActiveTab()) — no new DOM root,
 * per the master-backlog note that this has to live inside the
 * #app-canvas region Profile already owns, not shell chrome. */
/* Fixed 3 columns desktop / 1 column mobile (backlog ff1dd8a8,
 * 2026-07-26) — was auto-fill(300px), which combined with .prof-page's
 * old 640px cap above meant this never actually reached 3 columns
 * regardless of real browser width. Explicit breakpoint replaces that
 * now that .prof-page is elastic. */
.prof-hub-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2, 16px);
  padding: var(--space-3, 24px) 0 0;
}

@media (max-width: 640px) {
  .prof-hub-grid { grid-template-columns: repeat(1, 1fr); }
}

/* Whole-card hit target (role="button"/tabindex="0" set in JS) — a real
 * <button>-equivalent, not an <a>, since most cards don't have a real
 * destination yet (see the config array's visible()/action wiring in
 * app.js). Uses the same border/radius/hover language as
 * .prof-activity-stat/.prof-style-preset-card above rather than
 * inventing new card chrome. */
/* Now a real <button>, not a div[role="button"] (backlog 1cece0a2 item 4).
 * The border/background/cursor/text-align below already neutralised most
 * UA button chrome; the additions are the ones a <button> specifically
 * needs and a <div> never did: font-family/size do NOT inherit into form
 * controls, so without them the cards would fall back to the UA's 13.33px
 * system font, and width:100% restores the grid-cell fill that a block
 * <div> gave for free but a shrink-to-fit <button> does not. */
.prof-hub-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px);
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  background: var(--paper);
  cursor: pointer;
  text-align: start;
  width: 100%;
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  transition: background 0.15s ease-in-out, transform 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.prof-hub-card-soon {
  margin-inline-start: auto;
  align-self: center;
  flex: 0 0 auto;
  /* Mono, per guide §2's technical/metadata voice — this is a status
   * label about the product, not part of the card's prose. */
  font-family: var(--font-family-mono, 'JetBrains Mono', ui-monospace, monospace);
  font-size: var(--text-label-size, 12px);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
}

.prof-hub-card:hover {
  background: var(--surface-alt);
  border-color: var(--line-strong, var(--line));
  transform: translateY(-1px);
}

/* Cobalt focus ring (Master Design Guide §6) — same token/shape as every
 * other focus-visible rule in this file (.prof-activity-stat,
 * .prof-style-preset-card) and in apps/shared/design-tokens.css's
 * .yeja-focus-ring. */
.prof-hub-card:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

/* Guide §5 disabled spec. This rule ALREADY existed — the coming-soon
 * work (backlog 8fdcd3ef item 4) extends it rather than adding a
 * competing .is-coming-soon block, which is how this codebase ends up
 * with five divergent copies of the same component. aria-disabled is the
 * right hook: it is the attribute that actually describes the state, so
 * anything marked disabled gets the treatment for free.
 *
 * The hover lift is explicitly cancelled — with cursor:default and no
 * click listener bound (see wireHubCards()), a card that still rose on
 * hover would keep promising interactivity it does not have. */
.prof-hub-card[aria-disabled="true"] {
  opacity: 0.55;
  cursor: default;
}
.prof-hub-card[aria-disabled="true"]:hover {
  transform: none;
  background: var(--paper);
  border-color: var(--line);
}

.prof-hub-card-icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-alt);
  color: var(--yeja-cobalt, #1E88E5);
}

.prof-hub-card-icon .yeja-icon {
  font-size: 20px;
}

.prof-hub-card-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.prof-hub-card-title {
  margin: 0;
  font-size: 15.5px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
}

.prof-hub-card-desc {
  margin: 0;
  font-size: 14px;
  line-height: 1.4;
  color: var(--muted);
}

.prof-hub-card-badge {
  flex: 0 0 auto;
  font-family: var(--font-family-mono, 'JetBrains Mono', monospace);
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

/* ── Your Orders (A5, 2026-08-03) ──
 * .prof-mono is this section's own shared monospace/tabular-nums
 * utility — order ids, dates, and amounts all use it, same
 * --font-family-mono token .prof-hub-card-badge above already
 * established as this app's monospace convention. font-variant-numeric:
 * tabular-nums is the plan's own explicit spec for amounts (fixed-width
 * digits so a column of prices aligns), applied here at the utility
 * level rather than a second amount-only class since ids/dates read
 * better tabular too. */
.prof-mono {
  font-family: var(--font-family-mono, 'JetBrains Mono', monospace);
  font-variant-numeric: tabular-nums;
}

.prof-buyer-order-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.prof-buyer-order-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  width: 100%;
  padding: 12px 4px;
  border: none;
  border-bottom: 1px solid var(--line);
  background: none;
  font-family: inherit;
  text-align: start;
  cursor: pointer;
}

.prof-buyer-order-row:last-child { border-bottom: none; }
.prof-buyer-order-row:hover { background: var(--surface-alt); }

.prof-buyer-order-row-main {
  flex: 1;
  min-width: 0;
}

.prof-buyer-order-row-title {
  margin: 0 0 2px;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prof-buyer-order-row-meta {
  margin: 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-buyer-order-row-amount {
  flex: 0 0 auto;
  font-weight: 600;
  color: var(--ink);
}

.prof-buyer-orders-empty {
  text-align: center;
  padding: var(--space-4, 32px) var(--space-2, 16px);
}

.prof-buyer-orders-empty-title {
  margin: 0 0 8px;
  font-size: 17px;
  font-weight: 700;
  color: var(--ink);
}

.prof-buyer-orders-empty-desc {
  margin: 0 0 var(--space-3, 24px);
  color: var(--muted);
}

.prof-buyer-order-detail {
  margin-top: var(--space-3, 24px);
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 16px);
  padding: var(--space-3, 24px);
  background: var(--paper);
}

.prof-order-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2, 16px);
}

.prof-order-detail-header h3 { margin: 0 0 4px; }

/* Status timeline — a small dedicated stepper (see this file's own
 * app.js comment on orderStatusTimelineHTML() for why nothing existing
 * fit). Horizontal line of dots connected by a track; .is-done steps
 * (current status and everything before it in the flow) get a filled
 * dot + full ink color, later steps stay muted/hollow. */
.prof-order-timeline {
  display: flex;
  align-items: flex-start;
  margin: var(--space-3, 24px) 0;
  overflow-x: auto;
}

.prof-order-timeline-step {
  position: relative;
  flex: 1;
  min-width: 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
}

.prof-order-timeline-step::before {
  content: '';
  position: absolute;
  top: 5px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: var(--line);
  z-index: 0;
}

.prof-order-timeline-step:first-child::before { display: none; }

.prof-order-timeline-step.is-done::before { background: var(--yeja-cobalt, #1E88E5); }

.prof-order-timeline-dot {
  position: relative;
  z-index: 1;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--line);
}

.prof-order-timeline-step.is-done .prof-order-timeline-dot {
  background: var(--yeja-cobalt, #1E88E5);
  border-color: var(--yeja-cobalt, #1E88E5);
}

.prof-order-timeline-label {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}

.prof-order-timeline-step.is-done .prof-order-timeline-label {
  color: var(--ink);
  font-weight: 600;
}

.prof-order-timeline-branch {
  flex: 0 0 auto;
  margin-inline-start: var(--space-2, 16px);
  padding-inline-start: var(--space-2, 16px);
  border-inline-start: 1px solid var(--line);
  display: flex;
  align-items: center;
}

.prof-order-detail-amounts {
  margin: var(--space-3, 24px) 0;
  padding-top: var(--space-2, 16px);
  border-top: 1px solid var(--line);
}

.prof-order-detail-amount-row {
  display: flex;
  justify-content: space-between;
  padding: 4px 0;
  color: var(--muted);
}

.prof-order-detail-amount-row--total {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-weight: 700;
  font-size: 15.5px;
}

.prof-order-detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: var(--space-2, 16px);
}

.prof-order-detail-coa-status {
  margin: 8px 0 0;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  min-height: 1em;
}

/* ── Public portfolio boot skeleton (2026-08-01) ─────────────────────────
 * Replaces the bare <p>Loading…</p> that apps/portfolio-public/index.html
 * used to paint as its first frame. Shapes mirror the real header + pin
 * grid so first paint doesn't jump when the data arrives.
 *
 * Kept pf-local rather than promoted to apps/shared/components.css: that
 * file has no skeleton primitive today and 5+ apps consume it, so this
 * proves the pattern here first (plan Phase 4.2 promotes it once settled).
 *
 * Tokens only — no literal hex — so it inherits both themes. Shimmer is
 * the guide's §4 spec: 1.4s ease-in-out infinite. */
.pf-boot { padding: 8px 0 0; }

.pf-skeleton {
  background: linear-gradient(
    90deg,
    var(--surface-alt, var(--line)) 25%,
    var(--line) 37%,
    var(--surface-alt, var(--line)) 63%
  );
  background-size: 400% 100%;
  animation: pf-skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius-control, 8px);
}

@keyframes pf-skeleton-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.pf-boot-header { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; }
.pf-boot-avatar { width: 72px; height: 72px; border-radius: 50%; flex: none; }
.pf-boot-headtext { flex: 1; min-width: 0; }
.pf-boot-line { height: 14px; margin-bottom: 10px; }
.pf-boot-line--title { width: min(220px, 60%); height: 22px; }
.pf-boot-line--sub { width: min(120px, 35%); margin-bottom: 0; }

.pf-boot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}
.pf-boot-tile { aspect-ratio: 4 / 5; border-radius: var(--radius-card, 12px); }

/* Guide §4: every animation needs a reduced-motion fallback. Hold a flat
 * resting fill rather than strobing the gradient. */
@media (prefers-reduced-motion: reduce) {
  .pf-skeleton { animation: none; background: var(--line); }
}

/* Screen-reader-only text. MOVED to apps/shared/components.css on
 * 2026-08-03 (backlog fdf9ccb3) — apps/shared/loading.js emits this class
 * from shared code that any app can call, so a definition living only in
 * Profile's stylesheet would have rendered the "hidden" status message as
 * visible text in every app that doesn't load this file. The rule is
 * identical; only its home changed. Deliberately not re-declared here:
 * two copies is how a utility drifts. */

/* Support modal's Cancel button (2026-08-01). Replaces an inline
 * style="width:auto;padding:0 16px;" — .prof-address-delete is a
 * full-width row-delete button by default, and this one sits inline in a
 * modal action row. Moved out of the attribute so it lives with the rest
 * of the cascade (it's on the public payment path). */
.pf-modal-cancel-btn {
  width: auto;
  padding: 0 16px;
}

/* Org seat usage line (backlog 3b5a9462 Tier 2 item 3). Sits directly
 * under the Members heading as a quiet hint until the org is actually
 * full, at which point it becomes crimson — the one moment the number
 * blocks an action the owner is about to attempt. */
.prof-org-seats {
  margin: 0 0 var(--space-2, 16px);
}
.prof-org-seats.is-full {
  color: var(--yeja-crimson, #E53935);
  font-weight: 500;
}

/* Organization logo branding (backlog 3b5a9462 Tier 2 item 4). A logo is
 * a rectangular brandmark, not a portrait, so this deliberately does NOT
 * reuse .prof-avatar's circular crop + object-fit:cover — cropping a
 * wordmark to a circle would cut the word in half. Square box, contained
 * (never cropped), on a neutral surface so a transparent PNG still reads
 * in both themes. */
.prof-org-logo-row {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
  flex-wrap: wrap;
}

.prof-org-logo-preview {
  width: 64px;
  height: 64px;
  flex: 0 0 64px;
  border-radius: var(--radius-media);
  background: var(--surface-alt);
  object-fit: contain;
}

/* Empty state reuses the avatar's initial-letter treatment, since here
 * there is no wordmark to protect — just a single character to centre. */
.prof-org-logo-preview.is-empty {
  display: inline-grid;
  place-items: center;
  color: var(--muted);
  font-size: var(--text-h2-size, 24px);
  font-weight: var(--text-h2-weight, 600);
}

/* Small mark beside the org name in the panel header. */
.prof-org-heading {
  display: flex;
  align-items: center;
  gap: var(--space-1, 8px);
}

.prof-org-logo-mark {
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  border-radius: var(--radius-media);
  object-fit: contain;
}

/* Hub card drag-to-rearrange (backlog ac406d0d). Mirrors the Pinned
 * Showcase's own reorder affordances so the two feel like one gesture. */
.prof-hub-card[draggable="true"] { cursor: grab; }
.prof-hub-card.is-dragging {
  opacity: 0.5;
  cursor: grabbing;
}
/* Cobalt edge on the card you're about to drop onto — the drop target,
 * not the dragged card, is what the user is aiming at. */
.prof-hub-card.is-drop-target {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .prof-hub-card.is-dragging { opacity: 0.5; }
}

/* Portfolio link style chooser (backlog ac406d0d) — lets a user pick
 * between the username URL and the opaque id URL. Structured as a real
 * <fieldset>/<legend> with radios (not a bare toggle) because the two
 * options are mutually exclusive named choices, and the sample URL under
 * each label is the actual information the user is choosing between.
 * Follows .prof-roles-fieldset's own reset-the-fieldset-chrome approach
 * rather than inventing a second convention. */
.prof-url-mode {
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  padding: var(--space-2, 16px);
  margin: 0 0 var(--space-2, 16px);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.prof-url-mode legend {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  padding: 0 6px;
}
.prof-url-mode-hint {
  margin: 0 0 4px;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}
.prof-radio-label {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 8px;
  cursor: pointer;
}
.prof-url-mode-name {
  display: block;
}
/* The sample URL can be long and must not push the card into horizontal
 * overflow on mobile, so it wraps at any character rather than staying on
 * one line — a UUID has no spaces to break at. */
.prof-url-mode-sample {
  display: block;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  overflow-wrap: anywhere;
}
/* Ring on the real <input>, not the label: the input is what actually
 * receives keyboard focus, so styling the label would leave the control
 * looking unfocused while it is focused. */
.prof-url-mode input[type="radio"]:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* Analytics settings intro (backlog ac406d0d) — explains that the
 * artist's tags load ALONGSIDE Yeja's rather than replacing them, which
 * is the one thing about this panel that isn't self-evident. */
.prof-analytics-intro {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  max-width: 68ch;
}

/* The analytics block inside the Atelier card (2026-08-05). It is a
 * distinct subject from the publish/commission toggles directly above it,
 * so it needs a real break — without one the "Analytics" heading sat flush
 * under the "accepting commissions" checkbox and read as a label belonging
 * to that control group rather than the start of a new block. A hairline
 * rule plus the standard section gap, matching how the card's other
 * sub-blocks separate. */
.prof-analytics-fields {
  margin-top: var(--space-3, 24px);
  padding-top: var(--space-3, 24px);
  border-top: 1px solid var(--line);
}
.prof-analytics-fields h3 {
  margin: 0 0 4px;
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
}

/* ── Full inbox functions (backlog ac406d0d) ────────────────────────── */
.pf-inbox-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: var(--space-2, 16px);
}
.pf-inbox-search {
  flex: 1 1 200px;
  min-width: 0;
}
.pf-inbox-search:focus-visible,
.pf-inbox-folder-filter select:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* Label chips. Colour comes from a TOKEN NAME stored in the DB, mapped
 * here — never a stored hex, which could not theme. */
.pf-thread-row-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 4px;
}
.pf-label-chip {
  font-size: 11px;
  line-height: 1.6;
  padding: 0 8px;
  border-radius: var(--radius-pill);
  color: var(--yeja-canvas);
}
.pf-label-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-circle);
  margin-inline-end: 6px;
}
.pf-label-chip--cobalt  { background: var(--yeja-cobalt); }
.pf-label-chip--crimson { background: var(--yeja-crimson); }
.pf-label-chip--amber   { background: var(--yeja-amber); }
.pf-label-chip--mint    { background: var(--yeja-mint); }
.pf-label-chip--slate   { background: var(--yeja-gray-600); }

.pf-thread-row-star,
.pf-thread-row-muted {
  font-size: 14px;
  color: var(--muted);
  flex-shrink: 0;
}

/* Overflow menu — a native <details>, so keyboard operation and
 * expanded state come from the platform. */
.pf-thread-overflow { position: relative; }
.pf-thread-overflow-toggle {
  cursor: pointer;
  list-style: none;
  padding: 4px 8px;
  border-radius: var(--radius-control);
  color: var(--ink);
}
.pf-thread-overflow-toggle::-webkit-details-marker { display: none; }
.pf-thread-overflow-toggle::marker { content: ''; }
/* The conversation rows had NO focus style at all (2026-08-02, backlog
 * bd5a6a24). Proven against live production before fixing: with a row
 * focused, computed outline-style was 'none', box-shadow 'none', and no
 * loaded rule anywhere matched .pf-thread-row:focus-visible — so a
 * keyboard user tabbing the inbox got no indication of where they were,
 * on the primary control of this whole surface. WCAG 2.4.7.
 *
 * outline-offset is negative so the ring draws INSIDE the row: these rows
 * are flush to the pane edge and share a 1px border with their
 * neighbours, so an outset ring would be clipped on the left and overlap
 * the rows above and below. */
.pf-thread-row:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: -2px;
  border-radius: var(--radius-control, 8px);
}

.pf-thread-overflow-toggle:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}
.pf-overflow-menu {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + 4px);
  z-index: 20;
  min-width: 232px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-menu);
  box-shadow: 0 12px 32px color-mix(in oklch, var(--yeja-backdrop) 24%, transparent);
  padding: 6px;
}
.pf-overflow-item {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 6px;
  padding: 8px 10px;
  background: none;
  border: none;
  text-align: start;
  color: var(--ink);
  border-radius: var(--radius-control);
  cursor: pointer;
  font-size: 13px;
}
.pf-overflow-item:hover { background: var(--surface-alt); }
.pf-overflow-item:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: -2px;
}
.pf-overflow-item.is-current { font-weight: 600; }
.pf-overflow-item--danger { color: var(--yeja-crimson); }
.pf-overflow-group { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; }
.pf-overflow-group-title,
.pf-overflow-empty {
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  margin: 0 0 4px;
  padding: 0 10px;
}

/* Report / compose / folder modals */
.pf-report-field,
.pf-fl-add { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.pf-fl-add { flex-direction: row; align-items: center; flex-wrap: wrap; }
.pf-report-field > span { font-size: var(--text-label-size, 12px); color: var(--muted); }
.pf-report-sub { font-size: var(--text-label-size, 12px); color: var(--muted); margin: 0 0 12px; }
.pf-report-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.pf-fl-section { margin-bottom: var(--space-2, 16px); }
.pf-fl-list { list-style: none; padding: 0; margin: 0 0 8px; display: flex; flex-direction: column; gap: 6px; }
.pf-fl-list li { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* Programmatic focus landing point (backlog 1cece0a2 item 5). A tab
 * switch moves focus to the new pane's heading so a screen reader starts
 * at the top of the new content and reads its name. That heading is NOT
 * interactive and was never Tab-reached, so it must not paint a focus
 * ring — a block-level <h2> renders one as a full-card-width cobalt box,
 * which reads as a broken text input. The element stays focusable
 * (tabindex="-1") and announced; only the ring is suppressed. Every
 * genuinely interactive control keeps its 2px cobalt ring untouched. */
.prof-page .prof-focus-target:focus,
.prof-page .prof-focus-target:focus-visible {
  outline: none;
  box-shadow: none;
}

/* Thread-view loading skeleton (2026-08-02, backlog bd5a6a24). Replaces
 * a bare "Loading…" line in the message pane with the same shimmer the
 * app's boot state already uses, so there is one loading idiom, not two.
 * Shapes approximate the real thread (title, meta line, two message
 * blocks) so the pane does not jump when content arrives.
 *
 * prefers-reduced-motion is handled by .pf-skeleton's own guard further
 * up this file — these rules only add geometry, no new animation. */
.pf-thread-boot { padding: var(--space-2, 16px); }
.pf-thread-boot-title {
  height: 22px;
  width: min(240px, 55%);
  margin-bottom: 10px;
}
.pf-thread-boot-meta {
  height: 14px;
  width: min(160px, 35%);
  margin-bottom: var(--space-3, 24px);
}
.pf-thread-boot-msg {
  height: 64px;
  margin-bottom: var(--space-2, 16px);
}
.pf-thread-boot-msg--short { width: 70%; margin-bottom: 0; }

/* Pop-out reading view (2026-08-02, backlog bd5a6a24). Wider and taller
 * than a standard dialog card because its whole purpose is comfortable
 * reading of a long thread; capped by viewport units so it never exceeds
 * the screen, with the message list itself owning the scroll so the
 * header stays put. Reuses .pf-thread-messages for the message styling
 * rather than restyling bubbles a second time. */
/* max-width MUST be restated: .yeja-dialog-card sets max-width:480px and
 * is the same specificity, so it wins on source order (components.css is
 * loaded after this file) and silently capped this card at 480px — no
 * wider than an ordinary dialog, which defeats the entire "easier to
 * read" purpose. Caught by looking at the screenshot; the element was
 * present and correct in every other respect. */
.pf-popout-card.yeja-dialog-card {
  width: min(860px, 92vw);
  max-width: min(860px, 92vw);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
}
.pf-popout-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  margin-bottom: var(--space-2, 16px);
}
.pf-popout-title {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}
.pf-popout-messages {
  overflow-y: auto;
  /* Keeps the last message clear of the card's own bottom edge while
   * scrolled, and leaves room for a focus ring on any focusable content
   * inside a message. */
  padding-inline-end: 4px;
}

/* Help & Support FAQ (2026-08-02, backlog bd5a6a24). Native
 * <details>/<summary> disclosures — keyboard operation and expanded state
 * come from the platform. These are NOT menus and contain no
 * [role="menu"], so closeOpenOverflowMenus() deliberately leaves them
 * alone: an open FAQ answer must survive a click elsewhere on the page. */
.pf-help-faq {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Clearance for the first item's focus ring. The ring is drawn 2px
   * outside the summary, and without this it overlapped the section's
   * hint text above — visible in the light/dark screenshots, invisible to
   * any assertion, since the ring was present and correctly styled. */
  margin-top: var(--space-1, 8px);
}
.pf-help-item {
  border-bottom: 1px solid var(--line);
}
.pf-help-item:last-child { border-bottom: 0; }

.pf-help-question {
  cursor: pointer;
  padding: var(--space-2, 16px) 0;
  font-weight: 600;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
}
/* Chevron via ::after so the row keeps a single accessible name — an
 * <span> icon inside <summary> would be read out as part of the question. */
.pf-help-question::after {
  content: "+";
  flex: none;
  color: var(--muted);
  font-weight: 400;
}
.pf-help-item[open] > .pf-help-question::after { content: "–"; }
.pf-help-question::-webkit-details-marker { display: none; }
.pf-help-question:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
  border-radius: var(--radius-control, 8px);
}

.pf-help-answer {
  padding: 0 0 var(--space-2, 16px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2, 16px);
}
.pf-help-answer p {
  margin: 0;
  color: var(--muted);
  /* Comfortable reading measure — these are the longest prose blocks in
   * the app, and full-width lines are measurably harder to scan. */
  max-width: 68ch;
}

/* ── Apps directory (backlog fdf9ccb3, 2026-08-03) ──
 * The Account Hub's "Apps" card destination: every platform app with what
 * it does and who it is for.
 *
 * Rows, not a card grid. The hub's own cards (.pf-hub-card) are short
 * label+blurb tiles; these entries carry two sentences each, and in a
 * tile grid the descriptions set at wildly different heights and the
 * "For:" line stops being scannable down a column. A single-column list
 * on narrow screens widening to two on desktop keeps each description on
 * a readable measure while letting the eye run down the "For:" lines. */
.pf-apps-grid {
  list-style: none;
  margin: var(--space-2, 16px) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1, 8px);
}
@media (min-width: 900px) {
  .pf-apps-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* One rule for both the <button> (available) and <div> (unavailable)
 * forms — they differ only in interactivity, not in layout. The button
 * reset is here rather than inherited because .pf-apps-grid rows are the
 * only place in this app that render a full-bleed text block as a
 * control. */
.pf-app-row {
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px);
  text-align: start;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  color: inherit;
  font: inherit;
}
button.pf-app-row { cursor: pointer; }
button.pf-app-row:hover {
  background: var(--surface-alt);
  border-color: var(--yeja-cobalt);
}
button.pf-app-row:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}
/* Motion is opt-in per the global reduced-motion reset in
 * design-tokens.css — a colour transition is safe there, and that reset
 * neutralises it anyway for users who ask. */
button.pf-app-row {
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.pf-app-row--unavailable {
  opacity: 0.55;
}

.pf-app-icon {
  flex: 0 0 auto;
  font-size: 24px;
  line-height: 1;
  color: var(--yeja-cobalt);
}
/* The accent would read as "this one is special" on a greyed row, and
 * accents must mean something (design guide §1). */
.pf-app-row--unavailable .pf-app-icon { color: var(--muted); }

.pf-app-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.pf-app-name {
  display: flex;
  align-items: center;
  gap: var(--space-1, 8px);
  font-weight: 600;
  color: var(--ink);
}
.pf-app-desc,
.pf-app-who {
  font-size: 0.875rem;
  color: var(--muted);
  /* Descriptions wrap; a long one must not force the grid column wider
   * than its track and push the page into horizontal scroll. */
  overflow-wrap: anywhere;
}
.pf-app-who strong { font-weight: 600; }

.pf-app-badge {
  font-size: 0.75rem;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-alt);
  border: 1px solid var(--line);
  color: var(--muted);
}

/* ── Distinct error state (backlog f7a0fa25 item 2) ───────────────────────
 * .yeja-empty-state was reused for "nothing here yet" AND "the request
 * failed", so a load error was indistinguishable from an empty list — the
 * user could not tell that retrying would help, and had no way to retry.
 * Empty is a fact about the data; an error is a fact about the request. */
.pf-error-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px) 0;
}
.pf-error-state__message {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}

/* ── Security pane sections + sticky sub-nav (backlog 27e0c0a7) ───────────
 * The pane was eight flat, unrelated cards in one column with no grouping
 * and no way to reach the bottom except scrolling past everything above.
 * Now three titled sections in order of increasing consequence, with an
 * anchor sub-nav — anchors, not tabs, so every section stays in the DOM
 * and Ctrl-F still finds all of it. */
.prof-subnav {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 16px);
  padding: var(--space-1, 8px) 0;
  margin-bottom: var(--space-2, 16px);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.prof-subnav__link {
  color: var(--muted);
  text-decoration: none;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 6px 2px;
  border-bottom: 2px solid transparent;
  /* 44px minimum target (WCAG 2.2 AA), reached via padding + line-height
   * rather than a fixed height so a wrapped label still qualifies. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
.prof-subnav__link:hover { color: var(--ink); }
/* State is carried by BOTH colour and the underline, never colour alone. */
.prof-subnav__link[aria-current="true"] {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

.prof-section-heading {
  margin: var(--space-3, 24px) 0 var(--space-1, 8px);
  font-size: var(--text-label-size, 12px);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  /* Programmatically focused by the sub-nav; must NOT paint a focus ring —
   * a block-level <h2> would ring the full column width, which reads as a
   * rendering fault rather than a focus indicator. :focus-visible does not
   * match programmatic focus, so this is belt-and-braces. */
  outline: none;
}
.prof-section-heading:first-of-type { margin-top: 0; }

/* Delete Account in its own container, so an irreversible action can never
 * be mistaken for part of the routine export task above it. Guide §1:
 * crimson is reserved for destructive intent. */
.prof-danger-card {
  border-color: color-mix(in oklch, var(--yeja-crimson) 35%, transparent);
}

/* ── Public portfolio header (backlog 9a95c8cd) ───────────────────────────
 * Headline replaces the generic role line when the artist has written one;
 * the statement sits under it. Both are artist-authored and deliberately
 * public — see 20260806000012's own security note. */
.pf-headline {
  margin: 4px 0 0;
  color: var(--ink);
  font-size: var(--text-body-size, 15px);
}
.pf-bio {
  margin: var(--space-1, 8px) 0 0;
  max-width: 60ch; /* readable measure — a statement is prose, not a label */
  color: var(--muted);
  font-size: var(--text-body-size, 15px);
  line-height: 1.55;
  white-space: pre-line; /* honour the artist's own paragraph breaks */
}

/* --- Pricing calculator (backlog 6e5a63f1 / SELL-X, 2026-08-03) ---------
 * The amounts column of the gross/fee/net breakdown. Tabular figures so the
 * three rows' digits line up vertically as the artist types — with
 * proportional figures the numbers jitter column-to-column on every
 * keystroke, which reads as instability in a number people are trusting.
 * Colours reuse the platform's existing success/danger pair (--yeja-mint /
 * --yeja-crimson, the same one .yeja-status-chip.success and .toast-success
 * already use) rather than introducing a second green and red. */
/* The intro paragraph and the first field label are both small muted text,
 * so with no gap they read as one run-on sentence ("…not an estimate. Where
 * Are You Selling?"). Separate the prose from the controls. */
.prof-page #prof-pricing-form {
  margin-top: var(--space-3);
}

/* The breakdown sits below the 2-col .prof-form, not inside it, so it needs
 * its own top spacing to separate the answer from the inputs. */
.prof-page #prof-pricing-result {
  margin-top: var(--space-3);
}

.prof-page .prof-pricing-amount {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
  color: var(--ink);
}

.prof-page .prof-pricing-amount-fee { color: var(--yeja-crimson); }

.prof-page .prof-pricing-amount-net {
  color: var(--yeja-mint);
  font-size: 1.125rem;
}

/* The net row is the answer the artist actually came for, so it gets a
 * divider separating it from the two inputs that produce it. */
.prof-page .prof-pricing-net-row {
  border-top: 2px solid var(--yeja-border-strong);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
}

/* =======================================================================
 * TREASURE MAP - scoped --map-* token layer (P3, tracker 63777154)
 *
 * ISOLATION CONTRACT, stated here on purpose:
 * "No other surface may read --map-*. This layer may not be promoted into
 * --yeja-*. It exists because this one surface is deliberately
 * illustrative in a platform that is deliberately monochrome."
 *
 * WHY THIS IS A SANCTIONED EXCEPTION AND NOT DRIFT. The Master Design
 * Guide's thesis is "the artwork is the color, the chrome is monochrome
 * ink on warm paper", its accents are semantic-only, and its empty states
 * say "quiet is on-brand". A parchment treasure map contradicts all
 * three. CLAUDE.md says the guide wins over contradicting code, so this
 * is a SCOPED exception following the guide's own section 8 --exp-*
 * isolation precedent - documented, not smuggled in.
 *
 * THREE RULES MAKE IT DEFENSIBLE:
 * 1. --map-medal-earned IS var(--yeja-amber), never a new hex. The one
 *    accent keeps its platform-semantic role.
 * 2. EVERY CONTROL here is standard platform chrome - .yeja-pill-btn,
 *    .yeja-dialog-card, Cobalt 2px/2px focus rings, Title Case. The
 *    illustration is a picture; the buttons are Yeja buttons.
 * 3. It is recorded in the guide's section 8 per-app table.
 *
 * The literal values below are the ILLUSTRATION's palette - the one place
 * in this app where a raw hex is correct rather than drift, because these
 * are pigments in a picture, not themable chrome. Everything structural
 * still uses platform tokens.
 * ======================================================================= */
.prof-map-card {
  --map-parchment: #F4E4C1;
  --map-parchment-aged: #E8D3A9;
  --map-parchment-edge: #C9AE7E;
  --map-ink: #3B2C1B;
  --map-ink-soft: #6B5741;
  --map-sea: #A8C4CC;
  --map-sea-deep: #7FA3AE;
  --map-land: #D9C49A;
  --map-land-shadow: #BFA67C;
  --map-route: #8A6A45;
  --map-medal-earned: var(--yeja-amber);
  --map-medal-unearned: #B6A88F;
  --map-medal-glow: #FFD98A;
}
/* Parchment must still read as PARCHMENT in dark, never muddy brown -
 * only a screenshot catches that, so these are deliberately lifted rather
 * than darkened toward the page background. */
:root[data-theme="dark"] .prof-map-card {
  --map-parchment: #2A2318;
  --map-parchment-aged: #342B1E;
  --map-parchment-edge: #4A3D2A;
  --map-ink: #E8D9BE;
  --map-ink-soft: #B9A88A;
  --map-sea: #24343A;
  --map-sea-deep: #1A272C;
  --map-land: #3D3423;
  --map-land-shadow: #2B2417;
  --map-route: #A98A5F;
  --map-medal-unearned: #6B6152;
}

/* TRAP 1, and it has already cost a real session: .yeja-dialog-card sets
 * max-width:480px AND width:90vw at the same specificity, and
 * components.css loads AFTER this file - so it wins on source order. A
 * wider dialog MUST restate BOTH via a compound selector. This exact trap
 * silently capped the message pop-out and was caught only by a
 * screenshot; a presence check would have passed. */
.prof-map-card.yeja-dialog-card {
  width: min(1100px, 94vw);
  max-width: min(1100px, 94vw);
  max-height: 90vh;
  overflow-y: auto;
  gap: var(--space-2, 16px);
  /* box-sizing is NOT cosmetic here. .yeja-dialog-card carries
   * padding:32px, and a percentage width in the default content-box
   * sizing ADDS that padding on top - so 94vw became 94vw+64px and the
   * whole card hung 42px past a 375px viewport, dragging 18 descendant
   * elements into horizontal overflow with it. Measured, not guessed:
   * 417px wide inside a 375px viewport before this line. */
  box-sizing: border-box;
}

.prof-map-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2, 16px);
}
/* DELIBERATE DESIGN-GUIDE EXCEPTION (user request 2026-08-04).
 * The guide's type system says every app title is the base family; this one
 * title is Pirata One instead, because the overlay it heads is a pirate
 * treasure map. Scoped to THIS selector only — the subtitle, the CTA and
 * every zone pill stay on the base family, so the exception is one line of
 * flavour rather than a second type system.
 *
 * The face is loaded in apps/platform/index.html (the shell owns webfonts).
 * If that link is ever removed this silently falls back to the base stack —
 * which is why the fallback is spelled out rather than left to chance.
 *
 * Pirata One is a display face with a small x-height and no real weights:
 * font-weight:700 would synthesise a smeared faux-bold, so it is dropped to
 * 400, and the size is bumped since the same px reads visibly smaller here. */
.prof-map-title {
  margin: 0;
  font-family: "Pirata One", var(--font-family-base);
  font-size: calc(var(--text-h2-size, 22px) * 1.5);
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.15;
  color: var(--ink);
}
.prof-map-sub {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
}
.prof-map-close {
  border: none;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  padding: 6px;
  border-radius: 50%;
  line-height: 0;
}
.prof-map-close:hover { background: var(--surface-alt); }

.prof-map-progress {
  display: flex;
  align-items: center;
  gap: var(--space-2, 16px);
}
.prof-map-progress-track {
  flex: 1;
  height: 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.prof-map-progress-fill {
  height: 100%;
  background: var(--map-medal-earned);
}
/* The percentage is REAL TEXT beside the meter. Progress is never
 * conveyed by width or colour alone (guide section 6). */
.prof-map-progress-label {
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

/* LAYER 1 mounts inside .prof-map-stage, behind the hotspots. Until the
 * artwork exists this is a plain tinted ground - which is exactly the
 * point of building layer 2 first: the map is fully operable now and the
 * picture drops in later without touching one line of interaction. */
.prof-map-stage {
  position: relative;
  width: 100%;
  /* A fixed ratio keeps PERCENTAGE-positioned hotspots on their landmarks
   * at every width - the mechanism that makes the artwork droppable. */
  aspect-ratio: 16 / 9;
  /* flex-shrink:0 is LOAD-BEARING, not tidiness. .yeja-dialog-card is a
   * flex column with max-height:90vh, so on a short viewport this stage
   * is a flex item that WILL be crushed - aspect-ratio loses to flex
   * shrinking. Measured before this line: 713x49 instead of 713x401,
   * which collapsed every percentage-positioned hotspot into an
   * unreadable overlapping pile. Caught only by a screenshot; the DOM
   * check reported all 5 zones present and correctly labelled.
   * min-height keeps it honest if aspect-ratio is ever unsupported. */
  flex-shrink: 0;
  min-height: 260px;
  border-radius: var(--radius-card, 12px);
  border: 1px solid var(--map-parchment-edge);
  overflow: hidden;
}
/* LAYER 1 — the painted archipelago (2026-08-04). The gradient placeholder
 * this replaces was always scaffolding; the art is the real thing.
 *
 * The gradient survives as the background UNDER the image, so a slow or
 * failed image load still shows sea-and-land tones rather than a bare
 * rectangle — the map stays usable before the artwork arrives.
 *
 * background-size: cover with a fixed 16:9 stage means the painting is
 * never letterboxed and never crops meaningfully, which is what keeps the
 * percentage-positioned hotspots sitting on their islands at every width. */
.prof-map-terrain {
  position: absolute;
  inset: 0;
  background-color: var(--map-sea);
  background-image:
    url('/apps/profile/assets/map/map-base-light.webp'),
    linear-gradient(160deg, var(--map-sea) 0%, var(--map-sea-deep) 100%);
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}
/* The night painting is a SEPARATE work, not a filtered copy of the day
 * one — lit windows, a live lighthouse beam, molten forge. Swapping the
 * file is what makes dark mode feel painted rather than dimmed. */
:root[data-theme="dark"] .prof-map-terrain {
  background-color: var(--map-sea);
  background-image:
    url('/apps/profile/assets/map/map-base-dark.webp'),
    linear-gradient(160deg, var(--map-sea) 0%, var(--map-sea-deep) 100%);
}

.prof-map-zone {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px 8px 8px;
  border-radius: 999px;
  border: 1px solid var(--map-parchment-edge);
  background: var(--map-parchment);
  color: var(--map-ink);
  cursor: pointer;
  text-align: start;
  max-width: 42%;
}
.prof-map-zone:hover { background: var(--map-parchment-aged); }
/* Focus-ring contrast over an illustration is a real risk: Cobalt alone
 * can disappear over mid-tone sea. The parchment halo makes the ring read
 * over BOTH sea and land. */
.prof-map-zone:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--map-parchment);
}
.prof-map-zone-marker {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  border-radius: 50%;
  background: var(--map-parchment-aged);
  color: var(--map-ink);
}
.prof-map-zone.is-complete .prof-map-zone-marker {
  background: var(--map-medal-earned);
}
.prof-map-zone-body { display: flex; flex-direction: column; min-width: 0; }
.prof-map-zone-name {
  font-weight: 600;
  font-size: var(--text-label-size, 12px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.prof-map-zone-count {
  font-size: 0.6875rem;
  color: var(--map-ink-soft);
  /* Pushed to the right of the subtitle it shares a line with, and never
   * wrapped or squeezed — "1/4" is three characters and should always read
   * as three characters. */
  margin-inline-start: auto;
  white-space: nowrap;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

.prof-map-shelf-title {
  margin: 0;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.prof-map-shelf {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.prof-map-medal {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
}
.prof-map-medal.is-earned {
  border-color: var(--map-medal-earned);
  color: var(--ink);
  font-weight: 600;
}
.prof-map-medal.is-earned .yeja-icon { color: var(--map-medal-earned); }
/* Locked reuses the shipped stub pattern: aria-disabled + reduced opacity,
 * NOT the disabled property - a locked zone must stay reachable and
 * announced, and `disabled` would drop it out of the tab order entirely. */
.prof-map-medal.is-locked { opacity: 0.55; }

.prof-map-logbook-toggle {
  align-self: flex-start;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
  text-decoration: underline;
  cursor: pointer;
  padding: 4px;
}
.prof-map-logbook-toggle:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}
.prof-map-logbook-title {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-h3-size, 18px);
  color: var(--ink);
}
.prof-map-logbook-zone h3 {
  margin: var(--space-2, 16px) 0 4px;
  font-size: var(--text-label-size, 12px);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.prof-map-logbook-zone ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.prof-map-logbook-zone li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  font-size: var(--text-body-size, 15px);
}
.prof-map-logbook-status { color: var(--muted); }
.prof-map-error { margin: 0; color: var(--ink); }

/* Mobile is an MVP item, not polish. At 375px the hotspots stop being
 * absolutely positioned and become a plain readable list - a spatial map
 * on a phone is unusable, and the logbook proves the content never
 * depended on the spatial arrangement in the first place. */
@media (max-width: 560px) {
  .prof-map-stage {
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px;
  }
  /* On mobile the stage stops being a picture and becomes a stacked list,
   * so everything positioned AGAINST the painting has to go with it: the
   * terrain, the per-island halos, and the porthole avatar. Each was added
   * at a different time and each had to be added here — a floating face
   * and stray glows over a plain list, caught in a 380px screenshot. */
  .prof-map-terrain,
  .prof-map-halo { display: none; }
  .prof-map-zone {
    position: static;
    transform: none;
    max-width: 100%;
  }
}

/* Treasure Map hub-card meter (P3 section 3.3a, tracker 63777154).
 * Renders hidden and is revealed by applyMapProgress() only once real
 * data lands - never at 0%, which would misread as confirmed-zero rather
 * than not-loaded-yet (the same rule the badge slot follows). */
.prof-hub-map-meter {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  width: 100%;
}
.prof-hub-map-meter[hidden] { display: none; }
.prof-hub-map-meter-track {
  flex: 1;
  height: 4px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.prof-hub-map-meter-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--yeja-amber);
}
/* The percentage is real text beside the bar - progress is never conveyed
 * by width or colour alone (guide section 6). */
.prof-hub-map-meter-label {
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

/* The framing line under the greeting (2026-08-04). A new user needs to be
 * told what the seven islands ARE before the painting means anything. */
.prof-map-lede {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: var(--text-body-size, 15px);
  max-width: 62ch;
}
/* The island's real meaning, in the logbook where there is room for it.
 * On the map itself this same sentence rides in the tooltip and the
 * aria-label instead — a second line of prose on a small pill would fight
 * the artwork underneath it. */
.prof-map-logbook-story {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
  max-width: 62ch;
}

/* THE PORTHOLE (2026-08-04). Gemini painted a decorative round frame into
 * the upper-left of the base art with a deliberately empty middle; this
 * fills it with the user's face. Percentage-positioned like the hotspots
 * so it stays seated in its frame at every width. */
.prof-map-porthole {
  position: absolute;
  /* MEASURED against the artwork, not estimated. Scanning
   * map-base-light.webp for the painted disc gives a bounding box of
   * 150x146px within 1376x768 — centre 11.34% / 18.88%, and very nearly a
   * true circle in PIXELS.
   *
   * Sizing is therefore in the VERTICAL axis, not the horizontal: the
   * stage is 16:9, so equal percentages are NOT equal lengths. 19% of
   * height = 146px, the disc's real diameter; expressing the same circle
   * as a width percentage would need 10.6%, and any drift between the two
   * shows up as an off-centre face. Width is left to aspect-ratio.
   *
   * Inset one point under the painted brass ring so the photo sits inside
   * the frame rather than butting against it. */
  /* THE PAINTING IS CROPPED, SO PERCENTAGES OF THE STAGE ARE NOT PERCENTAGES
   * OF THE ARTWORK. This is the whole reason the avatar kept landing off
   * centre, through three attempts at re-measuring the disc.
   *
   * The disc itself is not in dispute: a flood fill of the cream matte in
   * map-base-light.webp gives a 151x150px circle centred at 11.34% / 18.68%
   * of the 1376x768 image. But .prof-map-terrain draws that image with
   * `background-size: cover`, and the stage (1.774) is slightly WIDER in
   * aspect than the art (1.792), so `cover` scales to fill the HEIGHT and
   * crops ~10px of width — 5px off each side. An 11.34% left therefore lands
   * ~4px right of where the ring actually is, which is precisely the "low and
   * right" the eye kept catching.
   *
   * Both aspects are FIXED — the stage is declared `aspect-ratio: 16/9` and
   * the art is 1376x768 — so the correction is a constant, not something to
   * measure per viewport:
   *     drawn width = (1376/768) / (16/9)      = 1.007812 x stage width
   *     cropped off each side = (1.007812-1)/2 = 0.390625% of stage width
   *     left = 11.34% * 1.007812 - 0.390625%   = 11.038%
   * It is written as the calc below rather than the answer so the derivation
   * survives: if the artwork is ever re-exported at a different size, only
   * the two ratios change.
   *
   * Vertical needs no correction: height is the fitted axis under `cover`, so
   * 18.68% of the stage IS 18.68% of the painting.
   *
   * FILL THE OPENING, do not sit inside it. A smaller disc centred in the
   * hole leaves a ring of cream, and because the brass is hand-painted and
   * slightly irregular that ring is thicker on one side — which reads as the
   * FACE being off-centre even when it is dead centre. Measured: the avatar
   * rendered 90px inside a 114px opening. 20.4% of stage height slightly
   * overfills the matte's own 19.53%, tucking the photo's edge under the
   * painted brass so no cream shows anywhere. */
  top: 18.68%;
  /* (1376/768) / (16/9) = 1.0078125 — the drawn width, as a multiple of the
   * stage width. Half the excess is cropped off the left. */
  inset-inline-start: calc(11.34% * 1.0078125 - (1.0078125 - 1) * 100% / 2);
  transform: translate(-50%, -50%);
  height: 20.4%;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* No border of our own — the painting already provides the brass ring.
   * Adding one would double it. The photo covers the matte entirely, so this
   * background only shows for the initial-letter fallback. */
  background: color-mix(in oklch, var(--map-parchment) 55%, transparent);
}
.prof-map-porthole img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* THE BRASS RING, RE-PAINTED ON TOP OF THE PHOTO.
 *
 * The avatar deliberately overfills the matte so no cream shows, which means
 * its hard circular edge lands ON the painted trim. This pseudo-element is a
 * second copy of the same artwork, positioned identically to .prof-map-terrain
 * and masked to an annulus, so the real painted brass is re-drawn over that
 * edge. The seam disappears behind the trim it is supposed to sit under —
 * the photo looks set INTO the porthole rather than pasted over it.
 *
 * It is a copy of the terrain rather than a separate asset because the ring
 * only has to line up with itself: same image, same `cover` sizing, same
 * centre, so it cannot drift from the painting at any viewport width.
 *
 * The mask keeps only the band from 47% to 63% of this box — outside the
 * photo's own radius (50% of a smaller box) and wide enough to cover the
 * whole brass. aria-hidden by nature: a pseudo-element is not in the a11y
 * tree, and the ring carries no information. */
.prof-map-porthole-initial {
  font-size: clamp(18px, 3.6vw, 46px);
  font-weight: 700;
  color: var(--map-ink);
}

/* ZONE PILLS — GLASS, not paper. The islands must read through them.
 *
 * Iterated twice: solid parchment first (a sticker on a painting), then
 * 62% with a 6px blur — still too much, and the blur was frosting the very
 * artwork the pill sits on. Now 26% tint with NO blur, so the water,
 * rooftops and coastline stay sharp underneath.
 *
 * Legibility comes from a text-shadow on the label instead of from hiding
 * the picture — the same trick used for captions over photography. */
.prof-map-zone {
  /* 44% tint + a light 3px blur: the islands, water and rooftops read
   * clearly through the pill, while the label stays legible over busy
   * water. 26% with no blur was tried and rejected — the artwork won, but
   * the text became hard work, which is a worse failure than a slightly
   * heavy pill. The heavier text-shadow does most of the legibility work
   * so the tint does not have to. */
  background: color-mix(in oklch, var(--map-parchment) 44%, transparent);
  backdrop-filter: blur(3px) saturate(1.08);
  -webkit-backdrop-filter: blur(3px) saturate(1.08);
  border-color: color-mix(in oklch, var(--map-parchment) 62%, transparent);
  box-shadow: 0 1px 6px color-mix(in oklch, var(--yeja-gray-1000) 20%, transparent);
  text-shadow:
    0 1px 2px color-mix(in oklch, var(--map-parchment) 95%, transparent),
    0 0 6px color-mix(in oklch, var(--map-parchment) 80%, transparent);
}
.prof-map-zone:hover {
  background: color-mix(in oklch, var(--map-parchment) 62%, transparent);
}
/* A completed zone earns a little more presence — settled, not opaque. */
.prof-map-zone.is-complete {
  background: color-mix(in oklch, var(--map-parchment) 42%, transparent);
}
/* At night the painting is darker, so the tint inverts: a dark veil rather
 * than a pale one, and the text-shadow goes dark to match. */
:root[data-theme="dark"] .prof-map-zone {
  background: color-mix(in oklch, var(--yeja-gray-1000) 52%, transparent);
  border-color: color-mix(in oklch, var(--map-parchment) 38%, transparent);
  box-shadow: 0 1px 8px color-mix(in oklch, var(--yeja-gray-1000) 40%, transparent);
  text-shadow:
    0 1px 3px color-mix(in oklch, var(--yeja-gray-1000) 95%, transparent),
    0 0 7px color-mix(in oklch, var(--yeja-gray-1000) 85%, transparent);
}
:root[data-theme="dark"] .prof-map-zone:hover {
  background: color-mix(in oklch, var(--yeja-gray-1000) 68%, transparent);
}
:root[data-theme="dark"] .prof-map-zone.is-complete {
  background: color-mix(in oklch, var(--yeja-gray-1000) 50%, transparent);
}

/* PER-ISLAND LIGHT (2026-08-04). The base painting shows every island
 * alive; this adds or withholds light so YOUR progress is visible on it.
 *
 * Untouched: a cool veil, as if that island is still under cloud.
 * Started:   the veil lifts.
 * Complete:  a warm amber glow, the island in full sun.
 *
 * Deliberately a light effect rather than a desaturating filter over the
 * artwork — dimming a painting reads as "broken image", while withheld
 * light reads as weather. It is also why this works without 14 separate
 * landmark paintings. */
.prof-map-halo {
  position: absolute;
  width: 26%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  /* Real mist, not a tint. 34% read as nothing on artwork this rich — the
   * promise "your islands light up as you build" was invisible, which was
   * the single most damaging thing about the screen. */
  background: radial-gradient(circle,
    color-mix(in oklch, var(--map-parchment) 72%, transparent) 0%,
    color-mix(in oklch, var(--map-parchment) 52%, transparent) 42%,
    color-mix(in oklch, var(--map-parchment) 18%, transparent) 62%,
    transparent 76%);
  transition: background 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.prof-map-halo.is-started {
  background: radial-gradient(circle,
    color-mix(in oklch, var(--map-sea-deep) 12%, transparent) 0%,
    transparent 65%);
}
.prof-map-halo.is-complete {
  background: radial-gradient(circle,
    color-mix(in oklch, var(--map-medal-glow) 42%, transparent) 0%,
    color-mix(in oklch, var(--map-medal-earned) 16%, transparent) 40%,
    transparent 68%);
}
/* At night the veil is heavier and the earned glow warmer — the same
 * logic the night painting itself follows. */
:root[data-theme="dark"] .prof-map-halo {
  background: radial-gradient(circle,
    color-mix(in oklch, var(--yeja-gray-1000) 78%, transparent) 0%,
    color-mix(in oklch, var(--yeja-gray-1000) 58%, transparent) 42%,
    color-mix(in oklch, var(--yeja-gray-1000) 22%, transparent) 62%,
    transparent 76%);
}
:root[data-theme="dark"] .prof-map-halo.is-started {
  background: radial-gradient(circle,
    color-mix(in oklch, var(--yeja-gray-1000) 22%, transparent) 0%,
    transparent 65%);
}
:root[data-theme="dark"] .prof-map-halo.is-complete {
  background: radial-gradient(circle,
    color-mix(in oklch, var(--map-medal-glow) 34%, transparent) 0%,
    color-mix(in oklch, var(--map-medal-earned) 14%, transparent) 42%,
    transparent 70%);
}
@media (prefers-reduced-motion: reduce) {
  .prof-map-halo { transition: none; }
}

/* THE STEP BADGE — bottom-right of each pill, the "where next?" answer.
 * lock = not yet reachable · check = done · arrow = go here next.
 * Overlaps the pill's corner so it reads as a stamp on the label rather
 * than another item in the row. Every state is also spoken in the
 * aria-label, so this is never icon-alone. */
/* .prof-map-zone is already position:absolute (it is percentage-placed on
 * the artwork), so it is already the badge's containing block. Nothing to
 * restate here — only the badge itself needs styling. */
.prof-map-zone-badge {
  position: absolute;
  inset-inline-end: -6px;
  bottom: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--map-parchment);
  box-shadow: 0 1px 4px color-mix(in oklch, var(--yeja-gray-1000) 35%, transparent);
}
.prof-map-zone-badge .yeja-icon {
  font-size: 13px;
  line-height: 1;
}
.prof-map-zone-badge.is-locked {
  background: color-mix(in oklch, var(--map-parchment-aged) 92%, transparent);
  color: var(--map-ink-soft);
}
.prof-map-zone-badge.is-done {
  background: var(--map-medal-earned);
  color: var(--map-ink);
}
/* The next step is the one thing on the map asking to be clicked, so it
 * gets the platform's action colour rather than the map palette. */
.prof-map-zone-badge.is-next {
  background: var(--yeja-cobalt-ink);
  color: var(--yeja-gray-0);
}
:root[data-theme="dark"] .prof-map-zone-badge {
  border-color: color-mix(in oklch, var(--map-parchment) 45%, transparent);
}
:root[data-theme="dark"] .prof-map-zone-badge.is-locked {
  background: color-mix(in oklch, var(--yeja-gray-1000) 78%, transparent);
  /* --map-ink, NOT --map-parchment. Inside .prof-map-card the dark theme
   * INVERTS both: --map-parchment becomes #2A2318 (a dark brown) and
   * --map-ink becomes #E8D9BE (the light one). Asking for "parchment" here
   * therefore painted a near-black glyph on this near-black disc —
   * measured at 1.06:1, an invisible lock. --map-ink is the light value in
   * this scope, and the light-mode rule above already uses --map-ink-soft
   * on its pale disc, so both themes now read glyph-on-disc the same way. */
  color: var(--map-ink);
}

/* THE CTA — the most important element on this screen, and the reason the
 * map is an interface rather than a trophy case. One action, one button,
 * above the artwork so it is read before the picture is admired. */
/* The CTA box is now a two-row STACK — the action line, then the progress
 * meter that used to occupy a full-width band of its own below the header.
 * Folding the meter in here buys back a whole row of vertical space for the
 * map, which is the only thing on this screen anyone opened it to see. */
.prof-map-cta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  border-radius: var(--radius-card, 12px);
  border: 1px solid var(--line);
  background: var(--surface);
  /* Bounded so a long action verb cannot push the close button off the
   * header, and so the box reads as a panel rather than a stretched strip. */
  min-width: 260px;
  max-width: 380px;
}
/* Row 1: label, verb, Go. */
.prof-map-cta-line {
  display: flex;
  align-items: center;
  gap: 10px;
}
.prof-map-cta-label {
  /* Smaller than the body text it sits beside: this is a caption for the
   * action, not a peer of it. */
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  white-space: nowrap;
}
.prof-map-cta-action {
  flex: 1;
  font-size: var(--text-label-size, 13px);
  font-weight: 600;
  color: var(--ink);
  min-width: 0;
}
.prof-map-cta-go { flex: 0 0 auto; }
.prof-map-cta-done {
  display: block;
  margin: 0;
  color: var(--muted);
  font-size: var(--text-label-size, 13px);
}

/* SUBTITLE AND COUNT SHARE ONE LINE — "Your studio   1/4". They were two
 * stacked rows, which made a three-line pill tall enough to cover the very
 * island it labels. The count is pushed to the right edge so the numbers
 * line up down the map instead of floating after ragged text. */
.prof-map-zone-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
/* The plain-language product name, ON the pill. Small and quiet — the
 * fantasy title leads, this explains it. */
.prof-map-zone-plain {
  font-size: 0.6875rem;
  color: var(--map-ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* AN ISLAND THIS ACCOUNT HAS NO MEDALS IN. Present and named, so the map is
 * the same archipelago for everybody, but visibly not yours to work on. The
 * scroll explains who it IS for. Dimmed rather than hidden or locked: a lock
 * implies "earn your way in", and that is not what is true here.
 *
 * Opacity alone was not enough to read as a different KIND of pill — at 0.62
 * over busy artwork it just looked like a pill on a lighter patch of sea. The
 * distinction now also drops the glass tint and the shadow, so a foreign
 * island reads as a label printed ON the map rather than a control resting
 * above it. That is the actual semantic difference. */
.prof-map-zone.is-foreign {
  opacity: 0.75;
  background: color-mix(in oklch, var(--map-parchment) 18%, transparent);
  border-style: dashed;
  border-color: color-mix(in oklch, var(--map-ink) 30%, transparent);
  box-shadow: none;
  font-style: italic;
}
:root[data-theme="dark"] .prof-map-zone.is-foreign {
  background: color-mix(in oklch, var(--yeja-gray-1000) 32%, transparent);
  border-color: color-mix(in oklch, var(--map-parchment) 26%, transparent);
}
.prof-map-zone.is-foreign .prof-map-zone-marker {
  /* Drains the marker of colour without touching the icon glyph itself. */
  filter: grayscale(1);
  background: color-mix(in oklch, var(--map-parchment) 30%, transparent);
}
.prof-map-zone.is-foreign:hover,
.prof-map-zone.is-foreign:focus-visible {
  /* Still fully readable when you go looking at it — the scroll is the whole
   * point of leaving these on the map. */
  opacity: 1;
  background: color-mix(in oklch, var(--map-parchment) 62%, transparent);
  border-style: solid;
}
:root[data-theme="dark"] .prof-map-zone.is-foreign:hover,
:root[data-theme="dark"] .prof-map-zone.is-foreign:focus-visible {
  background: color-mix(in oklch, var(--yeja-gray-1000) 62%, transparent);
}
/* The audience sentence, at the foot of a foreign island's scroll. */
.prof-map-scroll-note {
  display: block;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid color-mix(in oklch, var(--map-ink) 18%, transparent);
  font-style: italic;
  opacity: 0.85;
}
/* In dark mode the pill is a dark veil, so the plain line needs a LIGHT
 * colour and enough of it. 78% of parchment against a dark tint measured
 * as barely visible in the screenshot — the fantasy title read and its
 * translation did not, which defeats the point of having it. */
:root[data-theme="dark"] .prof-map-zone-plain {
  /* --map-ink, NOT --map-parchment. Inside .prof-map-card the whole map
   * palette FLIPS for dark: parchment becomes the dark ground (#2A2318)
   * and ink becomes the light cream (#E8D9BE). Mixing toward parchment
   * made this text darker on a dark pill — measured as oklch(0.26), which
   * is why it vanished. */
  color: color-mix(in oklch, var(--map-ink) 88%, transparent);
}
:root[data-theme="dark"] .prof-map-zone-count {
  color: color-mix(in oklch, var(--map-ink) 78%, transparent);
}

/* THE ROUTE. Drawn live rather than painted into the base, because it is
 * different for every account type and because travelled legs should look
 * different from the leg ahead.
 *
 * preserveAspectRatio="none" with a 0-100 viewBox means the SVG's
 * coordinates ARE the zone percentages — the line lands on the same points
 * as the pills by construction, with no second source of truth. */
.prof-map-route {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
/* The .prof-map-route-leg rules are GONE with the lines they styled — the
 * connecting route is no longer drawn at all (see renderRouteHTML). Each was
 * a standalone selector, so removing them cannot silently drop a live rule
 * the way a grouped selector would. .prof-map-route survives as the
 * positioned layer that carries the one remaining marker. */

/* "You are here" — the first island still unfinished. Now the ONLY thing on
 * the route layer, and the one spatial fact the dashed lines were carrying
 * that text elsewhere does not already state better. */
.prof-map-route-here {
  fill: var(--yeja-cobalt);
  stroke: var(--map-parchment);
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
}
:root[data-theme="dark"] .prof-map-route-here {
  stroke: color-mix(in oklch, var(--map-parchment) 60%, transparent);
}
/* On mobile the pills become a plain stacked list, so a route drawn over
 * absolute positions would point at nothing. */
@media (max-width: 560px) {
  .prof-map-route { display: none; }
}

/* The porthole hides on mobile HERE, not in the earlier media query.
 * .prof-map-porthole sets display:flex and is declared LATER in this file,
 * so it wins on source order at equal specificity — the earlier rule was
 * correct and simply outranked. The halo beside it obeyed, which is what
 * made the cause obvious: same media query, different outcome.
 * On mobile the stage is a plain stacked list; a face positioned against
 * artwork that is no longer there floats over the cards. */
@media (max-width: 560px) {
  .prof-map-porthole { display: none; }
}

/* CTA ON THE GREETING LINE. The header is already a flex row (title block
 * + close button); the CTA slots between them and takes the slack, so the
 * most important control sits where the eye lands instead of pushing the
 * artwork down a band.
 *
 * align-self:center rather than the header's flex-start: the CTA is now a
 * two-row box (action + meter) and is taller than the greeting block, so
 * top-aligning the two left the greeting floating against the box's first
 * line. Centring makes them read as one band. */
.prof-map-header .prof-map-cta {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0 var(--space-2, 16px);
  align-self: center;
}
.prof-map-header .prof-map-cta-action {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 900px) {
  /* Below this the greeting and the CTA stop fitting on one line, so the
   * header wraps and the CTA takes a row of its own. */
  .prof-map-header { flex-wrap: wrap; }
  .prof-map-header .prof-map-cta {
    order: 3;
    flex-basis: 100%;
    margin: var(--space-2, 16px) 0 0;
  }
}

/* ══ LAYER 3 — WEATHER AND THE SHIP ════════════════════════════════════
 * Decoration only. Nothing here is in the accessibility tree and nothing
 * here can be clicked: it sits between the painting and the route, so the
 * pills stay the topmost thing on the stage.
 *
 * All of it is CSS/SVG-declarative rather than a rAF loop, which is what
 * makes teardown free — the overlay removes these nodes and the animation
 * ends with them. No timer survives an unmount. */
.prof-map-weather {
  position: absolute;
  inset: 0;
  z-index: 1;              /* with the terrain, under halo/route/pills */
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}

/* MIST. Two very soft, very slow horizontal drifts at different speeds and
 * heights, so the sea never looks like a still photograph. Deliberately
 * weak — this is atmosphere, not a weather system; anything stronger fights
 * the painting it is laid over. */
.prof-map-mist {
  position: absolute;
  inset-inline-start: -60%;
  width: 220%;
  height: 46%;
  background: radial-gradient(ellipse 42% 58% at 30% 50%,
                color-mix(in oklch, var(--map-parchment) 26%, transparent) 0%,
                transparent 70%),
              radial-gradient(ellipse 34% 52% at 68% 45%,
                color-mix(in oklch, var(--map-parchment) 20%, transparent) 0%,
                transparent 72%);
  filter: blur(6px);
  will-change: transform;
}
.prof-map-mist-a { top: 6%;  animation: prof-map-drift 71s linear infinite; }
.prof-map-mist-b { top: 48%; animation: prof-map-drift 103s linear infinite reverse; opacity: 0.7; }
@keyframes prof-map-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(28%, 0, 0); }
}
/* At night the haze is moonlit, not sunlit. */
:root[data-theme="dark"] .prof-map-mist {
  background: radial-gradient(ellipse 42% 58% at 30% 50%,
                color-mix(in oklch, var(--map-parchment) 14%, transparent) 0%,
                transparent 70%),
              radial-gradient(ellipse 34% 52% at 68% 45%,
                color-mix(in oklch, var(--map-parchment) 10%, transparent) 0%,
                transparent 72%);
}

/* DRIFTING CLOUDS. Real cloud shapes — a cluster of overlapping soft ellipses
 * rather than one blurred blob — crossing the whole map at three different
 * heights, sizes and speeds. Slow enough (2-4 minutes a crossing) that the
 * motion is felt rather than watched; anything faster turns a painting into a
 * screensaver.
 *
 * Each starts off the left edge and ends off the right, so a cloud is never
 * seen to pop into or out of existence mid-sea. */
.prof-map-cloud {
  position: absolute;
  pointer-events: none;
  background:
    radial-gradient(ellipse 30% 46% at 22% 62%, color-mix(in oklch, var(--map-parchment) 62%, transparent) 0%, transparent 68%),
    radial-gradient(ellipse 34% 60% at 44% 44%, color-mix(in oklch, var(--map-parchment) 70%, transparent) 0%, transparent 66%),
    radial-gradient(ellipse 26% 44% at 66% 58%, color-mix(in oklch, var(--map-parchment) 58%, transparent) 0%, transparent 70%),
    radial-gradient(ellipse 20% 34% at 82% 66%, color-mix(in oklch, var(--map-parchment) 48%, transparent) 0%, transparent 72%);
  filter: blur(7px);
  will-change: transform;
}
.prof-map-cloud-a {
  top: 4%;  width: 30%; height: 20%;
  animation: prof-map-cloud-cross 155s linear infinite;
}
.prof-map-cloud-b {
  top: 21%; width: 22%; height: 14%;
  opacity: 0.7;
  animation: prof-map-cloud-cross 224s linear infinite;
  animation-delay: -70s;
}
.prof-map-cloud-c {
  top: 12%; width: 39%; height: 25%;
  opacity: 0.5;
  animation: prof-map-cloud-cross 286s linear infinite;
  animation-delay: -160s;
}
@keyframes prof-map-cloud-cross {
  from { transform: translate3d(-45%, 0, 0); }
  to   { transform: translate3d(340%, 0, 0); }
}
/* Night cloud is thinner and cooler — moonlit vapour, not sunlit cotton. */
:root[data-theme="dark"] .prof-map-cloud {
  background:
    radial-gradient(ellipse 30% 46% at 22% 62%, color-mix(in oklch, var(--map-parchment) 24%, transparent) 0%, transparent 68%),
    radial-gradient(ellipse 34% 60% at 44% 44%, color-mix(in oklch, var(--map-parchment) 30%, transparent) 0%, transparent 66%),
    radial-gradient(ellipse 26% 44% at 66% 58%, color-mix(in oklch, var(--map-parchment) 22%, transparent) 0%, transparent 70%),
    radial-gradient(ellipse 20% 34% at 82% 66%, color-mix(in oklch, var(--map-parchment) 18%, transparent) 0%, transparent 72%);
}

/* OCEAN SPARKLE. Points of specular light scattered across the sea, breathing
 * rather than strobing — a hard flicker over a painting reads as a rendering
 * fault, not as water.
 *
 * TWO layers on different sizes and rhythms, counter-drifting. One alone
 * pulses as a single sheet, which reads as the whole map changing brightness;
 * two out of phase read as separate glints catching the light. Sized in px so
 * the speckle stays fine at any stage width, and masked away from the top of
 * the frame where the painting is sky rather than sea. */
.prof-map-glitter {
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
  /* Sky has no sparkle. Fades in below the horizon and out at the border.
   * Only the ALPHA of these stops matters — a mask ignores hue entirely — but
   * the token is used regardless so no raw hex enters the sheet. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0 26%, var(--yeja-gray-1000) 40% 92%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0 26%, var(--yeja-gray-1000) 40% 92%, transparent 100%);
}
.prof-map-glitter-a {
  background-image:
    radial-gradient(circle at 18% 62%, color-mix(in oklch, var(--map-parchment) 78%, transparent) 0 1.1px, transparent 1.7px),
    radial-gradient(circle at 63% 28%, color-mix(in oklch, var(--map-parchment) 66%, transparent) 0 1px, transparent 1.6px),
    radial-gradient(circle at 84% 71%, color-mix(in oklch, var(--map-parchment) 72%, transparent) 0 1.1px, transparent 1.7px),
    radial-gradient(circle at 41% 84%, color-mix(in oklch, var(--map-parchment) 60%, transparent) 0 1px, transparent 1.6px);
  background-size: 190px 150px, 240px 195px, 165px 210px, 285px 165px;
  animation: prof-map-shimmer 7.5s ease-in-out infinite;
}
.prof-map-glitter-b {
  background-image:
    radial-gradient(circle at 34% 22%, color-mix(in oklch, var(--map-parchment) 70%, transparent) 0 0.9px, transparent 1.5px),
    radial-gradient(circle at 77% 49%, color-mix(in oklch, var(--map-parchment) 62%, transparent) 0 1px, transparent 1.6px),
    radial-gradient(circle at 12% 88%, color-mix(in oklch, var(--map-parchment) 66%, transparent) 0 0.9px, transparent 1.5px);
  background-size: 135px 120px, 205px 168px, 250px 140px;
  animation: prof-map-shimmer 11.5s ease-in-out infinite reverse;
  animation-delay: -3.5s;
}
@keyframes prof-map-shimmer {
  0%, 100% { opacity: 0.18; }
  50%      { opacity: 0.55; }
}

/* THE SHIP. Same coordinate space as the route, so it rides the drawn lane
 * exactly. The stage stretches its viewBox (preserveAspectRatio="none"), so
 * the hull is counter-scaled to stay a ship and not an ellipse. */
.prof-map-ship-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 3;              /* with the route it sails along */
  pointer-events: none;
  overflow: visible;
}
.prof-map-ship {
  /* 16/9 counter-scale: the viewBox is square but the box is not, so an
   * unscaled shape would be stretched horizontally by exactly that ratio.
   * The per-boat variants below multiply this, never replace it — dropping
   * the 0.5625 would squash that boat back into an ellipse. */
  transform: scale(0.5625, 1);
  transform-box: fill-box;
  transform-origin: center;
}
/* Three boats, three sizes, so they read as separate vessels at different
 * distances rather than one sprite stamped three times. The x-scale is the
 * counter-scale above times the size factor. */
.prof-map-ship-0 { transform: scale(0.5625, 1); }
.prof-map-ship-1 { transform: scale(0.4500, 0.80); }
.prof-map-ship-2 { transform: scale(0.6750, 1.20); }
.prof-map-ship-hull {
  fill: var(--map-ink);
  opacity: 0.82;
}
.prof-map-ship-mast {
  stroke: var(--map-ink);
  stroke-width: 0.16;
  opacity: 0.82;
}
.prof-map-ship-sail {
  fill: var(--map-parchment);
  stroke: var(--map-ink);
  stroke-width: 0.12;
  opacity: 0.95;
}
:root[data-theme="dark"] .prof-map-ship-hull,
:root[data-theme="dark"] .prof-map-ship-mast { opacity: 0.9; }

/* REDUCED MOTION: keep the atmosphere, remove the movement. The mist stays
 * as a static haze and the glitter as a fixed sparkle, so the map looks the
 * same — it simply stops moving. The ship holds station at the start of its
 * leg rather than disappearing, because it is a position indicator as much
 * as an animation. */
@media (prefers-reduced-motion: reduce) {
  .prof-map-mist,
  .prof-map-cloud,
  .prof-map-glitter { animation: none; }
  .prof-map-glitter { opacity: 0.3; }
  /* The clouds stay ON, simply parked where they start — a still cloud is
   * part of the picture; a missing one leaves a bare patch of sky. */
  .prof-map-cloud-a { transform: translate3d(40%, 0, 0); }
  .prof-map-cloud-b { transform: translate3d(180%, 0, 0); }
  .prof-map-cloud-c { transform: translate3d(300%, 0, 0); }
}
/* No room for weather in the mobile stacked list, and the stage is gone. */
@media (max-width: 560px) {
  .prof-map-weather,
  .prof-map-ship-layer { display: none; }
}

/* The boats' lane tracks live in <defs> and carry fill="none" inline, so no
 * stray closed curve can flood the map the way an unset route path could. */

/* THE SCROLL. Unfurls on hover/focus with rolled ends — the map's one bit
 * of theatre. scaleY from the top so it reads as unrolling downward, not
 * as a box fading in. */
.prof-map-scroll {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  width: max-content;
  max-width: 220px;
  transform: translateX(-50%) scaleY(0);
  transform-origin: top center;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
  transition: transform 0.28s cubic-bezier(0.34, 1.3, 0.64, 1), opacity 0.16s ease;
}
.prof-map-zone:hover .prof-map-scroll,
.prof-map-zone:focus-visible .prof-map-scroll {
  transform: translateX(-50%) scaleY(1);
  opacity: 1;
}
.prof-map-scroll-body {
  display: block;
  padding: 9px 12px;
  background:
    linear-gradient(180deg,
      color-mix(in oklch, var(--map-parchment-aged) 96%, transparent) 0%,
      color-mix(in oklch, var(--map-parchment) 98%, transparent) 22%,
      color-mix(in oklch, var(--map-parchment) 98%, transparent) 78%,
      color-mix(in oklch, var(--map-parchment-aged) 96%, transparent) 100%);
  color: var(--map-ink);
  font-size: 0.6875rem;
  line-height: 1.45;
  text-align: center;
  box-shadow: inset 0 0 12px color-mix(in oklch, var(--map-parchment-edge) 45%, transparent);
}
/* The rolled rods at each end. */
.prof-map-scroll-rod {
  display: block;
  height: 7px;
  border-radius: 4px;
  background: linear-gradient(180deg,
    var(--map-parchment-edge) 0%,
    color-mix(in oklch, var(--map-parchment) 92%, transparent) 45%,
    var(--map-route) 100%);
  box-shadow: 0 1px 3px color-mix(in oklch, var(--yeja-gray-1000) 32%, transparent);
  /* the wax seal hangs off this rod */
  position: relative;
}
/* A wax seal on the lower rod. Pure decoration, and the joke. */
.prof-map-scroll-rod:last-child::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -4px;
  width: 11px;
  height: 11px;
  margin-inline-start: -5.5px;
  border-radius: 50%;
  background: var(--yeja-crimson);
  box-shadow: 0 1px 3px color-mix(in oklch, var(--yeja-gray-1000) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .prof-map-scroll { transition: opacity 0.16s ease; transform: translateX(-50%) scaleY(1); }
  .prof-map-zone:hover .prof-map-scroll,
  .prof-map-zone:focus-visible .prof-map-scroll { transform: translateX(-50%) scaleY(1); }
}
/* No room for a hovering scroll in the mobile stacked list. */
@media (max-width: 560px) {
  .prof-map-scroll { display: none; }
  /* The mobile list has room and no hover, so pills stay open. */
  .prof-map-zone-meta { max-height: none; opacity: 1; overflow: visible; }
}

/* COLLAPSED PILLS. At rest a pill is its icon and title only — seven full
 * cards covered the painting they were meant to sit on. Hovering (or
 * focusing) expands it to reveal the plain-language line and the count.
 *
 * Height/opacity rather than display:none so the reveal can animate and
 * so the text stays in the accessibility tree at all times — the
 * aria-label already carries every fact regardless. */
/* Collapse the META ROW as one unit. The subtitle and the count now share a
 * line inside .prof-map-zone-meta, so animating them separately would fight
 * that flex row — and a max-height on a flex child collapses it to a sliver
 * rather than hiding it. */
.prof-map-zone-meta {
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-height 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease;
}
.prof-map-zone:hover .prof-map-zone-meta,
.prof-map-zone:focus-visible .prof-map-zone-meta {
  max-height: 1.4em;
  opacity: 1;
}
/* Tighter at rest, roomier once open. The expanded pill also gets a floor on
 * its width: the subtitle and the count now share a line, and at rest the
 * pill is only as wide as its title — without this the meta row opens into a
 * box too narrow for it and the subtitle ellipsises to nothing.
 *
 * The CEILING matters just as much. The Vault's subtitle ("Auctions &
 * provenance") is the longest on the map and made its pill 192px against
 * ~140px for every other one, which pushed it out over the painted border at
 * the right edge. Capping the width keeps the pills a consistent size and
 * keeps the far-east islands inside the frame. */
.prof-map-zone { padding: 6px 12px 6px 6px; }
.prof-map-zone:hover,
.prof-map-zone:focus-visible {
  padding: 8px 12px 8px 6px;
  min-width: 185px;
  max-width: 210px;
}

/* THE SCROLL WAITS. A 1s delay on the way IN so it only appears for a
 * deliberate hover, and none on the way OUT so leaving feels instant —
 * the standard tooltip asymmetry. The delay applies to the transform too,
 * or the parchment would unroll before it was allowed to be visible. */
.prof-map-scroll {
  transition:
    transform 0.34s cubic-bezier(0.34, 1.3, 0.64, 1) 1s,
    opacity 0.2s ease 1s;
}
.prof-map-zone:hover .prof-map-scroll,
.prof-map-zone:focus-visible .prof-map-scroll {
  transition-delay: 1s;
}
/* Leaving: no delay, quick fade. */
.prof-map-zone .prof-map-scroll {
  transition-delay: 1s;
}
.prof-map-zone:not(:hover):not(:focus-visible) .prof-map-scroll {
  transition:
    transform 0.14s ease 0s,
    opacity 0.12s ease 0s;
}
@media (prefers-reduced-motion: reduce) {
  .prof-map-zone-plain,
  .prof-map-zone-count { transition: none; }
}

/* EXPLICIT STACKING. Nothing here declared a z-index, so the layers stacked
 * on source order alone — and the terrain (absolutely positioned, inset:0)
 * ended up ON TOP of the pills, swallowing every pointer event. The pills
 * still worked by keyboard, which is why the bug survived until a real
 * hover was tested: elementFromPoint at a pill's exact centre returned
 * .prof-map-terrain.
 *
 * The three-layer contract now says so in CSS, not just in a comment:
 *   1 terrain (the painting)  ·  2 halo  ·  3 route  ·  4 pills + porthole
 * Interaction lives at the top, decoration below it. */
.prof-map-terrain { z-index: 1; }
.prof-map-halo    { z-index: 2; }
.prof-map-route   { z-index: 3; }
.prof-map-porthole { z-index: 4; }
.prof-map-zone    { z-index: 4; }
/* A hovered pill lifts above its neighbours so its scroll is never
 * clipped by the pill below it. */
.prof-map-zone:hover,
.prof-map-zone:focus-visible { z-index: 6; }

/* ── The celebration ─────────────────────────────────────────────────────
 * The payoff for earning a medal. Sits above the whole layer stack above
 * (terrain 1 / halo 2 / route 3 / pills 4 / hovered pill 6) because it is
 * a transient announcement over the map rather than a part of it.
 *
 * It is aria-hidden decoration and never focusable — the fact it conveys
 * also goes to the map's polite live region, so nothing here is the only
 * copy of anything.
 *
 * Colours are the --map-* pair, used as a PAIR: --map-parchment is the
 * surface and --map-ink the text, and BOTH invert together in dark. That
 * is the safe way to use these two, because the name is the MATERIAL, not
 * the lightness: asking for "parchment" in a dark rule yields #2A2318.
 * Verified in both themes rather than reasoned about. */
.prof-map-celebrate {
  /* FIXED, and neither absolute nor sticky — both were tried and both put
   * the banner off-screen, which only a screenshot caught.
   *
   * The banner is the LAST child of .prof-map-card, which is
   * `overflow-y: auto` with no `position`. So:
   *   absolute -> containing block is the VIEWPORT (no positioned
   *               ancestor), and it scrolls away with the card anyway.
   *   sticky   -> sticks within the card's CONTENT FLOW, and its flow
   *               position is below the medal shelf and the logbook. It
   *               rendered at y=984 in a 900px viewport: present in the
   *               DOM, correct in every computed style, and invisible.
   *
   * Fixed pins it to the visual viewport, which is what an announcement
   * over a modal actually wants. z-index sits above the map's own layer
   * contract (terrain 1 / halo 2 / route 3 / pills 4 / hovered pill 6)
   * AND above the dialog card itself. */
  position: fixed;
  inset-inline: 0;
  /* Below the map card's header, not across it. The header carries the
   * greeting, the Next Step panel and the close button; landing the banner
   * on top of them made the two texts collide illegibly. Measured against
   * the real header on a 900px viewport rather than guessed. */
  top: clamp(96px, 18vh, 176px);

  z-index: 60;
  display: flex;
  justify-content: center;
  /* NO `height: 0` here. It was carried over from the sticky attempt, where
   * it stopped the banner reserving a slot in the card's flow — but a flex
   * container of height 0 COLLAPSES ITS CHILD'S BOX, so the card painted no
   * background at all: the parchment fill was in the computed styles and
   * absent from the screen, and the text sat naked on top of the Next Step
   * panel underneath. `position: fixed` already takes the element out of
   * flow, so there is nothing left to reserve and nothing to zero out. */
  /* Never intercept a click meant for the map underneath — the banner is
   * click-to-dismiss, so the CARD re-enables pointer events, not this. */
  pointer-events: none;
}

.prof-map-celebrate-card {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 14px 26px;
  border-radius: var(--radius-lg, 14px);
  background: var(--map-parchment);
  color: var(--map-ink);
  border: 1px solid var(--map-parchment-edge);
  box-shadow: 0 10px 30px color-mix(in oklch, var(--yeja-gray-1000) 32%, transparent);
  text-align: center;
  max-width: min(420px, 86%);
}
.prof-map-celebrate-icon {
  font-size: 30px;
  line-height: 1;
  color: var(--map-medal-earned);
  /* The glow is the one flourish, and it is a shadow rather than a second
   * colour so it cannot fight the amber's semantic role. */
  text-shadow: 0 0 14px var(--map-medal-glow);
}
.prof-map-celebrate-title {
  margin: 0;
  font-weight: 700;
  font-size: 1.05rem;
}
.prof-map-celebrate-names {
  margin: 0;
  font-size: .9rem;
  color: var(--map-ink-soft);
}

/* Entrance. The banner is rendered empty and only gains .is-showing once
 * there is something to say, so this never animates a blank card. */
.prof-map-celebrate .prof-map-celebrate-card {
  opacity: 0;
  transform: translateY(-10px) scale(.96);
  transition: opacity .28s ease, transform .28s ease;
}
.prof-map-celebrate.is-showing .prof-map-celebrate-card {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* Reduced motion: the banner still APPEARS and is still announced — only
 * the movement goes. Hiding the news along with the animation would keep
 * real information from the people who opted out of motion. */
@media (prefers-reduced-motion: reduce) {
  .prof-map-celebrate .prof-map-celebrate-card {
    transition: none;
    transform: none;
  }
  .prof-map-celebrate.is-showing .prof-map-celebrate-card { transform: none; }
}

/* Bottom-anchored on narrow screens. The header stacks much taller at 375px,
 * and the desktop offset dropped the banner straight over the close button
 * and the Go CTA — dismissible, but covering the control you dismiss WITH.
 * Anchoring to the bottom keeps the whole header reachable at every width.
 * Caught by a mobile screenshot; the desktop geometry was correct and told
 * us nothing about this.
 *
 * Written as a TOP-LEVEL media query, not nested inside the rule above:
 * nested @media would have been the only instance of CSS nesting in this
 * ~5000-line stylesheet, and an unproven syntax is not worth introducing
 * for two declarations. */
@media (max-width: 640px) {
  .prof-map-celebrate {
    top: auto;
    bottom: clamp(16px, 5vh, 40px);
  }
}

/* ── Atelier Layout editor (2026-08-04) ───────────────────────────────
 * The artist-facing controls for the public Atelier's banner, gallery
 * width and process photographs. Lives in the Profile > Portfolio tab
 * (atelierLayoutEditorHTML / wireAtelierLayoutEditor in app.js). */
.prof-hero-mode-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}
.prof-hero-mode,
.prof-grid-size {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  font-size: 14px;
}
/* :has() gives the whole pill the selected state without a JS class —
 * the radio inside is the single source of truth. */
.prof-hero-mode:has(input:checked),
.prof-grid-size:has(input:checked) {
  background: var(--yeja-text-primary);
  color: var(--yeja-canvas);
  border-color: var(--yeja-text-primary);
}
.prof-grid-size-row { display: flex; gap: 8px; margin-bottom: 4px; }
.prof-grid-size { min-width: 52px; justify-content: center; }

.prof-hero-panel {
  padding: 12px 0 4px;
}
.prof-hero-panel[hidden] { display: none; }

.prof-hero-preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
}
.prof-hero-preset {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px;
  border: 2px solid var(--yeja-border);
  border-radius: var(--radius-media);
  background: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 12.5px;
  color: var(--ink);
}
.prof-hero-preset img {
  width: 100%;
  /* The presets are drawn 1600x400 — the same 4:1 strip the live banner
   * is, so the thumbnail previews the real proportion rather than a
   * differently-shaped crop of it. */
  aspect-ratio: 4 / 1;
  object-fit: cover;
  border-radius: var(--radius-control);
  display: block;
}
.prof-hero-preset.is-selected { border-color: var(--yeja-cobalt); }

.prof-hero-artwork-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  max-height: 260px;
  overflow-y: auto;
}
.prof-hero-artwork {
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius-media);
  background: var(--surface-alt);
  cursor: pointer;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}
.prof-hero-artwork img { width: 100%; height: 100%; object-fit: cover; display: block; }
.prof-hero-artwork.is-selected { border-color: var(--yeja-cobalt); }

.prof-hero-preview {
  width: 100%;
  max-width: 420px;
  aspect-ratio: 4 / 1;
  object-fit: cover;
  border-radius: var(--radius-media);
  border: 1px solid var(--yeja-border);
  margin-bottom: 10px;
  display: block;
}

.prof-process-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: flex-start;
}
/* Widened again 2026-08-05: the note became a rich-text editor with its
 * own toolbar, and a 168px column cannot hold nine toolbar buttons — they
 * wrapped to four rows taller than the thumbnail beside them. */
.prof-process-item { width: 300px; }
/* The remove button anchors to the THUMBNAIL now, not the whole item.
 * When the item held only an image those were the same box; with a side
 * switch and an editor below, `position:absolute; top:4px` against the
 * item would still paint at the item's top — which is the thumbnail — but
 * only by coincidence. Anchoring it to what it actually belongs to keeps
 * that true if the order ever changes. */
.prof-process-thumb-wrap { position: relative; }
.prof-process-side-row {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}
/* Size to the LABEL, not to half the row (2026-08-05). `flex: 1 1 0` made
 * each pill take an equal share of the full width, so "Left" and "Right"
 * — four and five characters — rendered as two ~470px slabs with the text
 * floating in the middle of each. They are now content-width and sit
 * together at the start, like every other pill row on the page. */
.prof-process-side-row .prof-hero-mode {
  flex: 0 0 auto;
  justify-content: center;
}
.prof-process-caption-label { display: block; margin-top: 8px; }
.prof-process-caption {
  width: 100%;
  box-sizing: border-box;   /* the 1px border must not push it past 100% */
  padding: 6px 8px;
  font-size: 13px;
  font-family: inherit;     /* a control does NOT inherit this by default */
  color: var(--yeja-text-primary);
  background: var(--yeja-canvas);
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-control, 6px);
  resize: vertical;         /* multi-line as of 2026-08-05 */
}
.prof-process-caption::placeholder { color: var(--yeja-text-muted); }
.prof-process-caption:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}
.prof-process-item img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-media);
  border: 1px solid var(--yeja-border);
  display: block;
}
.prof-process-remove {
  position: absolute;
  top: 4px;
  inset-inline-end: 4px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  /* Sits on a photograph, so per guide §1 it uses a color-mix scrim on
   * the ramp token rather than a raw rgba(). */
  background: color-mix(in oklch, var(--yeja-gray-1000) 62%, transparent);
  color: var(--yeja-gray-0);
  font-size: 12px;
  line-height: 1;
}
.prof-process-add {
  width: 120px;
  aspect-ratio: 4 / 3;
  display: grid;
  place-items: center;
  border: 1px dashed var(--yeja-text-muted);
  border-radius: var(--radius-media);
  cursor: pointer;
  color: var(--muted);
  font-size: 14px;
}
.prof-process-add:hover { border-color: var(--yeja-cobalt); color: var(--yeja-cobalt); }

/* ── Lightbox zoom bar (2026-08-04) ───────────────────────────────────
 * "Copy the art_board asset detail box features in the atelier" — the
 * same floating pill, control set and mono tabular percentage as
 * apps/art_board/styles.css's .modal-asset-bar, so the two detail views
 * behave identically. Guide §5's detail-view spec calls for exactly this.
 *
 * A flow sibling BELOW the stage, not an absolute overlay: absolutely
 * positioned it would sit on the artwork, which is the collision Art
 * Board already fixed once. */
/* [hidden] must actually hide. `display: flex` below outranks the hidden
 * attribute's UA `display: none`, so the bar stayed VISIBLE under a video
 * even though the element correctly reported hidden === true — the assertion
 * passed and only the screenshot showed the zoom controls still sitting
 * under a <video> they cannot act on. Caught live, 2026-08-05. */
.pf-zoom-bar[hidden] { display: none; }

.pf-zoom-bar {
  flex-shrink: 0;
  align-self: center;
  margin: 10px auto 0;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  background: var(--yeja-canvas);
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-pill);
  box-shadow: 0 4px 16px color-mix(in oklch, var(--yeja-gray-1000) 18%, transparent);
}
.pf-zoom-btn {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: var(--radius-pill);
  color: var(--yeja-text-primary);
}
.pf-zoom-btn:hover:not(:disabled) { background: var(--yeja-sidebar); }
.pf-zoom-btn:disabled { color: var(--yeja-text-muted); cursor: default; }
.pf-zoom-level {
  min-width: 46px;
  text-align: center;
  font-size: 11px;
  color: var(--yeja-text-muted);
  font-variant-numeric: tabular-nums;
}
/* The image itself: transform is set inline by pfApplyZoom. Transition
 * only when NOT dragging — during a drag it must track the pointer 1:1
 * (guide §4), which an easing curve would visibly lag. */
.pf-lightbox-image {
  transform-origin: center center;
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.pf-lightbox-image.is-zoomed { cursor: grab; }
.pf-lightbox-image.is-dragging { cursor: grabbing; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .pf-lightbox-image { transition: none; }
}

/* The "or choose from My Studio" disclosure in the Atelier Layout card. */
/* Artist Statement image controls, grouped (2026-08-05). The four
 * controls — upload, My Studio grid, side, size — were previously loose
 * siblings among the other Atelier Layout fields, so they did not read as
 * one toolset and the picker in particular went unnoticed. A hairline
 * box makes the grouping visible without adding a nested card. */
.prof-statement-block {
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-media);
  padding: 14px;
  margin-top: 8px;
}
/* The grid inside the block is capped shorter than the standalone one —
 * 14 artworks at 260px would push the side/size controls below the fold,
 * which is how the picker hid the controls that follow it. */
.prof-statement-block .prof-hero-artwork-grid { max-height: 168px; }
/* has-top-gap inside the block should not double the padding at the top. */
.prof-statement-block > .prof-field-hint:first-child { margin-top: 0; }

/* Every row in the block shares ONE left edge (2026-08-05). The Upload
 * pill sat inset from the thumbnails and the labels below it, because
 * .prof-bg-custom carried its own margin while the pill's padding pushed
 * its glyphs further in again — so nothing lined up with anything.
 * Zeroing the row's own offsets lets the block's 14px padding be the
 * single source of alignment. */
.prof-statement-block > .prof-bg-custom {
  margin-top: 0;
  margin-bottom: 12px;
}
.prof-statement-block > .prof-process-scale-row,
.prof-statement-block > .prof-process-side-row {
  margin-inline-start: 0;
  margin-inline-end: 0;
}
/* The slider label, track and readout align with the rows above rather
 * than centring in the leftover space. */
.prof-statement-block > .prof-process-scale-row { justify-content: flex-start; }

.prof-process-picker { margin-top: 10px; }
.prof-process-picker > summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--muted);
  padding: 4px 0;
}
.prof-process-picker > summary:hover { color: var(--ink); }
.prof-process-picker .prof-hero-artwork-grid { margin-top: 8px; }

/* Background wallpaper swatches in the Atelier Layout card. */
.prof-bg-pattern-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 8px;
}
.prof-bg-pattern {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 5px;
  border: 2px solid var(--yeja-border);
  border-radius: var(--radius-media);
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  color: var(--ink);
}
.prof-bg-pattern.is-selected { border-color: var(--yeja-cobalt); }
.prof-bg-pattern-swatch {
  display: block;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-control);
  background-color: var(--yeja-canvas);
  background-repeat: repeat;
  border: 1px solid var(--yeja-border);
}
.prof-hero-preset-anim {
  font-family: var(--font-family-mono);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* ── Floating Portfolio Style dock (2026-08-04) ───────────────────────
 * The Portfolio tab is long enough that Preview/Save scrolled off screen.
 * A sticky dock keeps them reachable from anywhere in the panel.
 *
 * position: sticky + bottom, NOT fixed: fixed would float over unrelated
 * tabs and over the shell's own chrome, and would have to be torn down on
 * tab change. Sticky is scoped to the scrolling panel by construction, so
 * it appears with the Style card and leaves with it. */
.prof-style-dock {
  /* FIXED, not sticky (2026-08-04 user instruction: "needs to float at
   * the bottom on the screen ... it's not doing that at the moment").
   *
   * Sticky only pins once its own flow position reaches the offset — with
   * the dock appended after the last card it spent most of the scroll
   * sitting in normal flow, i.e. exactly not floating. Fixed pins it to
   * the viewport for the whole time the Portfolio tab is open. Safe here
   * because mountPortfolioStyleDock() removes the element on every tab
   * switch, so it cannot leak onto other tabs — the teardown is what
   * makes fixed viable. */
  position: fixed;
  bottom: 20px;
  right: 24px;
  left: auto;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  width: fit-content;
  max-width: 100%;
  padding: 10px 12px;
  background: color-mix(in srgb, var(--yeja-canvas) 90%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-pill);
  box-shadow: 0 8px 28px color-mix(in oklch, var(--yeja-gray-1000) 16%, transparent);
}
.prof-style-dock-status {
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
  max-width: 40vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
.prof-style-dock .prof-btn-secondary,
.prof-style-dock .yeja-pill-btn { white-space: nowrap; }
@media (max-width: 560px) {
  /* `width: 100%` alone put the dock OFF-SCREEN (fixed 2026-08-30). The base
   * rule pins `right: 24px` with `left: auto`, so a 100%-wide box on a 390px
   * viewport had its right edge at 366 and its left edge at -24: a quarter of
   * the controls past the left gutter, and the whole bar sitting on top of a
   * .prof-btn-secondary underneath it (confirmed with elementFromPoint).
   * Resetting BOTH insets is what makes the width mean what it says. */
  .prof-style-dock {
    left: 12px;
    right: 12px;
    width: auto;
    justify-content: center;
  }
  .prof-style-dock-status { display: none; }
}

/* Custom wallpaper + process-image scale controls (2026-08-04). */
.prof-bg-custom {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 10px;
}
.prof-bg-custom-preview {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--radius-media);
  border: 1px solid var(--yeja-border);
  /* Tiled, not stretched — previews how it will actually repeat. */
  background-repeat: repeat;
}
.prof-bg-custom-btn { cursor: pointer; }
.prof-process-scale-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 8px 0 12px;
  font-size: 13px;
  color: var(--muted);
}
.prof-process-scale-row input[type="range"] { flex: 1; max-width: 260px; }
.prof-process-scale-row .mono { min-width: 46px; text-align: right; }

/* ── Per-section visibility switch (2026-08-05) ───────────────────────
 * Sits directly under each content card's <h2>, so "is this section on
 * my Atelier?" is answered in the same place as "what is in it?". The
 * ten switches used to live in one Portfolio Sections card ~2400px from
 * the content they governed. */
.prof-section-switch {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--yeja-border);
}
.prof-section-switch .prof-checkbox-label { margin: 0; }
/* Nested action switches (inside Get In Touch) read as children, not as
 * peers of the section's own switch. */
.prof-card-nested .prof-section-switch {
  border-bottom: none;
  padding-bottom: 0;
  margin-bottom: 6px;
}
/* The computed state for sections that auto-hide instead of toggling. */
.prof-section-switch--auto { border-bottom-color: transparent; }
.prof-section-auto {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  color: var(--muted);
}
.prof-section-auto .yeja-icon { font-size: 16px; }
.prof-section-auto.is-shown { color: var(--yeja-mint); }

/* ── Collapsible add-forms (2026-08-05) ───────────────────────────────
 * The Timeline and Collaborations ADD forms were permanently expanded —
 * ~670px and ~500px of empty inputs an artist scrolled past on every
 * visit whether or not they were adding anything. The entries themselves
 * stay visible; only the form folds.
 *
 * Deliberately NOT applied to the Skills inputs or the pin grid: those
 * ARE the card's content, and collapsing them repeats the mistake that
 * hid the statement image's My Studio picker inside a <details>. */
.prof-add-form { margin-top: 12px; }
.prof-add-summary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  list-style: none;          /* the default triangle fights the pill look */
  width: max-content;
}
.prof-add-summary::-webkit-details-marker { display: none; }
.prof-add-summary::before {
  content: "+";
  font-weight: 600;
  line-height: 1;
}
.prof-add-form[open] > .prof-add-summary::before { content: "\2212"; }  /* minus */
.prof-add-summary:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}
.prof-add-form > .prof-form { margin-top: 14px; }

/* Standalone labelled fields outside a .prof-form (2026-08-05) — the
 * Banner headline and the Artist Statement textarea, which moved out of
 * the deleted Portfolio Header form. Without this they inherit no layout
 * and render label-and-input side by side, unlike every other field. */
.prof-banner-headline,
.prof-statement-text {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 14px;
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
}
.prof-banner-headline input,
.prof-statement-text textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  font-family: inherit;
  font-size: var(--text-body-size, 14px);
  color: var(--yeja-text-primary);
  background: var(--yeja-canvas);
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-control, 6px);
}
.prof-banner-headline input:focus-visible,
.prof-statement-text textarea:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* RTL bidi isolation for user content (I18N-P6, 2026-08-23) — artist
 * statements, timeline entries and inquiry threads carry arbitrary-script
 * text. Rationale in apps/shared/components.css. */
[dir="rtl"] .pf-thread-view-title,
[dir="rtl"] .pf-story-meta,
[dir="rtl"] .pf-overflow-group-title,
[dir="rtl"] .prof-map-title,
[dir="rtl"] .prof-url-mode-name {
  unicode-bidi: plaintext;
}

/* ── Social links editor (2026-08-26) ─────────────────────────────────
 * Lives inside the Get In Touch card. Rows are a grid rather than a flex
 * row so the URL input is the only thing that grows — a flex row let the
 * <select> stretch with the platform-name length, so the inputs never
 * lined up down the column.
 * All colours are tokens; logical properties throughout so the whole
 * thing mirrors correctly under dir="rtl" for `fa`. */
.prof-social-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.prof-social-row {
  display: grid;
  grid-template-columns: auto minmax(0, 10rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
}

.prof-social-row-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 24px;
  block-size: 24px;
  color: var(--muted);
}

/* currentColor, never a brand hex — see apps/shared/platform-logos.js. */
.prof-social-row-glyph .platform-logo {
  inline-size: 20px;
  block-size: 20px;
  fill: currentColor;
}

.prof-social-row select,
.prof-social-row input[type="url"] {
  inline-size: 100%;
  min-inline-size: 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-body-size, 14px);
}

/* Repo law: a focus ring on EVERY interactive element, select included —
 * a :focus-visible list that names input but omits select has shipped as
 * a bug six times in this repo. */
.prof-social-row select:focus-visible,
.prof-social-row input[type="url"]:focus-visible,
.prof-social-remove:focus-visible,
#prof-social-add:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-social-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 32px;
  block-size: 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.prof-social-remove:hover {
  color: var(--yeja-crimson);
  border-color: var(--yeja-crimson);
}

.prof-social-remove .yeja-icon {
  font-size: 18px;
}

@media (max-width: 560px) {
  /* The select and URL stack rather than shrink to unusable widths; the
   * glyph and remove button stay on the first line with the select. */
  .prof-social-row {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }
  .prof-social-row input[type="url"] {
    grid-column: 1 / -1;
  }
}

/* ── Atelier draft chip (2026-08-26) ──────────────────────────────────
 * Sits in the floating dock beside Publish. Hidden entirely when there is
 * no draft, so an artist with nothing pending sees the dock they always
 * did rather than an empty badge.
 * Amber, not crimson: an unpublished draft is a state, not an error. */
.prof-draft-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in oklch, var(--yeja-amber) 18%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in oklch, var(--yeja-amber) 45%, transparent);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.prof-draft-chip[hidden] { display: none; }

/* The dock can now hold four controls; let it wrap rather than push the
 * publish button off-screen on a narrow window. */
@media (max-width: 720px) {
  .prof-style-dock { flex-wrap: wrap; justify-content: flex-end; }
}

/* Editor notice explaining what stages and what saves immediately —
 * curation (pins, timeline, skills) is deliberately NOT drafted, and
 * saying so is better than letting an artist assume it is. */
.prof-draft-notice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-block-start: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-card, 10px);
  background: color-mix(in oklch, var(--yeja-amber) 10%, transparent);
  border: 1px solid color-mix(in oklch, var(--yeja-amber) 30%, transparent);
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.prof-draft-notice .yeja-icon {
  font-size: 18px;
  color: var(--yeja-amber);
  flex: 0 0 auto;
}

/* ── Developer Access / MCP keys (2026-08-26) ─────────────────────────
 * Logical properties throughout so the card mirrors under dir="rtl".
 * The key PREFIX is shown, never the key itself — the server stores only
 * a sha256 hash and there is no path that can reveal the secret again. */
.prof-dev-key-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.prof-dev-key {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface);
}

.prof-dev-key.is-revoked {
  opacity: 0.55;
}

.prof-dev-key-name {
  font-weight: 600;
  color: var(--ink);
}

.prof-dev-key-prefix {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  color: var(--muted);
  /* The prefix can look like a full key at a glance; keep it visually
   * quieter than the name so nobody mistakes it for the secret. */
  padding: 2px 6px;
  border-radius: 4px;
  background: color-mix(in oklch, var(--ink) 6%, transparent);
}

.prof-dev-key-scope {
  font-size: 0.75rem;
  color: var(--muted);
  margin-inline-end: auto;
}

.prof-dev-key-revoked {
  font-size: 0.75rem;
  color: var(--yeja-crimson);
}

.prof-dev-setup {
  margin-block-start: 14px;
}

.prof-dev-setup > summary {
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--ink);
}

.prof-dev-setup > summary:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-dev-config {
  /* Wide content scrolls INSIDE its own container — the page body must
   * never scroll horizontally. */
  overflow-x: auto;
  margin-block-start: 8px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface-alt);
  color: var(--ink);
  font-size: 0.8125rem;
  line-height: 1.5;
  /* A config block is LTR even under RTL: it is code, not prose. */
  direction: ltr;
  text-align: start;
}

/* Key expiry — a LIVE countdown (2026-08-27, was a render-time number).
 * A key lives ten minutes, so a value computed once was wrong almost
 * immediately: an artist could read "Expires in 1 minute" about a key that
 * died four minutes ago, and the only symptom is a 401 from their agent
 * that looks like a bad paste. Ticks once a second, in mm:ss.
 *
 * tabular-nums so the digits do not jitter the row width every second. */
.prof-dev-key-expiry {
  font-size: 0.75rem;
  color: var(--yeja-amber-ink);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Under a minute the key is about to stop working mid-session. Colour is
 * not the only signal — the number itself is counting down in plain sight
 * — so this reinforces rather than carries the meaning. */
.prof-dev-key-expiry.is-urgent {
  color: var(--yeja-crimson);
  font-weight: 600;
}

.prof-dev-expiry-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-block-start: 8px;
}

/* The one-time key reveal (2026-08-27).
 *
 * The key used to be concatenated into a confirmDialog() message and was
 * CLIPPED by the 480px card: `yeja_atl_` + 32 base64url bytes is 52
 * characters with no break opportunity, so `overflow-wrap: normal` treated
 * it as a single unbreakable word — 446px of content in a 416px box, tail
 * lost. A secret shown exactly once has to be fully readable.
 *
 * `overflow-wrap: anywhere` rather than break-word: every character is
 * significant, so wrapping mid-token is correct here in a way it never is
 * for prose.
 *
 * It is a TEXTAREA, not an input, and that is not cosmetic: an <input> is
 * single-line by spec and ignores overflow-wrap outright — measured, 481px
 * of content in a 414px box — so the same clipping would simply have moved
 * one element along.
 *
 * LTR + text-align:start even under RTL — a key is an identifier, not
 * language, and mirroring it would reorder what the artist must paste. */
.prof-dev-key-reveal {
  width: 100%;
  box-sizing: border-box;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface-alt);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
  line-height: 1.6;
  overflow-wrap: anywhere;
  direction: ltr;
  text-align: start;
  /* The key is fixed-length, so the box never needs to be dragged bigger,
   * and a resize grabber on a read-only value is a control that does
   * nothing useful. */
  resize: none;
  display: block;
  /* Two lines EXACTLY, stated in ch and em rather than left to `rows`.
   *
   * `rows` is only a hint and loses to any stylesheet carrying a generic
   * `textarea { min-height }` — auth.css has one at 120px, which painted
   * two lines of key inside a four-line box. Sizing from the content makes
   * the box right wherever this dialog is opened, instead of right only on
   * the pages that happen not to override it.
   *
   * 52 characters at ~48ch per line wraps to 2; the height is those two
   * line-boxes plus the 12px padding above and below, plus 2px for the
   * border box and sub-pixel line-height rounding — without that last bit
   * the content measured 69px in a 67px box and the field scrolled by a
   * sliver, which is a scrollbar over a secret. */
  min-height: 0;
  height: calc((2 * 1.6em) + 26px);
}

.prof-dev-key-reveal:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* ── Per-client install block in the key dialog (2026-08-29) ──
 *
 * Sits under the key field: the key alone was never the hard part, finding
 * the right config shape for your particular agent was. */
.prof-dev-install {
  margin-block-start: 20px;
  padding-block-start: 16px;
  border-block-start: 1px solid var(--line);
}

.prof-dev-install-label {
  display: block;
  margin-block-end: 8px;
  font-weight: 600;
  color: var(--ink);
}

.prof-dev-client {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface-alt);
  color: var(--ink);
  font: inherit;
  /* The list is the artist's own agent names, which read left-to-right even
   * in an RTL locale, but the CONTROL still mirrors. */
  text-align: start;
}

.prof-dev-client:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-dev-config-live {
  width: 100%;
  box-sizing: border-box;
  margin-block-start: 10px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface-alt);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  overflow-wrap: anywhere;
  /* Config is code: it stays LTR and start-aligned even under an RTL locale,
   * because a mirrored JSON block is unreadable and unpasteable. */
  direction: ltr;
  text-align: start;
  resize: vertical;
  display: block;
}

.prof-dev-config-live:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-dev-install-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-block-start: 12px;
}

/* Preview links (2026-08-27).
 *
 * Mirrors .prof-dev-key-list rather than inventing a second row treatment —
 * they sit in the same card and are the same kind of thing: a short list of
 * short-lived credentials with an expiry and a revoke. */
.prof-dev-previews {
  margin-block-start: 14px;
}

.prof-dev-previews > summary {
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--ink);
}

/* ── Revert to Classic Yeja Themes (2026-08-28) ───────────────────────
 * Separated by a rule rather than nested in a card of its own: it is the
 * last thing in Developer Access and changes what visitors see, so it
 * needs to read as a distinct act without becoming a second card that
 * competes with the key controls above.
 * Logical properties so it mirrors under dir="rtl", same as the rest. */
.prof-dev-revert {
  margin-block-start: 18px;
  padding-block-start: 16px;
  border-block-start: 1px solid var(--line);
}

.prof-dev-revert h3 {
  margin: 0 0 6px;
  font-size: 0.9375rem;
  color: var(--ink);
}

/* Colour comes from the shared .yeja-pill-btn-danger variant, not from a
 * rule here: it already pairs --yeja-error-bg with --yeja-crimson, and
 * --yeja-error-bg is themed (it is #FDECEA light, #3B1717 dark). A local
 * `color`/`border-color` override left the base white fill in place, which
 * painted a white pill on the dark card. */
#prof-dev-revert-btn:disabled {
  opacity: 0.55;
  cursor: default;
}

.prof-dev-previews > summary:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-dev-preview-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.prof-dev-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface);
}

/* Expired and revoked rows are KEPT — unlike a dead API key, a past preview
 * is real history ("I sent someone a link on Tuesday") — so they are dimmed
 * rather than hidden. */
.prof-dev-preview.is-dead {
  opacity: 0.55;
}

.prof-dev-preview-when {
  font-weight: 600;
  color: var(--ink);
}

.prof-dev-preview-expiry {
  font-size: 0.8125rem;
  color: var(--muted);
}

.prof-dev-preview-uses {
  font-size: 0.75rem;
  color: var(--muted);
  margin-inline-end: auto;
}

.prof-dev-expiry-note .yeja-icon {
  font-size: 18px;
  color: var(--yeja-amber);
  flex: 0 0 auto;
}

/* --- Users, Messages & Inquiries: page heading + People search --------
 * (2026-08-26). Tokens only and logical properties throughout: `fa` is
 * RTL, so a physical `left`/`margin-left` here would strand the layout on
 * the wrong edge.
 *
 * No prefers-reduced-motion block is needed for the pill transition —
 * apps/shared/design-tokens.css carries a global `*` reduced-motion
 * safety net that already neutralises it. */

/* The view had no heading at all before this. Bare .pf-* headings here
 * are otherwise always inside a .yeja-card or .pf-section wrapper that
 * styles them, so this page-level one needs its own rule. */
.pf-inbox-heading {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-h2-size, 24px);
  line-height: var(--text-h2-line, 32px);
  font-weight: var(--text-h2-weight, 600);
  color: var(--ink);
}

/* `hidden` alone does NOT hide these pills: .yeja-category-pills sets
 * `display:flex`, and an author `display` rule beats the UA's built-in
 * `[hidden] { display: none }`. Verified live — the group was still
 * painted AND still tabbable with the attribute set. Scoped to this
 * class so it cannot change any other pill strip's behaviour. */
.pf-inquiry-filter-pills[hidden] {
  display: none;
}

.pf-inbox-search-hint {
  margin: 0 0 var(--space-2, 16px);
  color: var(--muted);
  font-size: var(--text-label-size, 12px);
  line-height: var(--text-label-line, 16px);
  max-inline-size: 70ch;
}

/* .yeja-category-pill ships no :focus-visible rule of its own, so the
 * existing Inbox | Archived pills have no ring. Rather than inherit that
 * gap, the new group states one. (Retrofitting the other 74 instances
 * repo-wide is its own task.) */
.pf-inbox-mode-pills .yeja-category-pill:focus-visible,
.pf-person-row:focus-visible,
.pf-person-card-actions a:focus-visible,
.pf-person-card-actions button:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.pf-people-loading {
  display: flex;
  justify-content: center;
  padding: var(--space-3, 24px) var(--space-2, 16px);
}

.pf-person-card {
  padding: var(--space-3, 24px);
}
.pf-person-card-head {
  display: flex;
  gap: var(--space-2, 16px);
  align-items: center;
  margin-block-end: var(--space-2, 16px);
}
.pf-person-card-avatar {
  inline-size: 72px;
  block-size: 72px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-alt);
  color: var(--ink);
  font-size: 28px;
  font-weight: 600;
}
.pf-person-card-avatar img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
.pf-person-card-name {
  margin: 0 0 4px;
  font-size: var(--text-h3-size, 16px);
  font-weight: var(--text-h3-weight, 600);
  color: var(--ink);
}
.pf-person-card-handle,
.pf-person-card-role {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-body-size, 14px);
}
.pf-person-card-bio {
  margin: 0 0 var(--space-2, 16px);
  color: var(--ink);
  max-inline-size: 60ch;
}
.pf-person-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1, 8px);
  align-items: center;
}

/* Bidi isolation for the user-supplied strings on these surfaces — the
 * same treatment, and the same reasoning, as the [dir="rtl"] block in
 * apps/shared/components.css.
 *
 * Confirmed live under fa: a Latin handle rendered as "mahsadejam@",
 * because '@' is a bidi-NEUTRAL character and picks up the paragraph's
 * RTL direction when it sits at the boundary of a Latin run. A name or
 * handle is arbitrary-script user content, so it must lay out by its own
 * first strong character, not the viewer's chosen direction.
 *
 * Scoped to the text-bearing elements only: the panes, pill order and
 * card layout must still genuinely mirror, which the logical properties
 * above already handle. */
[dir="rtl"] .pf-person-card-name,
[dir="rtl"] .pf-person-card-handle,
[dir="rtl"] .pf-person-card-role,
[dir="rtl"] .pf-person-card-bio,
[dir="rtl"] .pf-person-row .pf-thread-row-name,
[dir="rtl"] .pf-person-row .pf-thread-row-preview,
[dir="rtl"] .pf-compose-recipient {
  unicode-bidi: plaintext;
}

/* Read-only recipient, shown when Compose is opened from a People result:
 * those rows carry no email, so there is nothing for the user to type or
 * correct. */
.pf-compose-recipient {
  margin: 0;
  color: var(--ink);
  font-weight: 600;
}
.pf-compose-recipient-handle {
  color: var(--muted);
  font-weight: 400;
}

/* A pinned work whose image was never published (2026-08-26).
 * The artwork's file lives only in the PRIVATE studio-assets bucket, so
 * there is no public URL to render. Saying so beats painting an empty
 * rectangle the artist has to interpret — which is exactly how this was
 * first reported ("pinning artwork is showing blank"). */
.prof-pin-edit-thumb.is-unpublished {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 16px;
  background: var(--surface-alt);
  border: 1px dashed var(--line);
  color: var(--muted);
  cursor: not-allowed;
  text-align: center;
}

.prof-pin-edit-thumb.is-unpublished .yeja-icon {
  font-size: 28px;
  color: var(--yeja-amber);
}

.prof-pin-unpublished-note {
  font-size: 0.75rem;
  line-height: 1.35;
}

/* Public collection masthead + footer (2026-08-26).
 *
 * This page rendered with NO platform attribution at all — no logo, no
 * wordmark, no link home — so a visitor on a shared collection link had
 * no way to tell whose platform they were on. Deliberately NOT themeable
 * and not artist-configurable, same rule as the Atelier's masthead. */
.prof-public-brandbar {
  display: flex;
  align-items: center;
  padding: var(--space-2, 12px) 0;
  border-bottom: 1px solid var(--yeja-border);
  margin-bottom: var(--space-3, 24px);
}

.prof-public-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1, 8px);
  font-weight: 600;
  color: var(--yeja-text-primary);
  text-decoration: none;
  border-radius: var(--radius-control, 8px);
}

.prof-public-brand-mark {
  display: inline-flex;
  width: 28px;
  height: 28px;
}

.prof-public-brand-mark svg { width: 100%; height: 100%; }

.prof-public-footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2, 12px);
  margin-top: var(--space-4, 32px);
  padding-top: var(--space-3, 24px);
  border-top: 1px solid var(--yeja-border);
  font-size: var(--text-label-size, 12px);
  color: var(--yeja-text-muted);
}

.prof-public-footer-links {
  display: inline-flex;
  gap: var(--space-3, 24px);
}

.prof-public-footer a {
  color: inherit;
  border-radius: var(--radius-control, 8px);
}

/* Every interactive element gets the cobalt ring at 2px/2px offset. */
.prof-public-brand:focus-visible,
.prof-public-footer a:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* ── COLLAPSIBLE PORTFOLIO CARDS (2026-08-27) ──
 * The Portfolio tab is eleven stacked cards and roughly 6000px. Every card
 * except Atelier collapses; Atelier holds publish state and the shareable
 * link, so it is always open and has no toggle at all.
 *
 * The toggle is a real <button> promoted into the existing <h2>, so it
 * inherits the card heading's type scale rather than restating it. Only the
 * button reset and the affordances live here. */
.prof-card-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  /* `inherit` is load-bearing: the h2 already carries the card heading's
   * size, weight and colour, and restating them here would be a second
   * source of truth that drifts the next time the card style changes. */
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
}

.prof-card-toggle:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
  border-radius: 4px;
}

.prof-card-toggle-icon {
  flex: 0 0 auto;
  color: var(--muted);
  transition: transform 160ms ease;
}

/* Points down when open, right when closed. Rotating rather than swapping
 * the ligature keeps a single glyph and animates; a swap would jump. */
.prof-card-toggle[aria-expanded="false"] .prof-card-toggle-icon {
  transform: rotate(-90deg);
}

[dir="rtl"] .prof-card-toggle[aria-expanded="false"] .prof-card-toggle-icon {
  transform: rotate(90deg);
}

/* A collapsed card is just its heading. Without this the card keeps the
 * padding below a body that is no longer there, and reads as a broken box
 * rather than a closed one. */
.yeja-card.is-collapsed > h2 {
  margin-bottom: 0;
}

.prof-cards-toggle-all {
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .prof-card-toggle-icon { transition: none; }
}

/* ── FAVOURITE PREVIEW LINKS (2026-08-27) ──
 * A starred link does not expire. Up to three, enforced by a DB trigger.
 * Icon-only, so the accessible name comes from aria-label and the visible
 * affordance is the filled vs outlined star. */
.prof-preview-fav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border: 0;
  border-radius: var(--radius-pill, 24px);
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 140ms ease;
}

.prof-preview-fav:hover { color: var(--ink); }

/* Amber rather than cobalt: cobalt is this app's focus and primary-action
 * colour, and a permanently-cobalt star in a row of buttons reads as
 * "selected action" rather than "kept". */
.prof-preview-fav.is-on { color: var(--yeja-amber, #FFB300); }

.prof-preview-fav:focus-visible {
  outline: 2px solid var(--yeja-cobalt, #1E88E5);
  outline-offset: 2px;
}

.prof-preview-fav[disabled] { opacity: 0.5; cursor: default; }

/* "Does Not Expire" should not read like the expiry dates around it. */
.prof-dev-preview.is-favourite .prof-dev-preview-expiry {
  color: var(--ink);
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .prof-preview-fav { transition: none; }
}

/* ── Image Slots (2026-08-29) ────────────────────────────────────────
 * The picture spaces a templated/cloned Atelier page ships with. A slot is
 * empty until the artist picks one of their own public images for it.
 *
 * Tokens only, both themes: --line/--ink/--muted are already theme-driven
 * here, so nothing needs a dark-mode override of its own.
 * Logical properties so the list mirrors under dir="rtl", same as the rest
 * of this file. */
.prof-slot-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.prof-slot {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 10px;
}

/* Dashed while empty, solid once filled — the same visual language the
 * generated page itself uses for an unfilled slot, so the editor and the
 * page agree about what "empty" looks like. */
.prof-slot:not(.prof-slot--filled) .prof-slot-thumb {
  border-style: dashed;
}

.prof-slot-thumb {
  flex: 0 0 auto;
  inline-size: 56px;
  block-size: 56px;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}

.prof-slot-thumb img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}

.prof-slot-meta {
  flex: 1 1 auto;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.prof-slot-meta strong {
  font-size: 0.875rem;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prof-slot-meta span {
  font-size: 0.75rem;
  color: var(--muted);
}

.prof-slot-actions {
  flex: 0 0 auto;
  display: flex;
  gap: 6px;
}

/* A slot the page gave no id — it cannot be filled, and saying so is better
 * than omitting it and under-reporting how many spaces the page has. */
.prof-slot--broken {
  opacity: 0.7;
}

.prof-slots-count {
  margin: 8px 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
}

.prof-slots-warn {
  margin: 6px 0 0;
  font-size: 0.8125rem;
  color: var(--yeja-crimson);
}

@media (max-width: 560px) {
  .prof-slot {
    flex-wrap: wrap;
  }
  .prof-slot-actions {
    inline-size: 100%;
    justify-content: flex-end;
  }
}

/* ── The agent's brief (2026-08-29) ──────────────────────────────────
 * The interview an agent should conduct before building the Atelier, shown
 * to the artist it is about. Until this card existed the whole thing was
 * agent-only: nothing in apps/ read atelier_briefs, so a question the agent
 * skipped — or answered in the artist's name — was invisible.
 *
 * Tokens only, both themes. Logical properties so it mirrors under
 * dir="rtl", same as the rest of this file. */
.prof-brief-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.prof-brief-q {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
}

/* An unanswered REQUIRED question is the thing the artist is meant to act
 * on, so it carries the accent rather than being styled the same as an
 * answered one. */
.prof-brief-q:not(.prof-brief-q--answered) {
  border-inline-start: 3px solid var(--yeja-amber);
}

.prof-brief-q-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.prof-brief-q-head strong {
  font-size: 0.875rem;
  color: var(--ink);
}

.prof-brief-req {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.prof-brief-why {
  margin: 4px 0 0;
  font-size: 0.75rem;
  color: var(--muted);
}

.prof-brief-answer {
  margin: 8px 0 6px;
  font-size: 0.8125rem;
  color: var(--ink);
}

.prof-brief-unanswered {
  margin: 8px 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
  font-style: italic;
}

/* Who supplied the answer. Three states, deliberately distinct: an answer
 * the AGENT invented must not look like one the artist gave. */
.prof-brief-src {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.6875rem;
  border: 1px solid var(--line);
  color: var(--muted);
}

.prof-brief-src--artist { border-color: var(--yeja-cobalt); color: var(--yeja-cobalt); }
.prof-brief-src--agent  { border-color: var(--yeja-amber);  color: var(--yeja-amber); }

.prof-brief-count {
  margin: 8px 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
}

.prof-brief-warn {
  margin: 6px 0 0;
  font-size: 0.8125rem;
  color: var(--yeja-crimson);
}

/* ── Widgets card (2026-08-30) ───────────────────────────────────────────
 * One list for two storage models: the platform's built-in Support panel and
 * the artist's agent-built atelier_blocks rows. They differ underneath (a
 * boolean section key vs. a row with a block_id) and deliberately look
 * identical here, because the artist is asking "what panels are on my page",
 * not "which table is this in".
 *
 * Logical properties throughout so the card mirrors under dir="rtl". */
.prof-widget-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.prof-widget-row {
  display: grid;
  /* reorder | main | state | actions — main is the only track that flexes.
   * `minmax(0, 1fr)` rather than `1fr`: a bare 1fr carries an implicit
   * min-content floor, so a long widget title would push the actions off the
   * card instead of ellipsing. */
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 10px);
  background: var(--surface);
}

/* The built-in row carries its own ground so "provided by Yeja" reads at a
 * glance without a badge competing with the type label. */
.prof-widget-row.is-preset {
  background: var(--surface-alt, color-mix(in oklch, var(--ink) 3%, transparent));
}

.prof-widget-reorder {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.prof-widget-move {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 26px;
  block-size: 20px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}

.prof-widget-move .yeja-icon { font-size: 16px; }
.prof-widget-move:hover:not(:disabled) { background: var(--surface-alt, var(--line)); }
.prof-widget-move:disabled { opacity: 0.35; cursor: default; }
.prof-widget-move:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-widget-main { min-inline-size: 0; }

.prof-widget-title {
  margin: 0;
  font-weight: 500;
  color: var(--ink);
  /* Agent-written titles are arbitrary length; clip rather than reflow the
   * whole row around one long string. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prof-widget-meta {
  margin: 2px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  font-size: 0.75rem;
  color: var(--muted);
}

.prof-widget-type {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 500;
}

.prof-widget-origin {
  overflow: hidden;
  text-overflow: ellipsis;
}

.prof-widget-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* Delete reads as destructive without being a filled danger button: it sits
 * next to a neutral Hide, and two solid buttons side by side would make the
 * irreversible one no louder than the reversible one. */
.prof-widget-delete {
  /* --yeja-badge-danger-ink, NOT --yeja-crimson. Crimson (#E53935) is a
   * FILL colour: measured live as TEXT on this row it lands at 4.09:1 in
   * light mode, under the 4.5:1 AA floor. The badge-danger-ink token exists
   * for exactly this — danger text — and is theme-aware (#C62828 light,
   * #EF9A9A dark), so both modes clear AA. The BORDER may still be mixed
   * from crimson: a border is not text and carries no contrast minimum. */
  color: var(--yeja-badge-danger-ink);
  border-color: color-mix(in oklch, var(--yeja-crimson) 35%, transparent);
}

.prof-widget-delete:hover {
  background: var(--yeja-error-bg);
}

/* Below ~560px the four-column grid cannot hold: the actions wrap under the
 * title instead of squeezing the one track that carries meaning. */
@media (max-width: 560px) {
  .prof-widget-row {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }
  .prof-widget-actions {
    grid-column: 1 / -1;
    justify-content: flex-end;
  }
}

/* The built-in widget's control wears the same shape as the agent rows'
 * Hide/Show buttons. The checkbox stays a REAL checkbox in the DOM — the
 * existing delegated [data-section-toggle] handler reads it, and a screen
 * reader still announces a checked state — it is only visually replaced by
 * the label, which is what makes the row scan like its siblings. */
.prof-widget-preset-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  block-size: 36px;
  padding-inline: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill, 24px);
  background: var(--surface);
  color: var(--ink);
  font-size: 0.875rem;
  cursor: pointer;
  white-space: nowrap;
}

.prof-widget-preset-toggle:hover {
  background: var(--surface-alt, var(--line));
}

/* Clipped, not display:none — a hidden input is not focusable, which would
 * strip the row's only keyboard path to the control. */
.prof-widget-preset-toggle input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.prof-widget-preset-toggle:has(input:focus-visible) {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* Recently Removed: a safety net, not part of the page. Muted so it never
 * competes with the live widgets above it, and collapsed by default so an
 * artist who has deleted nothing never sees it. */
.prof-widget-trash {
  margin-block-start: 12px;
}

.prof-widget-trash > summary {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.prof-widget-row.is-removed {
  background: transparent;
  border-style: dashed;
}

/* The title of something deleted reads as past-tense, not as a live item. */
.prof-widget-row.is-removed .prof-widget-title {
  color: var(--muted);
}

/* "This panel's visibility lives in Widgets." A statement of fact plus a way
 * to get there — deliberately NOT a second switch, which would put us back to
 * two controls for one decision, which is what the Widgets card exists to
 * undo. */
.prof-widget-visibility-note {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-block: 8px 0;
  font-size: 0.8125rem;
  color: var(--muted);
}

.prof-widget-note-btn {
  block-size: 28px;
  padding-inline: 12px;
  font-size: 0.8125rem;
}

/* ── Card subsections (2026-08-30) ───────────────────────────────────────
 * Content and Atelier Style each hold several editors that used to be cards
 * of their own. A rule between them, not a border box: nesting a bordered
 * card inside a bordered card reads as a mistake, and the h3 already carries
 * the hierarchy. First-child has no rule — nothing to separate it from. */
.prof-style-subsection + .prof-style-subsection {
  margin-block-start: var(--space-4, 32px);
  padding-block-start: var(--space-4, 32px);
  border-block-start: 1px solid var(--line);
}

.prof-style-subsection > h3 {
  margin: 0 0 4px;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}

/* ── A widget's own editor, inside its row ───────────────────────────────
 * Full width beneath the row's grid, so the form is not squeezed into the
 * one flexible column the row header uses. */
.prof-widget-editor {
  grid-column: 1 / -1;
  margin-block-start: 10px;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--line);
}

.prof-widget-editor > summary {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.prof-widget-editor-body {
  margin-block-start: 12px;
}

/* ── Brief answers ───────────────────────────────────────────────────────
 * The artist writes here now, so the field has to look writable — a
 * read-only transcript styled as prose would not invite the correction this
 * card exists to make possible. */
.prof-brief-input {
  inline-size: 100%;
  margin-block-start: 8px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.5;
  resize: vertical;
}

.prof-brief-input:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.prof-brief-row-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-block-start: 8px;
}

/* Copy Key, directly under the key it copies (2026-08-30). */
.prof-dev-key-actions {
  display: flex;
  margin-block-start: 10px;
}

/* Close is pushed to the far end of the action row so it reads as the way
 * OUT rather than a third thing to copy. Ghost, not solid: Copy Config is
 * the action, Close is the exit. */
.prof-dev-close-btn {
  margin-inline-start: auto;
}

/* A widget that cannot render yet, and why. Amber rather than red: nothing is
 * broken and nothing was lost — the artist has one more step to take. */
.prof-widget-blocked {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 10px 0 0;
  padding: 10px 12px;
  border-radius: var(--radius-card, 10px);
  background: color-mix(in oklch, var(--yeja-amber) 10%, transparent);
  border: 1px solid color-mix(in oklch, var(--yeja-amber) 30%, transparent);
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.prof-widget-blocked .yeja-icon {
  font-size: 18px;
  color: var(--yeja-amber);
  flex: 0 0 auto;
}

.prof-widget-blocked > span:nth-of-type(2) { flex: 1 1 200px; min-inline-size: 0; }

/* A row's own hint line — an empty catalogue, or the app that owns the data. */
.prof-widget-hint {
  grid-column: 1 / -1;
  margin: 8px 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* ── Agent-widget content editor + SEO panel (2026-08-31) ──────────────── */
.prof-block-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-block-start: 12px;
}

.prof-block-field > span {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.8125rem;
  color: var(--muted);
}

.prof-block-field input,
.prof-block-field textarea {
  inline-size: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control, 8px);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  resize: vertical;
}

.prof-block-field input:focus-visible,
.prof-block-field textarea:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* A list item inside a widget — one row of a gallery, one question of an FAQ.
 * Numbered so the artist can match it against the live page. */
.prof-block-list { margin-block-start: 16px; }

.prof-block-list-item {
  display: flex;
  gap: 12px;
  padding-block: 10px;
  border-block-start: 1px solid var(--line-2, var(--line));
}

.prof-block-list-n {
  margin: 12px 0 0;
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  flex: 0 0 20px;
}

.prof-block-list-fields { flex: 1 1 auto; min-inline-size: 0; }

/* Over the length a search engine will actually show. Amber, not red: the
 * value still saves, it just gets truncated in the result page. */
.prof-seo-count {
  margin-inline-start: auto;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.prof-seo-count.is-over { color: var(--yeja-amber); font-weight: 600; }

/* Who can change this widget. Sits in the meta line beside the type, so the
 * answer is visible without opening the row — an artist should not have to
 * expand an editor to discover there are no fields in it. */
.prof-widget-managed {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.prof-widget-managed .yeja-icon { font-size: 14px; }

/* Cobalt for agent-only: it is the platform's "this is an AI surface" colour,
 * already used by Developer Access, and this badge is a pointer there. */
.prof-widget-managed.is-agent {
  background: color-mix(in oklch, var(--yeja-cobalt) 12%, transparent);
  color: var(--yeja-cobalt-ink, var(--yeja-cobalt));
}

/* Neutral for another-app: nothing is blocked, the content just lives
 * elsewhere. A second accent here would imply a second kind of warning. */
.prof-widget-managed.is-app {
  background: var(--surface-alt, color-mix(in oklch, var(--ink) 6%, transparent));
  color: var(--muted);
}

/* ── How The Atelier Works (2026-08-31) ──
 *
 * A collapsed explainer above every other card. Reuses .pf-help-item /
 * .pf-help-question from the Help tab for the Q&A rows — that pattern already
 * carries the border, the cobalt focus ring and the ::after chevron, and a
 * second disclosure style on one page would be the copy-paste drift this repo
 * keeps paying for.
 *
 * Everything here is token-driven. The structure diagrams in particular use
 * color-mix over --ink rather than literal greys, so they hold in both themes:
 * a hardcoded #e5e5e5 band would vanish on the dark canvas. */

.prof-howto-card { padding: 0; overflow: hidden; }

.prof-howto-summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  padding: var(--space-3, 24px);
}
.prof-howto-summary::-webkit-details-marker { display: none; }
.prof-howto-summary::after {
  content: "+";
  flex: none;
  color: var(--muted);
  font-size: 20px;
  line-height: 1;
}
.prof-howto[open] > .prof-howto-summary::after { content: "–"; }
.prof-howto-summary:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
  border-radius: var(--radius-control, 8px);
}
.prof-howto-summary:hover { background: var(--surface-alt); }

.prof-howto-summary-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.prof-howto-title { font-size: var(--text-h2-size, 20px); font-weight: 600; color: var(--ink); }
.prof-howto-sub { font-size: var(--text-body-sm-size, 14px); color: var(--muted); }

.prof-howto-body {
  padding: 0 var(--space-3, 24px) var(--space-3, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-3, 24px);
}

/* The three rules, before the Q&A. */
.prof-howto-rules {
  list-style: none;
  margin: 0;
  padding: var(--space-2, 16px);
  display: flex;
  flex-direction: column;
  gap: var(--space-2, 16px);
  background: var(--surface-alt);
  border-radius: var(--radius-card, 12px);
}
.prof-howto-rules li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2, 16px);
  font-size: var(--text-body-sm-size, 14px);
  color: var(--ink);
  line-height: 1.5;
}
.prof-howto-rules .yeja-icon { flex: none; color: var(--yeja-cobalt); font-size: 20px; }

/* ── What kinds of site replicate well ── */
.prof-howto-examples h3 {
  margin: 0 0 var(--space-1, 8px);
  font-size: var(--text-h3-size, 16px);
  font-weight: 600;
  color: var(--ink);
}

.prof-howto-kinds {
  list-style: none;
  margin: var(--space-2, 16px) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.prof-howto-kind {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2, 16px);
  padding: var(--space-2, 16px) 0;
  border-bottom: 1px solid var(--line);
}
.prof-howto-kind:last-child { border-bottom: 0; }
/* Fixed column so the three verdicts line up as a scannable rail rather
 * than ragging with the label text. 108px, not 84: at 84 "Works Well" wrapped
 * onto two lines inside the pill, which read as a rendering fault. */
.prof-howto-kind-verdict {
  flex: none;
  width: 108px;
  text-align: center;
  white-space: nowrap;
  align-self: flex-start;
}
.prof-howto-kind-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.prof-howto-kind-name { font-weight: 600; font-size: var(--text-body-sm-size, 14px); color: var(--ink); }
.prof-howto-kind-why { font-size: var(--text-body-sm-size, 14px); color: var(--muted); line-height: 1.5; }

/* ── The worked example ── */
.prof-howto-case {
  margin-top: var(--space-3, 24px);
  padding-top: var(--space-3, 24px);
  border-top: 1px solid var(--line);
}
.prof-howto-case h4 {
  margin: 0 0 var(--space-1, 8px);
  font-size: var(--text-h3-size, 16px);
  font-weight: 600;
  color: var(--ink);
}

.prof-howto-io {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2, 16px);
  margin: var(--space-2, 16px) 0;
}
/* min-width:0 because a 1fr track cannot shrink below its content, and the
 * diagram's labels would otherwise push the pair wider than the card. */
.prof-howto-io-panel { margin: 0; min-width: 0; }
.prof-howto-io-panel figcaption {
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: var(--space-1, 8px);
}

/* Structure diagrams. Bands are tinted from --ink so both themes work from
 * one definition; the output side is tinted cobalt to read as "yours". */
.prof-howto-diagram {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--space-1, 8px);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
}
.prof-howto-diagram .pd-band {
  border-radius: var(--radius-control, 8px);
  background: color-mix(in oklch, var(--ink) 8%, transparent);
  padding: var(--space-1, 8px);
  font-size: 11px;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.prof-howto-diagram.is-output .pd-band {
  background: color-mix(in oklch, var(--yeja-cobalt) 10%, transparent);
}
.prof-howto-diagram .pd-nav { min-height: 22px; }
.prof-howto-diagram .pd-hero { min-height: 54px; justify-content: center; }
.prof-howto-diagram .pd-cta { min-height: 30px; }

.prof-howto-diagram .pd-cells {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
}
/* An empty slot is an OUTLINE, a filled one is solid — the distinction the
 * caption depends on, and the whole point of the output panel.
 *
 * A FIXED HEIGHT, not aspect-ratio. 4:3 in a half-card column renders ~180px
 * tall and six of them made a supporting diagram the tallest thing on the
 * page. Capping max-height with aspect-ratio still set was worse: the ratio
 * then drove the WIDTH down to match the cap, leaving little squares pinned
 * to each end of a full-width track. The cell only has to read as an image
 * position, so it fills its column at a fixed height. */
.prof-howto-diagram .pd-cells i {
  display: block;
  height: 34px;
  border-radius: 4px;
  border: 1px dashed color-mix(in oklch, var(--ink) 30%, transparent);
}
.prof-howto-diagram.is-output .pd-cells i.is-filled {
  border: 0;
  background: color-mix(in oklch, var(--yeja-cobalt) 45%, transparent);
}

.prof-howto-io-note {
  margin: var(--space-1, 8px) 0 0;
  font-size: var(--text-body-sm-size, 14px);
  color: var(--muted);
  line-height: 1.5;
}
.prof-howto-io-caption {
  margin: 0 0 var(--space-2, 16px);
  font-size: var(--text-label-size, 12px);
  color: var(--muted);
  line-height: 1.5;
  font-style: italic;
}

.prof-howto-cta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2, 16px);
  padding-top: var(--space-3, 24px);
  border-top: 1px solid var(--line);
}
.prof-howto-cta .prof-field-hint { margin: 0; flex: 1 1 240px; }

@media (max-width: 700px) {
  /* One column: two structure diagrams side by side at 390px are two columns
   * of unreadable 11px labels. */
  .prof-howto-io { grid-template-columns: 1fr; }
  .prof-howto-kind { flex-direction: column; gap: var(--space-1, 8px); }
  .prof-howto-kind-verdict { width: auto; align-self: flex-start; }
  .prof-howto-summary { padding: var(--space-2, 16px); }
  .prof-howto-body { padding: 0 var(--space-2, 16px) var(--space-2, 16px); }
}

/* ── Download / Upload Theme (2026-08-31) ──
 * Tokens only, both themes. The panel chrome comes from the shared
 * .yeja-overlay-backdrop / .yeja-dialog-card pair every other modal here
 * uses; these are only the bits specific to the theme dialogs.
 *
 * The first cut invented .prof-modal and .prof-field, neither of which
 * exists. An unstyled <label> is INLINE, so "Theme Name" and "Short
 * Description" laid out side by side with their inputs wrapping between
 * them — caught by looking at the screenshot, not by any assertion. */
.prof-theme-modal { max-width: 520px; }

/* A stacked label+control. Explicitly block-level, which is the whole fix. */
.prof-theme-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 8px);
  margin-top: var(--space-2, 16px);
}
.prof-theme-field > span {
  font-size: var(--text-body-sm-size, 14px);
  font-weight: 600;
  color: var(--ink);
}
.prof-theme-field input,
.prof-theme-field select {
  width: 100%;
  min-width: 0;
}
/* ...except inside the price row, where the two share one line. The blanket
 * width:100% above beat the flex sizing and gave the currency select the
 * whole row, squeezing the amount input to a sliver at the right edge. */
.prof-theme-amount select { width: auto; }

.prof-theme-list {
  border: 1px solid var(--line);
  border-radius: var(--radius-card, 12px);
  padding: var(--space-2, 16px);
  margin: var(--space-2, 16px) 0;
}
.prof-theme-list legend {
  padding: 0 var(--space-1, 8px);
  font-size: var(--text-label-size, 12px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.prof-theme-list > * + * { margin-top: var(--space-2, 16px); }

.prof-theme-price-modes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 16px);
}

.prof-theme-amount {
  display: flex;
  gap: var(--space-1, 8px);
}
/* min-width:0 so the number input can shrink inside the flex row rather than
 * forcing the dialog wider than its max-width. */
.prof-theme-amount select { flex: 0 0 auto; }
.prof-theme-amount input { flex: 1 1 auto; min-width: 0; }

.prof-theme-owned-list {
  list-style: none;
  margin: var(--space-1, 8px) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.prof-theme-owned-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2, 16px);
  padding: var(--space-1, 8px) 0;
  border-bottom: 1px solid var(--line);
}
.prof-theme-owned-list li:last-child { border-bottom: 0; }

/* Two actions per row now (Install, Remove) plus a status chip, so the name
 * column has to be allowed to shrink or a long theme title pushes the buttons
 * out of the dialog. */
.prof-theme-owned-name {
  display: flex;
  align-items: center;
  gap: var(--space-1, 8px);
  min-width: 0;
  flex: 1 1 auto;
}
.prof-theme-owned-actions {
  display: flex;
  gap: var(--space-1, 8px);
  flex: 0 0 auto;
}
/* Destructive, but not the loudest thing in the dialog: this sits in a list
 * the artist is browsing, and a row of red buttons reads as a warning about
 * the list itself. The confirm carries the weight. */
/* --yeja-crimson, NOT --danger: that token is undefined repo-wide, and an
 * undefined var falls back to nothing, so the button would have rendered in
 * ordinary ink while looking correct in the source. */
.prof-theme-remove { color: var(--yeja-crimson); }
.prof-theme-remove:hover { border-color: var(--yeja-crimson); }

@media (max-width: 560px) {
  .prof-theme-owned-list li { flex-direction: column; align-items: flex-start; }
}
