/* Sheen Portal — Midnight Ember
   Palette, typography and motion ported verbatim from ZapCard so the two
   products read as one house. Midnight Ember is a COMMITTED single-accent
   world: charcoal ground, neon-ember orange, cream text, three semantics.

   A QC console needs more states than ZapCard has, so the extension stays
   inside that discipline rather than adding a rainbow: the three semantics
   take the three verdicts, redo takes a PALER EMBER from the same ramp, and
   the machine advisory takes NO hue at all — cool grey, always dashed — so an
   AI note reads as an annotation rather than a decision. Taxonomy is neutral.
   See DESIGN.md. */

@font-face{font-family:'Space Grotesk Variable';src:url('/static/fonts/space-grotesk.woff2') format('woff2');
  font-weight:300 700;font-display:swap}
@font-face{font-family:'Inter Variable';src:url('/static/fonts/inter.woff2') format('woff2');
  font-weight:100 900;font-display:swap}

:root{
  /* charcoal surfaces — higher number = darker, as in ZapCard */
  --ink-0:#0b0b0d; --ink-1:#141417; --ink-2:#1f1f26; --ink-3:#2a2a32;
  --ink-4:#383840; --ink-5:#4a4a53;

  /* cream text ramp */
  --fg:#ececef; --fg-dim:#cbcbd2; --fg-mut:#9c9ca6;

  /* the ember accent (ZapCard gold-*) */
  --brand:#ff6a1a;  --brand-hi:#ff7d33; --brand-lo:#ff9152; --brand-pale:#ffb488;
  --brand-g:rgba(255,106,26,.55); --brand-w:rgba(255,106,26,.14);

  /* ZapCard's three semantics carry the three verdicts */
  --em:#34d399;   --em-g:rgba(52,211,153,.42);  --em-w:rgba(52,211,153,.13);
  --ruby:#ff5a5a; --ruby-g:rgba(255,90,90,.45); --ruby-w:rgba(255,90,90,.14);
  /* one ramp step up, for 9.5px pill text: ruby over its own wash lands at
     4.46:1, just under AA for small text. Same hue, measured pass. */
  --ruby-lo:#ff7d7d;
  --cit:#fbbf24;  --cit-g:rgba(251,191,36,.40); --cit-w:rgba(251,191,36,.13);

  /* extensions, kept inside the single-accent logic */
  --sap:#5b9df9;  --sap-g:rgba(91,157,249,.38); --sap-w:rgba(91,157,249,.13);
  --top:var(--brand-pale);
  --top-g:rgba(255,180,136,.34); --top-w:rgba(255,180,136,.12);
  --am:#a6adbd;   --am-g:rgba(166,173,189,.30); --am-w:rgba(166,173,189,.10);
  --taxo:#9c9ca6; --taxo-w:rgba(156,156,166,.12);

  /* legacy aliases so existing markup keeps working */
  --bg:var(--ink-1); --card:var(--ink-2); --line:var(--ink-3);
  --ink:var(--fg); --mut:var(--fg-dim); --gold:var(--brand);
  --ok:var(--em); --bad:var(--ruby); --run:var(--cit); --blue:var(--sap);
  --pend:var(--ink-4); --ai:var(--am);

  --r:12px; --r-lg:16px;
  --display:'Space Grotesk Variable','Inter Variable',system-ui,sans-serif;
  --mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,monospace;
  --shadow:0 1px 2px rgba(0,0,0,.4),0 10px 30px -18px rgba(0,0,0,.7);
  /* ZapCard motion tokens (the subset this surface uses) */
  --ease:cubic-bezier(.21,1.02,.73,1);
  --dur-press:90ms; --dur-quick:180ms;

  /* ── A TYPE SCALE, because until now there was not one ──────────────────
     This file carries twenty distinct literal font-sizes and upload.css
     another sixteen, six of them crowded into the 11–13.5px band alone. That
     band is not a hierarchy: it is a set of independent guesses that happen
     to land near each other, and it is the reason the client's two screens do
     not read as one product — each was hand-tuned into the same band from a
     different starting point, so nothing on one lines up with anything on the
     other. Six steps, each far enough from its neighbour that choosing one is
     a decision rather than a drift.

     ADDITIVE, and deliberately so: not one existing declaration is re-pointed
     at these. Six pages read this file and none of their authors asked for a
     resize, so a new surface takes the scale and an old literal is converted
     when somebody is in that file for another reason anyway. */
  --t-eyebrow:11px;    /* the ONLY small text: uppercase, tracked, a label */
  --t-small:12.5px;    /* meta lines, counts, secondary prose */
  --t-body:13.5px;     /* body prose — the size .hint already reads at */
  --t-lede:16.5px;     /* the one sentence that states where the work is */
  --t-head:20px;       /* a heading that owns its part of the page */
  --t-title:28px;      /* the single title a page is known by */
  --lh-tight:1.18; --lh-body:1.6;

  /* ── A SPACING SCALE, on a ~1.55 ratio rather than a 4px grid ───────────
     A 4px grid makes every gap available and therefore every gap arbitrary:
     it is how .lotprog came to sit at 13px of padding beside .up-card's 16px
     on two screens the same client reads one after the other. The ratio is
     the whole point — the jump from --s-4 to --s-5 is where "generous" lives,
     so a section break reads as a section break without anyone measuring it,
     and there is no value available between them to split the difference
     with. */
  --s-1:4px; --s-2:8px; --s-3:14px; --s-4:22px; --s-5:34px; --s-6:54px;
}
*{box-sizing:border-box;margin:0}
body{background:
    radial-gradient(760px 460px at 85% -8%,rgba(255,106,26,.16),transparent 60%),
    radial-gradient(680px 480px at -10% 12%,rgba(255,60,0,.08),transparent 55%),
    var(--ink-1);
  background-attachment:fixed;
  color:var(--fg);
  font:15px/1.55 'Inter Variable',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  font-variant-numeric:tabular-nums}
/* ZapCard's universal press acknowledgement — feedback, not decoration */
button:not(:disabled){transition:transform var(--dur-press) var(--ease),
  color var(--dur-quick) ease,background-color var(--dur-quick) ease,
  border-color var(--dur-quick) ease,box-shadow var(--dur-quick) ease}
button:not(:disabled):active{transform:scale(.97)}
/* ── shell ─────────────────────────────────────────────────────────────── */
header{padding:15px 24px;display:flex;gap:14px;align-items:baseline;flex-wrap:wrap;
  border-bottom:1px solid var(--ink-3);
  background:linear-gradient(180deg,rgba(31,31,38,.72),rgba(20,20,23,.35))}
header h1{font-family:var(--display);font-size:17px;font-weight:700;letter-spacing:-.01em}
header h1 span{color:var(--brand);text-shadow:0 0 18px var(--brand-g)}
header .sub{color:var(--fg-dim);font-size:12.5px}
header .role{margin-left:auto;font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
  padding:4px 11px;border-radius:999px;font-weight:700;
  color:var(--brand);border:1px solid var(--brand);background:var(--brand-w);
  box-shadow:0 0 16px -4px var(--brand-g)}
main{max-width:1400px;margin:0 auto;padding:20px 24px 40px}
footer{padding:22px 24px;color:var(--fg-mut);font-size:12px;text-align:center;
  border-top:1px solid var(--ink-3);margin-top:34px}

/* ── client credit strip ──────────────────────────────────────────────────
   Directly under the header, full width, on every screen the client sees, so
   the balance is never more than a glance away while they judge. Emerald
   while credits remain, citrine when the bundle is nearly spent, ruby at zero
   — the same three semantics the verdicts use, because whether an image can
   still be accepted IS a verdict question. */
.credits{padding:7px 24px;text-align:center;font-size:13px;color:var(--fg-dim);
  border-bottom:1px solid var(--ink-3);font-variant-numeric:tabular-nums;
  background:linear-gradient(90deg,transparent 8%,var(--em-w),transparent 92%),
             var(--ink-1)}
.credits b{font-family:var(--mono);font-weight:650;font-size:14px;color:var(--em);
  letter-spacing:-.01em}
.credits.low{background:linear-gradient(90deg,transparent 8%,var(--cit-w),transparent 92%),
             var(--ink-1)}
.credits.low b{color:var(--cit)}
.credits.out{color:var(--fg);
  background:linear-gradient(90deg,transparent 8%,var(--ruby-w),transparent 92%),
             var(--ink-1)}
