/* ============================================================
   porcelain-shell.css — the Porcelain Futurism OS shell.
   Canon v5: docs/design/PORCELAIN_FUTURISM_2026-08-16.md
   Built to the Figma source of record: "JUWEL OS — Porcelain
   Futurism" (BT8PA8nvRjJQ1M0C13D0QX) — 01 Greeting · 02 Desk ·
   03 Consent · 04 Welcome · 05 Living desk · 06 Void.
   Token-pure (juwel-ds.css). Prefix .psh-. Entity = the little
   one (assets/juwel-entity*.svg), never a drawn gem.
   Law: ink = you act · iridescence = JUWEL acts.
   ============================================================ */

.psh{
  display:grid;grid-template-rows:64px 1fr auto;
  height:100dvh;background:var(--paper);color:var(--ink);
  font-family:var(--ff);overflow:hidden;
}

/* ─────────────────────────────── top bar ─────────────────── */
.psh-top{
  display:flex;align-items:center;gap:var(--s4);padding:0 28px;
  background:color-mix(in srgb,var(--surface) 72%,transparent);
  backdrop-filter:var(--glass-blur);
  border-bottom:1px solid var(--line);z-index:40;
}
.psh-brand{display:flex;align-items:center;gap:10px;}
.psh-wordmark{font-weight:600;font-size:13px;letter-spacing:.08em;white-space:nowrap;}

.psh-entity{width:22px;height:22px;object-fit:contain;display:block;flex:none;}

.psh-record{
  margin-inline:auto;display:inline-flex;align-items:center;gap:7px;cursor:pointer;
  padding:6px 14px;border-radius:var(--r-full);border:1px solid var(--line);
  background:var(--surface);font:inherit;font-size:12px;font-weight:500;color:var(--ink-2);
  transition:border-color var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out);
}
.psh-record:hover{border-color:var(--ink-4);color:var(--ink);}
.psh-dot{width:6px;height:6px;border-radius:50%;background:var(--jade);flex:none;}

.psh-top-right{display:flex;align-items:center;gap:12px;}
.psh-theme{
  width:32px;height:32px;display:grid;place-items:center;cursor:pointer;
  border:1px solid transparent;border-radius:var(--r-full);
  background:transparent;color:var(--ink-3);
}
.psh-theme:hover{background:var(--surface-2);color:var(--ink);}
.psh-clock{font-size:12.5px;font-weight:500;color:var(--ink-2);white-space:nowrap;}

/* ─────────────────────────────── rail ────────────────────── */
.psh-stage{position:relative;overflow:hidden;}

.psh-rail{
  position:absolute;left:24px;top:50%;transform:translateY(-50%);z-index:30;
  display:flex;flex-direction:column;gap:6px;padding:10px 8px;
  border-radius:22px;border:1px solid var(--line);
  background:color-mix(in srgb,var(--surface) 82%,transparent);
  backdrop-filter:blur(20px);box-shadow:var(--shadow-1);
}
.psh-rail-btn{
  width:38px;height:38px;display:grid;place-items:center;cursor:pointer;
  border:0;border-radius:12px;background:transparent;color:var(--ink-3);
  transition:background var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out);
  position:relative;
}
.psh-rail-btn:hover{background:var(--surface-2);color:var(--ink);}
.psh-rail-btn[aria-pressed="true"]{background:var(--surface-3);color:var(--ink);}
.psh-rail-sep{height:1px;background:var(--line);margin:4px 6px;}
.psh-rail-btn .psh-badge{
  position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;
  background:var(--irid);
}

/* ─────────────────────────────── desk ────────────────────── */
.psh-desk{
  position:absolute;inset:0;padding:26px 28px 8px 92px;
  display:flex;align-items:flex-start;gap:16px;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;
  /* `safe center` — plain `center` on an overflowing flex row pushes the
     first item to a NEGATIVE scroll offset the user can never reach
     (scrollLeft can't go below 0), clipping the newest surface behind the
     rail. `safe` falls back to start-alignment the instant content
     overflows, so every surface stays scrollable into view (shell QA
     2026-08-28). One surface still centers as before. */
  justify-content:safe center;
}
.psh-desk:empty{display:none;}
/* the tour card floats over the stage — give it room so it never
   covers a surface the user is being asked to look at */
