/* ==========================================================================
   PLATYPUS — css/platypus.css
   ALL styling for the shell. Token-driven: every geometry value is a custom
   property declared in the token block below. Nothing outside that block
   hard-codes a panel size, and no geometry number appears twice.

   Collapsing a panel = redefining exactly ONE token on <body> plus a
   transition. The panel itself only slides (transform) and never changes its
   own width or height.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Z-INDEX LADDER — place new UI IN a stratum, never at "max + 1".

     content     0 ..  50   #pt-canvas 0, .pt-empty 1, full-area view hosts 2
                            (#pt-pl-host, #pt-dn-host, #pt-um-host, …),
                            tile/label overlay 10, marquee 30
     frame      60 ..  90   #pt-ws-tabs 62, zoom/readouts 64
     floating  100 .. 120   #pt-nav 100 (floats over the canvas, under chrome)
     chrome    140 .. 170   #pt-browser + #pt-drawer + #pt-timeline
                            + #pt-bottombar 140,
                            #pt-ribbon + both reveal tabs 150,
                            #pt-appbar 160, #pt-inspector 170
     mascot         200     #pt-waddle (js/waddle.js) — decoration, ABOVE the
                            chrome so it crosses the whole window instead of
                            vanishing behind #pt-browser-tree and .pt-ribbon-body.
                            pointer-events:none is what makes that safe: it
                            passes over a panel without taking a click from it.
     veil          5000     .pt-busy — the one thing meant to cover everything
     pointer  9000 .. 9999  RESERVED: canvas tooltip, crumb/tile flash.
                            Cursor-followers paint over any panel they cross.
     flyout  10001 ..10002  .pt-flyout 10001, .pt-strip (sub-flyout) 10002

   Two load-bearing facts:
     - the app bar (160) is above the ribbon (150) SPECIFICALLY so a collapsed
       ribbon tucks behind it instead of translating through it;
     - flyouts start at 10001, one above the cursor-follower stratum — a
       tooltip at 9000 would otherwise paint straight across an open menu.
   Native <dialog>.showModal() uses the platform top layer and needs no z-index.
   Declare each z-index once per rule; a stale duplicate earlier in the same
   block silently wins.
   -------------------------------------------------------------------------- */

/* ==========================================================================
   1. TOKENS — :root carries the complete LIGHT palette + all geometry.
      body[data-theme="dark"] and body[data-theme="light"] override colour
      only, with identical key sets, so the toggle wins in both directions.
      No hex outside these blocks.
   ========================================================================== */
:root{
  /* -- futureplasma node canvas geometry (js/futureplasma.js NODE_W = node-w) */
  --pt-flow-cell:24px;
  --pt-flow-node-w:180px;
  /* -- shell geometry (the single source per edge) ----------------------- */
  --pt-appbar-h:40px;
  --pt-ribbon-h:82px;
  --pt-browser-w:252px;
  --pt-drawer-w:340px;
  --pt-timeline-h:132px;
  --pt-bottombar-h:26px;    /* the permanent bar under the timeline */
  --pt-tab-w:14px;          /* reveal-tab thickness (browser + drawer agree) */
  --pt-tab-h:64px;          /* reveal-tab length */
  --pt-gap:8px;             /* base gutter — every padding/margin is this or a calc() of it */

  /* derived edges — collapse redefines these, nothing else */
  --pt-top:calc(var(--pt-appbar-h) + var(--pt-ribbon-h));
  --pt-left:var(--pt-browser-w);
  --pt-right:var(--pt-drawer-w);
  /* The bottom edge is TWO surfaces: the bar is always there, the timeline is
     the part that comes and goes, so only the timeline's term drops on collapse. */
  --pt-bottom:calc(var(--pt-bottombar-h) + var(--pt-timeline-h));

  /* -- ribbon geometry ---------------------------------------------------- */
  --pt-ribbon-tab-h:24px;   /* tab-strip tab height (the reveal tabs own --pt-tab-h) */
  --pt-btn-w:54px;          /* responsive: 46px <=1080, 38px <=900 */
  --pt-btn-icon:16px;
  --pt-flyout-w:180px;

  /* -- workspace furniture ------------------------------------------------ */
  --pt-ws-tabs-h:26px;
  /* Ruler band depth. body.pt-rulers-off drops it to 0 and the canvas area
     reclaims the space — one token, same as every other collapse. Keep in sync
     with DEPTH in js/ruler.js. */
  --pt-ruler-d:20px;
  --pt-chip-h:46px;
  --pt-chip-w:104px;

  /* -- floating panels ---------------------------------------------------- */
  --pt-inspector-w:264px;
  --pt-inspector-min-w:172px;

  /* -- controls ----------------------------------------------------------- */
  --pt-ctl:24px;            /* app-bar / panel-head button box */
  --pt-dot:9px;             /* socket dot */
  --pt-dirty-dot:6px;
  --pt-radius:3px;          /* tight radii — desktop CAD, not consumer web */
  --pt-row-h:22px;
  --pt-anim:.18s;

  /* -- type --------------------------------------------------------------- */
  --pt-font:"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --pt-mono:ui-monospace, "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
  --pt-fs:12px;
  --pt-fs-sm:11px;
  --pt-fs-xs:9px;

  /* -- z ladder ----------------------------------------------------------- */
  --pt-z-canvas:0;
  --pt-z-overlay:1;         /* .pt-empty — above the canvas, UNDER the view hosts (2) */
  --pt-z-frame:61;          /* workspace frame furniture */
  --pt-z-furniture:62;      /* #pt-ws-tabs */
  --pt-z-nav:100;
  --pt-z-panel:140;         /* #pt-browser, #pt-drawer, #pt-timeline, #pt-bottombar */
  --pt-z-ribbon:150;        /* #pt-ribbon and both reveal tabs */
  --pt-z-appbar:160;
  --pt-z-inspector:170;
  --pt-z-veil:5000;         /* .pt-busy */
  --pt-z-pointer:9000;      /* RESERVED for cursor-followers */
  --pt-z-flyout:10001;
  --pt-z-strip:10002;

  /* -- LIGHT palette ------------------------------------------------------ */
  --pt-bg:#dfe8e9;
  --pt-panel:#f0f6f6;
  --pt-panel-2:#fbfdfe;
  --pt-line:#bfcccd;
  --pt-hover:#d2efec;
  --pt-fg:#191f2e;
  --pt-fg-dim:#565d6f;
  --pt-accent:#007070;
  --pt-accent-2:#6e3ead;    /* the per-theme glyph colour: re-tuned, never compromised */
  --pt-danger:#c2272d;
  --pt-ok:#297623;
  --pt-warn:#b85d00;
  --pt-veil:rgba(223,232,233,.72);
  --pt-shadow:0 6px 18px rgba(14,40,48,.16);
  --pt-armed:rgba(0,112,112,.14);
  --pt-canvas-bg:#d5dedf;
  --pt-ruler-bg:#e7eeef;
  --pt-ruler-tick:#889596;
  --pt-ruler-label:#4b5b5d;
  --pt-crosshair:rgba(0,112,112,.55);
  /* text on an accent / ok / danger / warn fill */
  --pt-on-accent:#ffffff;
  --pt-hint:#ba0c88;          /* UV magenta: clear of the coral danger and violet glyph */
  /* futureplasma node canvas — aliases restated per theme on purpose: a var()
     alias resolves on the element that declares it, so a :root-only alias
     would carry the light value under the dark body. */
  /* treemap label halo: none on light - any edge there reads as blur or outline */
  --pt-label-halo:none;
  --pt-flow-grid:rgba(0,80,90,.13);
  --pt-flow-edge:var(--pt-fg-dim);
  --pt-flow-edge-hot:var(--pt-accent);
  --pt-flow-port:var(--pt-panel);
  --pt-flow-point:var(--pt-panel-2);
}

/* DARK — default at boot (the pre-paint script stamps data-theme="dark"). */
body[data-theme="dark"]{
  --pt-bg:#0e1424;
  --pt-panel:#171d2f;
  --pt-panel-2:#1e2639;
  --pt-line:#353e54;
  --pt-hover:#283149;
  --pt-fg:#e8ebf4;
  --pt-fg-dim:#a6aec2;
  --pt-accent:#17edd5;
  --pt-accent-2:#c099ff;
  --pt-danger:#fe6b6d;
  --pt-ok:#86df65;
  --pt-warn:#fec348;
  --pt-veil:rgba(14,20,36,.70);
  --pt-shadow:0 8px 24px rgba(2,4,14,.62);
  --pt-armed:rgba(23,237,213,.16);
  --pt-canvas-bg:#0a0f1d;
  --pt-ruler-bg:#121828;
  --pt-ruler-tick:#525a6f;
  --pt-ruler-label:#9fa7bb;
  --pt-crosshair:rgba(23,237,213,.55);
  /* text on an accent / ok / danger / warn fill */
  --pt-on-accent:#0a0f1d;
  --pt-hint:#fd6cd4;          /* UV magenta: clear of the coral danger and violet glyph */
  /* treemap label halo: a soft glow reads as depth on dark */
  --pt-label-halo:0 1px 2px var(--pt-bg),0 0 3px var(--pt-bg);
  --pt-flow-grid:rgba(23,237,213,.07);
  --pt-flow-edge:var(--pt-fg-dim);
  --pt-flow-edge-hot:var(--pt-accent);
  --pt-flow-port:var(--pt-panel);
  --pt-flow-point:var(--pt-panel-2);
}

/* LIGHT spelled explicitly so the toggle wins coming back down. */
body[data-theme="light"]{
  --pt-bg:#dfe8e9;
  --pt-panel:#f0f6f6;
  --pt-panel-2:#fbfdfe;
  --pt-line:#bfcccd;
  --pt-hover:#d2efec;
  --pt-fg:#191f2e;
  --pt-fg-dim:#565d6f;
  --pt-accent:#007070;
  --pt-accent-2:#6e3ead;
  --pt-danger:#c2272d;
  --pt-ok:#297623;
  --pt-warn:#b85d00;
  --pt-veil:rgba(223,232,233,.72);
  --pt-shadow:0 6px 18px rgba(14,40,48,.16);
  --pt-armed:rgba(0,112,112,.14);
  --pt-canvas-bg:#d5dedf;
  --pt-ruler-bg:#e7eeef;
  --pt-ruler-tick:#889596;
  --pt-ruler-label:#4b5b5d;
  --pt-crosshair:rgba(0,112,112,.55);
  /* text on an accent / ok / danger / warn fill */
  --pt-on-accent:#ffffff;
  --pt-hint:#ba0c88;          /* UV magenta: clear of the coral danger and violet glyph */
  /* futureplasma node canvas — aliases restated per theme on purpose: a var()
     alias resolves on the element that declares it, so a :root-only alias
     would carry the light value under the dark body. */
  /* treemap label halo: none on light - any edge there reads as blur or outline */
  --pt-label-halo:none;
  --pt-flow-grid:rgba(0,80,90,.13);
  --pt-flow-edge:var(--pt-fg-dim);
  --pt-flow-edge-hot:var(--pt-accent);
  --pt-flow-port:var(--pt-panel);
  --pt-flow-point:var(--pt-panel-2);
}

/* ==========================================================================
   2. BASE
   ========================================================================== */
*,*::before,*::after{box-sizing:border-box}

/* The UA sheet's [hidden]{display:none} loses to ANY author display rule, so
   an author `display:flex` (e.g. .pt-busy) silently defeats the hidden
   attribute. Restate it here, in the author cascade, with !important so the
   hidden attribute keeps working wherever we set display. */
[hidden]{display:none !important}

html,body{
  /* both get the ground: a transparent body borrows whatever is behind it */
  height:100%;
  margin:0;
  background:var(--pt-bg);
}
body{
  font:var(--pt-fs)/1.4 var(--pt-font);
  color:var(--pt-fg);
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
}

button,input{font:inherit;color:inherit}

button{
  background:none;
  border:0;
  padding:0;
  margin:0;
  color:inherit;
  cursor:pointer;
}

h2{margin:0;font-size:var(--pt-fs);font-weight:600}
p{margin:0}
code{font-family:var(--pt-mono);font-size:var(--pt-fs-sm)}