.credits.out b{color:var(--ruby-lo)}
.credits .cnote{color:var(--ruby-lo);font-weight:600}
@media (max-width:700px){.credits{padding:6px 14px;font-size:12.5px}}

/* ── gate ──────────────────────────────────────────────────────────────── */
.gate{max-width:420px;margin:14vh auto;text-align:center}
/* The way from the batch-key gate to Google sign-in. Same treatment as the
   mirror link on signin.html, which has carried one since it was written —
   this is the half that was missing, and / is what portal.sheen.ai serves, so
   its absence left an invited client with no route in at all. */
.gate .alt{margin-top:30px;padding-top:18px;border-top:1px solid var(--ink-3);
  color:var(--fg-mut);font-size:12px}
.gate .alt a{color:var(--fg-dim);text-decoration:underline;text-underline-offset:3px;
  text-decoration-color:var(--ink-5)}
.gate .alt a:hover{color:var(--brand);text-decoration-color:var(--brand)}
.gate p{color:var(--fg-dim);font-size:14px}
.gate input{width:100%;padding:14px 16px;border-radius:var(--r);font-size:16px;
  border:1px solid var(--ink-4);background:var(--ink-1);color:var(--fg);margin:18px 0 10px}
.gate input:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 4px var(--brand-w)}
.err{color:var(--ruby);font-size:13px;min-height:18px}

/* ── buttons ───────────────────────────────────────────────────────────── */
.btn{display:inline-block;padding:10px 20px;border-radius:var(--r);cursor:pointer;
  font-size:14px;font-weight:600;text-decoration:none;
  border:1px solid var(--brand);color:#1a0d05;
  background:linear-gradient(180deg,var(--brand-hi),var(--brand));
  box-shadow:0 0 0 1px rgba(255,106,26,.3),0 6px 26px -6px var(--brand-g);
  transition:box-shadow .18s var(--ease),transform .18s var(--ease)}
.btn:hover{box-shadow:0 0 26px -3px var(--brand-g);transform:translateY(-1px)}
.btn.small{padding:6px 13px;font-size:12.5px;font-weight:600;
  background:var(--ink-2);border-color:var(--ink-4);color:var(--fg-dim);
  box-shadow:none}
.btn.small:hover{border-color:var(--brand);color:var(--brand);
  background:var(--brand-w);box-shadow:0 0 16px -6px var(--brand-g);transform:none}
.btn.small.on{border-color:var(--brand);color:var(--brand);background:var(--brand-w);
  box-shadow:0 0 16px -6px var(--brand-g)}
.btn.ok{border-color:var(--em);color:var(--em);background:var(--em-w)}
.btn.ok:hover{background:var(--em);color:#04120c;box-shadow:0 0 22px -6px var(--em-g)}
.btn.bad{border-color:var(--ruby);color:var(--ruby);background:var(--ruby-w)}
.btn.bad:hover{background:var(--ruby);color:#1a0405;box-shadow:0 0 22px -6px var(--ruby-g)}
/* An unavailable mode still has to be READABLE — opacity:.4 put "Slider" at
   2.75:1 on the SKUs that have no reference photo, which reads as a rendering
   fault rather than as "not available here" (DESIGN.md). */
.btn[disabled]{pointer-events:none;color:var(--fg-mut);border-color:var(--ink-3);
  background:transparent;box-shadow:none}
:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:6px}

/* ── THE card ──────────────────────────────────────────────────────────────
   One recipe — padding, var(--r), a 1px --ink-3 hairline, an --ink-2 ground —
   was written out by hand under six different names across three files
   (.up-card, .rn-start, .rn-run, .lotprog, .lt-form, .lt-up). Six independent
   spellings of one idea is how two of them ended up with different padding on
   screens the same client reads one after the other, and why a seventh surface
   had nothing to reuse and so became a seventh copy. Named once, here.

   The three lit variants are the only three things a card has to say before it
   is read: this ended well, this wants a look, this failed. They take the
   semantics DESIGN.md already assigns — emerald, citrine, ruby — so a card
   cannot invent a fourth meaning for itself.

   ADDITIVE: the six hand-rolled copies are left exactly as they are.
   Collapsing them touches every console page and belongs to whoever is next in
   those files, not to a change about the client's screens. */
.card{padding:var(--s-4);border-radius:var(--r);
  border:1px solid var(--ink-3);background:var(--ink-2)}
.card.lit-ok{border-color:color-mix(in srgb,var(--em) 40%,var(--ink-3));
  background:linear-gradient(180deg,var(--em-w),transparent),var(--ink-2)}
.card.lit-warn{border-color:color-mix(in srgb,var(--cit) 34%,var(--ink-3))}
.card.lit-bad{border-color:color-mix(in srgb,var(--ruby) 40%,var(--ink-3));
  background:linear-gradient(180deg,var(--ruby-w),transparent),var(--ink-2)}

/* ── instrument rail (admin batch ledger) ──────────────────────────────── */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:10px;margin:16px 0 6px}
.stat{position:relative;padding:13px 15px;border-radius:var(--r);overflow:hidden;
  background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.008)),
             var(--ink-2);
  border:1px solid rgba(255,255,255,.07);box-shadow:var(--shadow)}
.stat{border-color:color-mix(in srgb,var(--tint,var(--am)) 26%,var(--ink-3))}
.stat::before{content:"";position:absolute;inset:-40% -20% auto -20%;height:80%;
  border-radius:50%;pointer-events:none;
  background:radial-gradient(closest-side,
    color-mix(in srgb,var(--tint,var(--am)) 22%,transparent),transparent)}
.stat b{display:block;font-family:var(--mono);font-size:21px;font-weight:600;
  letter-spacing:-.02em;color:var(--tint,var(--fg))}
.stat .lb{color:var(--fg-mut);font-size:10.5px;margin-top:3px;
  letter-spacing:.09em;text-transform:uppercase;font-weight:600}

/* ── purchase budget pill ─────────────────────────────────────────────────
   A state WORD where the other tiles carry a number, so it cannot be misread
   as one more count. The tile's --tint is the state: emerald ok, citrine near
   cap, ruby exhausted, and the machine grey for "no purchase yet" — that one
   is an admin to-do, not a warning, so it gets no verdict hue. The dollar
   line beneath is admin-only by payload; here it is simply a second line. */
.stat.bstat b{display:flex;align-items:center;min-height:27px;font-size:0}
.bud{display:inline-block;padding:4px 12px;border-radius:999px;white-space:nowrap;
  font:700 12px/1.2 var(--display);letter-spacing:.02em;
  color:var(--tint);border:1px solid var(--tint);
  background:color-mix(in srgb,var(--tint) 13%,transparent);
  box-shadow:0 0 16px -7px var(--tint)}
.bmoney{font-family:var(--mono);font-size:11px;color:var(--fg-dim);margin-top:6px;
  letter-spacing:-.01em;font-variant-numeric:tabular-nums}
/* an operator's row holds the pill alone: let it take its natural width
   instead of stretching one tile across the page */
.stats.solo{display:flex;flex-wrap:wrap}
.stats.solo .stat{flex:0 1 auto;min-width:200px}

/* ── bucket rail ───────────────────────────────────────────────────────── */
.buckets{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0 4px}
/* buckets and tiles are real <button>s so they take focus and answer Enter;
   strip the UA chrome the design does not want */
.bucket,.tile{font:inherit;text-align:left;-webkit-appearance:none;appearance:none}
.bucket{display:flex;align-items:center;gap:8px;padding:8px 15px;cursor:pointer;
  border-radius:999px;font-size:13px;font-weight:600;
  border:1px solid var(--ink-3);background:var(--ink-1);color:var(--fg-dim);
  transition:border-color .16s var(--ease),color .16s var(--ease),
             box-shadow .16s var(--ease)}
.bucket:hover{border-color:var(--tint,var(--brand));color:var(--fg)}
.bucket b{font-family:var(--mono);font-weight:600;color:var(--fg);
  padding:1px 7px;border-radius:999px;background:var(--ink-3);font-size:12px}
.bucket.on{color:var(--tint,var(--brand));border-color:var(--tint,var(--brand));
  background:color-mix(in srgb,var(--tint,var(--brand)) 12%,transparent);
  box-shadow:0 0 18px -6px var(--tint,var(--brand))}
.bucket.on b{background:color-mix(in srgb,var(--tint,var(--brand)) 22%,transparent);
  color:var(--tint,var(--brand))}