.psh[data-guide="1"] .psh-desk{padding-top:150px;}
.psh[data-guide="1"] .psh-greet{padding-top:120px;}
.psh-desk::-webkit-scrollbar{height:8px;}
.psh-desk::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;}

/* LAUNCH DAY 2026-09-04. `inset:0` centred the greeting in the WHOLE
   stage, but the deck under it (companion chip + Assistant Setup card +
   composer + foot) is an auto-sized grid row, so the taller the viewport
   the further the hero drifted from the thing it is talking to:
   MEASURED before this change, 282px of empty porcelain between the
   hero sentence and the "Your assistant" chip at 1440x900 and 1503px at
   1440x2400, with the copy stranded at 45% of the page.
   The band is now anchored to the BOTTOM of the stage and capped, so the
   hero, the chip, the card and the composer read as one group at every
   height: the leftover space collects above the group (where the rail
   lives and where a surface opens) instead of inside it. Layout only,
   no magic margins: bottom inset + a capped band + place-items:center. */
.psh-greet{
  position:absolute;inset:auto 0 0 0;
  height:min(100%,var(--psh-greet-band,200px));
  display:grid;place-items:center;
  text-align:center;pointer-events:none;padding:0 28px 24px 92px;
}
.psh-greet[hidden]{display:none;}
/* Was .6s, and a screenshot of the tall desk caught it mid-fade: the
   greeting rendered a washed-out grey that is NOT the token colour.
   Capped at --dur-3 (.38s, under the 400ms ceiling) and the whole
   entrance is skipped for prefers-reduced-motion, which renders the
   FINAL state rather than the start state (see the media block at the
   foot of this file for the shell-wide reduced-motion kill). */
.psh-greet-in{animation:psh-greet-in var(--dur-3) var(--ease-out) both;}
@media (prefers-reduced-motion:reduce){
  .psh-greet-in{animation:none;opacity:1;transform:none;}
}
.psh-greet h1{
  font-size:clamp(30px,3.6vw,42px);font-weight:400;letter-spacing:-.025em;
  color:var(--ink);margin:0 0 10px;line-height:1.1;
}
.psh-greet p{font-size:15px;color:var(--ink-3);margin:0;}
@keyframes psh-greet-in{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:none;}}

/* the Void's ambient halo — retired. Still grounds. No wash, bloom, or halo. */
.psh-halo{display:none !important;}

/* JUJU — the little one, at hand */
.psh-juju{
  position:absolute;right:24px;bottom:20px;width:56px;height:56px;cursor:pointer;z-index:35;
  display:grid;place-items:center;border-radius:50%;border:1px solid var(--line);
  background:var(--surface);box-shadow:0 10px 40px color-mix(in srgb,var(--ink) 10%,transparent);
  animation:none;
  transition:transform var(--dur-1) var(--ease-out),box-shadow var(--dur-2) var(--ease-out);
}
/* LAUNCH DAY 2026-09-04. The mark was 34px inside a disc that
   pa-ui-clicks.css:267-276 shrinks to 38px with !important, so the
   hexagon's left and right points spilled OUTSIDE the white circle and
   from a distance it read as a badge cut off at the viewport edge
   (founder). 22px leaves an even 8px of porcelain on every side of a
   38px disc, so the mark sits IN the badge instead of bursting it. The
   ambient glow above moved off --topaz-deep (deep magenta, which cast a
   visible pink halo on first paint) onto --ink, same reason as the
   composer ring: no red anywhere on the resting desk. */