/* Focus: never suppress the ring without a replacement. */
:focus:not(:focus-visible){outline:none}
:focus-visible{outline:2px solid var(--pt-accent);outline-offset:1px}

/* Thin, themed scrollbars — CAD chrome, not OS chrome. */
*{scrollbar-width:thin;scrollbar-color:var(--pt-line) transparent}
*::-webkit-scrollbar{width:10px;height:10px}
*::-webkit-scrollbar-thumb{background:var(--pt-line);border-radius:var(--pt-radius);border:3px solid transparent;background-clip:content-box}
*::-webkit-scrollbar-track{background:transparent}

/* ==========================================================================
   3. SHELL FRAME — every fixed surface reads the same derived tokens.
   ========================================================================== */
#pt-appbar{
  position:fixed;
  top:0;left:0;right:0;
  height:var(--pt-appbar-h);
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  padding:0 var(--pt-gap);
  background:var(--pt-panel);
  border-bottom:1px solid var(--pt-line);
  z-index:var(--pt-z-appbar);
}

#pt-ribbon{
  position:fixed;
  top:var(--pt-appbar-h);left:0;right:0;
  height:var(--pt-ribbon-h);          /* never changes — the panel slides instead */
  background:var(--pt-panel-2);
  border-bottom:1px solid var(--pt-line);
  z-index:var(--pt-z-ribbon);
  transition:transform var(--pt-anim) ease;
}

#pt-browser{
  position:fixed;
  top:var(--pt-top);left:0;bottom:0;
  width:var(--pt-browser-w);
  display:flex;
  flex-direction:column;
  background:var(--pt-panel);
  border-right:1px solid var(--pt-line);
  z-index:var(--pt-z-panel);
  transition:transform var(--pt-anim) ease, top var(--pt-anim) ease;
}

/* Mirror of #pt-browser on the other edge: same shape, same collapse, and it
   likewise runs the full height so the timeline stops short of it. */
#pt-drawer{
  position:fixed;
  top:var(--pt-top);right:0;bottom:0;
  width:var(--pt-drawer-w);
  display:flex;
  flex-direction:column;
  background:var(--pt-panel);
  border-left:1px solid var(--pt-line);
  z-index:var(--pt-z-panel);
  transition:transform var(--pt-anim) ease, top var(--pt-anim) ease;
}

/* Sits ON the bottom bar, not on the viewport edge: the bar is the permanent
   floor, the strip is the storey that appears above it. */
#pt-timeline{
  position:fixed;
  left:var(--pt-left);right:var(--pt-right);bottom:var(--pt-bottombar-h);
  height:var(--pt-timeline-h);
  display:flex;
  flex-direction:column;
  background:var(--pt-panel);
  border-top:1px solid var(--pt-line);
  z-index:var(--pt-z-panel);
  transition:transform var(--pt-anim) ease, left var(--pt-anim) ease, right var(--pt-anim) ease;
}

/* Same shape as #pt-timeline minus the height: same edges, same slide-with-the-
   docks transition, so the two read as one stack. */
#pt-bottombar{
  position:fixed;
  left:var(--pt-left);right:var(--pt-right);bottom:0;
  height:var(--pt-bottombar-h);
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  padding:0 var(--pt-gap);
  background:var(--pt-panel);
  border-top:1px solid var(--pt-line);
  z-index:var(--pt-z-panel);
  transition:left var(--pt-anim) ease, right var(--pt-anim) ease;
}

/* Right cluster: same margin-left:auto as #pt-appbar-right, deliberately, so
   the two bars are read as one anatomy rather than two conventions. */
/* A6.1 is rank-gated (js/access.js RANK_DOORS) and this rule's own display
   would otherwise beat the UA's [hidden] rule. */
#pt-bottombar-right[hidden]{display:none}
#pt-bottombar-right{
  margin-left:auto;
  /* Shrinkable, NOT flex:none. #pt-appbar-right can afford flex:none because
     nothing in it truncates; this cluster holds the status line, which must be
     allowed to give ground. With flex:none the cluster kept its content width
     and ran off the right edge of the bar on a narrow viewport instead. */
  flex:0 1 auto;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  min-width:0;
}

/* The workspace is positioned by INSETS off the derived tokens, so a collapse
   is one token change and this transition. It fires no layout event of its own:
   chrome.js calls PT_Treemap.resize() on transitionend.
   Insets, not margins: a fixed box with inset:0 AND margins is over-constrained,
   and its used height lags the margin transition instead of tracking it. */
#pt-workspace{
  position:fixed;
  inset:var(--pt-top) var(--pt-right) var(--pt-bottom) var(--pt-left);
  display:flex;
  flex-direction:column;
  min-width:0;
  background:var(--pt-bg);
  transition:inset var(--pt-anim) ease;
}

/* ==========================================================================
   4. COLLAPSE — one token per edge, plus a slide on the panel itself.
      Never animate the custom property; the transition lives on the workspace
      margin and on each panel's transform.
      Collapse is a POSTURE and is deliberately never persisted.
   ========================================================================== */
body.pt-ribbon-collapsed   { --pt-top:var(--pt-appbar-h); }
body.pt-browser-collapsed  { --pt-left:0px; }
body.pt-drawer-collapsed   { --pt-right:0px; }
body.pt-timeline-collapsed { --pt-bottom:var(--pt-bottombar-h); }

body.pt-ribbon-collapsed #pt-ribbon    { transform:translateY(-100%); pointer-events:none; }
body.pt-browser-collapsed #pt-browser  { transform:translateX(-100%); pointer-events:none; }
body.pt-drawer-collapsed #pt-drawer    { transform:translateX(100%);  pointer-events:none; }
/* 100% only reaches the bar's top edge — the strip has to clear the bar too. */
body.pt-timeline-collapsed #pt-timeline{
  transform:translateY(calc(100% + var(--pt-bottombar-h)));
  pointer-events:none;
}

/* ==========================================================================
   5. REVEAL TABS — always in the DOM, shown by pure CSS. z at the ribbon
      stratum (150) so a tab is never buried by the surface it restores.
      Anything that can vanish leaves a handle behind.
   ========================================================================== */
#pt-browser-tab,
#pt-drawer-tab{
  display:none;
  position:fixed;
  align-items:center;
  justify-content:center;
  gap:calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  color:var(--pt-accent-2);
  font-size:var(--pt-fs-xs);
  box-shadow:var(--pt-shadow);
  z-index:var(--pt-z-ribbon);
}
body.pt-browser-collapsed  #pt-browser-tab { display:flex; }
body.pt-drawer-collapsed   #pt-drawer-tab  { display:flex; }
/* The timeline has no reveal tab: its handle is #pt-timeline-tab, now a
   permanent toggle inside #pt-bottombar (styled with the bar's other tenants,
   below). Nothing vanishes off-screen, so nothing needs a handle left behind. */

/* The two side tabs are the same strip; only the edge they hug differs. */
#pt-browser-tab,
#pt-drawer-tab{
  top:50%;
  width:var(--pt-tab-w);
  height:var(--pt-tab-h);
  margin-top:calc(var(--pt-tab-h) / -2);
}
#pt-browser-tab{
  left:0;
  border-left:0;
  border-radius:0 var(--pt-radius) var(--pt-radius) 0;
}
#pt-drawer-tab{
  right:0;
  border-right:0;
  border-radius:var(--pt-radius) 0 0 var(--pt-radius);
}
/* The grip is an affordance, not a label, so it is sized off the tab's own
   thickness rather than the text scale. fa-ellipsis-vertical is a narrow glyph —
   about a quarter of its em — so at --pt-tab-w it renders three fine dots down
   the middle of the strip with clearance either side. */
#pt-browser-tab > i,
#pt-drawer-tab > i{font-size:var(--pt-tab-w)}

#pt-browser-tab:hover,
#pt-drawer-tab:hover{background:var(--pt-hover);color:var(--pt-accent)}

/* ==========================================================================
   6. APP BAR SLOTS
   ========================================================================== */
/* A3 — the brand mark, unchanged as a plain span. ALT-clicking it sends the
   mascot for a walk (js/waddle.js), and that is deliberately given no affordance
   at all: no cursor, no hover, no focus ring. An easter egg that advertises
   itself is just an undocumented button. */
#pt-logo{
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 1.6);
  flex:none;
  color:var(--pt-accent-2);
  padding-right:calc(var(--pt-gap) / 2);
}
/* The artwork is a single-colour line drawing, so it is a MASK filled with
   currentColor rather than an <img>: one file serves both themes and it
   inherits --pt-accent-2 from the button exactly as the icon font did. An
   <img> here would be black line art, invisible on the dark theme.
   aspect-ratio is the cropped file's own 511x217 — keep them in step. */
#pt-logo-mark{
  height:calc(var(--pt-btn-icon) * 1.25);
  aspect-ratio:511 / 217;
  flex:none;
  background:currentColor;
  -webkit-mask:url(platylogo.png) center / contain no-repeat;
  mask:url(platylogo.png) center / contain no-repeat;
}
#pt-wordmark{
  font-size:var(--pt-fs-sm);
  font-weight:700;
  letter-spacing:.14em;
  color:var(--pt-fg);
}

/* --- generic icon button (app bar right cluster, nav cluster) ------------- */
.pt-icon-btn{
  flex:none;
  width:var(--pt-ctl);
  height:var(--pt-ctl);
  display:flex;
  align-items:center;
  justify-content:center;
  border:1px solid transparent;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  transition:background .12s ease, color .12s ease, border-color .12s ease;
}
.pt-icon-btn:hover{background:var(--pt-hover);color:var(--pt-fg)}
/* Stow toggle: LIT while the overlay is SHOWING, dimmed while stowed —
   the inverse of a pressed toggle, because the state reported is the panel's. */
.pt-icon-btn.is-lit,
.pt-panel-btn.is-lit{color:var(--pt-accent-2);border-color:var(--pt-line);background:var(--pt-hover)}

/* --- document identity chip --------------------------------------------- */
/* HIDDEN FOR NOW — the document chip and its save button (A2.1, A2.1.2).
   Hidden in CSS rather than cut from index.html on purpose: chrome.js's
   setDirty / setDocName / clearDocError / save all write to #pt-doc-chip,
   #pt-doc-name and #pt-doc-save, and $one() on a missing element silently
   no-ops, so deleting the markup would leave four writers quietly writing
   nowhere. The elements stay, the writers stay honest, and restoring this is
   deleting the one rule below.
   Note it is display:none, not visibility:hidden — the chip must give its
   space back to the app bar, not leave a gap where it used to sit. */
#pt-doc-chip{display:none !important}

#pt-doc-chip{
  flex:none;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  height:var(--pt-ctl);
  padding:0 calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  transition:border-color .12s ease, box-shadow .12s ease;
}
#pt-doc-name{
  width:13ch;
  min-width:0;
  background:transparent;
  border:0;
  padding:0;
  color:var(--pt-fg);
  font-size:var(--pt-fs-sm);
  text-overflow:ellipsis;
}
#pt-doc-name:focus{outline:none;color:var(--pt-accent)}
#pt-doc-save{
  position:relative;                 /* anchors the dirty dot */
  flex:none;
  width:var(--pt-row-h);
  height:var(--pt-row-h);
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
}
#pt-doc-save:hover{background:var(--pt-hover);color:var(--pt-fg)}

/* Dirty dot: 6px, in the CORNER of the save glyph (a centred dot lands on it),
   plus the glyph recoloured. .is-dirty lives on the chip; chrome.js's
   setDirty() is the only writer. */
#pt-doc-chip.is-dirty #pt-doc-save{color:var(--pt-accent)}
#pt-doc-chip.is-dirty #pt-doc-save::after{
  content:"";
  position:absolute;
  top:0;right:0;
  width:var(--pt-dirty-dot);
  height:var(--pt-dirty-dot);
  border-radius:50%;
  background:var(--pt-accent);
  box-shadow:0 0 0 1px var(--pt-panel);
}

/* Failure divides labour: the chip that failed goes red and STAYS red; the bar
   pulses three beats with NO fill-mode so it settles back. Never tint both. */
