/* ============================================================================
   justask UI
   THE single source of truth for tokens and components.
   Every screen links this file. Never redefine a token in a screen.

   Locked 2026-09-03 after Edo accepted the mascot, the aura and the composer.
   Read justask/DESIGN.md before changing anything here.
   ========================================================================== */

/* ---------------------------------------------------------------- TOKENS -- */
:root{
  color-scheme:dark;

  /* Surfaces. The FIRST step is large (dL .090) and the rest are small nudges
     (dL .035). Every dark reference Edo chose does this. Interpolating five
     evenly spaced surfaces is what actually reads as generated. */
  --canvas:#0E1111;      /* OKL .175 */
  --surface:#232626;     /* OKL .265 */
  --raised:#2B2F2F;      /* OKL .300 */
  --active:#343737;      /* OKL .335 */

  /* Hairlines are LIGHTER than their surface on dark, the inverse of light UI. */
  --line:#2B2F2F;
  --line-2:#343737;

  --text:#E6E8E8;        /* OKL .930. Never #FFFFFF, it blooms at APCA Lc 107. */
  --text-2:#ADB2B2;      /* OKL .760 */
  --text-3:#878E8E;      /* OKL .640. AA on canvas 5.68:1, on surface 4.57:1. */

  --action:#F0F2F2;      /* the single filled action per screen */
  --ink:#141717;         /* ink on that action */

  /* The Dots cream aura. The ONLY colour in the product, and it lives inside
     the mascot and nowhere else. Lifted from the site's own system. */
  --s1:#FAF2E0;          /* pale ivory */
  --s2:#F0E0BE;          /* cream */
  --s3:#E0C698;          /* champagne */

  /* AGENT HUE.
     The aura is now written in OKLCH so a single hue angle rotates it while
     LIGHTNESS AND CHROMA STAY FIXED. That is the whole trick: every agent
     colour is exactly as bright and exactly as saturated as every other, so no
     agent is louder than another and none of them can become a status colour.

     84 is the measured mean hue of the original cream (its stops run 80.7 to
     86.9, and that internal drift is preserved below as offsets, because it is
     part of why the aura reads as light rather than as paint).

     This does NOT widen "nothing is colour coded". Colour still lives only
     inside the mascot, and it identifies a CHARACTER, not a category, which is
     the same carve-out vendor marks get. His own reference set carries coloured
     character avatars and monochrome chrome, which is exactly this split. */
  --agent-h:84;

  /* Radius encodes LAYER, never size. A container is always rounder than its
     contents. Ratio 1.8 to 2.7 across every reference. */
  --r-sm:6px;            /* rows, tags, anything under ~32px tall */
  --r-md:12px;           /* controls inside a container */
  --r-lg:24px;           /* the outermost frame: composer, sheet, modal */
  --r-pill:999px;        /* ONLY the single primary action on a screen */

  --rail:248px;
  --ease:cubic-bezier(.2,0,.38,.9);
}

/* ------------------------------------------------------------------ BASE -- */
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;background:var(--canvas);color:var(--text);
  font-family:Figtree,-apple-system,BlinkMacSystemFont,"SF Pro Text",system-ui,sans-serif;
  font-size:14px;line-height:1.5;letter-spacing:-.006em;
  -webkit-font-smoothing:antialiased;
}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;letter-spacing:inherit}
:focus-visible{outline:2px solid var(--action);outline-offset:2px;border-radius:var(--r-sm)}
/* Author display rules beat the UA default, so hidden has to win explicitly. */
[hidden]{display:none!important}

/* Any figure. Ages, counts, credits. Always tabular so columns do not jitter. */
.num{font-variant-numeric:tabular-nums}

/* ==========================================================================
   MASCOT
   One disc, two capsules. Dark opaque GLASS: a sheen at the crown, the aura
   fused into the bottom, one hairline rim. The gradient exists here and
   NOWHERE else in the product. Nothing glows behind it.

   Size with --bot-size. Works from 16px to 160px unchanged.
   Every execution agent is this same head with different eyes.
   ========================================================================== */
@property --ax{syntax:"<percentage>";initial-value:50%;inherits:false}
@property --ay{syntax:"<percentage>";initial-value:86%;inherits:false}
@property --bx{syntax:"<percentage>";initial-value:38%;inherits:false}
@property --by{syntax:"<percentage>";initial-value:76%;inherits:false}

.bot{position:relative;width:var(--bot-size,96px);height:var(--bot-size,96px);
  flex:none;display:block}