.psh-juju img{width:22px;height:22px;object-fit:contain;display:block;}
.psh-juju:hover{transform:translateY(-3px) scale(1.06);box-shadow:0 16px 54px color-mix(in srgb,var(--ink) 16%,transparent);}
.psh-juju:active{transform:scale(.94);}
/* JUJU carries its own ambient box-shadow (the floating glow above), which
   at equal specificity always wins over the design system's zero-specificity
   :where(...):focus-visible ring — so Tab could land here with NO visible
   indicator at all (founder QA BUG-10, 2026-08-28). A real class selector
   outranks :where() and restores the ring while keyboard-focused. */
.psh-juju:focus-visible{
  outline:none;border-color:var(--focus-edge);
  box-shadow:inset 0 0 0 999px var(--focus-wash),0 0 0 1px var(--focus-edge);
}
@keyframes psh-float{0%,100%{translate:0 0;}50%{translate:0 -5px;}}

/* ───────────────────────── surfaces on the desk ──────────── */
.psh-surface{
  flex:none;width:440px;max-height:100%;
  display:flex;flex-direction:column;min-height:0;overflow:hidden;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-xl);
  box-shadow:var(--shadow-1);
  /* LAUNCH DAY 2026-09-04. Open and close must be the same move played
     both ways. Open was .55s here plus the .7s signature sweep at .2s
     delay below, so a window took ~.9s to settle, while close was a bare
     s.remove() in porcelain-shell.js, i.e. instant: the least symmetric
     pair on the desk. Both now sit on the design tokens (--dur-3 .38s /
     --dur-2 .26s, juwel-ds.css:156) with the one shared --ease-out.
     blur(7px) is gone from the open keyframe: it forced a full-surface
     filter raster on every frame, and opacity + translate already read
     as "materialise". The exit is the shorter token because leaving is
     quicker than arriving. */
  animation:psh-materialize var(--dur-3) var(--ease-out);
}
@keyframes psh-materialize{
  from{opacity:0;transform:translateY(28px) scale(.97);}
  to{opacity:1;transform:none;}
}
/* pa-vext-wrap.css:56-60 replaces the open animation above with
   vext-win-enter using !important, so the exit has to answer in kind:
   equal !important, higher specificity (.psh-surface.is-closing beats
   .psh-surface). It borrows THAT sheet's own duration and easing tokens
   so the two halves of the move cannot drift apart, and falls back to
   the porcelain tokens if pa-vext-wrap.css has not loaded.
   pa-vext-wrap.css:230 already sets --motion-dur-window:0.01ms under
   prefers-reduced-motion, so the exit inherits reduced motion. */