#pt-doc-chip.pt-error{border-color:var(--pt-danger);box-shadow:inset 0 0 0 1px var(--pt-danger)}
#pt-doc-chip.pt-error #pt-doc-name{color:var(--pt-danger)}
#pt-appbar:has(#pt-doc-chip.pt-error){animation:pt-appbar-error-pulse .4s 3}
@keyframes pt-appbar-error-pulse{
  50%{background:color-mix(in srgb, var(--pt-danger) 16%, var(--pt-panel))}
}

/* --- status line --------------------------------------------------------- */
#pt-status{
  /* Was flex:1 filling the app bar. In the bottom bar's right cluster it sizes
     to its text instead and truncates at max-width, so a long message never
     walks the cluster leftwards over the breadcrumb. */
  flex:0 1 auto;
  max-width:40ch;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  text-align:left;
  padding:0 calc(var(--pt-gap) / 2);
  height:var(--pt-ctl);
  line-height:var(--pt-ctl);
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  font-size:var(--pt-fs-sm);
}
#pt-status:hover{background:var(--pt-hover);color:var(--pt-fg)}
#pt-status.is-error{color:var(--pt-danger)}
#pt-status.is-ok{color:var(--pt-ok)}
#pt-status.is-warn{color:var(--pt-warn)}
/* Hints are hot pink and bold — they are the one status kind that is trying to
   catch an eye that was not already on the status line. */
#pt-status.is-hint{color:var(--pt-hint);font-weight:600}
/* The hover rule above would otherwise grey a hint out on the way to clicking it. */
#pt-status.is-hint:hover{color:var(--pt-hint)}
/* Same colour in the status log, so a hint is recognisable in the history too. */
.pt-flyout .pt-log-row[data-kind="hint"]{color:var(--pt-hint)}

/* --- right cluster ------------------------------------------------------- */
/* A pill, deliberately — the one exception to the tight --pt-radius, so the
   global controls read as one object. The visible backing is js/socketstatus.js's
   panel-coloured box-shadow ring, which follows this radius; round buttons keep
   a hover highlight on the end controls concentric with the curve. */
#pt-appbar-right{
  margin-left:auto;
  /* On top of #pt-appbar's own padding: socketstatus.js's 4px backing ring
     spreads past this box, and without it the pill sat 4px off the edge. */
  margin-right:var(--pt-gap);
  flex:none;
  display:flex;
  align-items:center;
  /* Full gutter between controls, half of one inside each end: at the tighter
     values the round buttons touched each other and the pill's own curve. */
  gap:var(--pt-gap);
  padding:0 calc(var(--pt-gap) / 2);
  border-radius:999px;
}
#pt-appbar-right .pt-icon-btn{border-radius:50%}
#pt-socket-dot{
  flex:none;
  width:var(--pt-dot);
  height:var(--pt-dot);
  border-radius:50%;
  background:var(--pt-fg-dim);
  margin:0 calc(var(--pt-gap) / 2);
  box-shadow:0 0 0 3px transparent;
  transition:background .12s ease, box-shadow .12s ease;
}
#pt-socket-dot.is-up  {background:var(--pt-ok)}
#pt-socket-dot.is-wait{background:var(--pt-warn)}
#pt-socket-dot.is-down{background:var(--pt-danger)}
#pt-socket-dot:hover{box-shadow:0 0 0 3px var(--pt-hover)}

/* ==========================================================================
   7. RIBBON TAB STRIP (in the app bar) + ribbon body
      The strip survives ribbon collapse and IS the way back in; there is no
      #pt-ribbon-tab. #pt-ribbon-collapse is rendered at the end of the strip
      by ribbon.js and must never slide away with the ribbon.
   ========================================================================== */
#pt-tabstrip{
  flex:none;
  display:flex;
  align-items:flex-end;
  gap:2px;
  height:100%;
  padding-bottom:0;
}
.pt-tab,
.pt-tab-cmd{
  height:var(--pt-ribbon-tab-h);
  padding:0 calc(var(--pt-gap) * 1.25);
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  font-size:var(--pt-fs-xs);
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--pt-fg-dim);
  border:1px solid transparent;
  border-bottom:0;
  border-radius:var(--pt-radius) var(--pt-radius) 0 0;
  transition:background .12s ease, color .12s ease;
}
.pt-tab:hover,
.pt-tab-cmd:hover{color:var(--pt-fg);background:var(--pt-hover)}
/* Active tab is a filled card in the RIBBON's surface colour and crosses the
   bar's bottom border, fusing with the group row below. */
.pt-tab.is-active{
  background:var(--pt-panel-2);
  color:var(--pt-fg);
  border-color:var(--pt-line);
  margin-bottom:-1px;
  padding-bottom:1px;
  /* A little larger than its neighbours: taller (the strip is flex-end, so it
     grows upward and still fuses with the row below) and a step up in type. */
  height:calc(var(--pt-ribbon-tab-h) + 4px);
  padding-left:calc(var(--pt-gap) * 1.5);
  padding-right:calc(var(--pt-gap) * 1.5);
  font-size:var(--pt-fs-sm);
}
.pt-tab-cmd{color:var(--pt-accent-2)}
/* A cmd tab lit as a VIEW posture (#pt-tab-futureplasma; js/futureplasma.js is
   the writer via PT_Ribbon.setArmed). Not .is-active: that card look is the
   switcher's, and a cmd tab never takes it. */
.pt-tab-cmd.is-armed{background:var(--pt-armed);color:var(--pt-fg);border-color:var(--pt-line)}
.pt-tab-cmd.is-armed > i{color:var(--pt-accent)}
#pt-ribbon-collapse{
  width:var(--pt-ctl);
  height:var(--pt-ribbon-tab-h);
  display:flex;
  align-items:center;
  justify-content:center;
  margin-left:calc(var(--pt-gap) / 2);
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
}
#pt-ribbon-collapse:hover{background:var(--pt-hover);color:var(--pt-fg)}

/* --- ribbon body: scrolls horizontally, NEVER wraps ---------------------- */
/* The ribbon is a flex row so the right region can be pinned by layout rather
   than by position:absolute, which would let it sit on top of the body's last
   group instead of taking width from it. */
#pt-ribbon{display:flex;align-items:stretch}

/* Persistent right region: the app bar's right cluster, one row down, which is
   what puts the view toggle under the socket dot. flex:none so it keeps its
   width while the body scrolls in whatever is left. */
.pt-ribbon-right{
  flex:none;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  padding:0 var(--pt-gap);
  border-left:1px solid var(--pt-line);
}
/* R1.5 is OUTSIDE the per-tab groups, so its buttons are not part of the grid
   those groups line up on and have no business being held to the grid's fixed
   --pt-btn-w: a label longer than one cell was simply ellipsised, which is how
   FutureCast (R13) arrived clipped. They size to their own label instead, and
   the region is flex:none so it takes exactly the width that needs. The
   min-width keeps a short label the same size and shape as a grid button, so
   nothing moves for the ones that already fitted. */
.pt-ribbon-right .pt-btn{
  width:auto;
  min-width:var(--pt-btn-w);
  padding-left:var(--pt-gap);
  padding-right:var(--pt-gap);
}
.pt-ribbon-right .pt-btn-label{overflow:visible;text-overflow:clip}

.pt-ribbon-body{
  /* flex:1 + min-width:0 — without min-width a flex item refuses to shrink
     below its content, and the overflow-x below would never engage. */
  flex:1;
  min-width:0;
  height:100%;
  display:flex;
  align-items:stretch;
  flex-wrap:nowrap;
  gap:var(--pt-gap);
  padding:0 var(--pt-gap);
  overflow-x:auto;
  overflow-y:hidden;
  scrollbar-width:none;
}
.pt-ribbon-body::-webkit-scrollbar{display:none}

.pt-group{
  flex:none;
  display:flex;
  flex-direction:column;
  justify-content:center;
  padding:calc(var(--pt-gap) / 2) var(--pt-gap);
  border-right:1px solid var(--pt-line);
}
.pt-group-tools{
  display:flex;
  align-items:flex-start;
  gap:calc(var(--pt-gap) / 2);
}
/* A SLOT centres itself in that row. flex-start above is right for ribbon
   buttons, which are all one height and hang their icon off the top edge; a
   slot is filled by a module with whatever it likes (R1.2.18's date boxes are
   32px against a 47px row), so top-aligning it leaves the content riding high
   against the buttons beside it. align-self here rather than on any one slot's
   own rule: every slot wants this, and a slot that did not could still override
   it from its own stylesheet. */
.pt-group-slot{
  align-self:center;
}
/* R1.4 — THE READOUT GROUP is not there unless it has something to say. Its
   two slots (W13.12.2, W16.3) are filled by the views, and a ribbon group
   standing empty is a divider with a gap behind it.

   display:none!important, and the !important is load-bearing: switchTab()
   toggles every .pt-group with jQuery, which writes an INLINE display, and an
   inline style beats any plain stylesheet rule. The same reason
   #pt-group-home-dates carries one. The marker is is-on, set by whichever
   module filled a slot, so the group follows its contents rather than either
   module having to know the other exists. */
#pt-group-home-readouts:not(:has(.is-on)){
  display:none!important;
}
/* And when it IS there it takes the whole run, up to the right region (R1.5).
   .pt-group is flex:none everywhere else, which is right for a column of
   buttons; a readout is a line of text and the room between the last group and
   the right region is exactly what it has to read in. min-width:0 is the half
   that matters under pressure: a flex item will not shrink below its content
   without it, and the ribbon never wraps — so with it the line ellipses and
   without it the whole body starts scrolling sideways instead. */
#pt-group-home-readouts{
  /* display:flex!important because switchTab() shows a group with jQuery, and
     jQuery writes an INLINE display:block that beats .pt-group's own
     display:flex. Measured, not guessed: the computed display on this group is
     block, which is why flex-direction and justify-content below were doing
     nothing and the line sat at the top of an 81px row. Same reason the rule
     above needs its !important to hide it. */
  display:flex!important;
  flex-direction:column;
  justify-content:center;
  flex:1 1 auto;
  min-width:0;
}
#pt-group-home-readouts .pt-group-tools{
  min-width:0;
}
/* Vertically centred in the row. .pt-group is a column with
   justify-content:center, so the tools row is only centred when it is the ONLY
   child — and every group renders a caption div below it, empty label or not,
   whose margin-top then pushes the readout up off the middle. This group has
   no caption to show, so the div goes rather than being an invisible weight.
   (#pt-group-home-dates hides its label for the same reason.) */
#pt-group-home-readouts .pt-group-label{
  display:none;
}
/* Caption renders BELOW the tools row. */
.pt-group-label{
  margin-top:calc(var(--pt-gap) / 4);
  text-align:center;
  font-size:var(--pt-fs-xs);
  letter-spacing:.09em;
  text-transform:uppercase;
  color:var(--pt-fg-dim);
}

/* --- ribbon buttons: four kinds, one DOM --------------------------------- */
.pt-btn{
  position:relative;
  width:var(--pt-btn-w);
  min-width:var(--pt-btn-w);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:flex-start;
  gap:calc(var(--pt-gap) / 4);
  padding:calc(var(--pt-gap) / 2) 2px;
  border:1px solid transparent;
  border-radius:var(--pt-radius);
  color:var(--pt-fg);
  transition:background .12s ease, border-color .12s ease, color .12s ease;
}
/* Child-scoped: a descendant selector would also match the caret's glyph and
   blow it up at the narrow breakpoints. */
.pt-btn > i{font-size:var(--pt-btn-icon);line-height:1;color:var(--pt-accent-2)}
/* R3.11: the app bar's platypus mark (#pt-logo-mark) as a ribbon glyph, masked
   so it takes the glyph colour in both themes. platylogo.png is 511x217. */
.pt-btn > i.pt-ic-platypus{display:inline-block;height:var(--pt-btn-icon);aspect-ratio:511 / 217;background:currentColor;
  -webkit-mask:url(platylogo.png) center / contain no-repeat;mask:url(platylogo.png) center / contain no-repeat}