/* ── tools ─────────────────────────────────────────────────────────────── */
.tools{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin:14px 0 16px}
.tools select,.tools input{padding:8px 12px;border-radius:10px;font-size:13px;
  border:1px solid var(--ink-3);background:var(--ink-1);color:var(--fg)}
.tools select:hover,.tools input:hover{border-color:var(--ink-4)}
.tools input::placeholder{color:var(--fg-mut)}
.tools select:focus,.tools input:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 3px var(--brand-w)}
.spacer{flex:1}

/* ── tile grid ─────────────────────────────────────────────────────────────
   Zones, not overlays. Nothing sits on the jewellery: the verdict is the lit
   edge, and every other signal is a labelled pill in the body where it has
   room to be read. */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:13px}
.bucket,.tile{font:inherit;text-align:left;-webkit-appearance:none;appearance:none}
/* ── selection ─────────────────────────────────────────────────────────
   A tick is an ADMIN INTENT, not a verdict, so it borrows the ember brand
   accent (--top) and never --em/--ruby/--cit: those three carry the review
   semantics, and a fourth meaning on them would make a selection read as a
   decision. The checkbox is a sibling of the tile (interactive content inside
   a <button> is invalid), positioned over its top-left corner. */
.cell{position:relative;display:flex}
.cell .pick{position:absolute;top:9px;left:9px;z-index:2;margin:0;
  width:20px;height:20px;flex:0 0 auto;cursor:pointer;
  appearance:none;-webkit-appearance:none;border-radius:6px;
  border:1.5px solid var(--ink-5);background:rgba(16,16,19,.72);
  backdrop-filter:blur(3px);
  transition:border-color .15s var(--ease),background .15s var(--ease)}
.cell .pick:hover{border-color:var(--top)}
.cell .pick:focus-visible{outline:2px solid var(--top);outline-offset:2px}
.cell .pick:checked{background:var(--top);border-color:var(--top)}
/* the tick is drawn, not a glyph, so it stays crisp at any zoom */
.cell .pick:checked::after{content:"";position:absolute;left:6px;top:2.5px;
  width:5px;height:10px;border:solid #17171a;border-width:0 2.2px 2.2px 0;
  transform:rotate(45deg)}
.cell.on .tile{border-color:var(--top)}
.cell.on .tile::after{border-color:var(--top);
  box-shadow:inset 0 0 26px -10px var(--top)}
/* the running count sits with the bulk buttons and is never hidden: a
   selection that acts later must be visible now */
.selnote{align-self:center;font-size:11.5px;color:var(--fg-mut)}
.selcount{align-self:center;font-family:var(--mono);font-size:11.5px;
  font-weight:650;color:var(--top);letter-spacing:-.01em}
.selcount .sub{color:var(--fg-mut);font-weight:400}
.btn.small.go{border-color:var(--top);color:var(--top)}
.btn.small.go:hover{background:var(--top);color:#17171a}

.tile{display:flex;flex-direction:column;width:100%;padding:0;position:relative;color:var(--fg);
  border-radius:var(--r-lg);overflow:hidden;cursor:zoom-in;background:var(--ink-2);
  border:1px solid var(--ink-3);
  transition:transform .2s var(--ease),box-shadow .2s var(--ease),
             border-color .2s var(--ease)}
.tile::after{content:"";position:absolute;inset:0;border-radius:inherit;
  pointer-events:none;border:1.5px solid transparent}
.tile:hover{transform:translateY(-3px);border-color:var(--ink-4);
  box-shadow:0 14px 34px -14px rgba(0,0,0,.9)}
.tile.v-approved::after{border-color:var(--em);box-shadow:inset 0 0 22px -10px var(--em-g)}
.tile.v-rejected::after{border-color:var(--ruby);box-shadow:inset 0 0 22px -10px var(--ruby-g)}
.tile.v-pending::after{border-color:var(--cit);box-shadow:inset 0 0 22px -10px var(--cit-g)}

.tile .shot{display:block;position:relative;background:#fff}
.tile img{width:100%;aspect-ratio:1;object-fit:cover;display:block;
  background:#fff;font-size:0;color:transparent}

.tile .body{display:flex;flex-direction:column;gap:5px;padding:10px 11px 11px}
.tile .idrow{display:flex;align-items:baseline;gap:8px}
.tile .sku{font-family:var(--mono);font-size:12px;font-weight:650;color:var(--fg);
  letter-spacing:-.01em}
.tile .ver{margin-left:auto;font-family:var(--mono);font-size:10px;font-weight:700;
  color:var(--top);border:1px solid var(--top);border-radius:999px;padding:0 6px}
/* No inline "·" separators: a wrapping row strands one at the end of a line at
   every viewport, which reads as a rendering fault. The gap does the work. */
.tile .specrow{display:flex;align-items:baseline;flex-wrap:wrap;
  column-gap:11px;row-gap:1px;font-size:11.5px;color:var(--fg-dim);
  line-height:1.4}
.tile .specrow.sub{font-size:11px;color:var(--fg-mut)}
.tile .spec.num{font-family:var(--mono);font-size:10.5px}
/* the swatch and its colour name are one unit: a lone dot on the line above a
   wrapped "Yellow Gold & Rose Gold" read as a rendering fault */
.tile .spec.metal{display:inline-flex;align-items:baseline;gap:6px}
.tile .spec.metal .mdot{align-self:center;flex:0 0 auto}
.mdot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;
  box-shadow:0 0 0 1px rgba(0,0,0,.45) inset}

/* state pills: each signal gets its own labelled chip with room to be read */
.tile .pills{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
/* A reject reason is the single most useful thing on a rejected tile, so it is
   never clipped: pills wrap to as many lines as the text needs. Only the state
   pills, which are two words at most, stay on one line. */
.p{font-size:9.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  padding:2px 7px;border-radius:6px;border:1px solid var(--ink-4);
  color:var(--fg-mut);max-width:100%;line-height:1.35}
.p.state{white-space:nowrap}
/* machine-assigned reason buckets: the machine-advisory rule — dashed and
   hueless, so an AI bucket can never be read as a human verdict. The client
   variant keeps sapphire's dash context via its title, not a hue. */

.p.ok{color:var(--em);border-color:var(--em);background:var(--em-w)}
.p.bad{color:var(--ruby-lo);border-color:var(--ruby);background:var(--ruby-w)}
.p.wait{color:var(--cit);border-color:var(--cit);background:var(--cit-w)}
.p.client{color:var(--sap);border-color:var(--sap);background:var(--sap-w)}
.p.warn{color:var(--cit);border-color:var(--cit);background:var(--cit-w);
  text-transform:none;letter-spacing:0}
/* Machine-assigned reason buckets. Declared AFTER every hued .p variant and
   with a class more specific than .p.client, because DESIGN.md's rule that
   machine surfaces are dashed and hueless outranks the state hues: a bucket
   the AI chose must never wear the client's sapphire. */
.p.auto,.p.auto.client,.p.auto.bad{border-style:dashed;color:var(--am);
  border-color:var(--am);background:transparent}
.p.redo{color:var(--top);border-color:var(--top);background:var(--top-w)}
.p.ai{color:var(--am);border:1px dashed var(--am);background:var(--am-w);
  font-family:var(--mono);letter-spacing:0}
/* A rejection note is why the image failed: give it four lines before clamping
   (the hover title still carries the rest) instead of the previous two. */
.tile .rmk{font-size:11px;color:var(--sap);line-height:1.4;margin-top:3px;
  display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;
  overflow:hidden}
.empty{color:var(--fg-mut);padding:52px 0;text-align:center;font-size:14px;
  grid-column:1/-1;border:1px dashed var(--ink-3);border-radius:var(--r-lg)}

/* ── machine advisory: ALWAYS dashed amethyst, never mistakable ───────── */
.stchip.ai{color:var(--am);border:1px dashed var(--am);
  background:linear-gradient(var(--am-w),var(--am-w)),rgba(11,11,13,.84);border-radius:999px;font-family:var(--mono)}
.stchip.ai{padding:2px 9px;font-size:11px}
.rai{border:1px dashed var(--am);border-radius:10px;padding:8px 11px;margin:0 0 11px;
  color:var(--am);font-size:11.5px;line-height:1.45;background:var(--am-w)}

.aitag{margin-left:auto;font-size:9px;letter-spacing:.1em;font-weight:700;
  color:var(--am);border:1px dashed var(--am);border-radius:999px;padding:1px 7px}

/* ── compare viewer ────────────────────────────────────────────────────── */
#cmp{position:fixed;inset:0;background:var(--ink-0);display:none;
  flex-direction:column;z-index:60}
#cmp.open{display:flex}
#cmpbar{display:flex;gap:7px;align-items:center;padding:10px 15px;
  border-bottom:1px solid var(--ink-3);overflow-x:auto;scrollbar-width:thin;
  background:linear-gradient(180deg,var(--ink-2),var(--ink-1))}
#cmpbar>*{flex:0 0 auto}
#cmpbar .btn.small{padding:5px 11px;font-size:12px}
#cmpbar .ttl{font-size:13.5px;font-weight:700;white-space:nowrap;font-family:var(--mono)}
#cmpbar .sub2{color:var(--fg-mut);font-size:12px;white-space:nowrap}
#cmpbar .sep{width:1px;height:20px;background:var(--ink-4)}
#cmpbar .zoom{color:var(--top);font-size:12px;min-width:46px;text-align:center;
  font-family:var(--mono);font-weight:600}