.psh-surface.is-closing{
  animation-name:psh-dematerialize !important;
  animation-duration:var(--motion-dur-window,var(--dur-2)) !important;
  animation-timing-function:var(--motion-ease-enter,var(--ease-out)) !important;
  animation-fill-mode:forwards !important;
  pointer-events:none;
}
@keyframes psh-dematerialize{
  from{opacity:1;transform:none;}
  to{opacity:0;transform:translateY(14px) scale(.985);}
}
.psh-surface-head{
  display:flex;align-items:center;gap:10px;padding:14px 16px 13px 20px;
  position:relative;flex:none;
}
.psh-surface-head::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:1.5px;
  background:var(--irid-soft);transform-origin:left center;
  animation:psh-sign var(--dur-3) var(--ease-out) both;
}
@keyframes psh-sign{from{transform:scaleX(0);}to{transform:scaleX(1);}}
.psh-surface-head img{width:15px;height:15px;object-fit:contain;flex:none;}
.psh-surface-name{
  font-weight:550;font-size:14px;letter-spacing:-.006em;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.psh-surface-x{
  margin-left:auto;flex:none;width:26px;height:26px;cursor:pointer;
  border:0;border-radius:50%;background:transparent;color:var(--ink-3);
  display:grid;place-items:center;
}
.psh-surface-x:hover{background:var(--surface-2);color:var(--ink);}
.psh-surface-body{flex:1;min-height:0;overflow-y:auto;padding:20px 22px 22px;}

/* surface content — the Figma card anatomy, built on jui- primitives */
.psh-h{font-size:22px;font-weight:650;letter-spacing:-.02em;line-height:1.22;margin:0;}
.psh-k{font-size:12.5px;color:var(--ink-3);margin:0;}
.psh-stats{display:flex;gap:28px;flex-wrap:wrap;}
.psh-stat{display:flex;flex-direction:column;gap:3px;}
.psh-stat-l{font-size:10px;font-weight:550;letter-spacing:.10em;text-transform:uppercase;color:var(--ink-3);}
.psh-stat-v{font-size:21px;font-weight:650;letter-spacing:-.015em;}
.psh-stat-h{font-size:11px;color:var(--ink-3);}
.psh-rule{height:1px;background:var(--line);border:0;margin:0;}
/* BUG-29: rows() now emits UL/LI (real list semantics) with .psh-sep
   separator spans. The UL is chrome-free so nothing visual changed, and
   the separators are sr-only-style clipped: absent from layout and paint,
   PRESENT in innerText/clipboard/AT. white-space:pre keeps the trailing
   "\n" separator alive through text extraction. */
.psh-rows{list-style:none;margin:0;padding:0;}
.psh-sep{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:pre;
}
.psh-row{display:flex;align-items:baseline;gap:14px;padding:7px 0;border-bottom:1px solid var(--line-2);}
.psh-row:last-child{border-bottom:0;}
.psh-row-t{font-size:13px;font-weight:500;color:var(--ink-2);min-width:44px;flex:none;}
.psh-row-l{flex:1;font-size:14.5px;min-width:0;}
.psh-tag{
  font-size:10px;font-weight:550;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-2);border:1px solid var(--line);border-radius:var(--r-full);
  padding:3px 10px;flex:none;
}
/* .psh-nest — a nested list level from BUG-08's compose.js parser
   (os/porcelain-apps/compose.js). One row group per depth, indented and
   given a hairline rail so nesting reads at a glance instead of just
   sitting further right. compose.js also sets an inline margin-left as a
   host-agnostic fallback; this class is additive polish, not a
   replacement — removing the inline style is fine, this still indents. */
.psh-nest{position:relative;padding-left:14px;}
.psh-nest::before{
  content:"";position:absolute;left:0;top:2px;bottom:2px;width:1px;
  background:var(--line);
}
.psh-nest .psh-row:last-child{border-bottom:0;}
.psh-surface-body > * + *{margin-top:16px;}
.psh-surface-body > .psh-k{margin-top:6px;}
.psh-acts{display:flex;gap:8px;flex-wrap:wrap;}
.psh-btn{
  cursor:pointer;font:inherit;font-size:12.5px;font-weight:550;
  padding:8px 16px;border-radius:var(--r-full);
  border:1px solid var(--line);background:var(--surface);color:var(--ink);
  transition:background var(--dur-1) var(--ease-out),border-color var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out);
}
.psh-btn:hover{background:var(--surface-2);border-color:var(--ink-4);}
.psh-btn.ink{background:var(--ink);border-color:var(--ink);color:var(--surface);}
.psh-btn.ink:hover{filter:brightness(.62);}
.psh-btn.ghost{border-color:transparent;color:var(--ink-2);}
.psh-btn.ghost:hover{color:var(--ink);background:var(--surface-2);}

/* ───────────────────────────── deck ──────────────────────── */
.psh-deck{display:flex;flex-direction:column;align-items:center;gap:9px;padding:6px 28px 14px;z-index:36;}

.psh-voice{
  /* BUG-18: this is a status region, not a control — no cursor:pointer,
     it does not promise a click. See os-porcelain.html for the markup
     change (button -> div role="status"). */
  display:flex;align-items:center;gap:9px;max-width:min(640px,90vw);
  padding:6px 15px;border-radius:var(--r-full);border:1px solid var(--line);
  background:color-mix(in srgb,var(--surface) 86%,transparent);
  font:inherit;font-size:12.5px;color:var(--ink-2);
  animation:psh-node-in .3s var(--ease-out);
}
.psh-voice[hidden]{display:none;}
.psh-voice img{width:15px;height:15px;object-fit:contain;flex:none;}
.psh-voice span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
@keyframes psh-node-in{from{opacity:0;transform:translateY(7px);}to{opacity:1;transform:none;}}