.pt-btn-label{
  width:100%;
  font-size:var(--pt-fs-xs);
  line-height:1.2;
  text-align:center;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.pt-btn:hover{background:var(--pt-hover);border-color:var(--pt-line)}

.pt-btn-split{padding-bottom:calc(var(--pt-gap) * 1.5)}
.pt-btn-caret{
  position:absolute;
  left:0;right:0;
  margin:auto;
  bottom:0;
  width:100%;
  height:calc(var(--pt-gap) * 1.4);
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:var(--pt-fs-xs);
  color:var(--pt-fg-dim);
  border-radius:0 0 var(--pt-radius) var(--pt-radius);
}
.pt-btn-caret:hover{background:var(--pt-line);color:var(--pt-fg)}

/* Lit only when the APP calls setArmed — a tool never lights itself. */
.pt-btn.is-armed{background:var(--pt-armed);border-color:var(--pt-accent);color:var(--pt-fg)}
.pt-btn.is-armed > i{color:var(--pt-accent)}
.pt-btn.is-disabled{opacity:.38;pointer-events:none}

/* ==========================================================================
   8. FLYOUTS + one-row sub-flyout strip
      Appended to <body> by ribbon.js (that is what clears the ribbon's and the
      app bar's overflow/stacking contexts), positioned inline after measuring.
      No !important on top/left/right/bottom here.
   ========================================================================== */
.pt-flyout{
  position:absolute;
  min-width:var(--pt-flyout-w);
  max-width:calc(var(--pt-flyout-w) * 2.4);
  padding:calc(var(--pt-gap) / 2) 0;
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  box-shadow:var(--pt-shadow);
  z-index:var(--pt-z-flyout);
}
.pt-flyout.is-wide{min-width:calc(var(--pt-flyout-w) * 1.6)}
/* A plate of doors only: it sizes to the longest name plus its chevron rather
   than to the command width. But min-width:0 let four-letter site codes shrink
   it to 88px, which reads as a tooltip rather than a menu — so it keeps a floor
   of three quarters of the standard plate. Still narrower than .is-wide; just
   no longer cramped. */
.pt-flyout.is-snug{min-width:calc(var(--pt-flyout-w) * 0.75)}
.pt-flyout[data-depth="2"]{z-index:var(--pt-z-strip)}

.pt-flyout-item{
  width:100%;
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  padding:calc(var(--pt-gap) / 2) var(--pt-gap);
  text-align:left;
  font-size:var(--pt-fs-sm);
  color:var(--pt-fg);
}
.pt-flyout-item > i{width:1.2em;text-align:center;color:var(--pt-accent-2)}
.pt-flyout-item > span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pt-flyout-item:hover{background:var(--pt-hover)}
.pt-flyout-item.is-disabled{opacity:.38;pointer-events:none}
.pt-flyout-tick{flex:none;color:var(--pt-accent)}
/* MUST be a <span>: a <button> inside a <button> is dropped by the browser. */
/* Hard right, not floating a gutter short of it.
   `.pt-flyout-item > span` above is a CLASS PLUS A TYPE (0,0,1,1) and so beat a
   bare `.pt-flyout-more` (0,0,1,0): the chevron span inherited flex:1 and split
   the row's slack with the label, drifting inwards the moment the plate was
   wider than its longest name. Matching on the class through the same child
   combinator settles it, and the negative margin pulls the glyph back through
   the row's own padding so it reads as the plate's edge — where it points. */
.pt-flyout-item > .pt-flyout-more{flex:none;color:var(--pt-fg-dim);
  padding:0 calc(var(--pt-gap) / 4);margin-right:calc(var(--pt-gap) / -2)}
.pt-flyout-more:hover{color:var(--pt-fg)}
.pt-flyout-sep{height:1px;margin:calc(var(--pt-gap) / 2) 0;background:var(--pt-line)}

/* --- app menu (A5.1) — the third tenant of .pt-flyout ---------------------
   Same rule as the status log: reuse the plate, add only what is genuinely new.
   What is new here is an identity block that is NOT a row, and one terminal row
   that takes --pt-danger. Both themes come free — every value is a token. */
.pt-appmenu-id{
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  padding:calc(var(--pt-gap) / 2) var(--pt-gap);
  font-size:var(--pt-fs-sm);
  cursor:default;                       /* reads as information, not a target */
}
.pt-appmenu-id > i{width:1.2em;text-align:center;color:var(--pt-accent-2)}
.pt-appmenu-who{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.pt-appmenu-user{
  color:var(--pt-fg);
  font-weight:600;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.pt-appmenu-rank{
  align-self:flex-start;
  font-size:var(--pt-fs-xs);
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--pt-fg-dim);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  padding:0 calc(var(--pt-gap) / 2);
}
/* The row is an <a> when signed out; kill the underline the shared class has
   no reason to carry. */
a.pt-flyout-item{text-decoration:none}
/* Terminal row: the colour IS the warning, so the hover stays the ordinary one
   rather than inventing a red ground that exists nowhere else in the chrome. */
.pt-flyout-item.is-danger,
.pt-flyout-item.is-danger > i{color:var(--pt-danger)}
.pt-flyout-item.is-danger:hover{background:var(--pt-hover);color:var(--pt-danger)}

/* Status log reuses .pt-flyout — resist a second menu component. */
.pt-flyout .pt-log-row{
  display:flex;
  gap:var(--pt-gap);
  padding:2px var(--pt-gap);
  font-size:var(--pt-fs-sm);
  color:var(--pt-fg-dim);
}
.pt-flyout .pt-log-time{flex:none;font-family:var(--pt-mono);font-size:var(--pt-fs-xs)}

.pt-strip{
  position:absolute;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  padding:calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-accent);
  border-radius:var(--pt-radius);
  box-shadow:var(--pt-shadow);
  z-index:var(--pt-z-strip);
}
.pt-strip > i{color:var(--pt-accent-2)}
.pt-strip-label{font-size:var(--pt-fs-sm);color:var(--pt-fg-dim);white-space:nowrap}
.pt-strip-field{
  width:calc(var(--pt-flyout-w) - var(--pt-gap) * 2);
  height:var(--pt-ctl);
  padding:0 calc(var(--pt-gap) / 2);
  background:var(--pt-panel);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  color:var(--pt-fg);
  font-size:var(--pt-fs-sm);
}
.pt-strip-ok,
.pt-strip-close{
  flex:none;
  width:var(--pt-ctl);
  height:var(--pt-ctl);
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
}
.pt-strip-ok{color:var(--pt-accent)}          /* accent marks only the affirmative one */
.pt-strip-ok:hover,
.pt-strip-close:hover{background:var(--pt-hover)}

/* ==========================================================================
   9. PANEL CHROME — shared head / foot / body for browser, timeline, inspector
   ========================================================================== */
/* The drawer's head joins by id, not by class: js/drawer.js codes against the
   contract ids and the markup carries no .pt-panel-head to hook. */
.pt-panel-head,
#pt-drawer-head{
  flex:none;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  height:calc(var(--pt-ctl) + var(--pt-gap) / 2);
  padding:0 calc(var(--pt-gap) / 2) 0 var(--pt-gap);
  background:var(--pt-panel-2);
  border-bottom:1px solid var(--pt-line);
}
.pt-panel-title,
#pt-drawer-title{
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 1.6);
  flex:1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  font-size:var(--pt-fs-xs);
  letter-spacing:.11em;
  text-transform:uppercase;
  color:var(--pt-fg-dim);
}
.pt-panel-title > i{color:var(--pt-accent-2)}
.pt-panel-btn{
  flex:none;
  width:var(--pt-row-h);
  height:var(--pt-row-h);
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  transition:background .12s ease, color .12s ease;
}
/* :not(.is-lit) so a pinned (lit) button keeps its accent colour under the cursor */
.pt-panel-btn:hover:not(.is-lit){background:var(--pt-hover);color:var(--pt-fg)}
.pt-count{flex:none;font-size:var(--pt-fs-xs);color:var(--pt-fg-dim);font-family:var(--pt-mono)}