@media (max-width:820px){#cmpbar .sub2{display:none}}
#stage{flex:1;position:relative;overflow:hidden;background:var(--ink-0)}
.panes{position:absolute;inset:0;display:flex;gap:2px}
.pane{position:relative;flex:1;overflow:hidden;background:#101013;cursor:grab;
  touch-action:none}
.pane.drag{cursor:grabbing}
/* Pane 1 is a column of the client's own photographs. Each sub-pane owns its
   own scroll-free box, so the shared zoom still maps the same spot on the piece
   in every frame. Side-by-side mode shows both; slider and flicker need a
   single before frame, so the stack collapses to the active one. */
.bstack{position:absolute;inset:0;display:flex;gap:2px}
.bpane{position:relative;flex:1;overflow:hidden;background:#101013;cursor:grab;
  touch-action:none;min-width:0}
.bpane.drag{cursor:grabbing}
/* `.pane img` and `.pane .lbl` already reach these as descendants — only the
   button reset and the switch affordance are new. */
.bpane .lbl{-webkit-appearance:none;appearance:none;font-family:inherit;
  cursor:default}
.bpane .lbl:disabled{opacity:1;color:var(--fg-mut)}   /* caption, still readable */
/* only a real switch is clickable — a static caption never pretends to be */
.bpane .lbl.sw{pointer-events:auto;cursor:pointer;color:var(--fg-dim)}
.bpane .lbl.sw:hover{color:var(--fg);border-color:var(--fg-mut)}
/* which photo carries into slider and flicker */
.bpane .lbl.on{color:var(--brand-lo);border-color:var(--brand)}
.pane img{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform;
  -webkit-user-drag:none;user-select:none;pointer-events:none}
.pane .lbl{position:absolute;left:11px;top:11px;z-index:3;font-size:10px;
  letter-spacing:.12em;font-weight:700;text-transform:uppercase;padding:4px 10px;
  border-radius:999px;pointer-events:none;backdrop-filter:blur(8px);
  background:rgba(11,11,13,.84);border:1px solid var(--ink-4);color:var(--fg-mut)}
.pane .lbl.aft{color:var(--brand);border-color:var(--brand);
  background:linear-gradient(var(--brand-w),var(--brand-w)),rgba(11,11,13,.84)}
.panes.slider{gap:0}
.panes.slider .pane.p2{position:absolute;inset:0;flex:none;
  clip-path:inset(0 0 0 var(--split,50%))}
#handle{position:absolute;top:0;bottom:0;left:50%;width:2px;background:var(--brand);
  z-index:4;cursor:ew-resize;display:none;box-shadow:0 0 16px 1px var(--brand-g)}
.panes.slider~#handle{display:block}
#handle::after{content:'';position:absolute;left:-14px;top:50%;width:30px;height:30px;
  margin-top:-15px;border-radius:50%;background:var(--brand);
  box-shadow:0 3px 14px rgba(0,0,0,.6),0 0 22px -2px var(--brand-g)}
#cmpnote{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);
  padding:7px 15px;border-radius:999px;font-size:11.5px;color:var(--fg-dim);
  background:rgba(20,20,23,.92);border:1px solid var(--ink-4);
  backdrop-filter:blur(10px);transition:opacity .4s var(--ease);z-index:5}
#cmpnote.gone{opacity:0;pointer-events:none}
#verdict{display:flex;gap:9px;align-items:center;padding:11px 15px;flex-wrap:wrap;
  border-top:1px solid var(--ink-3);
  background:linear-gradient(0deg,var(--ink-2),var(--ink-1))}
.stchip{font-size:11.5px;padding:4px 11px;border-radius:999px;color:var(--fg-dim);
  border:1px solid var(--ink-4);background:var(--ink-2)}
.stchip.bad{color:var(--ruby);border-color:var(--ruby);background:var(--ruby-w)}
.stchip.ok{color:var(--em);border-color:var(--em);background:var(--em-w)}
.stchip.wait{color:var(--cit);border-color:var(--cit);background:var(--cit-w)}
.stchip.client{color:var(--sap);border-color:var(--sap);background:var(--sap-w)}
.stchip.quote{color:var(--fg-dim);font-style:italic;max-width:44ch;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* the client's balance, riding along in the viewer where the header strip
   is covered — a count, so mono and tabular like every other figure */
.stchip.credit{font-family:var(--mono);font-variant-numeric:tabular-nums;
  color:var(--em);border-color:var(--em-g);background:var(--em-w)}
/* ── out-of-credits banner ────────────────────────────────────────────────
   Takes the whole first row of the verdict bar so the dark Approve button
   beneath it has a stated reason; a disabled control with no cause on screen
   reads as a fault. Ruby, because "can I still accept this?" is a verdict
   question and the answer is no. */
.vbanner{flex:1 1 100%;padding:8px 13px;border-radius:10px;font-size:13px;
  font-weight:600;line-height:1.4;color:var(--ruby-lo);
  border:1px solid var(--ruby);background:var(--ruby-w)}
.note{flex:1;min-width:180px;padding:8px 13px;border-radius:10px;font-size:16px;
  border:1px solid var(--ink-3);background:var(--ink-0);color:var(--fg)}
.note:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-w)}

/* ── reject-reason picker ──────────────────────────────────────────────── */
#rpick{position:fixed;inset:0;display:none;place-items:center;z-index:70;
  background:rgba(8,8,10,.76);backdrop-filter:blur(4px)}
#rpick.open{display:grid}
.rbox{width:min(470px,93vw);padding:20px;border-radius:var(--r-lg);
  background:linear-gradient(180deg,var(--ink-2),var(--ink-1));
  border:1px solid var(--ink-4);box-shadow:var(--shadow)}
.rbox h3{font-family:var(--display);font-size:15.5px;font-weight:700;margin-bottom:12px}
.rbox h3::after{content:"";display:block;width:34px;height:2px;margin-top:7px;
  background:var(--ruby);box-shadow:0 0 12px 0 var(--ruby-g)}
.ritem{display:flex;gap:11px;align-items:center;padding:8px 11px;border-radius:10px;
  cursor:pointer;font-size:13.5px;user-select:none;border:1px solid transparent;
  width:100%;text-align:left;font-family:inherit;color:var(--fg);background:none;
  -webkit-appearance:none;appearance:none;
  transition:background .14s var(--ease)}
.ritem:hover{background:var(--ink-2)}
.ritem.on{background:var(--ruby-w);border-color:var(--ruby);color:var(--fg)}
/* ordered AFTER .ritem.on on purpose: a machine suggestion stays visibly
   machine even once the human has accepted it */
.ritem.ai{border:1px dashed var(--am)}
.ritem.ai.on{border:1px dashed var(--am);background:var(--ruby-w)}
.ritem.ai.on .rk{background:var(--ruby);color:#1a0405}
.rk{min-width:22px;height:22px;border-radius:7px;background:var(--ink-3);
  display:grid;place-items:center;font-size:11px;font-weight:700;
  font-family:var(--mono);color:var(--fg-dim)}
.ritem.on .rk{background:var(--ruby);color:#1a0405}
.rbox input{width:100%;margin:12px 0;padding:9px 13px;border-radius:10px;font-size:16px;
  border:1px solid var(--ink-3);background:var(--ink-0);color:var(--fg)}
.rbox input:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-w)}
.rtext{flex:1}
.rk-none{color:var(--fg-mut);background:none}
.rrow{display:flex;align-items:center;gap:10px}
.rrow .hint{color:var(--fg-mut);font-size:11.5px;flex:1}