.bot .head{
  position:absolute;inset:0;border-radius:50%;
  /* the edge catch. was rgb(250,242,224) = oklch(.9626 .0252 86.9) */
  border:1px solid oklch(.9626 .0252 calc(var(--agent-h) + 2.9) / .13);
  background:
    /* the glass sheen at the crown. Achromatic, so it never takes the hue:
       a sheen is the light source, not the colour of the object. */
    radial-gradient(ellipse 64% 40% at 48% 9%,
      rgba(255,255,255,.16) 0%, rgba(255,255,255,0) 70%),
    /* the aura, pooled at the bottom, breathing and drifting.
       Half Light: vertical radius 58%, so it rises to the midline.
       Each stop keeps its original hue offset from the base. */
    radial-gradient(ellipse 74% 58% at var(--ax) var(--ay),
      oklch(.9109 .0481 calc(var(--agent-h) + 1.8) / .38) 0%,
      oklch(.8373 .0673 calc(var(--agent-h) - 2.3) / .24) 36%,
      oklch(.6976 .0881 calc(var(--agent-h) - 3.3) / .08) 62%,
      transparent                                          80%),
    /* the dark body. Neutral, always: the bead is the same object whoever
       is wearing it, and only the light inside it changes. */
    radial-gradient(circle at 42% 32%, #23282A 0%, #171B1C 64%, #101314 100%);
  animation:bot-aura-rise 9s ease-in-out infinite alternate,
            bot-aura-drift 13s ease-in-out infinite alternate;
}
@keyframes bot-aura-rise{from{--ay:89%}to{--ay:80%}}
@keyframes bot-aura-drift{from{--ax:44%}to{--ax:56%}}

/* Working. The aura lifts and quickens from INSIDE, because nothing may glow
   behind the bead. Put data-state="running" on any ancestor. */
[data-state="running"] .bot .head{
  animation-duration:3.6s,5.2s;
  background:
    radial-gradient(ellipse 64% 40% at 48% 9%,
      rgba(255,255,255,.18) 0%, rgba(255,255,255,0) 70%),
    radial-gradient(ellipse 80% 66% at var(--ax) var(--ay),
      oklch(.9341 .0395 calc(var(--agent-h) + 2.1) / .54) 0%,
      oklch(.8373 .0673 calc(var(--agent-h) - 2.3) / .34) 36%,
      oklch(.6976 .0881 calc(var(--agent-h) - 3.3) / .12) 62%,
      transparent                                          80%),
    radial-gradient(circle at 42% 32%, #23282A 0%, #171B1C 64%, #101314 100%);
}

/* A SECOND body of light, counter phased against the first and on a different
   period, so a working bead looks like something is turning over inside it
   rather than a loop playing back. The two never line up: 4.4s against 3.1s.

   Resting is byte for byte untouched. This exists only while a run is going,
   which is also what keeps it off the banned list: an infinite animation is
   only allowed when it is reporting a job that is genuinely running. And it
   is INSIDE the bead, because nothing may ever glow behind it. */
.bot .head::after{content:"";position:absolute;inset:0;border-radius:50%;
  opacity:0;pointer-events:none;transition:opacity .55s var(--ease);
  background:radial-gradient(ellipse 50% 38% at var(--bx) var(--by),
    rgba(250,242,224,.30) 0%,
    rgba(224,198,152,.13) 46%,
    rgba(0,0,0,0)         72%)}
[data-state="running"] .bot .head::after{opacity:1;
  animation:bot-pool-x 4.4s ease-in-out infinite alternate,
            bot-pool-y 3.1s ease-in-out infinite alternate}
@keyframes bot-pool-x{from{--bx:31%}to{--bx:67%}}
@keyframes bot-pool-y{from{--by:79%}to{--by:61%}}

.bot .eyes-svg{position:absolute;inset:0;width:100%;height:100%;z-index:2}
.bot .eyes{fill:var(--s1);animation:bot-look 9s ease-in-out infinite}
@keyframes bot-look{
  0%,26%{transform:translateX(0)}
  34%,48%{transform:translateX(3px)}
  56%,72%{transform:translateX(-3px)}
  80%,100%{transform:translateX(0)}
}
/* The blink has to live on the <use> element, not on the .eye rects. Those
   rects exist only inside <defs> and inside the use shadow tree, and a document
   stylesheet reaches NEITHER: only inherited properties cross that boundary,
   which is why the eyes are cream (fill inherits) but never actually blinked.
   Two elements, two transforms: .eyes carries the look, <use> carries the
   blink, so neither overwrites the other. */
.bot .eyes use{transform-box:fill-box;transform-origin:center;
  animation:bot-blink 8.4s ease-in-out infinite}

/* Irregular on purpose: one blink, then a double a beat later. An even pulse
   reads as a loop playing. Real eyes are not metronomes. */
@keyframes bot-blink{
  0%,30%,34%,75%,79%,83%,100%{transform:scaleY(1)}
  32%,77%,81%{transform:scaleY(.08)}
}
[data-state="running"] .bot .eyes use{animation-duration:5.2s}

/* The hero instance breathes. A 20px one in a list must not. */
.bot--hero{animation:bot-breathe 5s ease-in-out infinite}
@keyframes bot-breathe{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}
[data-state="running"] .bot--hero{animation-duration:2.2s}

/* The eyes go where you are. Pointer driven, so nothing loops: the idle sweep
   takes back over a couple of seconds after you stop moving. Applied by ui.js
   to the hero instance only, because a 20px bead in a row tracking the cursor
   is the Clippy failure. */
.bot--track .eyes{animation:none;
  transform:translate(var(--ex,0px),var(--ey,0px));
  transition:transform .14s linear}

/* The bead in flight, on send. Fixed, so it can leave the column it started in
   and land in the row that picks up the job. */
.bot--flying{position:fixed;z-index:60;pointer-events:none;
  transform-origin:top left;will-change:transform}

/* ==========================================================================
   SURFACE
   A fill step plus ONE lighter hairline. That is it.

   This is NOT glass and must not become glass. Glass is the mascot's material
   alone. backdrop-filter on a content card is on the ban list, and on a flat
   dark ground there is nothing behind it to refract anyway.

   The two still read as one family, because both carry the same lighter rim.
   A sheen was tried here and removed: it was decoration nobody asked for, it
   appears in none of the references, and it broke contrast measurement.
   ========================================================================== */
.surface{
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  transition:border-color .16s var(--ease);
}
.surface:focus-within{border-color:var(--line-2)}

/* A card is the same material at the card radius, not the frame radius.
   Radius encodes layer: 24 is an outermost frame, 12 is a card.

   A card is a FLEX COLUMN with one gap, so spacing between its parts is a
   single number instead of a pile of margins that never quite agree. Grid
   items stretch, so `margin-top:auto` on the last child pins every action to
   the same baseline across a row. */
.card{display:flex;flex-direction:column;gap:14px;
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-md);padding:18px;text-align:left;width:100%;
  transition:background .14s var(--ease),border-color .14s var(--ease)}
.card:hover{background:var(--raised);border-color:var(--line-2)}
.card > *{margin:0}
.card-foot{margin-top:auto}
/* not yet connected: quieter, and it does not pretend to be a live object */
.card--muted{background:none}
.card--muted:hover{background:var(--surface)}

.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(324px,1fr));gap:12px}