/* LAUNCH DAY 2026-09-04 — founder: "all of the red needs to be deleted".
   The resting and hover ring on the composer used to be --irid-soft /
   --irid, i.e. the brand gradient magenta #F98CF9 -> orange #FF3814
   (juwel-ds.css:44-57). On the porcelain ground a red-to-pink outline
   around the ONE text field of the OS reads as a validation error, not
   as brand, and it was the most saturated thing on the screen. Those
   two colours are the mosaic's (Proposal A), not a focus ring's.
   So: the pill wears the design system's own quiet porcelain hairline
   (--line) at rest, deepens it to --ink-4 when anything focuses it, and
   takes the ds focus treatment (--focus-fill tint + 1px --focus-edge,
   founder law T1-W1-RING, proof os/__tests__/focus-ring-system.test.js)
   when a KEYBOARD lands in it. The ambient drop shadow moved off
   --topaz-deep (a deep magenta, which cast a visible pink halo under
   the pill on first paint) onto plain --ink. Red now means error and
   nothing else. */
.psh-composer{
  display:flex;align-items:center;gap:12px;width:min(640px,92vw);
  padding:8px 8px 8px 24px;border-radius:32px;
  border:1.5px solid transparent;
  background-image:linear-gradient(var(--surface),var(--surface)),
                   linear-gradient(var(--line),var(--line));
  background-origin:border-box;background-clip:padding-box,border-box;
  box-shadow:0 14px 44px color-mix(in srgb,var(--ink) 7%,transparent);
  transition:box-shadow var(--dur-2) var(--ease-out),
             background-image var(--dur-2) var(--ease-out);
}
.psh-composer:focus-within{
  background-image:linear-gradient(var(--surface),var(--surface)),
                   linear-gradient(var(--ink-4),var(--ink-4));
  box-shadow:0 16px 60px color-mix(in srgb,var(--ink) 12%,transparent);
}
.psh-composer input{
  flex:1;min-width:0;border:0;outline:none;box-shadow:none;background:none;
  font:inherit;font-size:15px;color:var(--ink);
}
.psh-composer input::placeholder{color:var(--ink-3);}
/* The ds-wide :where(input):focus-visible rule paints an inset focus wash —
   inside THIS borderless input it reads as a stray box floating in the pill
   (founder bug 2026-08-28). Focus is signalled on the PILL itself via
   .psh-composer:focus-within (iridescent ring), so the inner input stays
   chromeless. Keyboard users lose nothing: the pill ring is the indicator. */
.psh-composer input:focus-visible{box-shadow:none;background:none;border:0;outline:none;}
/* BUG-30 (2026-08-29): the claim above ("keyboard users lose nothing") was
   measured false — :focus-within only swaps --irid-soft for --irid and
   deepens an ambient drop shadow, neither of which meets the 3:1 non-text
   contrast a keyboard user needs to LOCATE the primary input of the OS.
   Fix per founder law T1-W1-RING (fill shift, nothing floats): when the
   input is keyboard-focused the PILL itself takes the ds focus treatment —
   warm --focus-fill tints the pill's own background and a 1px --focus-edge
   hugs its 32px radius at offset 0 (7.11:1 light / 11.45:1 dark vs surface,
   proof: os/__tests__/focus-ring-system.test.js). The inner input stays
   chromeless (the kill rule above is still correct — the indicator lives on
   the pill, not boxed inside it). Browsers match :focus-visible for ANY
   focus of a text field, same behavior the ds-wide rule always had. */
.psh-composer:has(input:focus-visible){
  background-image:linear-gradient(var(--focus-fill),var(--focus-fill)),
                   linear-gradient(var(--focus-edge),var(--focus-edge));
  box-shadow:0 16px 60px color-mix(in srgb,var(--ink) 12%,transparent),
    0 0 0 1px var(--focus-edge);
}
.psh-mic{
  width:34px;height:34px;flex:none;display:grid;place-items:center;cursor:pointer;
  border:0;border-radius:50%;background:transparent;color:var(--ink-3);
}
.psh-mic:hover{background:var(--surface-2);color:var(--ink);}
/* WEIRD-2: the hot mic is visibly hot — filled, tinted, pulsing. The pulse
   is opacity-only so prefers-reduced-motion can kill it without losing the
   fill, which stays as the state indicator. */