/* ── expectations panel ────────────────────────────────────────────────────
   The client's sentence is the record; everything else is chrome around it. At
   rest a rule is one line plus a scope note, so six rules read as a list rather
   than an essay. Severity is a 3px bar on the left of the SENTENCE (not the
   card) so it reads as punctuation on the text, not a decorative rail. */
#xpanel{position:fixed;inset:0;display:none;place-items:center;z-index:75;
  background:rgba(8,8,10,.78);backdrop-filter:blur(5px)}
#xpanel.open{display:grid}
.xbox{width:min(680px,95vw);max-height:88vh;overflow-y:auto;padding:24px 26px 26px;
  border-radius:var(--r-lg);border:1px solid var(--ink-4);
  background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.006)),
             var(--ink-2);box-shadow:var(--shadow)}
.xhead{display:flex;gap:14px;align-items:flex-start;margin-bottom:4px}
.xhead h3{font-family:var(--display);font-size:20px;font-weight:700;
  letter-spacing:-.015em;flex:0 0 auto}
.xhead .hint{color:var(--fg-dim);font-size:12.5px;max-width:38ch;line-height:1.45}
.xhead .btn{margin-left:auto}

.xlist{display:flex;flex-direction:column;gap:22px;margin:22px 0 26px}
.xgroup{display:flex;flex-direction:column;gap:7px}
.xgh{font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
  color:var(--fg-mut);display:flex;align-items:center;gap:8px;margin-bottom:2px}
.xgh .n{font-family:var(--mono);font-size:10px;color:var(--fg-mut);
  border:1px solid var(--ink-4);border-radius:999px;padding:0 6px;font-weight:600}

.xitem{padding:12px 14px;border-radius:var(--r);background:var(--ink-1);
  border:1px solid var(--ink-3)}
/* A paused rule is still the client's own sentence and must stay readable, so
   the state is carried by the "paused" pill and a recessed surface — not by
   fading the text to 2.8:1. */
.xitem.off{background:transparent;border-style:dashed}
.xitem.off .xsaid{color:var(--fg-dim)}
.xitem.off .xsev{opacity:.45}
.xtop{display:flex;align-items:flex-start;gap:11px}
.xsev{flex:0 0 3px;align-self:stretch;min-height:20px;border-radius:2px;margin-top:2px}
.xsev.must{background:var(--ruby)}
.xsev.flag{background:var(--cit)}
.xsaid{flex:1;font-size:14.5px;line-height:1.5;color:var(--fg)}
.xacts{display:flex;gap:6px;flex:0 0 auto;opacity:.5;transition:opacity .15s var(--ease)}
.xitem:hover .xacts,.xitem:focus-within .xacts{opacity:1}
.xact{font-size:11px;font-weight:600;padding:4px 10px;border-radius:8px;cursor:pointer;
  border:1px solid var(--ink-4);background:transparent;color:var(--fg-dim);
  font-family:inherit}
.xact:hover{border-color:var(--fg-dim);color:var(--fg)}
.xact.del{border-color:var(--ruby);color:var(--ruby);background:var(--ruby-w);
  margin-left:8px}
.xact.del:hover{background:var(--ruby);color:#1a0405}

.xfoot{display:flex;align-items:center;gap:7px;margin:8px 0 0 14px}
.xwhere{font-size:11.5px;color:var(--fg-mut)}
.xdot{font-size:11px;color:var(--ink-5)}
.xsevw{font-size:11.5px;font-weight:650}
.xsevw.must{color:var(--ruby)}
.xsevw.flag{color:var(--cit)}
.xoff{font-size:9.5px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--cit);border:1px solid var(--cit);border-radius:999px;padding:1px 7px}
.xmore{margin-left:auto;font-size:11px;font-weight:600;color:var(--brand-lo);
  background:none;border:0;cursor:pointer;padding:2px 0;font-family:inherit}
.xmore:hover{color:var(--brand)}
.xderived{margin:10px 0 0 14px;display:flex;flex-direction:column;gap:9px}
.xderived[hidden]{display:none}
.xderived .dv{padding:9px 12px;border-radius:9px;background:var(--ink-0);
  border:1px solid var(--ink-3)}
.xderived p{font-size:12px;line-height:1.5;color:var(--fg-dim)}
.xl{display:block;font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--fg-mut);margin-bottom:4px;font-weight:700}
.xlist .empty{padding:34px 20px;font-size:13.5px;line-height:1.6}
.xlist .empty b{color:var(--fg)}

/* authoring: one question at a time */
.xnew{padding-top:20px;border-top:1px solid var(--ink-3)}
.xnew h4{font-family:var(--display);font-size:14px;font-weight:700;
  margin-bottom:4px;color:var(--fg)}
.xnew .sub{font-size:12px;color:var(--fg-mut);margin-bottom:12px}
.xrow{display:flex;gap:9px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
.xrow textarea{flex:1;min-width:250px;padding:11px 13px;border-radius:10px;
  border:1px solid var(--ink-4);background:var(--ink-0);color:var(--fg);
  font:inherit;font-size:16px;resize:vertical;line-height:1.5}
.xrow textarea::placeholder{color:var(--fg-mut)}
.xrow textarea:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 3px var(--brand-w)}
.xrow select,.xrow input{padding:8px 11px;border-radius:10px;font-size:13px;
  border:1px solid var(--ink-3);background:var(--ink-1);color:var(--fg);
  font-family:inherit}
.xrow input{font-family:var(--mono);font-size:12px}
.xscope{display:flex;flex-direction:column;gap:9px;padding:12px 13px;
  border-radius:10px;background:var(--ink-1);border:1px solid var(--ink-3);
  margin-bottom:10px}
.xsl{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
/* selects sit on the same baseline metrics as the scope pills, so the row
   reads as one sentence rather than a form */
.xsl select{padding:5px 9px;font-size:11.5px;border-radius:999px;
  border:1px solid var(--ink-4);background:var(--ink-2);color:var(--fg-dim)}
.xsl select:hover{color:var(--fg);border-color:var(--fg-mut)}
.xlead{font-size:12.5px;color:var(--fg-mut)}
.xslots{display:flex;gap:6px}
.xs{font-size:11.5px;color:var(--fg-mut);display:flex;gap:5px;align-items:center;
  padding:5px 10px;border-radius:999px;border:1px solid var(--ink-3);cursor:pointer;
  white-space:nowrap;transition:color .12s var(--ease),border-color .12s var(--ease)}
.xs:has(input:checked){color:var(--brand-lo);border-color:var(--brand);
  background:var(--brand-w)}
.xs input{accent-color:var(--brand)}
#xdraftbox{margin-top:6px}
#xdraftbox textarea{width:100%;padding:10px 13px;border-radius:10px;
  border:1px solid var(--ink-3);background:var(--ink-0);color:var(--fg);
  font:inherit;font-size:14px;resize:vertical;line-height:1.5}
#xdraftbox textarea:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 3px var(--brand-w)}
#xmic{font-size:15px;padding:8px 12px}
#xmic.live{border-color:var(--ruby);color:var(--ruby);background:var(--ruby-w);
  box-shadow:0 0 18px -5px var(--ruby-g)}

/* ── toast ─────────────────────────────────────────────────────────────── */
#toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,14px);
  padding:11px 20px;border-radius:999px;font-size:13.5px;font-weight:600;
  background:linear-gradient(180deg,var(--ink-2),var(--ink-1));
  border:1px solid var(--brand);color:var(--fg);opacity:0;pointer-events:none;
  z-index:99;box-shadow:var(--shadow),0 0 26px -10px var(--brand-g);
  transition:opacity .22s var(--ease),transform .22s var(--ease)}
#toast.show{opacity:1;transform:translate(-50%,0)}

