/* ============================================================================
   Hub-ONLY components, layered over the SHARED tokens.css.
   ----------------------------------------------------------------------------
   The shared design system (palette, type, topbar, buttons, cards, forms,
   responsive rules) lives in cockpit/apps/shared/tokens.css and is served to
   this app straight from there (config.assets.paths) — ONE file for Hub and
   Cockpit, which is what keeps the two headers identical. This file may only
   hold what exists nowhere in the Cockpit: the nameplate fleet, the Brain
   detail ledger, onboarding, API docs, toasts. A selector that exists in the
   shared file must not reappear here — later-loaded rules would win and the
   drift this file replaced (552 forked lines, two different headers, no phone
   spacing) would quietly return. Guarded by spec/config/shared_tokens_spec.rb.
   ============================================================================ */

/* ---- Hub shell (the one layout thing the Cockpit does not have) ----
   `body.pf { margin: 0 }` moved to the shared tokens.css — the Cockpit needed
   it too (its topbar was floating on an 8px cream margin). */
body.pf-hub { display: flex; flex-direction: column; min-height: 100dvh; }
body.pf-hub .pf-content { flex: 1 0 auto; width: 100%; }

/* ---- shared-header parity: the Sign-out door is a button_to here ---- */
.pf-baract-form { margin: 0; display: contents; }

/* ============================================================================
   SIGNATURE: brushed-metal "nameplate" VM tile (.plate/.rivet/.engr/.lamp/.etch)
   Ported verbatim from F2-reference.html. Only ever used inside tokens.css pages
   (no Tailwind), so the terse class names cannot collide.
   ============================================================================ */
/* auto-fit (not auto-fill) so a lone Brain fills the content column and lines up
   with the full-width hosting card above it — no tiny card marooned on the left.
   min(100%, …) lets the track shrink on mobile instead of overflowing. */
.pf-vm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); gap: 22px; align-items: start; }