/* ---- form field, for anything the user types into a sheet --------------- */
.field{display:block;width:100%;height:38px;padding:0 12px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);
  color:var(--text);font:inherit;font-size:14px;outline:none;
  transition:border-color .14s var(--ease)}
.field::placeholder{color:var(--text-3)}
.field:focus{border-color:var(--line-2)}
.field + .field{margin-top:8px}

/* ==========================================================================
   COMPOSER
   A stack, never a line: context above, input, controls below.
   Container 24, controls 12, ratio 2.0, which is what grok measures.
   ========================================================================== */
.composer{width:100%;padding:8px}          /* compose with .surface */

.composer textarea{
  display:block;width:100%;background:none;border:0;resize:none;outline:none;
  color:var(--text);font:inherit;font-size:16px;line-height:24px;
  padding:8px 12px 12px;min-height:56px;max-height:184px;caret-color:var(--action);
}
.composer textarea::placeholder{color:var(--text-3)}

/* While a run is going, the live stage word REPLACES the placeholder and the
   send control becomes stop. The cheapest single move that stops the product
   reading as a chat box, so it is a component, not a one off. */
.composer .live{display:none;padding:8px 12px 12px;
  font-size:16px;line-height:24px;color:var(--text-2);min-height:56px}
[data-state="running"] .composer textarea{display:none}
[data-state="running"] .composer .live{display:block}

/* Asking and revising are two different bars. Once there is a draft on screen
   the composer gets shorter and its placeholder names the edit act, because
   you are no longer starting something, you are changing something. */
.composer--slim textarea{min-height:40px}
.composer--slim .live{min-height:40px}

/* In the inbox the composer writes a reply, not a prompt. Same component, a
   shorter resting height, because a reply is a sentence and a prompt is a task. */
.dock .composer{margin-top:14px}
.dock .composer textarea{font-size:14px;line-height:21px;min-height:42px}

.tools{display:flex;align-items:center;gap:4px;padding:0 4px}

/* ------------------------------------------------------------- CONTROLS -- */
/* Icon button. 36px, radius 12. The workhorse. */
.tbtn{width:36px;height:36px;border-radius:var(--r-md);display:grid;place-items:center;
  color:var(--text-3);transition:background .14s var(--ease),color .14s var(--ease)}
.tbtn:hover{background:var(--raised);color:var(--text)}

/* Text button with a chevron. Model picker, destination, any dropdown trigger. */
.picker{display:flex;align-items:center;gap:6px;height:36px;padding:0 10px;
  border-radius:var(--r-md);font-size:14px;color:var(--text-2);
  transition:background .14s var(--ease),color .14s var(--ease)}
.picker:hover{background:var(--raised);color:var(--text)}
.picker svg{color:var(--text-3)}
.picker:hover svg{color:var(--text)}

/* The ONE filled action on a screen. Only this gets the pill. */
.send{width:36px;height:36px;border-radius:var(--r-pill);flex:none;margin-left:4px;
  background:var(--action);color:var(--ink);display:grid;place-items:center;
  transition:opacity .14s var(--ease),transform .1s var(--ease)}
.send:disabled{opacity:.3;cursor:default}
.send:not(:disabled):hover{opacity:.88}
.send:not(:disabled):active{transform:scale(.94)}

/* Text button. Every action that is NOT the one filled action on the screen.
   Radius 12 like every other control, never a pill: the pill is reserved so
   that the primary action stays legible without colour. */
/* A control is never underlined, whether it is a <button> or an <a>. The app
   builds these as buttons so it never came up; the site builds them as links,
   and the underline has to be off on the component rather than patched per
   page, or the two drift. */
.btn,.cta,.btn--block,.btn--quiet{text-decoration:none}

.btn{display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 12px;
  border-radius:var(--r-md);background:var(--raised);color:var(--text);font-size:14px;
  transition:background .14s var(--ease),color .14s var(--ease)}
.btn:hover{background:var(--active)}
.btn--quiet{background:none;color:var(--text-2)}
.btn--quiet:hover{background:var(--raised);color:var(--text)}

/* Full width, for a card footer where the action spans the card.
   It carries a HAIRLINE instead of a fill, and that is not cosmetic. A card
   fills to --raised on hover, and a filled button is also --raised, so the two
   land on the same value and the button dissolves into the card exactly when
   the user is reaching for it. One mechanism per element: the card owns fill,
   the button owns the hairline, and they can never collide.
   Radius is r-sm because a control is always less round than its container. */
.btn--block{width:100%;height:32px;justify-content:center;
  border-radius:var(--r-sm);background:none;
  border:1px solid var(--line-2);color:var(--text-2);font-size:13px;
  transition:background .14s var(--ease),color .14s var(--ease),
             border-color .14s var(--ease)}
.btn--block:hover{background:var(--active);border-color:var(--active);color:var(--text)}

/* An icon button carrying a pending mark. Wake uses it; so will notifications. */
.tbtn--marked{position:relative}
.tbtn--marked .dot{position:absolute;top:6px;right:6px;width:5px;height:5px;
  border-radius:50%;background:var(--text-2)}
.tbtn--marked[data-pending="false"] .dot{display:none}

/* ==========================================================================
   STATE MARKS
   State is shape and weight, NEVER hue. A filled disc against a hollow ring at
   an identical diameter. Colour coding is banned and those tints fail in
   greyscale anyway.
   ========================================================================== */
.mark{width:6px;height:6px;border-radius:50%;flex:none;box-sizing:border-box}
.mark--on{background:var(--text-2)}      /* unread, live, needs attention */
.mark--off{border:1px solid var(--text-3)}   /* read, idle */