.psh-mic[data-listening]{
  background:var(--receipt-bg);color:var(--receipt-ink);
  animation:psh-mic-pulse 1.6s var(--ease-out) infinite;
}
@keyframes psh-mic-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--topaz-deep) 34%,transparent);}50%{box-shadow:0 0 0 7px transparent;}}
@media (prefers-reduced-motion:reduce){.psh-mic[data-listening]{animation:none;}}
.psh-send{
  width:44px;height:44px;flex:none;display:grid;place-items:center;cursor:pointer;
  border:0;border-radius:50%;background:var(--receipt-bg);color:var(--receipt-ink);
  transition:transform var(--dur-1) var(--ease-out),background var(--dur-1) var(--ease-out);
}
.psh-send:hover{filter:brightness(.72);transform:scale(1.05);}
.psh-send:active{transform:scale(.92);}
.psh-foot{font-size:11px;color:var(--ink-3);text-align:center;}

/* ───────────────────────── consent moment ────────────────── */
.psh-veil{
  position:fixed;inset:0;z-index:520;display:grid;place-items:center;padding:24px;
  background:color-mix(in srgb,var(--paper) 72%,transparent);backdrop-filter:blur(10px);
  animation:psh-fade .22s var(--ease-out);
}
.psh-veil[hidden]{display:none;}
@keyframes psh-fade{from{opacity:0;}}

/* a veil (welcome / consent) is a real modal moment: the shell behind it
   must go inert — not just visually dimmed. Without this, Tab walks
   straight through the blur into the record pill, theme toggle, rail nav
   and composer, which is both a focus leak and (since those controls keep
   their hover/focus paint) a layer of visual noise bleeding through the
   glass (founder QA 2026-08-28). `:has()` lets us do this from CSS alone,
   no JS aria-hidden bookkeeping required. */
.psh:has(> .psh-veil:not([hidden])) > .psh-top,
.psh:has(> .psh-veil:not([hidden])) > .psh-stage > .psh-rail,
.psh:has(> .psh-veil:not([hidden])) > .psh-stage > .psh-juju,
.psh:has(> .psh-veil:not([hidden])) > .psh-deck{
  visibility:hidden;
}

.psh-consent-card{
  width:min(460px,100%);display:flex;flex-direction:column;gap:13px;
  background:var(--surface);border:1px solid var(--line);border-radius:28px;
  box-shadow:var(--shadow-3);padding:30px 34px 24px;
  animation:psh-pop .3s var(--ease-out);
}
@keyframes psh-pop{from{opacity:0;transform:translateY(18px) scale(.96);}}
.psh-whisper{width:58px;height:3px;border-radius:2px;background:var(--irid);flex:none;}
.psh-gate{font-size:10.5px;font-weight:550;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);}
.psh-amount{font-size:56px;font-weight:800;letter-spacing:-.035em;line-height:1;}
.psh-amount[hidden]{display:none;}
.psh-consent-card h2{font-size:17px;font-weight:600;letter-spacing:-.01em;margin:0;}
.psh-consent-card p{font-size:13.5px;color:var(--ink-2);margin:0;line-height:1.5;}
.psh-consent-row{display:flex;gap:10px;align-items:center;margin-top:2px;}
.psh-consent-k{font-size:11px;color:var(--ink-3);}