@media (prefers-reduced-motion:reduce){
  *{transition-duration:.01ms !important;animation-duration:.01ms !important}
  .tile:hover,.btn:hover{transform:none}
}
@media (max-width:700px){
  main{padding:16px 14px 30px}
  .panes{flex-direction:column}
  .grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
  .xbox,.rbox{padding:16px}
  /* On a phone the chrome must not out-scroll the work: stats go three-up and
     compact, and the buckets become one swipeable row instead of five stacked
     ones — roughly 700px of preamble recovered before the first tile. */
  .stats{grid-template-columns:repeat(3,1fr);gap:7px;margin:12px 0 4px}
  .stat{padding:9px 10px}
  .stat b{font-size:19px}
  .stat .lb{font-size:8.5px;letter-spacing:.07em;margin-top:2px}
  /* a third of a phone is narrower than "Near cap · 82%" on one line, and the
     tile clips overflow — so the pill wraps here instead of losing its digits */
  .stat.bstat b{min-height:0}
  .bud{white-space:normal;text-align:center;font-size:11px;padding:3px 9px;
    line-height:1.3}
  .bmoney{font-size:10px;margin-top:4px}
  .buckets{flex-wrap:nowrap;overflow-x:auto;margin:12px -14px 4px;
    padding:2px 14px 6px;scrollbar-width:none;
    -webkit-overflow-scrolling:touch;scroll-snap-type:x proximity}
  .buckets::-webkit-scrollbar{display:none}
  .bucket{flex:0 0 auto;padding:7px 13px;font-size:13px;scroll-snap-align:start}
  .tools{gap:7px;margin:12px 0 13px}
  .tools input,.tools select{font-size:16px}   /* no iOS zoom-on-focus */
  .tools #fsku{flex:1 1 100%}
}

/* ── motion pass ───────────────────────────────────────────────────────────
   Principles applied, not decoration:
   · ENTERS ease out on the Vaul/Sonner curve (0.32,0.72,0,1) — quick start,
     long soft landing. EXITS are faster than enters (140ms vs 220ms): leaving
     should feel decisive, arriving should feel placed.
   · ORIGIN-AWARE. An overlay rises from below because that is where it came
     from; the toast comes from the edge it lives on. Nothing scales from
     nowhere.
   · BLUR is part of the palette, not just transform+opacity — a panel resolves
     into focus rather than merely fading.
   · INTERRUPTIBLE. Every overlay can be reopened mid-exit; the JS guards the
     closing state rather than queueing.
   · Nothing animates on page load.
   · Under reduced-motion, FEEDBACK survives (press compression, verdict flash)
     and DECORATION stops. */
:root{--enter:cubic-bezier(.32,.72,0,1); --exit:cubic-bezier(.4,0,1,1)}

@keyframes ov-scrim-in{from{opacity:0}to{opacity:1}}
@keyframes ov-scrim-out{from{opacity:1}to{opacity:0}}
@keyframes ov-panel-in{
  from{opacity:0;transform:translateY(14px) scale(.97);filter:blur(6px)}
  to{opacity:1;transform:none;filter:blur(0)}}
@keyframes ov-panel-out{
  from{opacity:1;transform:none;filter:blur(0)}
  to{opacity:0;transform:translateY(8px) scale(.985);filter:blur(3px)}}

#rpick.open,#xpanel.open,#epanel.open{animation:ov-scrim-in 160ms var(--enter) both}
#rpick.open>.rbox,#xpanel.open>.xbox,#epanel.open>.ebox{animation:ov-panel-in 220ms var(--enter) both}
#rpick.closing,#xpanel.closing,#epanel.closing{animation:ov-scrim-out 140ms var(--exit) both}
#rpick.closing>.rbox,#xpanel.closing>.xbox,#epanel.closing>.ebox{animation:ov-panel-out 140ms var(--exit) both}

/* the compare viewer is a full surface, so it resolves rather than rises */
@keyframes cmp-in{from{opacity:0;transform:scale(1.01)}to{opacity:1;transform:none}}
#cmp.open{animation:cmp-in 180ms var(--enter) both}

/* THE AUTHORED MOMENT the contract promised: a verdict lights the tile edge,
   and the light blooms rather than snapping on. */
.tile::after{transition:border-color 260ms var(--enter),box-shadow 260ms var(--enter)}
.tile:hover::after{border-color:var(--ink-5)}
.tile.v-approved:hover::after{box-shadow:inset 0 0 30px -8px var(--em-g)}
.tile.v-rejected:hover::after{box-shadow:inset 0 0 30px -8px var(--ruby-g)}
.tile.v-pending:hover::after{box-shadow:inset 0 0 30px -8px var(--cit-g)}

/* Verdict feedback: the bar itself confirms the decision in the verdict's own
   colour before the queue advances. FEEDBACK, so reduced-motion keeps it. */
@keyframes bar-ok{0%{box-shadow:inset 0 3px 0 0 var(--em),0 0 0 rgba(0,0,0,0)}
  40%{box-shadow:inset 0 3px 0 0 var(--em),0 -14px 34px -18px var(--em-g)}
  100%{box-shadow:inset 0 0 0 0 transparent}}
@keyframes bar-bad{0%{box-shadow:inset 0 3px 0 0 var(--ruby),0 0 0 rgba(0,0,0,0)}
  40%{box-shadow:inset 0 3px 0 0 var(--ruby),0 -14px 34px -18px var(--ruby-g)}
  100%{box-shadow:inset 0 0 0 0 transparent}}
#verdict.flash-ok{animation:bar-ok 420ms var(--enter) both}
#verdict.flash-bad{animation:bar-bad 420ms var(--enter) both}

/* picker rows: the toggle should feel physical, so the key badge takes the pop */
.ritem{transition:background 140ms var(--enter),border-color 140ms var(--enter)}
.rk{transition:transform 140ms var(--enter),background 140ms var(--enter)}
.ritem.on .rk{transform:scale(1.12)}

/* stepping between images: cross-fade so a slow proxy fetch does not flash */
.pane img{transition:opacity 140ms var(--enter)}
.pane img.loading{opacity:0}

@media (prefers-reduced-motion:reduce){
  /* decoration off; feedback (press compression, verdict flash) stays */
  #rpick.open,#xpanel.open,#epanel.open,
  #rpick.open>.rbox,#xpanel.open>.xbox,#epanel.open>.ebox,
  #rpick.closing,#xpanel.closing,#epanel.closing,
  #rpick.closing>.rbox,#xpanel.closing>.xbox,#epanel.closing>.ebox,
  #cmp.open{animation:none}
  .tile::after,.pane img,.ritem,.rk{transition:none}
}

/* spec strip: the client's own sheet, on screen while you judge against it */
#cspec{display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  padding:8px 15px;border-bottom:1px solid var(--ink-3);background:var(--ink-1);
  font-size:12px;color:var(--fg-dim)}
#cspec .lbl{font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--fg-mut);font-weight:700}
#cspec .sv{display:flex;align-items:baseline;gap:6px;padding:3px 10px;
  border-radius:999px;border:1px solid var(--ink-3);background:var(--ink-2);
  color:var(--fg);font-weight:550}
#cspec .sv i{font-style:normal;font-size:9.5px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--fg-mut);font-weight:700}
#cspec .sv .mdot{align-self:center}
#cspec .num{font-family:var(--mono);font-size:11px}
@media (max-width:700px){#cspec{padding:7px 12px;font-size:11.5px}}

/* ── in-review edit (admin) ─────────────────────────────────────────────────
   Same overlay idiom as #rpick/#xpanel: fixed scrim + box, .open/.closing
   drive the shared ov- keyframes. Sits above the reject picker (70) and below
   the expectations panel (75) — an edit is opened FROM the viewer, never from
   inside another modal. */
#epanel{position:fixed;inset:0;display:none;place-items:center;z-index:72;
  background:rgba(8,8,10,.76);backdrop-filter:blur(4px)}
#epanel.open{display:grid}
.ebox{width:min(600px,94vw);max-height:88vh;overflow-y:auto;padding:20px;
  border-radius:var(--r-lg);background:linear-gradient(180deg,var(--ink-2),var(--ink-1));
  border:1px solid var(--ink-4);box-shadow:var(--shadow)}
.ehead{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}
.ehead h3{margin:0;font-family:var(--display)}
.ehead h3::after{content:"";display:block;width:34px;height:3px;margin-top:6px;
  border-radius:2px;background:var(--brand)}
.etarget{font-family:var(--mono);font-size:12.5px;color:var(--fg-dim);
  margin:0 0 10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.etarget .mdot{width:9px;height:9px}
.echips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.echip{font:600 11px/1 var(--display);letter-spacing:.02em;color:var(--fg-dim);
  background:var(--ink-2);border:1px dashed var(--ink-4);border-radius:999px;
  padding:6px 11px;cursor:pointer;-webkit-appearance:none}
.echip:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-w)}
/* a full preset inserts thousands of characters, so it is marked as the
   heavier action: solid border and the brand hue rather than the dashed
   outline the one-line fixes use */