/* Foot slot: a flex:none SIBLING below the scroller, never a child of it. */
.pt-panel-foot{
  flex:none;
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  height:var(--pt-row-h);
  padding:0 var(--pt-gap);
  background:var(--pt-panel-2);
  border-top:1px solid var(--pt-line);
  font-size:var(--pt-fs-xs);
  color:var(--pt-fg-dim);
}
.pt-foot-item{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pt-foot-right{margin-left:auto;flex:none;font-family:var(--pt-mono)}

/* ==========================================================================
   10. BROWSER TREE
       overflow-x is set EXPLICITLY: with only overflow-y set it computes to
       auto and one over-wide row grows a horizontal scrollbar. Rows ellipsize.
   ========================================================================== */
.pt-tree{
  flex:1;
  min-height:0;
  overflow-y:auto;
  overflow-x:hidden;
  padding:calc(var(--pt-gap) / 2) 0;
}
.pt-tree-row{
  width:100%;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  height:var(--pt-row-h);
  padding:0 var(--pt-gap) 0 calc(var(--pt-gap) / 2);
  font-size:var(--pt-fs-sm);
  color:var(--pt-fg);
  text-align:left;
}
.pt-tree-row[data-depth="1"]{padding-left:calc(var(--pt-gap) * 2)}
.pt-tree-row[data-depth="2"]{padding-left:calc(var(--pt-gap) * 3.5)}
.pt-tree-row[data-depth="3"]{padding-left:calc(var(--pt-gap) * 5)}
/* A row sets display:flex, which beats the [hidden] attribute's UA display:none
   — so collapsing a branch by setting el.hidden would silently do nothing
   without this. One rule, so every tree can use the attribute rather than
   inline styles. */
.pt-tree-row[hidden]{display:none}
.pt-tree-row:hover{background:var(--pt-hover)}
.pt-tree-row.is-current{background:var(--pt-armed);color:var(--pt-fg)}
.pt-tree-twisty{flex:none;width:calc(var(--pt-gap) * 1.25);text-align:center;color:var(--pt-fg-dim);font-size:var(--pt-fs-xs)}
.pt-tree-icon{flex:none;color:var(--pt-accent-2)}
/* ONE shrinking element per row; every trailing control is fixed-width and
   right-aligned so the value column lines up across rows. */
.pt-tree-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Flex, not text-align: an inline badge sits on the text baseline and reads
   low in the row. min-width, not width: a 4-digit count must not overflow. */
.pt-tree-value{flex:none;min-width:5ch;display:flex;align-items:center;justify-content:flex-end;font-family:var(--pt-mono);font-size:var(--pt-fs-xs);color:var(--pt-fg-dim)}
/* B2.14.2: the treemap glyph in the Projects row's value column. The same slot
   the Participants wrench (B2.7.1) sits in, but this one takes a click — hence
   the cursor and the hover, which a passive mark has no business wearing. */
.pt-tree-mapglyph{cursor:pointer;padding:0 1px;transition:color .12s}
.pt-tree-mapglyph:hover{color:var(--pt-accent)}
.pt-tree-pid{font-family:var(--pt-mono);font-size:var(--pt-fs-xs);color:var(--pt-fg-dim);margin-right:.6ch}
/* B2.15 project rows: the pid in hot pink — it is what tickets and other
   systems quote — and a long description scrolls instead of being cut. The
   name box is a size container, so the inner span can slide by exactly its
   own overflow (100cqw - 100%, clamped at 0): a short name never moves, and
   nothing is measured in JS. Back and forth, with a rest at each end. */
.pt-tree-project .pt-tree-name{display:flex;align-items:baseline}
.pt-tree-project .pt-tree-pid{flex:none;color:var(--pt-hint)}
.pt-tree-nva{color:var(--pt-warn)}
.pt-tree-project.is-nva{background:color-mix(in srgb,var(--pt-warn) 14%,transparent)}
.pt-tree-value .pt-tree-nva{margin-left:4px}
/* UAT: the same light blue as the strip's UAT pill (js/people.js .is-uat) */
.pt-tree-uat{color:#4a9fd8}
.pt-tree-live{color:var(--pt-ok)}
.pt-tree-project.is-uat{background:color-mix(in srgb,#4a9fd8 14%,transparent)}
/* B2.15.4: the open project's --pt-armed wash wins over its UAT/NVA wash; at
   equal specificity those two came later and hid which project was open. */
.pt-tree-project.is-current.is-uat,.pt-tree-project.is-current.is-nva{background:var(--pt-armed)}
.pt-tree-marquee{flex:1;min-width:0;overflow:hidden;container-type:inline-size}
.pt-tree-marquee>span{display:inline-block;white-space:nowrap;animation:pt-marquee 9s linear infinite alternate}
@keyframes pt-marquee{0%,20%{transform:translateX(0)}80%,100%{transform:translateX(min(0px,calc(100cqw - 100%)))}}
@media (prefers-reduced-motion:reduce){.pt-tree-marquee>span{animation:none}}
/* min-width: four digits wide whatever the count (4ch + 2 x .8ch padding + 2 x
   1px border), so the column lines up on both edges; a fifth digit grows it
   rather than clipping. */
.pt-tree-count{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:1.8em;min-width:calc(5.6ch + 2px);padding:0 .8ch;line-height:1;font-size:var(--pt-fs-sm);font-variant-numeric:tabular-nums;border-radius:999px;background:var(--pt-panel-2);border:1px solid var(--pt-line);color:var(--pt-fg)}
/* Flex centres the LINE BOX, not the ink: digits have no descender and the
   browser rounds ascent/descent to whole px, so the number rides ~0.5-1px
   high (measured, ui-monospace 11px). Where text-box exists, trim the line
   box to cap-height/baseline and pad the pill back to 1.8em from that — the
   ink is then centred by construction in any font. Elsewhere the flex rule
   above stands. */
@supports (text-box:trim-both cap alphabetic){
  .pt-tree-count{display:inline-block;text-align:center;text-box:trim-both cap alphabetic;padding-block:calc((1.8em - 1cap) / 2 - 1px)}
}

/* ==========================================================================
   11. WORKSPACE FURNITURE — plus the tenants of #pt-bottombar (the crumb
       cluster and the timeline toggle), which read the same chip vocabulary
       even though the bar itself is a shell surface up in section 3.
   ========================================================================== */
#pt-ws-tabs{
  flex:none;
  height:var(--pt-ws-tabs-h);
  display:flex;
  align-items:stretch;
  gap:2px;
  padding:0 calc(var(--pt-gap) / 2);
  background:var(--pt-panel);
  border-bottom:1px solid var(--pt-line);
  position:relative;
  z-index:var(--pt-z-furniture);
  /* Pills never shrink or wrap, so the strip scrolls sideways (scrollbar hidden;
     renderTabs scrolls the active pill into view). scroll-padding keeps that
     pill clear of the sticky + on the right. */
  overflow-x:auto;
  overflow-y:hidden;
  scrollbar-width:none;
  scroll-padding-right:calc(var(--pt-row-h) + var(--pt-gap));
}
#pt-ws-tabs::-webkit-scrollbar{display:none}
.pt-ws-tab{
  flex:none;
  white-space:nowrap;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  padding:0 var(--pt-gap);
  margin:calc(var(--pt-gap) / 4) 0;
  border-radius:var(--pt-radius);
  /* Outlined, so a tab reads as a tab before it is hovered or active — the
     strip is otherwise flat on the panel colour. The pills stretch to the
     strip's height between fixed margins, so the border costs no height. */
  border:1px solid var(--pt-line);
  font-size:var(--pt-fs-sm);
  color:var(--pt-fg-dim);
}
.pt-ws-tab:hover{background:var(--pt-hover);color:var(--pt-fg);border-color:var(--pt-accent)}
.pt-ws-tab.is-active{background:var(--pt-accent);color:var(--pt-on-accent);border-color:var(--pt-accent)}
.pt-ws-add{
  flex:none;
  width:var(--pt-row-h);
  margin:calc(var(--pt-gap) / 4) 0;
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  font-size:var(--pt-fs-xs);
  /* Always reachable: sticks to the strip's right edge once the pills overflow,
     painted on the strip's own ground (the shadow covers the flex gap). */
  position:sticky;
  right:0;
  background:var(--pt-panel);
  box-shadow:-2px 0 0 var(--pt-panel);
}
.pt-ws-add:hover{background:var(--pt-hover);color:var(--pt-fg)}

/* User workspace tabs (js/futureplasma.js renderTabs — the strip's one writer;
   a tab IS a Drawflow module). The trailing ✕ is a <span> role=button: a
   <button> inside a <button> is dropped by the parser. Every colour is a token
   or an alpha over the pill's own fill, so both themes come free. */
.pt-ws-tab[data-ws]{padding-right:calc(var(--pt-gap) / 2)}
.pt-ws-label{white-space:nowrap}
.pt-ws-close{
  flex:none;
  width:1.5em;
  height:1.5em;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-xs);
  line-height:1;
  color:inherit;
  opacity:.6;
}
.pt-ws-tab:hover .pt-ws-close{opacity:.85}
.pt-ws-close:hover{opacity:1;background:var(--pt-line)}
.pt-ws-tab.is-active .pt-ws-close:hover{background:rgba(255,255,255,.22)}
/* Inline rename: the pill steps aside (not [hidden] — the pill's own
   display:flex would win) and an <input> sized to its text takes the slot. */
.pt-ws-tab.is-renaming{display:none}
.pt-ws-rename{
  flex:none;
  align-self:center;
  min-width:6ch;
  height:calc(var(--pt-ws-tabs-h) - var(--pt-gap) / 2);
  padding:0 calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  color:var(--pt-fg);
  border:1px solid var(--pt-accent);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-sm);
  outline:none;
}

/* Close callout (W7.4): the one question the strip asks, anchored under the
   tab by js/futureplasma.js openCallout. It lives on <body> at the FLYOUT
   rung (10001, see the z-ladder) — #pt-ws-tabs is its own stacking context at
   the furniture rung (62), so a child popover could never clear #pt-nav. */
#pt-ws-callout{
  position:fixed;
  z-index:var(--pt-z-flyout);
  min-width:220px;
  max-width:320px;
  padding:var(--pt-gap);
  background:var(--pt-panel-2);
  color:var(--pt-fg);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  box-shadow:var(--pt-shadow);
  font-size:var(--pt-fs-sm);
}
.pt-ws-callout-msg{margin:0 0 var(--pt-gap);line-height:1.4}
.pt-ws-callout-actions{display:flex;gap:calc(var(--pt-gap) / 2);justify-content:flex-end}
.pt-ws-callout-btn{
  padding:calc(var(--pt-gap) / 2) var(--pt-gap);
  background:var(--pt-panel);
  color:var(--pt-fg);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-sm);
}
.pt-ws-callout-btn:hover{background:var(--pt-hover)}
.pt-ws-callout-btn.is-danger{background:var(--pt-danger);border-color:var(--pt-danger);color:var(--pt-on-accent)}
.pt-ws-callout-btn.is-danger:hover{filter:brightness(1.12)}

/* A tenant of #pt-bottombar now, not a workspace row: the bar owns the ground,
   the border and the height, so this is only the crumb-and-chips cluster and
   the flex slot it claims. */
#pt-crumbs{
  flex:1;
  min-width:0;
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
}
/* Leftmost in the cluster, ahead of the path. It used to sit in the browser
   foot and inherited that block's size and colour; here it states both. */
#pt-version{
  flex:none;
  font-family:var(--pt-mono);
  font-size:var(--pt-fs-xs);
  color:var(--pt-fg-dim);
  opacity:.65;
}
.pt-crumb-path{
  flex:1;
  min-width:0;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  overflow:hidden;
  white-space:nowrap;
}
.pt-crumb{
  flex:none;
  padding:0 calc(var(--pt-gap) / 2);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-sm);
  color:var(--pt-accent);
}
.pt-crumb:hover{background:var(--pt-hover)}
.pt-crumb.is-current{color:var(--pt-fg);cursor:default}
.pt-crumb.is-current:hover{background:none}
.pt-crumb-sep{flex:none;color:var(--pt-fg-dim);font-size:var(--pt-fs-sm)}

/* The readout IS the control. Max three chips. The separator is drawn by the
   chip that FOLLOWS one, so a missing chip can never leave a lone divider. */
.pt-readouts{flex:none;display:flex;align-items:center}
.pt-readout{
  position:relative;
  padding:0 var(--pt-gap);
  font-size:var(--pt-fs-xs);
  font-family:var(--pt-mono);
  color:var(--pt-fg-dim);
  border-radius:var(--pt-radius);
}
.pt-readout:hover{background:var(--pt-hover);color:var(--pt-fg)}
.pt-readout + .pt-readout::before{
  content:"";
  position:absolute;
  left:0;top:20%;bottom:20%;
  width:1px;
  background:var(--pt-line);
}

/* The timeline toggle, the bar's right-hand tenant. It reads like a readout
   chip on purpose — same row, same weight — but it is lit while the strip is
   open, because the state it reports is the strip's, not the button's. */
#pt-timeline-tab{
  flex:none;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  padding:0 var(--pt-gap);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-xs);
  color:var(--pt-fg-dim);
}
#pt-timeline-tab:hover{background:var(--pt-hover);color:var(--pt-fg)}
body:not(.pt-timeline-collapsed) #pt-timeline-tab{color:var(--pt-accent-2)}

/* --- ruler workspace (WEBAPPUX 04 §1–2) ---------------------------------
   The frame takes the flex slot; the canvas area is inset inside it by the
   ruler depth. The inset lives HERE and never on #pt-canvas-area, because
   treemap.js sizes the canvas backing store off its parent's rect — the area
   must stay exactly the canvas box.
   Rulers are top + left only: the treemap always fits its viewport, so a
   bottom/right pair would restate a coordinate that is never scrolled away. */
#pt-ruler-frame{
  position:relative;
  flex:1;
  min-height:0;
  background:var(--pt-ruler-bg);
}
.pt-ruler{
  position:absolute;
  display:block;
  z-index:var(--pt-z-frame);
  background:var(--pt-ruler-bg);
}
/* A <canvas> is a REPLACED element: with left+right (or top+bottom) and
   width:auto it falls back to its 300x150 intrinsic size and the far offset is
   dropped. Both dimensions must be stated explicitly — never inset a bare
   canvas on all four sides. */
#pt-ruler-top{
  top:0;
  left:var(--pt-ruler-d);
  width:calc(100% - var(--pt-ruler-d));
  height:var(--pt-ruler-d);
}
#pt-ruler-left{
  top:var(--pt-ruler-d);
  left:0;
  width:var(--pt-ruler-d);
  height:calc(100% - var(--pt-ruler-d));
}
#pt-ruler-corner{
  position:absolute;
  top:0;left:0;
  width:var(--pt-ruler-d);
  height:var(--pt-ruler-d);
  z-index:calc(var(--pt-z-frame) + 1);
  background:var(--pt-ruler-bg);
  border-right:1px solid var(--pt-line);
  border-bottom:1px solid var(--pt-line);
}
#pt-crosshair-tog{
  display:flex;align-items:center;justify-content:center;
  width:100%;height:100%;padding:0;border:0;
  background:transparent;color:var(--pt-fg-dim);font-size:11px;cursor:pointer;
}
#pt-crosshair-tog:hover{ color:var(--pt-fg) }
#pt-crosshair-tog[aria-pressed="true"]{ color:var(--pt-accent) }
#pt-crosshair-tog:focus-visible{ outline:2px solid var(--pt-accent); outline-offset:-2px }
/* One token to nothing, exactly like every other collapse. */
body.pt-rulers-off{ --pt-ruler-d:0px; }
body.pt-rulers-off .pt-ruler,
body.pt-rulers-off #pt-ruler-corner{ display:none }

/* Crosshair + the bars painted into each ruler band. All four are shown and
   hidden together by .is-tracking on the frame — never four booleans. */
.pt-crosshair,.pt-ruler-mark{
  position:absolute;
  display:none;
  pointer-events:none;
}
#pt-ruler-frame.is-tracking .pt-ruler-mark,
body.pt-crosshair-on #pt-ruler-frame.is-tracking .pt-crosshair{ display:block }
body.pt-rulers-off .pt-crosshair,
body.pt-rulers-off .pt-ruler-mark{ display:none !important }