/* ───────────────────────────── welcome ───────────────────── */
.psh-welcome-card{
  width:min(480px,100%);display:flex;flex-direction:column;align-items:center;gap:16px;
  text-align:center;background:var(--surface);border:1px solid var(--line);
  border-radius:32px;box-shadow:var(--shadow-3);padding:44px 44px 34px;
  animation:psh-pop .34s var(--ease-out);
}
.psh-welcome-card img{width:52px;height:52px;object-fit:contain;}
.psh-welcome-card h1{font-size:28px;font-weight:700;letter-spacing:-.02em;margin:0;}
.psh-welcome-card p{font-size:14px;color:var(--ink-2);margin:0;line-height:1.55;}
.psh-field{display:flex;flex-direction:column;gap:7px;width:100%;}
.psh-field span{font-size:10px;font-weight:550;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);}
.psh-field input{
  width:100%;padding:13px 16px;text-align:center;
  border:1px solid var(--line);border-radius:var(--r-full);
  background:var(--surface-2);font:inherit;font-size:15px;font-weight:500;color:var(--ink);
  outline:none;transition:border-color var(--dur-1) var(--ease-out),background var(--dur-1) var(--ease-out);
}
.psh-field input:focus{background:var(--focus-fill);border-color:var(--focus-edge);}
.psh-chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;}

/* ─────────────────────────── tour guide ──────────────────── */
.psh-guide{
  position:fixed;top:78px;left:50%;transform:translateX(-50%);z-index:460;
  width:min(490px,92vw);display:flex;flex-direction:column;gap:10px;
  padding:14px 18px 15px;border-radius:20px;
  background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow-2);
  animation:psh-guide-in .3s var(--ease-out);
}
.psh-guide[hidden]{display:none;}
@keyframes psh-guide-in{from{opacity:0;transform:translate(-50%,-8px);}}
.psh-guide-top{display:flex;align-items:center;gap:10px;}
.psh-guide-top img{width:16px;height:16px;object-fit:contain;}
.psh-steps{display:flex;gap:5px;}
.psh-step{width:6px;height:6px;border-radius:50%;background:var(--line);}
.psh-step.on{background:var(--irid);}
.psh-skip{margin-left:auto;cursor:pointer;border:0;background:none;font:inherit;font-size:11px;color:var(--ink-3);}
.psh-skip:hover{color:var(--ink);}
.psh-guide p{font-size:13.5px;color:var(--ink-2);margin:0;}

/* ───────────────────────────── toast ─────────────────────── */
.psh-toasts{
  position:fixed;bottom:118px;left:50%;transform:translateX(-50%);z-index:600;
  display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;
}
.psh-toast{
  padding:9px 18px;border-radius:var(--r-full);background:var(--ink);color:var(--surface);
  font-size:12.5px;box-shadow:var(--shadow-2);animation:psh-node-in .22s var(--ease-out) both;
}

/* ─────────────────────────── responsive ──────────────────── */
@media (max-width:820px){
  /* BUG-16: 6 rail buttons + theme + mic + the input row were 25-34px on
     a touch surface — below the 44×44 minimum in both WCAG 2.5.5 and
     Apple HIG, and the rail is the PRIMARY navigation on mobile. Rail
     width grows from 46px (34+12 padding) to 56px (44+12); .psh-desk's
     padding-left already matches that new edge (10+56=66) with no other
     change needed. */
  .psh-rail{left:10px;padding:8px 6px;border-radius:18px;}
  .psh-rail-btn{width:44px;height:44px;}
  .psh-theme{width:44px;height:44px;}
  .psh-mic{width:44px;height:44px;}
  .psh-composer input{min-height:44px;}
  .psh-desk{padding-left:66px;padding-right:14px;justify-content:flex-start;}
  .psh-greet{padding-left:66px;padding-right:14px;}
  /* BUG-15: `86vw` was measured against the FULL viewport, not the space
     left after .psh-desk's own left/right padding (66px + 14px on this
     breakpoint). At 390px that put the surface's right edge past the
     viewport with no way to scroll to it — the desk only needs to scroll
     when there is a SECOND card to reach, not to see the first one.
     Subtracting the desk's own gutters makes a single surface fit exactly
     inside the visible frame; multiple surfaces still overflow into the
     desk's real horizontal scroll, which now gets a visible edge cue below. */
  .psh-surface{width:min(calc(100vw - 66px - 14px),440px);}
  .psh-record{display:none;}
  .psh-greet h1{font-size:28px;}

  /* BUG-15: scroll affordance. When the desk truly does have more than one
     surface to reach, a soft trailing-edge fade signals "there is more"
     without JS — pure background-attachment trick, updates itself as the
     user scrolls, costs nothing when there is nothing to scroll to. */
  .psh-desk{
    background:
      linear-gradient(to left,var(--paper),color-mix(in srgb,var(--paper) 0%,transparent) 100%) right/24px 100% no-repeat local,
      linear-gradient(to right,var(--paper),color-mix(in srgb,var(--paper) 0%,transparent) 100%) left/24px 100% no-repeat local,
      linear-gradient(to left,color-mix(in srgb,var(--ink) 14%,transparent),transparent) right/24px 100% no-repeat scroll,
      linear-gradient(to right,color-mix(in srgb,var(--ink) 14%,transparent),transparent) left/24px 100% no-repeat scroll;
    background-repeat:no-repeat;
    background-color:var(--paper);
  }
}
@media (prefers-reduced-motion:reduce){
  .psh *,.psh *::before,.psh *::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
}