.echip.full{border-style:solid;border-color:var(--brand);color:var(--brand);
  background:var(--brand-w)}
.echip.full::before{content:"\2261 ";opacity:.75}
.eseg.wrap{flex-wrap:wrap;border-radius:var(--r)}
.eseg.wrap button{border-radius:0}
#eprompt{width:100%;box-sizing:border-box;background:var(--ink-0);color:var(--fg);
  border:1px solid var(--ink-4);border-radius:10px;padding:10px 12px;font-size:16px;
  font-family:inherit;resize:vertical;min-height:86px}
#eprompt:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-w)}
#eprompt::placeholder{color:var(--fg-mut)}
.erow{display:flex;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}
.erow.foot{margin-top:16px}
.elead{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--fg-mut)}
.eseg{display:inline-flex;border:1px solid var(--ink-4);border-radius:999px;
  overflow:hidden;background:var(--ink-1)}
.eseg button{font:600 12px/1 var(--display);color:var(--fg-dim);background:none;
  border:none;padding:8px 14px;cursor:pointer;-webkit-appearance:none}
.eseg button+button{border-left:1px solid var(--ink-3)}
.eseg button.on{color:#17171a;background:var(--top)}
.eseg button:disabled{color:var(--fg-mut);cursor:default}
.eseg.dim{opacity:.45;pointer-events:none}
.ehint{font-size:11.5px;color:var(--fg-mut)}

/* the tray: running/finished edits, bottom-right, visible over the viewer but
   under every modal and the toast */
/* #cmp's verdict bar owns the bottom of the viewer; the tray lifts above it
   rather than covering Approve/Reject. #etray is a following sibling of #cmp
   (index.html), so the general sibling combinator reaches it. */
#cmp.open ~ #etray{bottom:86px}
#etray{position:fixed;right:16px;bottom:16px;z-index:65;display:flex;
  flex-direction:column;gap:8px;max-width:min(340px,88vw)}
.ejob{display:flex;align-items:center;gap:10px;padding:10px 12px;
  border-radius:var(--r);background:linear-gradient(180deg,var(--ink-2),var(--ink-1));
  border:1px solid var(--cit);box-shadow:var(--shadow);font-size:12.5px;
  color:var(--fg-dim)}
.ejob .who{font-family:var(--mono);font-size:11.5px;color:var(--fg)}
.ejob .sub{font-size:10.5px;color:var(--fg-mut)}
.ejob.done{border-color:var(--em)}
.ejob.err{border-color:var(--ruby)}
.ejob .spin{width:14px;height:14px;flex:none;border-radius:50%;
  border:2px solid var(--ink-4);border-top-color:var(--cit);
  animation:espin .9s linear infinite}
@keyframes espin{to{transform:rotate(360deg)}}
.ejob .act{margin-left:auto;flex:none;font:600 11px/1 var(--display);
  color:var(--em);background:var(--em-w);border:1px solid var(--em);
  border-radius:999px;padding:6px 10px;cursor:pointer;-webkit-appearance:none}
.ejob.err .act{color:var(--ruby-lo);background:var(--ruby-w);border-color:var(--ruby)}
.ejob .x{flex:none;background:none;border:none;color:var(--fg-mut);cursor:pointer;
  font-size:14px;padding:2px 4px;-webkit-appearance:none}
.ejob .x:hover{color:var(--fg)}

/* tile pill for a slot with an edit in flight — admin intent hue, like Redo */
.p.edit{border-color:var(--top);color:var(--top);background:var(--top-w)}

/* the render pane flashes once when an edit lands while its slot is open */
@keyframes editland{0%{box-shadow:inset 0 0 0 3px var(--top),0 0 34px -6px var(--top-g)}
  100%{box-shadow:inset 0 0 0 0 transparent}}
.pane.landed{animation:editland 1.1s var(--ease) 1}
@media (prefers-reduced-motion:reduce){
  .ejob .spin{animation:none;border-top-color:var(--cit)}
  .pane.landed{animation:none}
}

/* ── edit: the source picker shows the images themselves ─────────────────
   Labels alone ("Photo 1", "Photo 2") cannot tell a reviewer that two of an
   earring's angles are the same view — the thumbnails can, at a glance. */