/* ==========================================================================
   RAIL
   One 28px row pitch for EVERY list in the sidebar, nav and recents alike.
   Groups separated by whitespace, never a rule. Labels sentence case: not one
   of the 18 references uses an uppercase letter spaced label.
   ========================================================================== */
.rail{border-right:1px solid var(--line);display:flex;flex-direction:column;
  min-height:0;padding:8px;width:var(--rail)}

.rail-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  height:28px;padding:0 8px;border-radius:var(--r-sm);
  color:var(--text-2);font-size:14px;
  transition:background .14s var(--ease),color .14s var(--ease)}
.rail-row:hover{background:var(--surface);color:var(--text)}
.rail-row[aria-current="page"]{background:var(--surface);color:var(--text);font-weight:500}
.rail-row svg{flex:none;color:var(--text-3)}
.rail-row[aria-current="page"] svg{color:var(--text)}
.rail-row .n{margin-left:auto;font-size:12px;color:var(--text-3)}
/* a destination that does not exist yet. Dimmed, still readable, no colour. */
.rail-row[aria-disabled="true"]{color:var(--text-3);cursor:default}
.rail-row[aria-disabled="true"]:hover{background:none;color:var(--text-3)}
.rail-row .t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.rail-label{font-size:12px;color:var(--text-3);padding:0 8px;margin:48px 0 8px}
.rail-group{display:flex;flex-direction:column;gap:2px}
.rail-scroll{flex:1;overflow-y:auto;min-height:0;margin:0 -8px;padding:0 8px}
.rail-scroll::-webkit-scrollbar{width:8px}
.rail-scroll::-webkit-scrollbar-thumb{background:var(--raised);border-radius:4px;
  border:2px solid var(--canvas)}

/* An identity row: workspace at the top, the user at the bottom. 44px. */
.id-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  height:44px;padding:0 8px;border-radius:var(--r-sm);
  transition:background .14s var(--ease)}
.id-row:hover{background:var(--surface)}
.id-row .id-t{flex:1;min-width:0;text-align:left}
.id-row .id-t b{display:block;font-size:14px;font-weight:600;line-height:18px}
.id-row .id-t span{display:block;font-size:12px;line-height:16px;color:var(--text-3)}
.id-row .chev{flex:none;color:var(--text-3)}

/* Square mark for a workspace, round for a person. */
.mark-sq{width:24px;height:24px;border-radius:var(--r-sm);flex:none;
  background:var(--action);color:var(--ink);
  display:grid;place-items:center;font-size:12px;font-weight:600}
.mark-rd{width:24px;height:24px;border-radius:50%;flex:none;
  background:var(--raised);color:var(--text-2);
  display:grid;place-items:center;font-size:12px;font-weight:500}

/* ==========================================================================
   CONTENT ROWS
   Anything listed in the main pane. 40px, radius 6, hover fill only.
   ========================================================================== */
.row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  height:40px;padding:0 12px;border-radius:var(--r-sm);
  transition:background .14s var(--ease)}