/* ── THE MARK's states — a hexagon has no face, so state is motion ──
   rest breathes · think spins · happy flares. Driven by data-mood on .psh. */
/* LAUNCH DAY 2026-09-04: `transform` is off this transition. The three
   mood states below are all ANIMATIONS on the same elements, and a
   transform transition on an element whose transform an animation is
   already driving makes the spin stutter at the moment the mood flips.
   Nothing here needs a transform transition; filter is the only property
   that actually changes outside an animation. */
.psh-entity, .psh-juju img, .psh-voice img, .psh-surface-head img,
.psh-guide-top img, .psh-welcome-card img{
  transition:filter var(--dur-2) var(--ease-out);
}
.psh[data-mood="think"] .psh-juju img{animation:psh-mark-spin 1.15s linear infinite;}
.psh[data-mood="think"] .psh-entity{animation:psh-mark-spin 1.15s linear infinite;}
.psh[data-mood="happy"] .psh-juju img{animation:psh-mark-flare .9s var(--ease-out);}
.psh[data-mood="happy"] .psh-entity{animation:psh-mark-flare .9s var(--ease-out);}
@keyframes psh-mark-spin{to{transform:rotate(360deg);}}
@keyframes psh-mark-flare{
  0%{transform:scale(1);filter:saturate(1) brightness(1);}
  40%{transform:scale(1.22);filter:saturate(1.5) brightness(1.12);}
  100%{transform:scale(1);filter:saturate(1) brightness(1);}
}

/* ============================================================
   FIRST-PAINT PHONE GATE — LAUNCH DAY 2026-09-04
   MEASURED before this rule: a phone painted the full desktop desk
   (11-button rail, centred greeting, JUJU badge, foot line) and only
   swapped to the talk surface once pa-boot.js -> pa-mobile.js had run,
   injected porcelain-mobile.css and set body[data-pa-talk]. Every one
   of those hides lived in porcelain-mobile.css:53-86 behind that JS-set
   body attribute, so none of them could be true at first paint.
   This sheet is linked in the HEAD of os-porcelain.html, so the same
   four elements are hidden by the width alone, before a byte of JS
   runs. porcelain-mobile.css keeps its body[data-pa-talk] rules
   untouched (they still carry the drawer/app modes and they are what
   os/__tests__/porcelain-mobile-canon.test.js P3/P7 assert); this is
   purely the paint that happens BEFORE them.
   767px, not 768: pa-mobile.js isPhone() is `w < PHONE_MAX` (768), so
   768 itself is a desktop and must keep its rail.
   The deck is deliberately NOT hidden: the composer is the one surface
   both widths share, and it is already at the bottom of the shell grid.
   ============================================================ */
@media (max-width:767px){
  .psh-rail,
  .psh-greet,
  .psh-juju,
  .psh-foot{
    display:none;
  }
}