.erow.srcrow{flex-direction:column;align-items:stretch;gap:8px}
.srchead{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.epicks{display:flex;flex-wrap:wrap;gap:10px}
.epick{position:relative;display:flex;flex-direction:column;align-items:center;
  gap:7px;padding:7px 7px 6px;border:1px solid var(--ink-4);border-radius:10px;
  background:var(--ink-1);color:var(--fg-dim);cursor:pointer;
  font:600 11px/1 var(--display);letter-spacing:.02em;-webkit-appearance:none;
  transition:border-color .12s,background .12s,color .12s}
.epick:hover{border-color:var(--brand);color:var(--brand)}
.epick img{width:84px;height:84px;object-fit:contain;border-radius:6px;
  background:#fff;display:block}
.epick.noimg img{display:none}
.epick.noimg::before{content:"no image";width:84px;height:84px;display:flex;
  align-items:center;justify-content:center;border-radius:6px;
  background:var(--ink-3);color:var(--fg-mut);font-weight:500}
.epick .pl{max-width:92px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.epick .tick{position:absolute;top:5px;right:5px;width:18px;height:18px;
  border-radius:50%;background:var(--brand);color:#17171a;font-size:11px;
  display:none;align-items:center;justify-content:center}
.epick.on{border-color:var(--brand);background:var(--brand-w);color:var(--fg)}
.epick.on .tick{display:flex}

/* ── per-image spend ───────────────────────────────────────────────────── */
.stchip.cost{cursor:pointer;border-color:var(--ink-4);color:var(--fg-dim);
  font-variant-numeric:tabular-nums}
.stchip.cost:hover{border-color:var(--brand);color:var(--brand)}
#sppanel{position:fixed;inset:0;background:rgba(6,6,8,.72);z-index:60;
  display:none;align-items:center;justify-content:center;padding:24px}
#sppanel.open{display:flex}
.spbox{background:var(--ink-1);border:1px solid var(--ink-3);border-radius:14px;
  padding:18px;width:min(860px,100%);max-height:84vh;overflow:auto;
  box-shadow:0 24px 70px rgba(0,0,0,.55)}
.sphead{display:flex;align-items:baseline;gap:12px;margin-bottom:12px;
  flex-wrap:wrap}
.sphead h3{margin:0;font:600 15px/1.2 var(--display)}
.sptab{width:100%;border-collapse:collapse;font-size:12.5px}
.sptab th{text-align:left;font:600 10.5px/1 var(--display);letter-spacing:.06em;
  text-transform:uppercase;color:var(--fg-mut);padding:6px 10px 6px 0;
  border-bottom:1px solid var(--ink-3)}
.sptab td{padding:7px 10px 7px 0;border-bottom:1px solid var(--ink-2);
  color:var(--fg-dim);vertical-align:top}
.sptab td.num{font-variant-numeric:tabular-nums;color:var(--fg);font-weight:600}
.sptab td.nt{color:var(--fg-mut)}
.bchip{display:inline-block;font:600 9.5px/1 var(--display);letter-spacing:.04em;
  text-transform:uppercase;padding:3px 6px;border-radius:4px;
  background:var(--ink-3);color:var(--fg-mut)}
.bchip.measured{background:var(--em-w);color:var(--em)}
.bchip.partial{background:var(--cit-w);color:var(--cit)}
.bchip.unpriced{background:var(--ruby-w);color:var(--ruby-lo)}
.bchip.void{background:var(--ruby-w);color:var(--ruby-lo)}
.bchip.submit{background:var(--cit-w);color:var(--cit)}
.spfoot{display:block;margin-top:12px;line-height:1.5}
.stchip.cost.short{border-color:var(--cit-g);color:var(--cit)}
.spsplit{font-size:11.5px;color:var(--fg-mut)}
.spwarn{display:inline-block;margin-top:6px;font-size:11.5px;color:var(--cit);
  line-height:1.45;max-width:64ch}

/* ── run board (the shared lots page's Enhancement section) ────────────────
   The panel is built by static/enhance.js into the staff bench on /upload
   (the operator board that used to build it is retired), so its styles live
   here rather than beside markup that is created at run time. It borrows the
   bench's own vocabulary — .lt-* boxes, the pill ramp, the clip-path progress
   bar — because a run is one more thing in the same room, not a new product.
   What it does NOT borrow is the money vocabulary: no dollars, no caps, one
   state word for an operator. */
.rn-start{padding:14px 16px;border-radius:var(--r);border:1px solid var(--ink-3);
  background:var(--ink-2);margin-bottom:20px}
.rn-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.rn-head b{font-family:var(--display);font-size:14px;font-weight:700;color:var(--fg)}
.rn-start .hint{color:var(--fg-dim);font-size:12.5px;line-height:1.5;
  margin:6px 0 0;max-width:72ch}
.rn-filters{display:flex;gap:14px 22px;flex-wrap:wrap;margin-top:13px}
.rn-f{flex:1 1 280px;min-width:0;display:flex;flex-direction:column;gap:6px}
.rn-lb{font:700 9.5px/1 var(--display);letter-spacing:.12em;text-transform:uppercase;
  color:var(--fg-mut)}
.rn-f textarea{padding:8px 11px;border-radius:9px;font:inherit;font-size:12.5px;
  color:var(--fg);border:1px solid var(--ink-4);background:var(--ink-1);
  resize:vertical;min-height:52px}
.rn-f textarea:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 3px var(--brand-w)}
.rn-f textarea::placeholder{color:var(--fg-mut)}
.rn-f .sub{font-size:11.5px;color:var(--fg-mut);line-height:1.45}
.rn-f .sub.bad{color:var(--cit)}
.rn-chips{display:flex;gap:7px;flex-wrap:wrap;align-items:center;min-height:30px}
.rn-chip{padding:5px 12px;border-radius:999px;cursor:pointer;
  font:600 12px/1.3 var(--display);color:var(--fg-dim);
  border:1px solid var(--ink-4);background:var(--ink-1)}
.rn-chip:hover{color:var(--fg);border-color:var(--ink-5)}
.rn-chip.on{color:var(--brand);border-color:var(--brand);background:var(--brand-w)}
.rn-none{font-size:11.5px;color:var(--fg-mut);line-height:1.45}
.rn-act{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:14px}
.rn-why{font-size:12px;color:var(--cit);line-height:1.45;max-width:64ch}
.rn-why:empty{display:none}
/* budget: a word, never a number the client's money can be read off */
.rn-bud{display:inline-block;padding:4px 10px;border-radius:999px;
  font:600 11px/1.3 var(--display);letter-spacing:.02em;
  border:1px solid var(--ink-4);color:var(--fg-dim);background:var(--ink-1)}
.rn-bud.ok{color:var(--em);border-color:var(--em);background:var(--em-w)}
.rn-bud.warn{color:var(--cit);border-color:var(--cit);background:var(--cit-w)}
.rn-bud.bad{color:var(--ruby-lo);border-color:var(--ruby);background:var(--ruby-w)}
.rn-bud.none{border-style:dashed;color:var(--fg-mut)}

.rn-list{display:flex;flex-direction:column;gap:10px}
.rn-list:empty{display:none}
.rn-run{padding:12px 14px;border-radius:var(--r);border:1px solid var(--ink-3);
  background:var(--ink-2)}
.rn-run.running{border-color:color-mix(in srgb,var(--brand) 36%,var(--ink-3))}
.rn-run.failed{border-color:color-mix(in srgb,var(--ruby) 36%,var(--ink-3))}
.rn-rhead{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:9px}
.rn-id{font-family:var(--mono);font-size:11px;color:var(--fg-mut)}
.rn-meta{font-size:11.5px;color:var(--fg-dim)}
.rn-rhead .btn.small{padding:4px 10px;font-size:11.5px}
/* run status pills, on the board's own lot-pill ramp */
.p.run-planned{border-style:dashed;color:var(--fg-dim);border-color:var(--ink-5)}
.p.run-running{color:var(--brand);border-color:var(--brand);background:var(--brand-w)}
.p.run-paused{color:var(--cit);border-color:var(--cit);background:var(--cit-w)}
.p.run-done{color:var(--em);border-color:var(--em);background:var(--em-w)}
.p.run-partial{color:var(--cit);border-color:var(--cit);background:var(--cit-w)}
.p.run-failed{color:var(--ruby-lo);border-color:var(--ruby);background:var(--ruby-w)}
.p.run-cancelled{border-style:dashed;color:var(--fg-mut);border-color:var(--ink-4)}
/* revealed, not resized — same clip-path bar the uploaders use, so a run and
   an upload read as the same kind of progress */
.rn-bar{position:relative;height:8px;border-radius:4px;background:var(--ink-4);
  overflow:hidden}
.rn-bar .fill{display:block;height:100%;width:100%;--p:0%;background:var(--brand);
  clip-path:inset(0 calc(100% - var(--p)) 0 0);
  transition:clip-path var(--dur-quick) ease}
.rn-run.done .rn-bar .fill{background:var(--em)}
.rn-run.partial .rn-bar .fill,.rn-run.paused .rn-bar .fill{background:var(--cit)}
.rn-run.failed .rn-bar .fill,.rn-run.cancelled .rn-bar .fill{background:var(--ruby)}
.rn-nums{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px 14px;
  font-family:var(--mono);font-size:11.5px;color:var(--fg-dim);
  font-variant-numeric:tabular-nums}
.rn-nums .k{color:var(--fg-mut)}
.rn-nums b{color:var(--fg);font-weight:600}
.rn-nums .ng b{color:var(--ruby-lo)}
.rn-fails{margin-top:10px;max-height:240px;overflow-y:auto;display:flex;
  flex-direction:column;gap:1px}
.rnf{display:flex;align-items:baseline;gap:10px;padding:5px 8px;border-radius:7px;
  font-size:12px;background:var(--ink-1);box-shadow:inset 2px 0 0 var(--ruby)}
.rnf .k{flex:0 0 auto;font-family:var(--mono);font-size:11.5px;color:var(--fg-dim);
  max-width:34ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rnf .e{flex:1 1 auto;min-width:0;font-size:11px;color:var(--ruby-lo);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media (max-width:700px){
  .rn-filters{flex-direction:column}
  .rnf{flex-direction:column;gap:2px}
  .rnf .k,.rnf .e{max-width:none;white-space:normal}
}
@media (prefers-reduced-motion:reduce){.rn-bar .fill{transition:none}}

/* ── how far a lot has got ─────────────────────────────────────────────────
   One strip above the stats row. Segments are sized by share of the total and
   sit in a single track, so the eye reads "how much is left" without doing
   arithmetic. The key underneath names each segment, because colour alone is
   not an answer for anyone who cannot separate these two hues. */
.lotprog{margin:0 0 14px;padding:12px 14px;border-radius:var(--r);
  background:var(--ink-1);border:1px solid var(--ink-3)}
.lotprog-top{display:flex;justify-content:space-between;align-items:baseline;
  margin-bottom:8px;font-size:13px;color:var(--fg)}
.lotprog-top span{color:var(--fg-mut);font-variant-numeric:tabular-nums}
.lotprog-track{display:flex;height:8px;border-radius:999px;overflow:hidden;
  background:var(--ink-3)}
.lotprog-track .seg{display:block;height:100%}
/* NO transition on the segment widths, for two reasons that both matter.
   The first is the rule the rest of the portal already follows and wrote
   down at upload.css:292 — a fill is REVEALED with clip-path, never
   resized, because animating width forces a layout pass and a progress
   event fires many times a second. The second is that it would not have
   animated anyway: renderProgress() in app.js rebuilds this whole strip
   through innerHTML, so every segment is a brand-new element with no
   previous width to interpolate from. If this ever needs to move, patch
   the widths in place and animate clip-path the way .lt-upbar .fill and
   .cap .fill do — do not bring width back. */
.lotprog-track .seg.acc{background:linear-gradient(90deg,var(--em),var(--em))}
.lotprog-track .seg.chg{background:linear-gradient(90deg,var(--brand-lo),var(--brand))}
.lotprog-key{display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:9px;
  font-size:12px;color:var(--fg-mut);font-variant-numeric:tabular-nums}
.lotprog-key .dot{display:inline-block;width:7px;height:7px;border-radius:50%;
  margin-right:6px;vertical-align:middle}
.lotprog-key .dot.acc{background:var(--em)}
.lotprog-key .dot.chg{background:var(--brand)}
.lotprog-key .dot.rest{background:var(--ink-5)}