/* Crosshair spans the canvas only; marks sit in the ruler bands, one rung up. */
#pt-crosshair-h{
  left:var(--pt-ruler-d); right:0; height:1px;
  margin-top:var(--pt-ruler-d);
  background:var(--pt-crosshair);
  z-index:var(--pt-z-pointer);
}
#pt-crosshair-v{
  top:var(--pt-ruler-d); bottom:0; width:1px;
  margin-left:var(--pt-ruler-d);
  background:var(--pt-crosshair);
  z-index:var(--pt-z-pointer);
}
#pt-ruler-mark-h{
  top:0;
  width:1px;
  height:var(--pt-ruler-d);
  margin-left:var(--pt-ruler-d);
  background:var(--pt-accent);
  z-index:calc(var(--pt-z-pointer) + 1);
}
#pt-ruler-mark-v{
  left:0;
  height:1px;
  width:var(--pt-ruler-d);
  margin-top:var(--pt-ruler-d);
  background:var(--pt-accent);
  z-index:calc(var(--pt-z-pointer) + 1);
}

/* The coordinate readout is display, not a control — no hover affordance. */
.pt-readout.is-static{cursor:default;min-width:64px;text-align:center}
.pt-readout.is-static:hover{background:none;color:var(--pt-fg-dim)}

/* --- canvas area: the only place that insets off the furniture tokens ---- */
#pt-canvas-area{
  position:absolute;
  inset:var(--pt-ruler-d) 0 0 var(--pt-ruler-d);
  min-height:0;
  background:var(--pt-canvas-bg);
  overflow:hidden;
}
#pt-canvas{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  display:block;
  z-index:var(--pt-z-canvas);
}

/* --- futureplasma: the Drawflow node canvas, a sibling of #pt-canvas -------
   body.pt-view-futureplasma is the ONE switch (js/futureplasma.js writes it):
   it shows #pt-flow and hides the treemap-only furniture. The area itself is
   never hidden, so treemap.js measure() keeps a real box throughout.
   Engine root: the public stylesheet's .parent-drawflow{display:flex} loses
   to the id here, so the default is display:none without a [hidden] attr. */
#pt-flow{
  position:absolute;
  inset:0;
  display:none;
  z-index:var(--pt-z-canvas);
  background:var(--pt-canvas-bg);
  cursor:grab;
}
#pt-flow:active{cursor:grabbing}
body.pt-view-futureplasma #pt-flow{display:block}
body.pt-view-futureplasma #pt-canvas,
body.pt-view-futureplasma #pt-canvas-area > .pt-empty,
body.pt-view-futureplasma #pt-nav,
/* #pt-crumbs, not #pt-bottombar: the crumb cluster describes a treemap root
   this view has not got, but the bar and its timeline toggle are shell. */
body.pt-view-futureplasma #pt-crumbs{display:none}
/* Backdrop on the STATIC host, never the moving layer. */
#pt-flow.pt-flow-grid{
  background-size:var(--pt-flow-cell) var(--pt-flow-cell);
  background-image:linear-gradient(to right,var(--pt-flow-grid) 1px,transparent 1px),
                   linear-gradient(to bottom,var(--pt-flow-grid) 1px,transparent 1px);
}
/* Engine skin — drawflow.min.css hardcodes cyan/red/yellow/steelblue; every
   colour below is a token, so the data-theme flip re-skins nodes and the SVG
   edges live with no repaint hook. */
#pt-flow .drawflow .drawflow-node{
  width:var(--pt-flow-node-w);
  min-height:40px;
  padding:0;
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-left:3px solid var(--pt-fg-dim);
  border-radius:var(--pt-radius);
  color:var(--pt-fg);
  box-shadow:var(--pt-shadow);
  overflow:visible;                       /* ports overhang the card */
}
#pt-flow .drawflow .drawflow-node.selected{
  background:var(--pt-panel-2);
  border-color:var(--pt-accent);
  box-shadow:0 0 0 3px var(--pt-armed),var(--pt-shadow);
}
#pt-flow .drawflow .drawflow-node.pt-node-source{border-left-color:var(--pt-ok)}
#pt-flow .drawflow .drawflow-node.pt-node-filter{border-left-color:var(--pt-accent)}
#pt-flow .drawflow .drawflow-node.pt-node-transform{border-left-color:var(--pt-accent-2)}
#pt-flow .drawflow .drawflow-node.pt-node-merge{border-left-color:var(--pt-warn)}
#pt-flow .drawflow .drawflow-node.pt-node-sink{border-left-color:var(--pt-danger)}
.pt-node-card{padding:var(--pt-gap);font-size:var(--pt-fs);line-height:1.3}
.pt-node-head{display:flex;gap:calc(var(--pt-gap) / 2);align-items:center;font-weight:600}
.pt-node-head > i{color:var(--pt-accent-2)}

/* Per-node transport, pinned to the right of the head. margin-left:auto is what
   pushes it there — the head is already flex, so no extra wrapper is needed.
   Which buttons show is driven entirely by the group's data-state, so the JS sets
   one attribute and never touches individual buttons. */
.pt-node-run{display:inline-flex;gap:2px;margin-left:auto}
.pt-node-run-btn{
  display:flex;
  align-items:center;
  justify-content:center;
  width:18px;
  height:18px;
  padding:0;
  border:0;
  border-radius:var(--pt-radius);
  background:transparent;
  color:var(--pt-fg-dim);
  font-size:9px;
  line-height:1;
  cursor:pointer;
}
.pt-node-run-btn:hover{background:var(--pt-hover);color:var(--pt-fg)}
.pt-node-run-btn:focus-visible{outline:2px solid var(--pt-accent);outline-offset:1px}
/* Resting: play only. Running: pause + stop. Paused: play + stop. */
.pt-node-run[data-state="idle"] .pt-node-pause,
.pt-node-run[data-state="idle"] .pt-node-stop,
.pt-node-run[data-state="running"] .pt-node-play,
.pt-node-run[data-state="paused"] .pt-node-pause{display:none}
.pt-node-run[data-state="running"] .pt-node-pause{color:var(--pt-warn)}
.pt-node-run[data-state="running"] .pt-node-stop,
.pt-node-run[data-state="paused"] .pt-node-stop{color:var(--pt-danger)}
.pt-node-field{
  width:100%;
  box-sizing:border-box;
  margin-top:calc(var(--pt-gap) / 2);
  min-height:calc(var(--pt-row-h) * 2);
  background:var(--pt-panel);
  color:var(--pt-fg);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  font:inherit;
  resize:vertical;
}
#pt-flow .drawflow .drawflow-node .input,
#pt-flow .drawflow .drawflow-node .output{
  width:14px;height:14px;
  background:var(--pt-flow-port);
  border:2px solid var(--pt-fg-dim);
}
#pt-flow .drawflow .drawflow-node .input:hover,
#pt-flow .drawflow .drawflow-node .output:hover{border-color:var(--pt-accent)}
#pt-flow .drawflow .drawflow-node .input{left:-22px}
#pt-flow .drawflow .drawflow-node .output{right:-8px}
/* Second port sits a third of the way down so two-input cards do not stack ports at the top. */
#pt-flow .drawflow .drawflow-node .inputs .input:nth-child(2),
#pt-flow .drawflow .drawflow-node .outputs .output:nth-child(2){top:33%}
#pt-flow .drawflow .connection .main-path{stroke:var(--pt-flow-edge);stroke-width:3px}
/* Marching ants. The dash pattern plus a looping offset is the whole effect —
   the keyframe travels exactly one dash+gap (8+6) so the loop is seamless with no
   visible jump. Deliberately NOT on every connector at rest: a canvas of edges all
   crawling at once is noise, and it animates forever for no information. It marks
   the edge you are pointing at or have selected, which is the state that exists.
   The reduced-motion block at the end of this file neutralises the animation and
   leaves a plain dashed line, which still reads as selected. */
@keyframes pt-flow-ants{to{stroke-dashoffset:-14}}
#pt-flow .drawflow .connection .main-path:hover,
#pt-flow .drawflow .connection .main-path.selected{
  stroke:var(--pt-flow-edge-hot);
  stroke-dasharray:8 6;
  animation:pt-flow-ants .55s linear infinite;
}
#pt-flow .drawflow .connection .point{fill:var(--pt-flow-point);stroke:var(--pt-accent)}
#pt-flow .drawflow .connection .point.selected,
#pt-flow .drawflow .connection .point:hover{fill:var(--pt-flow-edge-hot)}
/* The delete bubble. Stock drawflow.min.css writes a literal "x" text node and
   centres it vertically with line-height:30px. Our box-sizing is border-box, so
   the 30px includes a 2px border and the content box is only 26px tall — the 30px
   line box overflows it and drops the glyph ~1.5px low (measured). Horizontal was
   always fine; this is a vertical-only bug. Blank the text and draw fa-xmark
   instead, centred with flex, which measures the content box and so cannot drift.
   ponytail: CSS-only because drawflow is a CDN build we do not vendor — patching
   its contextmenu() would mean forking it. Revisit only if we ever self-host. */
#pt-flow .drawflow-delete{
  background:var(--pt-danger);
  border-color:var(--pt-panel-2);
  color:var(--pt-panel-2);
  display:flex;
  align-items:center;
  justify-content:center;
  line-height:1;
  font-size:0;
}
#pt-flow .drawflow-delete::before{
  font:900 14px/1 "Font Awesome 6 Free";
  content:"\f00d";
}

/* Catalogue rows (js/futureplasma.js swaps these into #pt-browser-tree while
   the view is up): the tree row look plus a drag affordance. */
.pt-fp-row{cursor:grab}
.pt-fp-row:active,
.pt-fp-row.is-dragging{cursor:grabbing}
.pt-fp-row.is-dragging{opacity:.5}
.pt-fp-row:hover .pt-tree-twisty{color:var(--pt-fg)}

/* Empty state: inset to the canvas area only, frame visible around it.
   pointer-events off the backdrop so it can never swallow a canvas click;
   only the card itself is interactive.
   Hidden by default — it is a render of "the treemap has no root", flipped by
   the single writer in treemap.js draw(): body.classList.toggle('pt-tree-empty',
   !currentRoot()). With no writer it simply never shows, and never blocks the
   tiles. Scoped to the canvas area so an unrelated .pt-empty elsewhere (e.g. the
   inspector's empty note) is never turned into a full-bleed overlay.
   It sits at 1: above the canvas, but UNDER every full-area view host (they all
   paint at z-index 2 inside this same area). Each view also hides it by its own
   view class, but that is now belt and braces — the card can no longer end up
   floating over a view whose author did not know to write that rule. */
#pt-canvas-area > .pt-empty{
  position:absolute;
  inset:0;
  display:none;
  align-items:center;
  justify-content:center;
  padding:calc(var(--pt-gap) * 3);
  pointer-events:none;
  z-index:var(--pt-z-overlay);
}
body.pt-tree-empty:not(.pt-view-futureplasma) #pt-canvas-area > .pt-empty{display:flex} /* the flow view keeps the empty card down */
.pt-empty-card{
  pointer-events:auto;
  max-width:calc(var(--pt-flyout-w) * 2.2);
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:var(--pt-gap);
  padding:calc(var(--pt-gap) * 2.5);
  text-align:center;
  /* No fill, no border: the picture is the whole card and the canvas shows
     through around it. --pt-panel here put a light slab over the treemap. */
  background:transparent;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
}
.pt-empty-card > i{font-size:calc(var(--pt-btn-icon) * 1.8);color:var(--pt-accent-2)}
.pt-empty-card h2{color:var(--pt-fg)}
.pt-empty-card p{font-size:var(--pt-fs-sm);line-height:1.6}
/* Wordless card: the picture is the whole content, so the card is just a frame
   around it. Capped against the canvas too, or a short viewport clips it. */
.pt-empty-platypus{
  display:block;
  width:100%;
  max-width:calc(var(--pt-flyout-w) * 1.6);
  max-height:46vh;
  height:auto;
  object-fit:contain;
}

/* Inline "nothing here" note inside a panel body — plain flow text, never an
   overlay. app.js should inject <p class="pt-empty-note"> rather than .pt-empty. */
.pt-empty-note{color:var(--pt-fg-dim);font-size:var(--pt-fs-sm)}

/* The [hidden] guard is load-bearing: this author rule sets display:flex, which
   beats the UA stylesheet's [hidden]{display:none} no matter the specificity.
   Without it the veil is welded on. Single writer toggles the attribute. */