.row:hover{background:var(--surface)}
.row .lead{flex:none;width:88px;font-size:14px;color:var(--text);font-weight:500}
.row .body{flex:1;min-width:0;font-size:14px;color:var(--text-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .trail{flex:none;font-size:14px;color:var(--text-3)}
/* Attention is weight and contrast, never a coloured chip. */
.row--needs .body{color:var(--text);font-weight:500}

/* ==========================================================================
   VENDOR MARKS
   The ONE exception to "nothing colour coded". A vendor's own logo, in its own
   colours, ONLY inside a connection object. The colour identifies an ACCOUNT,
   not a category, which is why it is not colour coding. Six of the 18
   references do exactly this and none of them colours a button, row or status.

   Never larger than 20px. Never on a button. Never anywhere but a connection.
   Solid fills only: no vendor gradient, so Instagram and Canva use their solid
   forms rather than their gradient lockups.
   ========================================================================== */
.vendor{width:20px;height:20px;flex:none;display:block}
.vendor--sm{width:16px;height:16px}

/* ==========================================================================
   TAGS
   Atomic grants, written the way the connector reference writes them. Neutral,
   never coloured, because a permission is not a category.
   ========================================================================== */
.tag{display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 8px;
  border-radius:var(--r-sm);background:var(--raised);
  font-size:12px;color:var(--text-2);white-space:nowrap}
/* a grant that is deliberately NOT given. Dimmer, hollow mark, no colour. */
.tag--denied{background:none;border:1px solid var(--line-2);color:var(--text-3)}
.tags{display:flex;gap:6px;flex-wrap:wrap;align-items:center}

/* ==========================================================================
   SHEET
   A floating layer. Larger fill step plus the lighter hairline and NO shadow.
   On a dark ground a black shadow is fog: every dark reference lifts with fill
   and a light rim instead. The scrim does the separating.
   ========================================================================== */
.scrim{position:fixed;inset:0;background:rgba(8,10,10,.62);z-index:40;
  opacity:0;pointer-events:none;transition:opacity .2s var(--ease)}
.scrim[data-open="true"]{opacity:1;pointer-events:auto}

.sheet{position:fixed;z-index:41;left:50%;top:50%;
  transform:translate(-50%,-46%) scale(.98);
  width:min(520px,calc(100vw - 48px));max-height:calc(100vh - 96px);overflow:auto;
  background:var(--raised);border:1px solid var(--line-2);border-radius:var(--r-lg);
  opacity:0;pointer-events:none;
  transition:opacity .2s var(--ease),transform .22s var(--ease)}
.sheet[data-open="true"]{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
.sheet-head{display:flex;align-items:center;gap:12px;padding:20px 22px 0}
.sheet-body{padding:16px 22px 22px}
.sheet h2{font-size:16px;font-weight:500;margin:0;line-height:22px}
.sheet .by{font-size:12px;color:var(--text-3);margin:2px 0 0}
.sheet .label{font-size:12px;color:var(--text-3);margin:20px 0 8px}
.sheet p{margin:0;font-size:14px;color:var(--text-2);line-height:21px}

/* The single filled action on a sheet. A pill, because it is the only one. */
.cta{display:flex;align-items:center;justify-content:center;width:100%;height:40px;
  margin-top:22px;border-radius:var(--r-pill);
  background:var(--action);color:var(--ink);font-size:14px;font-weight:500;
  transition:opacity .14s var(--ease),transform .1s var(--ease)}
.cta:hover{opacity:.88}
.cta:active{transform:scale(.99)}
/* The same action sized to its label rather than its container, for a page that
   is not a sheet. Same fill, same pill, same weight: it is one action wearing
   one look, and a second filled style would be exactly the thing the pill rule
   exists to prevent. */
.cta--inline{width:auto;padding:0 20px;margin-top:0}

/* ==========================================================================
   PRESENCE
   Overlapping avatars with a ground coloured ring.
   ========================================================================== */
.faces{display:flex;align-items:center}
.face{width:24px;height:24px;border-radius:50%;flex:none;background:var(--surface);
  color:var(--text-2);border:2px solid var(--canvas);margin-right:-6px;
  display:grid;place-items:center;font-size:12px;font-weight:500}
.face:last-child{margin-right:0}

/* ==========================================================================
   THREAD
   Three things are visible: what you asked, what you got, and whether it is
   done. The process sits behind one row, closed.

   Most people want the draft, not the trace. Printing every stage on the page
   made the run legible and the answer hard to find, which is the wrong trade
   when four people in five never open it. The trace still exists, in full,
   one click away, so the proof is available without being in the way.
   ========================================================================== */
.thread{width:100%;display:flex;flex-direction:column;flex:1 0 auto}

/* A typed block: a small label naming the artifact, then the artifact. */
.block{margin-bottom:22px}
.block:last-child{margin-bottom:0}
.block__label{font-size:12px;line-height:16px;color:var(--text-3);margin:0 0 8px}

/* ---- the process, closed by default ------------------------------------ */
/* The row carries the receipt even when shut: which agent took it, and how
   many sources it read. That is the part worth glancing at. */
.work{margin:0 -12px 22px}
.work__row{display:flex;align-items:center;gap:10px;width:100%;height:40px;
  padding:0 12px;border-radius:var(--r-sm);text-align:left;
  font-size:14px;color:var(--text-2);
  transition:background .14s var(--ease),color .14s var(--ease)}
.work__row:hover{background:var(--surface);color:var(--text)}
.work__row .chev{margin-left:auto;flex:none;color:var(--text-3);
  transition:transform .18s var(--ease),opacity .24s var(--ease)}
.work__row .work__sum{transition:opacity .24s var(--ease)}

/* While the bead is still flying in, the row is a landing pad: the real bead
   is held back so the clone can become it, and the text arrives after. */
.work--landing .work__row{pointer-events:none}
.work--landing .work__row .bot{visibility:hidden}
.work--landing .work__row .work__sum,
.work--landing .work__row .chev{opacity:0}
.work[data-open="true"] .work__row .chev{transform:rotate(180deg)}
.work__panel{padding:16px 12px 0}
.work[data-open="false"] .work__panel{display:none}

/* ---- the trace, inside the panel --------------------------------------- */
/* Stages named after the artifact they produce rather than the effort they
   spend: Routing, Plan, Sources read. State is the node shape plus the label
   weight. Never hue, never a tick. */
.step{display:grid;grid-template-columns:18px minmax(0,1fr);column-gap:14px}
.step__rail{position:relative}
/* the spine, drawn from under the node to the bottom of the block */
.step__rail::before{content:"";position:absolute;left:50%;transform:translateX(-50%);
  top:15px;bottom:0;width:1px;background:var(--line)}
.step:last-child .step__rail::before{display:none}

.step__node{position:absolute;left:50%;top:4px;transform:translateX(-50%);
  width:7px;height:7px;border-radius:50%;box-sizing:border-box;background:var(--text-3)}
.step__node::after{content:"";position:absolute;inset:-4px;border-radius:50%;
  border:1px solid var(--line-2);opacity:0}

.step__main{min-width:0;padding-bottom:26px}
.step:last-child .step__main{padding-bottom:0}
.step__label{font-size:12px;line-height:16px;color:var(--text-3);margin:0 0 8px}

/* not reached yet: hollow node, quiet label */
.step[data-state="pending"] .step__node{background:none;border:1px solid var(--line-2)}
/* happening now: filled node, label carries the weight */
.step[data-state="active"] .step__node{background:var(--text)}
.step[data-state="active"] .step__label{color:var(--text);font-weight:500}

/* The ring breathes only while a run is genuinely going. An infinite animation
   next to data that is not changing is on the ban list. */
[data-state="running"] .step[data-state="active"] .step__node::after{opacity:1;
  animation:step-ring 1.9s var(--ease) infinite}
@keyframes step-ring{0%{transform:scale(.65);opacity:.9}70%,100%{transform:scale(1.6);opacity:0}}

/* ---- what goes inside a block ------------------------------------------ */
/* Request. The thing you asked, set as the anchor of the column. Not a bubble:
   nothing in the reference set draws a chat bubble. */
.ask-text{margin:0;font-size:16px;line-height:24px;font-weight:500;color:var(--text)}

/* Routing, printed rather than hidden behind a spinner. The bead wears the
   receiving agent's eyes, so the handoff is legible before you read the words. */
.handoff{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.handoff b{font-size:14px;font-weight:500;color:var(--text)}
.handoff span{font-size:14px;color:var(--text-3)}

/* Plan. Numbered, because a plan you can count is a plan you can check. */
.plan{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:7px}
.plan li{display:flex;gap:12px;font-size:14px;line-height:21px;color:var(--text-2)}
.plan li i{font-style:normal;flex:none;width:14px;color:var(--text-3)}

/* Sources read. Content rows, pulled out to the row padding so the hover fill
   lines up with the block edge. Every source is openable and carries its age. */
.rows{display:flex;flex-direction:column;margin:0 -12px}

/* ---- the artifact the run leaves behind -------------------------------- */
.artifact{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md)}
.artifact__head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
.artifact__head b{font-size:14px;font-weight:500;color:var(--text)}
.artifact__head .trail{margin-left:auto;font-size:12px;color:var(--text-3)}
.artifact__body{padding:12px 16px 16px;display:flex;flex-direction:column;gap:9px}
.artifact__acts{display:flex;align-items:center;gap:4px;padding:8px;
  border-top:1px solid var(--line)}
.artifact__acts .trail{margin-left:auto;padding-right:8px;font-size:12px;color:var(--text-3)}

/* ---- the draft, before it is a draft ----------------------------------- */
/* The gap between sending and reading is the most obvious place in the product
   to say something is happening, and a stage word in the composer is too quiet
   to carry it. So the space the answer will occupy is claimed at once, in the
   shape it is going to land in.

   Opacity and a stagger, never a sweeping gradient: a gradient on a surface the
   product owns is banned, and a stagger reads as a wave anyway. Gated on
   `running`, so it cannot pulse next to something that is not moving. */
.skel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md)}
.skel span{display:block;height:10px;border-radius:var(--r-sm);background:var(--raised)}
.skel__head{padding:16px 16px 0}
.skel__body{padding:15px 16px 16px;display:flex;flex-direction:column;gap:14px}
.skel__foot{padding:8px;border-top:1px solid var(--line)}
.skel__foot span{height:32px;width:132px;border-radius:var(--r-md)}

[data-state="running"] .skel span{animation:skel-pulse 1.6s ease-in-out infinite}
.skel__body span:nth-child(2){animation-delay:.09s}
.skel__body span:nth-child(3){animation-delay:.18s}
.skel__body span:nth-child(4){animation-delay:.27s}
.skel__body span:nth-child(5){animation-delay:.36s}
.skel__body span:nth-child(6){animation-delay:.45s}
.skel__foot span{animation-delay:.54s}
@keyframes skel-pulse{0%,100%{opacity:.38}50%{opacity:1}}

.slide{display:flex;gap:12px;font-size:14px;line-height:21px;color:var(--text-2)}
.slide i{font-style:normal;flex:none;width:16px;color:var(--text-3)}
.slide b{color:var(--text);font-weight:500}

/* Terminal line. Done, Blocked, Stopped. Not Success, not Complete, and never
   a tick carrying hue. */
.term{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.term b{font-size:14px;font-weight:500;color:var(--text)}
.term span{font-size:14px;color:var(--text-3)}

/* NOT sticky. It used to be, back when it was one text box parked at the foot
   of a long thread. It now carries three options and a field of your own, and
   pinned to the bottom with a solid background it painted straight over the
   comment being answered on any short thread. The options are the content, so
   the column scrolls as one piece. */
.dock{width:100%;flex:none;padding:16px 0 24px}

/* ==========================================================================
   AGENT HUE
   Six angles on one wheel, every one at the cream's exact lightness and
   chroma. Set --agent-h on any ancestor and every bead below it wears it.

   THREE BANDS ARE DELIBERATELY ABSENT, and two of them were caught by the
   linter on a build that had already shipped past review:
     250-290  violet. Where shadcn zinc and every generated palette lives.
     175-225  cyan. "Cyan on dark" is named as a top tell alongside violet.
     295-345  magenta, which reads as the violet family at a glance.

   A first version used teal 195 and pink 335, and impeccable returned
   "Cyan gradient background" and "Purple/violet gradient background". These
   beads are 28px characters and not backgrounds, so that was arguably a
   false positive. It was taken anyway: this is the ONE coloured object in
   the product, owned by someone who has rejected four builds for looking
   generated, and being right on a technicality is not worth the risk.

   What is left is a warm arc plus two greens, which also simply sits better
   on a #0E1111 ground than a cyan ever did.
   ========================================================================== */
[data-hue="cream"]{--agent-h:84}
[data-hue="gold"] {--agent-h:66}
[data-hue="amber"]{--agent-h:45}
[data-hue="rose"] {--agent-h:20}
[data-hue="moss"] {--agent-h:140}
[data-hue="jade"] {--agent-h:162}

/* The picker. Six beads in a row, and picking one is picking a face, not a
   swatch: each option is the real mascot at 28px, so what you choose is
   exactly what you get. A grid of colour squares would be a swatch picker for
   a product that has no swatches. */
.hues{display:flex;align-items:center;gap:8px}
.hue{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;
  transition:background .14s var(--ease)}
.hue:hover{background:var(--surface)}
.hue .bot{--bot-size:28px}
/* Selected is a ring, at the same diameter, so nothing moves when it changes.
   Shape and contrast, exactly like every other state in the product. */
.hue[aria-pressed="true"]{background:var(--surface);
  box-shadow:inset 0 0 0 1px var(--line-2)}

/* ==========================================================================
   TABS
   A row of section switches above a panel. Sentence case, never uppercase
   letter spaced. The active one is a fill step plus full contrast plus weight,
   which is the same "state is never hue" rule the whole product uses.

   Not a pill. Only the single primary action on a screen gets a pill, and a
   tab is not an action, it is a place.
   ========================================================================== */
.tabs{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
.tab{height:32px;padding:0 12px;border-radius:var(--r-sm);
  font-size:14px;color:var(--text-3);white-space:nowrap;
  transition:background .14s var(--ease),color .14s var(--ease)}
.tab:hover{background:var(--surface);color:var(--text)}
.tab[aria-selected="true"]{background:var(--surface);color:var(--text);font-weight:500}
.tab .n{margin-left:6px;font-size:12px;color:var(--text-3)}
.tab[aria-selected="true"] .n{color:var(--text-2)}

/* ==========================================================================
   CONVERSATION ROW
   One line of the inbox. Everything that would be a coloured chip elsewhere is
   carried by weight and by the mark:

     unread          filled disc      .mark--on
     read            hollow ring      .mark--off
     needs a person  the whole row goes to full contrast, .conv--needs

   The intent word ("Complaint", "Question") sits on the muted tier as plain
   text. A category is information, not an alarm, so it does not get a colour
   and it does not get a border.
   ========================================================================== */
.conv{display:flex;gap:10px;width:100%;text-align:left;padding:10px 12px;
  border-radius:var(--r-sm);align-items:flex-start;
  transition:background .14s var(--ease)}
.conv:hover{background:var(--surface)}
.conv[aria-current="true"]{background:var(--surface)}
.conv .mark{margin-top:7px}
.conv .in{flex:1;min-width:0}
.conv .who{display:flex;align-items:baseline;gap:8px}
.conv .who b{font-size:14px;font-weight:500;color:var(--text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.conv .who .age{margin-left:auto;flex:none;font-size:12px;color:var(--text-3)}
.conv .said{margin:2px 0 0;font-size:14px;line-height:20px;color:var(--text-3);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.conv .meta{margin-top:6px;display:flex;align-items:center;gap:8px;
  font-size:12px;color:var(--text-3)}
.conv .meta i{font-style:normal}
/* Aligned to the name line, not the middle of the row: the eye reads the
   name first and the tile should meet it there. */
.conv .thumb{margin-top:2px}
.conv--needs .said{color:var(--text)}
.conv--needs .meta{color:var(--text-2)}

/* ==========================================================================
   POST THUMBNAIL
   A comment without the post it sits under is a comment read blind: "mine
   arrived cracked" means one thing under a candle and another under a refill.

   Square, because every platform crops a grid to square and a thumbnail that
   disagrees with the platform is a thumbnail people misread. Radius 6, since a
   control never exceeds its container and the row it sits in is 6.

   The fallback is NOT a broken-image glyph and NOT a grey box: it is the media
   kind in words. A post really can have no image, and saying "Text" is more use
   than a torn-page icon.
   ========================================================================== */
.thumb{width:var(--thumb,40px);height:var(--thumb,40px);flex:none;
  border-radius:var(--r-sm);overflow:hidden;background:var(--raised);
  display:grid;place-items:center;position:relative}
.thumb img{width:100%;height:100%;object-fit:cover;display:block}
.thumb i{font-style:normal;font-size:10px;line-height:12px;color:var(--text-3);
  text-align:center;padding:2px}

/* A video reads as a video without an icon library: one small triangle. */
.thumb--video::after{content:"";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);border-style:solid;border-width:5px 0 5px 8px;
  border-color:transparent transparent transparent rgba(255,255,255,.82)}

/* Several comments from the same post, stacked. Two hairlines behind the tile
   say "more of these" without a count nobody asked for. */
.thumb--many::before{content:"";position:absolute;inset:-3px -3px 3px 3px;
  border-radius:var(--r-sm);border:1px solid var(--line-2);z-index:-1}

/* ==========================================================================
   MESSAGE
   A turn in a comment thread or a DM. Not a bubble: nothing in the reference
   set draws a chat bubble, and a two-sided bubble layout wastes half the width
   on a surface that is mostly one-sided anyway.

   Ours is separated from theirs by an indent and a lighter ground, not by
   floating it to the right.
   ========================================================================== */
.msg{padding:10px 0}
.msg + .msg{border-top:1px solid var(--line)}
.msg .from{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
.msg .from b{font-size:12px;font-weight:500;color:var(--text-2)}
.msg .from span{font-size:12px;color:var(--text-3)}
/* Capped at a readable measure. A 720px column at 14px runs to about 90
   characters, which the linter flags and which genuinely is hard to track. */
.msg p{margin:0;font-size:14px;line-height:21px;color:var(--text);max-width:56ch}
.msg--out{padding-left:16px;border-left:1px solid var(--line-2);margin-left:2px}
.msg--out p{color:var(--text-2)}

/* ==========================================================================
   REPLY
   The draft the agent wrote, and the ONE thing on the screen that gets sent.

   Deliberately NOT three equal cards side by side. Three equal options ask the
   reader to compare three paragraphs of similar text, which is the most
   expensive way to make the cheapest decision. One recommendation is open, its
   strategy is named, and the alternatives sit behind a row that says how many
   there are. Same content, one decision at a time.
   ========================================================================== */
.reply{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md)}
.reply__head{display:flex;align-items:center;gap:8px;padding:12px 14px 0}
.reply__head b{font-size:14px;font-weight:500;color:var(--text)}
.reply__head .why{font-size:12px;color:var(--text-3);margin-left:auto;text-align:right;max-width:34ch}
/* Sized from its content by fit() in ui-inbox.js, so no floor: a one-line
   reply is one line tall. A 64px floor put a third of a card of nothing under
   every option, and three options made that a screenful. */
.reply textarea{display:block;width:100%;max-width:58ch;resize:none;border:0;
  background:none;overflow:hidden;min-height:0;
  color:var(--text);font:inherit;line-height:21px;padding:10px 14px 12px;
  outline:none}
.reply textarea::placeholder{color:var(--text-3)}
.reply__acts{display:flex;align-items:center;gap:8px;padding:4px 6px;
  border-top:1px solid var(--line)}
.reply__acts .trail{margin-left:auto;padding-right:4px;font-size:12px;color:var(--text-3)}

/* A receipt from the voice check. Text, not a coloured chip: the rule that was
   broken and the words that broke it. `fail` is the brand's own prohibition and
   blocks sending, `warn` is a generic tell and only informs. The difference is
   carried by weight, exactly like every other state in this product. */
.lint{display:flex;flex-direction:column;gap:4px;padding:0 14px 12px;margin:0;list-style:none}
.lint li{font-size:12px;line-height:16px;color:var(--text-3);max-width:54ch}
.lint li b{font-weight:500;color:var(--text-2)}
.lint li[data-sev="fail"]{color:var(--text-2)}
.lint li[data-sev="fail"] b{color:var(--text)}

/* Every option, stacked. A column and not three side by side, because the
   thread is about 600px wide and three columns of that is 190px each, which is
   a paragraph of reply text at four words a line.

   The recommendation is first, marked, and holds the only filled send. The
   others are complete and sendable, just quieter. */
/* The agent stepped out. One row: what is true, and the button that changes
   it. No heading, no icon, no coloured panel — it is a status line, and the
   only reason it is a box at all is to keep the button attached to its
   sentence. */
.took{display:flex;align-items:center;gap:12px;margin:0 0 14px;padding:9px 12px;
  border:1px solid var(--line-2);border-radius:8px;background:var(--surface)}
.took__txt{flex:1;min-width:0;font-size:13px;color:var(--text-2)}

/* Three options, stacked. A column and not three across, because the thread is
   about 600px wide: three columns of that is 190px each, which is a reply at
   four words a line. Replient can lay them side by side because their three are
   one paragraph in three arrangements and nobody reads past the first line. */
.replies{display:flex;flex-direction:column;gap:8px}
.reply--first{background:var(--raised)}
/* The alternatives are a step quieter so the eye starts at the top of the list,
   but all three are complete and all three send. */
.replies .reply:not(.reply--first) textarea{color:var(--text-2)}

/* The header row: what these are, and the button that replaces them. */
.dock__head{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.dock__head .icobtn{margin-left:auto}
.dock__head .block__label{margin:0}
/* ==========================================================================
   A SETTINGS ROW
   One decision per row, the label ABOVE its control: a label beside a control
   makes the eye travel twice for every field. Separated by a rule rather than
   a card each, because a page of cards reads as a page of unrelated things.

   NOT called .field — that name is taken by the input itself (height:38px),
   and a wrapper wearing it collapses every row onto the next.
   ========================================================================== */
.setting{padding:22px 0;border-top:1px solid var(--line)}
.setting:first-child{border-top:0;padding-top:0}
.setting > label,.setting > .lab{display:block;font-size:14px;font-weight:500;
  color:var(--text);margin-bottom:3px}
.setting > p{margin:0 0 12px;font-size:13px;line-height:19px;color:var(--text-3);
  max-width:58ch}

/* A choice with an explanation beside it. The whole row is the label, so the
   text is a hit target and not just decoration next to a 13px box. */
.opt{display:flex;align-items:flex-start;gap:12px;cursor:pointer}
.opt input{margin-top:2px;flex:none}
.opt span{font-size:13px;line-height:19px;color:var(--text-2)}

/* Something went wrong, said where the person is already looking. Text and
   a rule, not a coloured panel: an error that shouts is an error people
   learn to dismiss without reading. */
.notice{margin:0 0 14px;padding:9px 12px;border:1px solid var(--line-2);
  border-radius:8px;background:var(--surface);
  font-size:13px;line-height:19px;color:var(--text-2)}

.dock__note{margin:0 0 10px;font-size:13px;line-height:19px;color:var(--text-3)}



/* ==========================================================================
   SWATCH
   A measured brand value: a colour, a typeface, a logo. The one place a
   customer's own colour appears, and it appears as DATA about them, in a fixed
   size chip, never as the product's own chrome. Same carve-out as a vendor mark.
   ========================================================================== */
.swatches{display:flex;flex-wrap:wrap;gap:8px}
.swatch{display:flex;align-items:center;gap:8px;height:32px;padding:0 10px 0 4px;
  border-radius:var(--r-sm);background:var(--raised);font-size:12px;color:var(--text-2)}
.swatch i{width:24px;height:24px;border-radius:4px;flex:none;
  border:1px solid rgba(255,255,255,.12)}
.swatch .num{color:var(--text-3)}

/* ==========================================================================
   EMPTY
   A surface with nothing in it names the action instead of apologising. No
   illustration, no "nothing here yet", no shrug.
   ========================================================================== */
.empty{padding:48px 0;max-width:46ch}
.empty b{display:block;font-size:16px;font-weight:500;color:var(--text);margin-bottom:6px}
.empty p{margin:0 0 16px;font-size:14px;line-height:21px;color:var(--text-3)}

/* ------------------------------------------------------------------- APP -- */
.app{display:grid;grid-template-columns:var(--rail) minmax(0,1fr);height:100vh}
.topbar{display:flex;align-items:center;gap:12px;height:52px;padding:0 24px;flex:none}
.spacer{flex:1}
.icobtn{width:28px;height:28px;border-radius:var(--r-sm);display:grid;place-items:center;
  color:var(--text-3);transition:background .14s var(--ease),color .14s var(--ease)}
/* flex:none because this now also sits in .dock__head, a flex row whose
   label grows; without it the button is squeezed out of square. */
.icobtn{flex:none}
.icobtn:disabled{color:var(--text-3);cursor:default}
/* Working. The glyph turns, not the button, so the hit area stays still. */
.icobtn[data-busy="true"] svg{animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  .icobtn[data-busy="true"] svg{animation:none;opacity:.5}
}
.icobtn:hover{background:var(--surface);color:var(--text)}

@media (max-width:900px){
  .app{grid-template-columns:1fr}
  .rail{display:none}
}
@media (prefers-reduced-motion:reduce){
  *{transition:none!important}
  .bot .head,.bot .eyes,.bot .eyes use,.bot--hero{animation:none!important}
  .step__node::after{animation:none!important}
}