.plate { position: relative; border-radius: 13px; padding: 22px 22px 20px;
  background: linear-gradient(158deg,#E4E6E9,#F4F5F7 34%,#D5D8DD 63%,#EAECEF);
  border: 1px solid #C6CAD0;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), inset 0 -2px 3px rgba(0,0,0,.05), 0 14px 30px -20px rgba(28,30,33,.4); }
.plate::after { content: ""; position: absolute; inset: 0; border-radius: 13px; pointer-events: none;
  background: repeating-linear-gradient(94deg,rgba(255,255,255,0) 0 2px,rgba(0,0,0,.02) 2px 3px); }
.rivet { position: absolute; width: 7px; height: 7px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,#fff,#aeb4bc 55%,#7f858e); box-shadow: 0 1px 1px rgba(0,0,0,.3); }
.rivet.tl { top: 9px; left: 9px; } .rivet.tr { top: 9px; right: 9px; }
.rivet.bl { bottom: 9px; left: 9px; } .rivet.br { bottom: 9px; right: 9px; }
.pl-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; position: relative; z-index: 1; padding: 0 6px; }
.engr { font-family: var(--pf-disp); font-weight: 700; font-size: 20px; letter-spacing: .01em; color: #232830;
  text-shadow: 0 1px 0 rgba(255,255,255,.9), 0 -1px 0 rgba(0,0,0,.1); }
.engr-link { text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.engr-link:hover, .engr-link:focus-visible { color: var(--pf-navy); text-decoration: underline; text-underline-offset: 3px; }

/* The serial stamped under the nameplate: our id + Hetzner's, always visible. */
/* The topic: the line that tells six identically named Brainies apart. Sits
   directly under the name because it IS the name you think in. 16px on the
   input — anything smaller and iOS Safari zooms the page on focus. */
.pl-topic { position: relative; z-index: 1; display: flex; gap: 8px; align-items: center; margin-top: 10px; padding: 0 6px; }
.pl-topic-input { flex: 1; min-width: 0; font-family: var(--pf-sans); font-size: 16px; color: var(--pf-ink);
                  background: var(--pf-surface); border: 1px solid var(--pf-line-2); border-radius: 8px;
                  padding: 8px 10px; }
.pl-topic-input::placeholder { color: var(--pf-faint); }
.pl-topic-input:focus { outline: 2px solid var(--pf-navy); outline-offset: 1px; }
.pl-topic-save { flex: none; font-family: var(--pf-sans); font-size: 13px; font-weight: 600; color: var(--pf-navy);
                 background: var(--pf-surface-2); border: 1px solid var(--pf-line-2); border-radius: 8px;
                 padding: 8px 12px; cursor: pointer; }
.pl-topic-save:hover { background: var(--pf-line); }

.pl-ids { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 8px; padding: 0 6px; }
.pl-id { font-family: var(--pf-mono); font-size: 10.5px; letter-spacing: -.01em; color: #6b717a;
  text-transform: uppercase; white-space: nowrap; }
.pl-id b { color: #3c424b; font-weight: 600; text-transform: none; user-select: all; }

/* The Claude Code account this Brain runs as. One line, directly under the
   serial stamps, because it identifies the plate the same way they do.
   `min-width: 0` + the ellipsis on the address: a long mail address must
   shorten, never widen the plate — a card that grows a horizontal scrollbar at
   390px is worse than a truncated address you can still hover for the rest. */
.pl-account { position: relative; z-index: 1; display: flex; align-items: center; gap: 6px;
  margin-top: 6px; padding: 0 6px; min-width: 0;
  font-family: var(--pf-mono); font-size: 11px; color: #6b717a; }
.pl-account svg { width: 12px; height: 12px; flex: none; }
.pl-account-label { flex: none; text-transform: uppercase; letter-spacing: -.01em; }
.pl-account-mail { min-width: 0; color: var(--pf-navy); font-weight: 600; user-select: all;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On a phone the label is what pushes the address into the ellipsis — and the
   half it eats is the DOMAIN, which is exactly the half that tells two accounts
   apart. The silhouette already says "account", so the words go and the address
   gets the width. Measured at 390px: with the label, …@gmail.…; without it, the
   whole address fits. */
@media (max-width: 760px) {
  .pl-account-label { display: none; }
}

.lamp { display: inline-flex; align-items: center; gap: 7px; font-family: var(--pf-sans); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: #4a5058; white-space: nowrap; }
.lamp .b { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.lamp.live .b { background: var(--pf-green); box-shadow: 0 0 0 3px var(--pf-mint), 0 0 6px rgba(39,79,66,.5); }
.lamp.cast .b { background: var(--pf-warn); box-shadow: 0 0 0 2px rgba(180,115,42,.22), 0 0 7px var(--pf-warn); }
.lamp.dead .b { background: var(--pf-bad); box-shadow: 0 0 0 2px rgba(178,58,58,.2); }
.etch { position: relative; z-index: 1; margin-top: 14px; padding: 12px 14px; border-radius: 8px;
  background: rgba(18,47,77,.06); box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; }
.etch div { font-family: var(--pf-mono); font-size: 11.5px; color: #3c424b; overflow: hidden; text-overflow: ellipsis; }
.etch b { color: var(--pf-navy); font-weight: 600; }

/* CEO health verdict — the plain-language green/orange reading that leads the
   active plate. Technical signals + controls hide under .pl-tech below it. */
.pl-health { position: relative; z-index: 1; margin-top: 16px; display: flex; align-items: flex-start; gap: 11px;
  padding: 14px 16px; border-radius: 9px; border: 1px solid transparent;
  background: rgba(18,47,77,.05); box-shadow: inset 0 1px 2px rgba(0,0,0,.06); }
.pl-health-dot { flex: none; width: 11px; height: 11px; margin-top: 3px; border-radius: 50%;
  background: var(--pf-faint); }
.pl-health-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pl-health-copy strong { font-family: var(--pf-sans); font-size: 13.5px; font-weight: 650; color: var(--pf-navy); }
.pl-health-detail { font-family: var(--pf-sans); font-size: 12px; color: #5a616b; line-height: 1.4; }
.pl-health.is-green  { background: color-mix(in srgb, var(--pf-ok) 9%, #fff); border-color: color-mix(in srgb, var(--pf-ok) 26%, transparent); }
.pl-health.is-green  .pl-health-dot { background: var(--pf-ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-ok) 20%, transparent); }
.pl-health.is-orange { background: color-mix(in srgb, var(--pf-warn) 11%, #fff); border-color: color-mix(in srgb, var(--pf-warn) 32%, transparent); }
.pl-health.is-orange .pl-health-dot { background: var(--pf-warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-warn) 22%, transparent); }
.pl-health.is-orange .pl-health-copy strong { color: var(--pf-warn); }
.pl-health.is-red    { background: color-mix(in srgb, var(--pf-bad) 10%, #fff); border-color: color-mix(in srgb, var(--pf-bad) 30%, transparent); }
.pl-health.is-red    .pl-health-dot { background: var(--pf-bad); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-bad) 20%, transparent); }
.pl-health.is-red    .pl-health-copy strong { color: var(--pf-bad); }
.pl-health.is-setup  .pl-health-dot { background: var(--pf-faint); }

/* Machine usage: CPU / memory / disk, three gauges on the face of the plate.

   A fixed three-column grid, NOT auto-fit: the point of these numbers is that
   the owner compares them down a page of plates, and columns that reflow per
   card would put "Disk" in a different place on every one of them. At 3 x ~90px
   plus gaps the row still fits a 320px phone, which is the narrowest plate we
   support.

   The bar is the instrument; the percentage is the confirmation. Both are
   present because a bar alone cannot be quoted and a number alone cannot be
   scanned. */
.pl-gauges { position: relative; z-index: 1; margin-top: 10px; padding: 11px 14px 12px; border-radius: 8px;
  background: rgba(18,47,77,.06); box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 12px; }
.pl-gauge { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 0 6px; min-width: 0; }
.pl-gauge-label { font-family: var(--pf-sans); font-size: 10px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: #6b717a; }
.pl-gauge-value { font-family: var(--pf-mono); font-size: 13px; font-weight: 650; color: var(--pf-navy);
  justify-self: end; font-variant-numeric: tabular-nums; }
/* Full row under the pair above it. 5px tall: an instrument, not a decoration —
   thick enough to read at arm's length, thin enough that three of them do not
   turn the plate into a chart. */
.pl-gauge-bar { grid-column: 1 / -1; margin-top: 5px; height: 5px; border-radius: 3px;
  background: rgba(18,47,77,.13); overflow: hidden; }
.pl-gauge-bar > span { display: block; height: 100%; border-radius: 3px; background: var(--pf-navy);
  /* min-width: a 0.4% disk is still a real reading, and a bar of zero pixels
     would render it as "unmeasured" — the one thing this file must never do. */
  min-width: 2px; transition: width .3s ease; }
.pl-gauge-detail { grid-column: 1 / -1; margin-top: 4px; font-family: var(--pf-mono); font-size: 10.5px;
  color: #6b717a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Amber and red are earned, and only disk can earn red. See BrainResources for
   why memory stops at amber and CPU is never coloured at all. */
.pl-gauge.is-warn .pl-gauge-value { color: var(--pf-warn); }
.pl-gauge.is-warn .pl-gauge-bar > span { background: var(--pf-warn); }
.pl-gauge.is-critical .pl-gauge-value { color: var(--pf-bad); }
.pl-gauge.is-critical .pl-gauge-bar > span { background: var(--pf-bad); }
/* At 390px the three "63.2 GB of 76.1 GB" details cannot all fit, and three
   ellipsised strings are noise. The percentage and the bar carry the glance;
   the absolute figures come back the moment there is room for them. */
@media (max-width: 560px) {
  .pl-gauges { gap: 9px 9px; padding: 10px 12px 11px; }
  .pl-gauge-detail { display: none; }
}

/* The web server's IP — the one thing a CEO needs, to forward a domain at. */
.ws-addr { position: relative; z-index: 1; margin-top: 12px; padding: 12px 14px; border-radius: 10px;
  background: linear-gradient(180deg, #fff, #f4f6f8); border: 1px solid var(--pf-line);
  display: flex; flex-direction: column; gap: 2px; }
.ws-addr-label { font-family: var(--pf-sans); font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--pf-faint); }
.ws-addr-ip { font-family: var(--pf-mono); font-size: 20px; font-weight: 600; color: var(--pf-navy);
  letter-spacing: .01em; user-select: all; }
.ws-addr-hint { font-family: var(--pf-sans); font-size: 11.5px; color: var(--pf-faint); }

/* Cockpit "your custom URL" banner — the customer's live hosting address. */
.pf-url-card { padding: 18px 20px; border-radius: var(--pf-radius); border: 1px solid var(--pf-line);
  background: linear-gradient(180deg, #fff, #f4f6f8); }
.pf-url-label { margin: 4px 0 2px; font-size: 15px; color: var(--pf-ink); }
.pf-url-value { display: inline-block; font-family: var(--pf-mono); font-size: 24px; font-weight: 700;
  color: var(--pf-navy); letter-spacing: .01em; text-decoration: none; word-break: break-all; }
.pf-url-value:hover { text-decoration: underline; }

.pl-tech { position: relative; z-index: 1; margin-top: 16px; }
.pl-tech > summary { list-style: none; cursor: pointer; user-select: none;
  font-family: var(--pf-sans); font-size: 11.5px; font-weight: 500; color: #6b7079;
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 2px; }
.pl-tech > summary::-webkit-details-marker { display: none; }
.pl-tech > summary::before { content: "▸"; font-size: 10px; transition: transform .15s ease; }
.pl-tech[open] > summary::before { transform: rotate(90deg); }
.pl-tech > summary:hover { color: var(--pf-navy); }
.pl-more { position: relative; z-index: 1; margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(18,47,77,.10); }
.pl-more-link { font-family: var(--pf-sans); font-size: 12px; font-weight: 600; color: var(--pf-navy);
  text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
.pl-more-link:hover { text-decoration: underline; text-underline-offset: 3px; }

.pl-acts { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.plate .pf-btn, .plate form { margin: 0; }
/* On a tile plate the component runs small — same recipe, tile-scale.
   SIZE only: this rule outranks every variant (two classes beat one), so a
   `color` here repaints them all — it turned the navy-filled primary into
   navy-on-navy, an unreadable blank pill (owner report, 2026-08-03). Each
   variant keeps its own text color from tokens.css. */
.plate .pf-btn { min-height: 0; font-size: 12px; font-weight: 500;
  border-radius: 8px; padding: 7px 12px; }

/* ---- per-Brain detail page: the plate, unfolded ---------------------------
   A key/value ledger in the plate's own etched vocabulary. `auto-fit` collapses
   to one column on a phone with no media query. Hairlines come from each cell's
   1px shadow, not the container's background — a part-filled last row must end
   in a clean edge, not a grey block. */
.pf-kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1px;
  background: var(--pf-surface); border: 1px solid var(--pf-line); border-radius: var(--pf-radius); overflow: hidden; }
.pf-kv > div { background: var(--pf-surface); padding: 13px 15px; min-width: 0;
  box-shadow: 0 0 0 1px var(--pf-line); }
.pf-kv dt { font-family: var(--pf-sans); font-size: 11px; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--pf-faint); margin: 0 0 5px; }
.pf-kv dd { margin: 0; font-size: 14px; color: var(--pf-ink); overflow-wrap: anywhere; }
.pf-kv dd.pf-mono { font-size: 12.5px; user-select: all; }
.pf-kv dd.is-empty { color: var(--pf-faint); }

/* The detail page's traffic light — the same verdict the plate leads with,
   sized up. Colours come from .pl-health.is-* above; only the scale changes. */
.pf-verdict { display: flex; align-items: flex-start; gap: 13px; padding: 16px 18px;
  border-radius: var(--pf-radius); border: 1px solid transparent; }
.pf-verdict .pl-health-copy strong { font-size: 16px; }
.pf-verdict .pl-health-detail { font-size: 13px; }
.pf-verdict .pl-health-dot { width: 13px; height: 13px; margin-top: 4px; }

/* ---- "forge" primary action (navy pill w/ anvil glyph) ---- */

/* ---- secret reveal (SSH private key) ---- */
.pf-secret { filter: blur(5px); transition: filter .15s ease; user-select: none; }
.pf-secret.is-revealed { filter: none; user-select: text; }
.pf-key-actions { display: flex; align-items: center; gap: 8px; margin-top: 6px; }

/* ---- onboarding gate steps ---- */
.pf-steps { display: flex; flex-direction: column; gap: 10px; }
.pf-step { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--pf-line); border-radius: var(--pf-radius-sm); background: var(--pf-paper); }
.pf-step .pf-step-num { width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; background: var(--pf-line); color: var(--pf-muted); flex: none; }
.pf-step.is-done .pf-step-num { background: var(--pf-mint); color: var(--pf-green); }
.pf-step .pf-step-body { flex: 1; min-width: 0; }
.pf-empty { color: var(--pf-muted); border: 1px dashed var(--pf-line-2); border-radius: var(--pf-radius); padding: 22px; text-align: center; }

/* ---- base-image build progress rail ---- */
.pf-phases { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.pf-phase { position: relative; display: inline-flex; align-items: center; gap: 7px; padding: 4px 11px 4px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 600; font-family: var(--pf-mono); border: 1px solid var(--pf-line); color: var(--pf-muted); background: var(--pf-paper); }
.pf-phase::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.pf-phase.is-done   { color: var(--pf-ok);   background: color-mix(in srgb, var(--pf-ok) 10%, transparent); border-color: color-mix(in srgb, var(--pf-ok) 30%, transparent); }
.pf-phase.is-active { color: var(--pf-warn); background: color-mix(in srgb, var(--pf-warn) 12%, transparent); border-color: color-mix(in srgb, var(--pf-warn) 35%, transparent); }
.pf-phase.is-active::before { animation: pf-phase-pulse 1.1s ease-in-out infinite; }
@keyframes pf-phase-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .pf-phase.is-active::before { animation: none; } }

/* ============================================================================
   Craftsman layer — tactile refinements for the nameplate fleet.
   Casting lifecycle (molten plate → cooled), breathing lamps, glint sweeps,
   in-flight button states, plaque onboarding, token-styled toasts.
   ============================================================================ */

/* -- lamps: breathing when live, pulsing while casting ---------------------- */
.lamp.live .b { animation: pf-lamp-breathe 3.2s ease-in-out infinite; }
@keyframes pf-lamp-breathe {
  0%, 100% { box-shadow: 0 0 0 3px var(--pf-mint), 0 0 6px rgba(39,79,66,.5); }
  50%      { box-shadow: 0 0 0 4px var(--pf-mint), 0 0 12px rgba(39,79,66,.75); }
}
.lamp.cast .b { animation: pf-lamp-cast 1.4s ease-in-out infinite; }
@keyframes pf-lamp-cast {
  0%, 100% { box-shadow: 0 0 0 2px rgba(180,115,42,.22), 0 0 7px var(--pf-warn); }
  50%      { box-shadow: 0 0 0 3px rgba(180,115,42,.34), 0 0 12px var(--pf-warn); }
}

/* -- online plate: slow glint sweep across the brushed metal ---------------- */
.glint { position: absolute; inset: 0; border-radius: 13px; overflow: hidden; pointer-events: none; z-index: 1; }
.glint::after { content: ""; position: absolute; top: -40%; bottom: -40%; left: 0; width: 34%;
  background: linear-gradient(100deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.5) 48%, rgba(255,255,255,0) 100%);
  transform: translateX(-140%) skewX(-14deg); animation: pf-glint 9s ease-in-out infinite; }
@keyframes pf-glint {
  0%   { transform: translateX(-140%) skewX(-14deg); }
  16%  { transform: translateX(440%) skewX(-14deg); }
  100% { transform: translateX(440%) skewX(-14deg); }
}

/* -- casting plate: molten placeholder, engraving still cooling ------------- */
.plate.casting { animation: pf-molten 2.6s ease-in-out infinite;
  border-color: color-mix(in srgb, var(--pf-warn) 42%, #C6CAD0); }
@keyframes pf-molten {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255,255,255,.95), inset 0 -2px 3px rgba(0,0,0,.05),
             0 14px 30px -20px rgba(28,30,33,.4), 0 0 0 1px rgba(180,115,42,.18), 0 0 18px -2px rgba(180,115,42,.28); }
  50%      { box-shadow: inset 0 1px 0 rgba(255,255,255,.95), inset 0 -2px 3px rgba(0,0,0,.05),
             0 14px 30px -20px rgba(28,30,33,.4), 0 0 0 1px rgba(180,115,42,.32), 0 0 30px 0 rgba(180,115,42,.42); }
}
.plate.casting .engr { filter: blur(.7px); opacity: .62; }
.cast-copy { position: relative; z-index: 1; margin-top: 14px; padding: 12px; border-radius: 8px;
  background: rgba(180,115,42,.07); box-shadow: inset 0 1px 2px rgba(0,0,0,.08);
  font-family: var(--pf-mono); font-size: 11.5px; color: #7a5a30; }
.cast-copy .dots::after { content: ""; display: inline-block; width: 1.2em; text-align: left;
  animation: pf-dots 1.6s steps(4, end) infinite; }
@keyframes pf-dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }

/* mini casting rail: Pour ▸ Cool ▸ Stamp ▸ Ready */
.cast-rail { list-style: none; display: flex; align-items: center; gap: 6px; margin: 10px 0 0; padding: 0; }
.cast-rail li { display: inline-flex; align-items: center; gap: 5px; font-family: var(--pf-mono); font-size: 10.5px;
  font-weight: 600; letter-spacing: .02em; color: var(--pf-faint); }
.cast-rail li::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.cast-rail .sep { color: #b9bec5; font-size: 9px; }
.cast-rail .sep::before { display: none; }
.cast-rail li.done { color: var(--pf-ok); }
.cast-rail li.active { color: var(--pf-warn); }
.cast-rail li.active::before { animation: pf-phase-pulse 1.1s ease-in-out infinite; box-shadow: 0 0 6px var(--pf-warn); }

/* -- button lifecycle: rest → pressed/in-flight → success -------------------- */

/* -- attention plate: highlighted etch row + friendly inline hint ------------ */
.etch div.etch-alert { color: var(--pf-bad); background: color-mix(in srgb, var(--pf-bad) 9%, transparent);
  margin: -2px -6px; padding: 2px 6px; border-radius: 5px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pf-bad) 22%, transparent); }
.etch div.etch-alert b { color: var(--pf-bad); }
.plate-hint { position: relative; z-index: 1; display: flex; align-items: center; gap: 8px; margin-top: 10px;
  padding: 8px 11px; border-radius: 8px; font-size: 12px; color: #8a3030;
  background: color-mix(in srgb, var(--pf-bad) 7%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pf-bad) 20%, transparent); }
.plate-hint svg { width: 13px; height: 13px; flex: none; }
/* The element stays on the page with nothing to say, so the dismissal it
   remembers can be forgotten when the situation clears (advice_controller.js).
   Two classes' worth of specificity, because .plate-hint's own display:flex
   would otherwise win on source order and paint an empty amber bar. */
.plate-hint[hidden] { display: none; }
/* Dismiss. Quiet — it must not compete with the advice it closes — and pushed
   to the end of the row so the reading order stays text-then-control. The 28px
   box is the hit area; the glyph inside it is 13px like every other icon here. */
.plate-hint-x { flex: none; margin: -4px -4px -4px auto; padding: 0; width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center; border: 0; border-radius: 7px;
  background: none; color: inherit; opacity: .55; cursor: pointer; }
.plate-hint-x:hover, .plate-hint-x:focus-visible { opacity: 1;
  background: color-mix(in srgb, currentColor 12%, transparent); }
.plate-hint b { font-weight: 600; }
/* A recommendation, not a fault: amber, and it sits on the face of the plate
   rather than inside "Technical details", because the owner is the one who
   decides when to act on it. Nothing is broken, so nothing is red. */
.plate-hint.is-advice { align-items: flex-start; color: #7a4d17; line-height: 1.45;
  background: color-mix(in srgb, var(--pf-warn) 8%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pf-warn) 24%, transparent); }
.plate-hint.is-advice svg { margin-top: 2px; }
.plate-hint.is-advice strong { display: block; font-weight: 600; margin-bottom: 1px; }
/* On the detail page it sits under the full-width verdict, where a 12px line run
   across the whole viewport is unreadable. Match the verdict's weight and hold
   the measure to something the eye can track. */
.pf-verdict + .plate-hint.is-advice { font-size: 13px; padding: 11px 14px; }
.pf-verdict + .plate-hint.is-advice span { max-width: 78ch; }

/* -- commissioning plaque: engraved onboarding ------------------------------- */
.plaque { position: relative; background: var(--pf-surface); border: 1px solid var(--pf-line); border-radius: var(--pf-radius);
  padding: 24px 26px 22px; box-shadow: 0 14px 30px -24px rgba(28,30,33,.35), inset 0 0 0 1px rgba(255,255,255,.7); }
.plaque::before { content: ""; position: absolute; inset: 7px; border-radius: 8px; pointer-events: none;
  border: 1px solid var(--pf-line); opacity: .8; }
.plaque-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; position: relative; }
.plaque-head .pf-h2 { margin: 0; }
.plaque-head .rule { flex: 1; height: 1px; background: linear-gradient(90deg, var(--pf-line-2), rgba(213,216,221,0)); margin-bottom: 4px; }
.pf-step.is-done { background: linear-gradient(180deg, #FDFCF9, var(--pf-paper)); }
.pf-step.is-done .pf-step-body strong { color: var(--pf-muted); text-shadow: 0 1px 0 rgba(255,255,255,.85); }
.pf-step.is-done .pf-step-num { box-shadow: inset 0 1px 2px rgba(39,79,66,.28), 0 1px 0 rgba(255,255,255,.9); }
.pf-step.is-active { border-color: color-mix(in srgb, var(--pf-warn) 32%, var(--pf-line)); background: var(--pf-surface); }
.pf-step.is-active .pf-step-num { background: color-mix(in srgb, var(--pf-warn) 15%, #fff); color: var(--pf-warn);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pf-warn) 35%, transparent); }

/* active build phase: soft shimmer travelling through the pill */
.pf-phase.is-active { overflow: hidden; }
.pf-phase.is-active::after { content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: linear-gradient(100deg, rgba(180,115,42,0) 20%, rgba(180,115,42,.14) 50%, rgba(180,115,42,0) 80%);
  transform: translateX(-100%); animation: pf-phase-shimmer 2.2s ease-in-out infinite; }
@keyframes pf-phase-shimmer { 0% { transform: translateX(-100%); } 60%, 100% { transform: translateX(100%); } }

/* indeterminate hairline progress under the build rail */
.pf-progress { position: relative; height: 3px; margin-top: 12px; border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--pf-warn) 12%, transparent); max-width: 430px; }
.pf-progress::after { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 36%; border-radius: 999px;
  background: linear-gradient(90deg, rgba(180,115,42,.15), var(--pf-warn), rgba(180,115,42,.15));
  animation: pf-rail-run 1.8s cubic-bezier(.45,.1,.55,.9) infinite; }
@keyframes pf-rail-run { 0% { transform: translateX(-110%); } 100% { transform: translateX(390%); } }

/* -- toasts: the one flash system -------------------------------------------- */
/* Below the bar, never on it: at top:18px the toast parked exactly over the
   brand and the My Foundry door for its whole lifetime — the owner sent the
   screenshot back with "komplett overlapped" (2026-07-29). The bar is 72px
   (56px on phones); the toast clears it on both. */
.pf-toast { position: fixed; top: 84px; left: 50%; transform: translateX(-50%); z-index: 60;
  display: flex; align-items: center; gap: 9px; max-width: min(92vw, 560px);
  background: var(--pf-surface); border: 1px solid var(--pf-line-2); border-radius: 10px;
  padding: 10px 16px; font: 500 13.5px/1.4 var(--pf-sans); color: var(--pf-ink);
  box-shadow: 0 18px 40px -18px rgba(28,30,33,.5);
  animation: pf-toast-in .25s ease-out both;
  transition: opacity .3s ease, transform .3s ease; }
.pf-toast::before { content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none; }
.pf-toast-ok::before  { background: var(--pf-ok);  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-ok) 18%, transparent); }
.pf-toast-bad::before { background: var(--pf-bad); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-bad) 16%, transparent); }
.pf-toast-bad { border-color: color-mix(in srgb, var(--pf-bad) 35%, var(--pf-line-2)); }
.pf-toast.is-dismissed { opacity: 0; transform: translateX(-50%) translateY(-10px); }
@keyframes pf-toast-in { from { opacity: 0; transform: translateX(-50%) translateY(-8px); } }
.pf-toast + .pf-toast { top: 132px; }

/* Phone: the toast goes to the BOTTOM.
   Floating it at the top could only ever cover something — first it sat ON the
   bar (brand and My Foundry door unreadable, owner screenshot 2026-07-29), and
   moved below the bar it covered the first card's heading instead. A 390px
   column has no free top strip. Bottom is also where a phone user expects a
   transient confirmation (iOS toast, Android snackbar) and where the thumb
   already is. It clears the safe area and never touches the header. */
@media (max-width: 760px) {
  .pf-toast {
    top: auto;
    bottom: calc(16px + env(safe-area-inset-bottom));
    left: 12px;
    right: 12px;
    transform: none;
    max-width: none;
  }
  .pf-toast + .pf-toast { top: auto; bottom: calc(74px + env(safe-area-inset-bottom)); }
  /* The shared entry/exit keyframes carry translateX(-50%) for the centred
     desktop toast; on the phone the box is left/right-anchored, so it rises
     instead of sliding down out of a centre it no longer has. */
  .pf-toast { animation-name: pf-toast-in-bottom; }
  .pf-toast.is-dismissed { transform: translateY(10px); }
}

@media (prefers-reduced-motion: reduce) {
  .lamp.live .b, .lamp.cast .b, .glint::after, .plate.casting, .cast-rail li.active::before,
  .cast-copy .dots::after, .pf-phase.is-active::after, .pf-progress::after, .spin, .pf-toast { animation: none; }
}

/* ---- API docs (/api/docs) ---- */
.pf-code { background: var(--pf-surface-2); border: 1px solid var(--pf-line); border-radius: var(--pf-radius-sm);
           padding: 12px 14px; margin: 10px 0 0; font-family: var(--pf-mono); font-size: 12.5px;
           line-height: 1.6; color: var(--pf-ink); overflow-x: auto; white-space: pre; }
.pf-endpoint { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; font-family: var(--pf-mono); font-size: 14px;
  /* long endpoint paths wrap instead of painting past the card — painted
     overflow widens the mobile layout viewport and un-anchors the toasts */
  overflow-wrap: anywhere; min-width: 0; }
.pf-method { font-family: var(--pf-sans); font-size: 11.5px; font-weight: 700; letter-spacing: .1em;
             color: var(--pf-navy); background: color-mix(in srgb, var(--pf-navy) 10%, transparent);
             border-radius: 6px; padding: 2px 8px; }
.pf-docs-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 10px;
  /* B1: on narrow screens the table scrolls inside itself — never the page.
     A page-wide overflow also widens the layout viewport, which un-anchors
     every position:fixed toast. */
  display: block; overflow-x: auto; }
.pf-docs-table td { max-width: 62ch; }
.pf-docs-table th, .pf-docs-table td { text-align: left; padding: 7px 12px 7px 0;
             border-bottom: 1px solid var(--pf-line); vertical-align: top; }
.pf-docs-table th { font-size: 11.5px; font-weight: 600; text-transform: uppercase;
             letter-spacing: .12em; color: var(--pf-faint); }

/* ---- "Advanced" disclosure (Brain detail page) ----
   Collapsed by default: a quiet mono summary in the page's footer voice; the
   body only exists once opened. Deliberately undersells what's inside. */
.pf-adv { margin-top: 26px; }
.pf-adv > summary { cursor: pointer; font-family: var(--pf-mono); font-size: 12px;
                    color: var(--pf-faint); user-select: none; width: fit-content; }
.pf-adv > summary:hover { color: var(--pf-muted); }
.pf-adv-body { margin-top: 12px; background: var(--pf-surface); border: 1px solid var(--pf-line);
               border-radius: var(--pf-radius); padding: 18px 20px; }

/* ---- the model, and the switch ----
   The owner asked to see whether Opus or Fable is doing the work. The label is
   read from the transcript (what ran), never from the config (what was asked
   for) — those disagree the moment somebody types /model. */
.pf-model { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pf-model-now { font-weight: 600; }
.pf-model-swap { display: flex; gap: 6px; flex-wrap: wrap; }

/* ---- plain data table (settings: usage, keys) — the docs-table skin ---- */
.pf-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.pf-table th, .pf-table td { text-align: left; padding: 7px 12px 7px 0;
             border-bottom: 1px solid var(--pf-line); vertical-align: top; }
.pf-table th { font-size: 11.5px; font-weight: 600; text-transform: uppercase;
             letter-spacing: .12em; color: var(--pf-faint); }

/* ---- phone ---- */
@media (max-width: 760px) {
  /* At 24px the default hostname breaks mid-word ("primef / oundry.ai"). */
  .pf-url-value { font-size: 16px; overflow-wrap: anywhere; }
  /* Wide tables scroll in place rather than pushing the page sideways. */
  .pf-table-scroll { overflow-x: auto; }
}

/* ---- the door row (settings hero): chat first, boards second ---- */
.pf-door-row { display: flex; gap: 10px; flex-wrap: wrap; }
@media (max-width: 760px) {
  /* One thumb, one door: the buttons take the full card width and stack. */
  .pf-door-row { flex-direction: column; }
  .pf-door-row .pf-btn { width: 100%; justify-content: center; min-height: 46px; box-sizing: border-box; }
}

/* ---- phone bar: the Hub's labels are longer than the Cockpit's ----
   "Prime Foundry" + "My Foundry" + "Sign out" outgrow a 390px bar with the
   desktop paddings and pill icons. The icons go first (the words are the
   affordance), then the paddings tighten. Scoped to the Hub: the Cockpit's
   shorter labels fit as they are, and its bar stays untouched. */
@media (max-width: 760px) {
  body.pf-hub .pf-bar { gap: 8px; }
  body.pf-hub .pf-acct-plain .pf-baract svg { display: none; }
  body.pf-hub .pf-baract { padding: 8px 9px; font-size: 12.5px; }
}

/* The Cockpit chat page moved the phone match-count into the search BAR
   ("3/12") and hides the meta row; the Hub Desk keeps the classic meta row —
   its bar has no counter to take over. */
@media (max-width: 900px) {
  body.pf-hub .chat-search-meta { display: flex; }
  body.pf-hub .chat-search-meta[hidden] { display: none; }
}

@keyframes pf-toast-in-bottom { from { opacity: 0; transform: translateY(10px); } }

/* ---- Touch targets + reading measure (breakpoint-audit GUIDELINES.md) ---- */
/* Y4: hints are prose — hold them to a reading measure. */
/* 62ch of a proportional face ~= 80-85 real characters per line (ch measures
   the "0" glyph; average glyphs run narrower). 75ch read as ~100. */
/* Leads are NOT capped: a one-sentence intro under a full-width H1 that wraps
   at half the page reads as a stray column, not as typography (owner,
   2026-08-23). They are one or two lines; the measure rule is for hint
   paragraphs, which are the ones that run long. */
.pf-hint { max-width: 62ch; }
/* long unbroken tokens (endpoint names, field names) wrap instead of
   widening the page — a page-wide overflow un-anchors every fixed toast */
code.pf-mono { overflow-wrap: anywhere; }

@media (pointer: coarse) {
  /* T1/T2: every control reaches the 44px floor on touch. */
  /* element-qualified to out-rank tokens.css's `button.pf-baract { min-height:0 }` */
  .pf-btn, a.pf-baract, button.pf-baract { min-height: 44px; }
  .pf-btn-sm { min-height: 44px; min-width: 44px; }
  /* the input beside a pill grows with it, or the row misaligns again */
  .pf-input, .pf-select { min-height: 44px; }
  /* Text links (auth switch, Settings/API docs footer nav) grow invisibly. */
  .pf-link { display: inline-block; padding: 13px 4px; margin: -13px -4px; }
  /* footer link keeps its horizontal box — a negative margin at the wrap edge
     reads as an off-viewport stickout */
  .pf-foot-inner a { display: inline-block; padding: 13px 6px; margin: -13px 0; }
  /* Y2: the SSH key textareas hold 16px so iOS does not zoom on focus. */
  textarea.pf-input { font-size: 16px; }
}

/* ---- Platform alert (topbar) — red must be visible on every Hub page,
   the agreed substitute for a push channel (owner's decision 13.08.). */
.pf-platform-alert { color: var(--pf-bad); border-color: color-mix(in srgb, var(--pf-bad) 45%, var(--pf-line-2)); }
.pf-platform-alert-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--pf-bad);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-bad) 18%, transparent); flex: none; }

/* Signed-in devices. Each row is one revocable credential — the button has to
   read as safe, because the one above it (Regenerate) replaces the key the
   whole fleet runs on. */
.pf-devices { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.pf-device { display: flex; align-items: center; gap: 12px; padding: 10px 12px;
             background: var(--pf-surface-2); border: 1px solid var(--pf-line); border-radius: 8px; }
.pf-device-name { font-weight: 600; color: var(--pf-ink); }
.pf-device-meta { margin-left: auto; font-size: 12px; color: var(--pf-muted); }

/* ── Admin fleet table ──────────────────────────────────────────────────────
   Operator-facing, so it is a table and not a wall of plates: the job here is
   to scan thirty rows for the one that is wrong, which a grid of tiles is bad
   at and a sorted table is good at.

   Everything is scoped under .pf-fleet. The first version of this block
   defined a bare `.lamp`, which this stylesheet has owned since the nameplate
   tiles were built — the redefinition leaked into the header and pushed it
   6px past the viewport on mobile. The system specs caught it; the lesson is
   that a new component does not get to name a class the design system already
   uses. */
.pf-fleet { width: 100%; border-collapse: collapse; margin: 1rem 0 2rem; font-size: .95rem; }
.pf-fleet th { text-align: left; font-weight: 600; padding: .5rem .6rem; border-bottom: 2px solid var(--pf-ink); }
.pf-fleet td { padding: .5rem .6rem; border-bottom: 1px solid var(--pf-line); vertical-align: top; }
.pf-fleet .fleet-mono { font-family: var(--pf-mono, ui-monospace, monospace); }
.pf-fleet .fleet-muted { color: var(--pf-muted); }
.pf-fleet tr.fleet-red td { background: rgba(178, 58, 58, .06); }
.pf-fleet tr.fleet-orange td { background: rgba(180, 115, 42, .08); }
.pf-fleet .fleet-dot { display: inline-block; width: .7rem; height: .7rem; border-radius: 50%;
                       margin-right: .45rem; vertical-align: -1px; flex: none; }
.pf-fleet .fleet-dot-green  { background: var(--pf-green); }
.pf-fleet .fleet-dot-orange { background: var(--pf-warn); }
.pf-fleet .fleet-dot-red    { background: var(--pf-bad); }
/* Wide content must scroll inside its own box, never widen the page. */
.pf-fleet-scroll { overflow-x: auto; }

/* ── Operator: the Admin pill, the accounts filter, the borrowed-account banner ─
   The banner is amber and full-bleed on purpose. The dangerous thing about
   borrowing an account is not the borrowing, it is forgetting: an operator two
   pages deep who thinks he is in his own Hub will press Delete on a Brain that
   belongs to a customer. So it sits above the flash, on every page, and it
   carries the way out — a banner that only warns makes you hunt for the exit. */
.pf-baract.is-active { color: var(--pf-navy); background: var(--pf-surface-2); }

.pf-admin-search { display: flex; gap: 8px; align-items: center; margin: 0 0 .5rem; flex-wrap: wrap; }
.pf-admin-search .pf-input { max-width: 320px; flex: 1 1 200px; }

.pf-fleet .pf-fleet-right { text-align: right; white-space: nowrap; }

.pf-impersonating { background: color-mix(in srgb, var(--pf-warn) 14%, var(--pf-surface));
                    border-bottom: 1px solid color-mix(in srgb, var(--pf-warn) 45%, var(--pf-line-2)); }
/* padding-BLOCK only: `padding: 10px 0` would zero .pf-wrap's own gutter and
   put the text flush against the phone's edge — the trap tokens.css warns
   about twice, and this banner walked straight into it the first time. */
.pf-impersonating-inner { display: flex; gap: 12px; align-items: center; justify-content: space-between;
                          flex-wrap: wrap; padding-block: 10px; font-size: .92rem; color: var(--pf-ink); }
.pf-impersonating strong { font-family: var(--pf-mono, ui-monospace, monospace); font-weight: 600; }
.pf-impersonating-form { margin: 0; flex: none; }