.pt-busy[hidden]{display:none}
.pt-busy{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--pt-veil);
  z-index:var(--pt-z-veil);
}
.pt-busy-card{
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  padding:var(--pt-gap) calc(var(--pt-gap) * 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  box-shadow:var(--pt-shadow);
  font-size:var(--pt-fs-sm);
}
.pt-busy-card > i{color:var(--pt-accent)}

/* Floating nav cluster — a real background, because it occludes the canvas. */
#pt-nav{
  position:absolute;
  left:50%;
  bottom:calc(var(--pt-gap) * 1.5);
  transform:translateX(-50%);
  display:flex;
  gap:calc(var(--pt-gap) / 4);
  padding:calc(var(--pt-gap) / 4);
  background:var(--pt-panel);
  border:1px solid var(--pt-line);
  border-radius:calc(var(--pt-radius) * 2);
  box-shadow:var(--pt-shadow);
  z-index:var(--pt-z-nav);
}
.pt-nav-btn{
  width:calc(var(--pt-ctl) + var(--pt-gap) / 2);
  height:calc(var(--pt-ctl) + var(--pt-gap) / 2);
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:var(--pt-radius);
  color:var(--pt-fg-dim);
  transition:background .12s ease, color .12s ease;
}
.pt-nav-btn:hover{background:var(--pt-hover);color:var(--pt-accent)}
/* W6.4 — the sites, left of the three commands. A word, not a glyph: the
   site code IS its icon. The divider goes with the slot when it is empty. */
.pt-nav-sites{display:flex;gap:calc(var(--pt-gap) / 4)}
.pt-nav-site{width:auto;padding:0 calc(var(--pt-gap) / 1.4);font-size:var(--pt-fs-sm);font-weight:600;letter-spacing:.03em}
.pt-nav-site.is-current{color:var(--pt-accent);background:var(--pt-hover)}
.pt-nav-sep{width:1px;align-self:stretch;margin:calc(var(--pt-gap) / 4);background:var(--pt-line)}
.pt-nav-sites:empty,.pt-nav-sites:empty + .pt-nav-sep{display:none}
/* With the three commands hidden the sites ARE the cluster, so no sites (before
   session:state, or an account with none) means no cluster — not an empty pill. */
#pt-nav:has(.pt-nav-sites:empty){display:none}

/* W6.4.1 — THE HANDLE. The strip above is the chooser for an EMPTY work area
   and goes the moment a view stands in one, which leaves the one thing you
   cannot do from inside a view: go straight to another site. This brings it
   back over the view without closing it.

   It is there only while a view is (body.pt-view-up, written by fitNavSites),
   because a handle for a strip that is already up is noise, and not at all
   when there are no sites to show — the same :has test the cluster itself
   makes, asked one element higher.

   The revealed strip needs !important and it is not a specificity fight: every
   view hides #pt-nav with a body-class rule of its own AND fitNavSites writes
   display:none INLINE on the element. A stylesheet !important is the one thing
   that clears both. */
#pt-nav-peek{
  display:none;
  /* One above --pt-z-panel clears the bars and the drawer and nothing else;
     higher would also clear the app bar and the inspector, which are not this
     tab's business. It only works from the SHELL: #pt-canvas-area clips, and a
     handle on its edge inside it painted but took no clicks. */
  position:fixed;
  /* ON THE RIGHT, ON THE STRIP'S OWN LINE. Both of these are spelt out of the
     same tokens the strip is, so the handle and the thing it reveals share a
     top edge and a bottom edge: the strip sits --pt-gap * 1.5 above the bottom
     of #pt-canvas-area, which is itself --pt-bottom above the window, and it is
     as tall as one .pt-nav-btn plus the strip's padding and its two borders.
     Read the strip's rules before changing either — a figure typed here
     instead of derived would drift the moment the strip's padding changed.
     test_nav_peek.js measures both against #pt-canvas-area, so drift fails
     rather than slides. */
  right:var(--pt-right);
  bottom:calc(var(--pt-bottom) + var(--pt-gap) * 1.5);
  height:calc(var(--pt-ctl) + var(--pt-gap) + 2px);
  /* The right of the WORK AREA, which is the right of the screen whenever the
     drawer is closed — and when it is open the handle tucks against it rather
     than floating over it, which it would, being one z-index above the panels.
     Carried by the drawer's own transition, so it slides with it instead of
     jumping when it comes and goes. */
  align-items:center;
  justify-content:center;
  /* AS THIN AS THE TWO REVEAL TABS (D1.1), off the same token: this is the same
     affordance on the same kind of edge, and a wide amber slab beside them read
     as a different class of thing. The length stays the strip's, so it is a
     14px sliver on the strip's own line rather than a 64px one at mid-height. */
  width:var(--pt-tab-w);
  background:var(--pt-warn);
  border:1px solid var(--pt-warn);
  border-right:0;
  /* Rounded on the side it opens from and flat on the edge it is mounted to,
     so it reads as a tab tucked against that edge rather than a pill floating
     near it — the same shape it had against the bottom, turned a quarter. */
  border-radius:calc(var(--pt-radius) * 2) 0 0 calc(var(--pt-radius) * 2);
  color:var(--pt-on-accent);
  font-size:var(--pt-fs-sm);
  line-height:1;
  cursor:pointer;
  z-index:calc(var(--pt-z-panel) + 1);
  transition:filter .12s ease, right var(--pt-anim) ease, bottom var(--pt-anim) ease;
}
/* --pt-warn is a dark amber on light and a bright one on dark, so the glyphs on
   it go the other way. Two declarations rather than a third token: this is the
   only element that sits ON the warn colour. */
body[data-theme="dark"] #pt-nav-peek{color:var(--pt-bg)}
body.pt-view-up #pt-nav-peek{display:flex}
body.pt-view-up:has(.pt-nav-sites:empty) #pt-nav-peek{display:none}
/* The handle and the strip are never up together: it is the strip's understudy,
   on stage only while the strip is off it. The press that brings the strip back
   takes the handle away in the same beat, which is why the grip is a thing you
   only ever see over a view. */
body.pt-nav-peek #pt-nav-peek{display:none}
#pt-nav-peek:hover{filter:brightness(1.12)}
/* Sized off the tab's own thickness, not the text scale, exactly as the reveal
   tabs size theirs: fa-ellipsis-vertical is about a quarter of its em, so at
   --pt-tab-w it renders three fine dots down the middle with clearance either
   side. */
#pt-nav-peek > i{font-size:var(--pt-tab-w)}
#pt-nav-peek:focus-visible{outline:2px solid var(--pt-accent);outline-offset:2px}
/* The braille that flanked the chevron is gone with the width it needed: at
   --pt-tab-w there is room for the grip and nothing else, which is the same
   trade the reveal tabs make — at 14px an icon cannot name a destination, so
   the title and the aria-label carry it. */
/* Revealed, the strip stays exactly where it lives — bottom-centre of the work
   area, the same seat it has when nothing is standing in one. Only its
   VISIBILITY is what the handle changes: a strip that moved when it was asked
   for would be a second control, and the one place it is ever looked for is the
   place it is already in. */
body.pt-nav-peek #pt-nav{display:flex !important}

/* ==========================================================================
   12. TIMELINE CHIPS — click LOCATES, never replays.
   ========================================================================== */
.pt-chips{
  flex:1;
  min-height:0;
  display:flex;
  align-items:center;
  gap:calc(var(--pt-gap) / 2);
  padding:var(--pt-gap);
  overflow-x:auto;
  overflow-y:hidden;
}
.pt-chip{
  flex:none;
  width:var(--pt-chip-w);
  height:var(--pt-chip-h);
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:1px;
  padding:0 calc(var(--pt-gap) / 1.4);
  text-align:left;
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-bottom:2px solid var(--pt-fg-dim);
  border-radius:var(--pt-radius);
  transition:background .12s ease, border-color .12s ease;
}
.pt-chip:hover{background:var(--pt-hover)}
.pt-chip.is-current{border-color:var(--pt-accent)}
.pt-chip[data-type="drill"] {border-bottom-color:var(--pt-accent)}
.pt-chip[data-type="up"]    {border-bottom-color:var(--pt-accent-2)}
.pt-chip[data-type="root"]  {border-bottom-color:var(--pt-warn)}
.pt-chip[data-type="reload"]{border-bottom-color:var(--pt-ok)}
.pt-chip-n{font-family:var(--pt-mono);font-size:var(--pt-fs-xs);color:var(--pt-fg-dim)}
.pt-chip-label{font-size:var(--pt-fs-sm);color:var(--pt-fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pt-chip-sub{font-size:var(--pt-fs-xs);color:var(--pt-fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pt-chips-hint{font-size:var(--pt-fs-sm);color:var(--pt-fg-dim);padding-left:calc(var(--pt-gap) / 2)}

/* ==========================================================================
   13. INSPECTOR — floating, draggable, stowable.
       Default position comes from PLAIN SPECIFICITY. There is deliberately no
       !important on top/left/right/bottom anywhere in this section: jQuery UI
       writes inline top/left during a drag and an !important sheet rule would
       freeze that axis while the other still moves.
   ========================================================================== */
#pt-inspector{
  position:fixed;
  top:calc(var(--pt-top) + var(--pt-gap));
  /* Off --pt-right so it rests beside the drawer rather than on top of it.
     Only the resting place: a drag writes inline left/top and wins from there. */
  right:calc(var(--pt-right) + var(--pt-gap) * 2);
  width:var(--pt-inspector-w);
  display:flex;
  flex-direction:column;
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  box-shadow:var(--pt-shadow);
  z-index:var(--pt-z-inspector);
  transition:width var(--pt-anim) ease;   /* never a positional property */
}
/* Stow: display:none, driven by chrome.js's stow() — the single setter.
   A stow is a PREFERENCE and persists forever (unlike a collapse). */
body.pt-inspector-stowed #pt-inspector{display:none}

/* The grip is a <span>: jQuery UI's default `cancel` list (input, textarea,
   button, select, option) is evaluated BEFORE `handle`, so a <button> grip is
   silently cancelled. That same default is what lets the header's own buttons
   act instead of dragging. */
.pt-panel-grip{
  flex:none;
  display:flex;
  align-items:center;
  padding-right:calc(var(--pt-gap) / 2);
  color:var(--pt-fg-dim);
  cursor:move;
  touch-action:none;
  user-select:none;
}
.ui-draggable-dragging{transition:none !important}

.pt-panel-body{
  padding:var(--pt-gap);
  max-height:calc(100vh - var(--pt-top) - var(--pt-bottom) - var(--pt-gap) * 6);
  overflow-y:auto;
  overflow-x:hidden;
}
.pt-section-label{
  margin:calc(var(--pt-gap) / 2) 0 calc(var(--pt-gap) / 4);
  font-size:var(--pt-fs-xs);
  letter-spacing:.11em;
  text-transform:uppercase;
  color:var(--pt-fg-dim);
}
.pt-row{
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  height:var(--pt-row-h);
  font-size:var(--pt-fs-sm);
}
.pt-row-label{flex:none;width:7ch;color:var(--pt-fg-dim)}
.pt-row-value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--pt-mono);font-size:var(--pt-fs-xs)}
.pt-panel-actions{margin-top:var(--pt-gap);display:flex;justify-content:flex-end}
.pt-text-btn{
  padding:calc(var(--pt-gap) / 3) calc(var(--pt-gap) / 1.4);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-xs);
  color:var(--pt-fg-dim);
}
.pt-text-btn:hover{background:var(--pt-hover);color:var(--pt-fg)}

/* Minify = the SAME panel with the words taken out. Not a second panel.
   chrome.js re-anchors to the right edge on the way in and out so a narrower
   panel left at a fixed left does not appear to jump sideways. */
#pt-inspector.is-min{width:var(--pt-inspector-min-w)}
#pt-inspector.is-min .pt-section-label,
#pt-inspector.is-min .pt-row-label,
#pt-inspector.is-min .pt-panel-actions{display:none}

/* Pin = Esc-immunity and nothing else. Tilted at rest, upright + accented
   while holding. */
#pt-inspector-pin i{transform:rotate(-38deg);transition:transform .12s ease}
#pt-inspector.is-pinned #pt-inspector-pin{color:var(--pt-accent)}
#pt-inspector.is-pinned #pt-inspector-pin i{transform:none}

/* ==========================================================================
   14. TOOLS DRAWER — innards only. Its frame, collapse and reveal tab live
       with their siblings in sections 3–5, and its head reuses .pt-panel-head
       (section 9) by id. Nothing here states a colour of its own.
   ========================================================================== */
#pt-drawer-body{
  flex:1;
  min-height:0;
  overflow-y:auto;
  overflow-x:hidden;
  padding:var(--pt-gap);
}
#pt-drawer-empty{
  padding:calc(var(--pt-gap) * 2) 0;
  text-align:center;
  color:var(--pt-fg-dim);
  font-size:var(--pt-fs-sm);
}
.pt-drawer-field{
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  min-height:var(--pt-row-h);
  margin-bottom:calc(var(--pt-gap) / 2);
  font-size:var(--pt-fs-sm);
}
.pt-drawer-field > label{flex:none;width:7ch;color:var(--pt-fg-dim)}
/* Read-only values are mono and ellipsize: an id is scanned character by
   character, and a long one must not widen the panel. */
.pt-drawer-field > output{
  flex:1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  font-family:var(--pt-mono);
  font-size:var(--pt-fs-xs);
}
.pt-drawer-field > input{
  flex:1;
  min-width:0;
  height:var(--pt-row-h);
  padding:0 calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  outline:none;
}
.pt-drawer-field > input:focus{border-color:var(--pt-accent)}

/* A URL and a paragraph of notes cannot sit next to a 7ch label in a 280px
   panel — the field stacks instead, and only for those. */
.pt-drawer-field.is-stacked{
  display:block;
  min-height:0;
}
.pt-drawer-field.is-stacked > label{
  display:block;
  width:auto;
  margin-bottom:calc(var(--pt-gap) / 3);
}
.pt-drawer-field.is-stacked > input,
.pt-drawer-field > textarea{
  width:100%;
  box-sizing:border-box;
}
.pt-drawer-field > textarea{
  padding:calc(var(--pt-gap) / 2);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  color:inherit;
  font:inherit;
  font-size:var(--pt-fs-sm);
  resize:vertical;
  outline:none;
}
.pt-drawer-field > textarea:focus{border-color:var(--pt-accent)}
#pt-drawer-iss-title{
  margin:0 0 var(--pt-gap);
  font-size:var(--pt-fs-sm);
  line-height:1.35;
  word-break:break-word;
}
/* The link wraps rather than ellipsizing: a truncated URL is unreadable, and
   this is the one thing in the drawer a person is meant to read in full. */
.pt-drawer-link{
  display:block;
  margin:0 0 var(--pt-gap);
  font-size:var(--pt-fs-xs);
  color:var(--pt-accent);
  text-decoration:none;
  word-break:break-all;
}
.pt-drawer-link:hover{text-decoration:underline}
.pt-drawer-err{
  display:block;
  font-size:var(--pt-fs-xs);
  color:var(--pt-danger);
  word-break:break-word;
}

/* Destructive, so the word is red but the button stays outline-only until
   hover — a filled red plate would out-shout every other control in the shell. */
.pt-drawer-danger{
  margin-top:var(--pt-gap);
  padding:calc(var(--pt-gap) / 3) calc(var(--pt-gap) / 1.4);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  font-size:var(--pt-fs-xs);
  color:var(--pt-danger);
}
.pt-drawer-danger:hover{background:var(--pt-hover);border-color:var(--pt-danger)}

/* ==========================================================================
   15. RESPONSIVE — ribbon button width shrinks at two breakpoints. The label
       is capped to the same width, which is why split buttons wear `short`.
   ========================================================================== */
@media (max-width:1080px){
  :root{--pt-btn-w:46px;--pt-btn-icon:15px;--pt-browser-w:212px}
}
@media (max-width:900px){
  :root{--pt-btn-w:38px;--pt-btn-icon:14px;--pt-browser-w:188px;--pt-inspector-w:224px}
  #pt-wordmark{display:none}
}

/* ==========================================================================
   16. REDUCED MOTION — every pulse resolves to a STATIC equivalent, never to
       nothing. The error wash becomes a persistent red bottom border.
   ========================================================================== */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    transition-duration:.001ms !important;
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
  }
  #pt-appbar:has(#pt-doc-chip.pt-error){
    animation:none;
    border-bottom-color:var(--pt-danger);
  }
}

/* ==========================================================================
   17. AUTH PAGES — register.html / signin.html.
       Standalone focused pages, not the full chrome: one centred card on the
       app ground. Token-only, like everything above; both themes come free
       because nothing here restates a colour.

       body.pt-auth OVERRIDES the shell's `overflow:hidden` — the shell is a
       fixed-height grid that must never scroll, but a card page with a long
       error state has to. Setting it on the page class rather than on `body`
       keeps the shell rule intact.
   ========================================================================== */
body.pt-auth{
  overflow:auto;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:calc(var(--pt-gap) * 3);
  min-height:100%;
}

.pt-auth-card{
  width:100%;
  max-width:calc(var(--pt-browser-w) + var(--pt-inspector-w));
  background:var(--pt-panel);
  border:1px solid var(--pt-line);
  border-radius:calc(var(--pt-radius) * 2);
  box-shadow:var(--pt-shadow);
  padding:calc(var(--pt-gap) * 3);
  display:flex;
  flex-direction:column;
  gap:calc(var(--pt-gap) * 2);
}

.pt-auth-brand{
  display:flex;
  align-items:center;
  gap:var(--pt-gap);
  color:var(--pt-fg-dim);
  font-size:var(--pt-fs-sm);
  letter-spacing:.14em;
  text-transform:uppercase;
}
.pt-auth-brand > i{font-size:var(--pt-btn-icon);color:var(--pt-accent-2)}

.pt-auth-card h1{
  margin:0;
  font-size:calc(var(--pt-fs) * 1.6);
  font-weight:600;
  line-height:1.25;
  color:var(--pt-fg);
}
.pt-auth-sub{margin:0;color:var(--pt-fg-dim);font-size:var(--pt-fs)}

/* Identity / note blocks. .is-bad recolours the whole block, so a state change
   is one class write and never a second colour writer. */
.pt-auth-block{
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-left:calc(var(--pt-radius) + 1px) solid var(--pt-accent-2);
  border-radius:var(--pt-radius);
  padding:calc(var(--pt-gap) * 1.5);
  color:var(--pt-fg);
  font-size:var(--pt-fs);
  display:flex;
  gap:var(--pt-gap);
  align-items:flex-start;
}
.pt-auth-block > i{margin-top:1px;color:var(--pt-accent-2)}
.pt-auth-block.is-bad{border-left-color:var(--pt-danger)}
.pt-auth-block.is-bad > i{color:var(--pt-danger)}
.pt-auth-block strong{color:var(--pt-accent)}
.pt-auth-block-body{min-width:0}
.pt-auth-block-note{display:block;margin-top:calc(var(--pt-gap) / 2);color:var(--pt-fg-dim);font-size:var(--pt-fs-sm)}

/* Field. The badge target for a section tag is this wrapper, never the
   <input> — the overlay appends the badge as a CHILD and a form control
   cannot host one. */
.pt-auth-field{display:flex;flex-direction:column;gap:calc(var(--pt-gap) / 2)}
.pt-auth-field label{color:var(--pt-fg-dim);font-size:var(--pt-fs-sm)}
.pt-auth-field input{
  height:calc(var(--pt-ctl) * 1.4);
  padding:0 var(--pt-gap);
  background:var(--pt-panel-2);
  border:1px solid var(--pt-line);
  border-radius:var(--pt-radius);
  color:var(--pt-fg);
}
.pt-auth-field input:focus-visible{border-color:var(--pt-accent)}
.pt-auth-hint{color:var(--pt-fg-dim);font-size:var(--pt-fs-sm)}

/* Primary action. Disabled while a ceremony is in flight — pointer-events
   stays ON (unlike .pt-btn.is-disabled) so the cursor still says "no". */
.pt-auth-go{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:var(--pt-gap);
  width:100%;
  min-height:calc(var(--pt-ctl) * 1.6);
  padding:0 calc(var(--pt-gap) * 2);
  background:var(--pt-accent);
  border:1px solid var(--pt-accent);
  border-radius:var(--pt-radius);
  color:var(--pt-on-accent);       /* on the accent fill in BOTH themes */
  font-size:calc(var(--pt-fs) * 1.1);
  font-weight:600;
  cursor:pointer;
  transition:filter var(--pt-anim) ease, opacity var(--pt-anim) ease;
}
.pt-auth-go:hover:not(:disabled){filter:brightness(1.12)}
.pt-auth-go:disabled{opacity:.45;cursor:not-allowed}
.pt-auth-go .fa-spinner{display:none}
.pt-auth-go.is-busy .pt-auth-go-icon{display:none}
.pt-auth-go.is-busy .fa-spinner{display:inline-block;animation:pt-auth-spin .9s linear infinite}
@keyframes pt-auth-spin{to{transform:rotate(360deg)}}

/* Status region. Always in the DOM and always aria-live, so a screen reader
   hears every outcome; empty text simply renders as nothing. */
.pt-auth-status{
  min-height:calc(var(--pt-row-h));
  font-size:var(--pt-fs);
  color:var(--pt-fg-dim);
  display:flex;
  gap:var(--pt-gap);
  align-items:flex-start;
}
.pt-auth-status:empty{display:none}
.pt-auth-status.is-error{color:var(--pt-danger)}
.pt-auth-status.is-ok{color:var(--pt-ok)}
.pt-auth-status.is-busy{color:var(--pt-fg-dim)}

.pt-auth-foot{
  display:flex;
  flex-wrap:wrap;
  gap:calc(var(--pt-gap) * 2);
  padding-top:var(--pt-gap);
  border-top:1px solid var(--pt-line);
  font-size:var(--pt-fs-sm);
}
.pt-auth-foot a,.pt-auth-card a{color:var(--pt-accent);text-decoration:none}
.pt-auth-foot a:hover,.pt-auth-card a:hover{text-decoration:underline}

/* Success link — a real anchor, promoted to look like the primary action. */
.pt-auth-continue{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:var(--pt-gap);
  min-height:calc(var(--pt-ctl) * 1.6);
  border:1px solid var(--pt-ok);
  border-radius:var(--pt-radius);
  color:var(--pt-ok) !important;
  font-weight:600;
}
.pt-auth-continue:hover{background:var(--pt-hover);text-decoration:none !important}

@media (max-width:480px){
  body.pt-auth{padding:var(--pt-gap);align-items:flex-start}
  .pt-auth-card{padding:calc(var(--pt-gap) * 2)}
}

/* ==========================================================================
   AUTH GATE (#pt-gate) — the "Checking your session…" veil over the shell.
   UX ONLY: this hides the app from a signed-out visitor, it does not protect
   anything. The real gate is server-side (session_at() in every socket
   handler). See js/auth-gate.js.

   body.pt-gating is added PRE-PAINT by the inline script at the top of
   index.html's <body>, exactly like data-theme, and removed by
   js/auth-gate.js on reveal.

   visibility, NOT display:none, on #pt-app. The shell must still LAY OUT
   while it is hidden: js/treemap.js measures the canvas at boot, and a canvas
   that boots at 0x0 stays 0x0 — display:none would trade a flash for a blank
   treemap. visibility:hidden paints nothing and measures correctly.
   ========================================================================== */
body.pt-gating #pt-app{visibility:hidden}

#pt-gate{
  position:fixed;
  inset:0;
  z-index:400;                 /* above #pt-inspector (170) and every flyout */
  display:flex;
  align-items:center;
  justify-content:center;
  padding:calc(var(--pt-gap) * 3);
  background:var(--pt-bg);     /* opaque: the shell must not read through it */
  overflow:auto;
}
/* Higher specificity than the #pt-gate display:flex above, so the plain
   `hidden` attribute is enough to retire the veil. */
#pt-gate[hidden]{display:none}

@media (max-width:480px){
  #pt-gate{padding:var(--pt-gap);align-items:flex-start}
}
