:root{--ground:#EEF2F6;--panel:#fff;--panel2:#F5F8FB;--ink:#0F1720;--ink2:#3E4C5A;
--muted:#6B7B8B;--rule:#D8E1E9;--rule2:#BFCCD8;--accent:#1F5FA9;--accent-ink:#16487F;
--accent-soft:#E3EDF8;--ok:#2E6F53;--warn:#8C6318;--crit:#A6353F;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
@media(prefers-color-scheme:dark){:root{--ground:#0D1217;--panel:#141B22;--panel2:#1A232B;
--ink:#E3EAF1;--ink2:#B0BECB;--muted:#7F8E9D;--rule:#232E38;--rule2:#32404C;
--accent:#74AEEC;--accent-ink:#A6CCF5;--accent-soft:#15273A;--ok:#6FC49B;--warn:#D8A857;--crit:#E0808A}}
*{box-sizing:border-box}
body{margin:0;background:var(--ground);color:var(--ink);font:14px/1.45 var(--sans)}
a{color:var(--accent-ink)}
.shell{display:grid;grid-template-columns:212px minmax(0,1fr);min-height:100vh}
@media(max-width:820px){.shell{grid-template-columns:1fr}.nav{display:none}}
.nav{background:var(--panel2);border-right:1px solid var(--rule);padding:12px 0}
/* THE NAV STAYS PUT. It is a grid column, so by default it is as tall as its
   own content and scrolls away with the page -- which on a table of five
   hundred rows means the way out of the page is a scroll back to the top.
   Sticky to the viewport, its own scrollbar when the list of pages is longer
   than the window, and `align-self:start` because a stretched grid item
   cannot be sticky at all. */
.nav{position:sticky;top:0;align-self:start;height:100vh;overflow-y:auto}
.nav a{display:flex;align-items:center;gap:8px;padding:8px 16px;color:var(--ink2);
  text-decoration:none;font-size:.9rem;cursor:pointer;border-left:3px solid transparent}
.nav a:hover{background:var(--panel);color:var(--ink)}
.nav a.on{background:var(--panel);color:var(--accent-ink);font-weight:600;
  border-left-color:var(--accent)}
.nav .sep{height:1px;background:var(--rule);margin:8px 12px}
.nav .heading{padding:6px 16px 2px;font-size:.62rem;letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}
.top{display:flex;align-items:center;gap:14px;padding:10px 18px;background:var(--panel);
  border-bottom:1px solid var(--rule);position:sticky;top:0;z-index:5}
.brand{display:flex;align-items:center;gap:9px;padding:2px 16px 12px}
.brand img{width:26px;height:26px}
.brand strong{display:block;font-size:.95rem;letter-spacing:.01em}
.brand span{display:block;font-size:.68rem;color:var(--muted)}
.top select,.top input{font:inherit;padding:5px 8px;border:1px solid var(--rule2);
  border-radius:3px;background:var(--panel);color:var(--ink)}
.grow{flex:1}
.pill{display:inline-block;font-family:var(--mono);font-size:.68rem;padding:2px 8px;
  border-radius:99px;background:var(--panel2);color:var(--muted);border:1px solid var(--rule2)}
.pill.ok{background:#DCEFE4;color:#1B5E3F;border-color:transparent}
.pill.wait{background:#F6E7C8;color:#6B4C0E;border-color:transparent}
.pill.bad{background:#F5DAD6;color:#7A2418;border-color:transparent}
/* IN PROGRESS BREATHES, so a stalled lease and a running one are not the same
   grey chip. Stopped for anybody who has asked their machine to stop moving
   things -- a status column is not worth a headache. */
.pill.run{background:#D9E6F7;color:#1C4577;border-color:transparent}
.pill.run::before{content:"";display:inline-block;width:5px;height:5px;
  border-radius:99px;background:currentColor;margin-right:5px;vertical-align:1px;
  animation:beat 1.4s ease-in-out infinite}
@keyframes beat{0%,100%{opacity:.35}50%{opacity:1}}
@media(prefers-reduced-motion:reduce){.pill.run::before{animation:none}}
/* THE WORK COLUMN: one chip per step, in pipeline order, always in the same
   places -- a column that reflows as steps finish cannot be read down. */
.steps{display:flex;gap:4px;flex-wrap:wrap}
.steps .pill{cursor:default}
@media(prefers-color-scheme:dark){.pill.ok{background:#17372A;color:#8FD6B4}
  .pill.wait{background:#4A3A12;color:#F0D79A}
  .pill.bad{background:#4A1F19;color:#F0A79A}
  .pill.run{background:#17263F;color:#9AC2F0}}
.badge{display:inline-block;min-width:16px;padding:0 5px;border-radius:99px;
  background:var(--crit);color:#fff;font-size:.66rem;font-weight:600;
  font-family:var(--mono);text-align:center;vertical-align:1px}
.toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);
  background:var(--ink);color:var(--paper);padding:9px 16px;border-radius:4px;
  font-size:.85rem;box-shadow:0 6px 24px rgba(0,0,0,.25);z-index:50;
  animation:rise .18s ease-out}
@keyframes rise{from{opacity:0;transform:translate(-50%,6px)}}
@media(prefers-reduced-motion:reduce){.toast{animation:none}}
/* THE OVERLAY SITS ON THE PICTURE, so both have to be measured the same way:
   the wrapper is the size of the image and the svg fills it exactly. */
.shot{position:relative;display:inline-block;line-height:0;max-width:100%}
.shot .big{display:block;max-width:100%;height:auto}
.outlines{position:absolute;inset:0;width:100%;height:100%}
.outlines.off{display:none}
/* TWO LINES PER INSTANCE, one behind the other: a pale wide one and a thin dark
   one on top. A single stroke has to be dark enough to see on a white plate
   and light enough to see on a black beetle, and there is no such colour --
   the halo is what makes a hairline readable over both, and a hairline is
   what lets somebody judge whether the outline actually follows the instance.
   A six-pixel line covers the very edge it is meant to show. */
.outlines .halo{fill:none;stroke:rgba(255,255,255,.9);stroke-width:3.2;
  vector-effect:non-scaling-stroke;pointer-events:none}
.outlines .line{fill:none;stroke:#12467F;stroke-width:1.1;
  vector-effect:non-scaling-stroke;cursor:pointer;pointer-events:all}
/* NON-SCALING STROKE, or a line drawn in a 22000-pixel coordinate system is
   invisible on screen and enormous when zoomed in. `pointer-events:all` so
   the inside of an unfilled shape is still something you can point at. */
.outlines .line:hover{stroke:#A6353F;stroke-width:2.2;
  fill:rgba(166,53,63,.12)}
@media(prefers-color-scheme:dark){.outlines .line{stroke:#0B3566}}
/* THE HOVER CARD sits over the picture and must never be under the cursor:
   pointer-events off, or moving onto it counts as leaving the shape and the
   two fight each other. */
.peek{position:absolute;transform:translate(-50%,calc(-100% - 12px));
  pointer-events:none;background:var(--panel);border:1px solid var(--rule2);
  border-radius:6px;padding:10px 12px;display:flex;gap:12px;align-items:center;
  box-shadow:0 10px 30px rgba(0,0,0,.3);z-index:3;line-height:1.35;
  max-width:420px}
/* BIG ENOUGH TO IDENTIFY SOMETHING BY. The point of hovering is to see the
   instance without opening it, and a 46-pixel square of a beetle is a dark
   blob -- at 150 the legs and the elytra are there. */
.peek img{width:150px;height:150px;object-fit:contain;border-radius:4px;
  background:var(--panel2)}
.peek .facts{min-width:130px}
.peek strong{display:block;font-size:1rem;margin-bottom:3px}
.peek span{display:block;color:var(--muted);font-family:var(--mono);
  font-size:.76rem}
.peek em{display:block;margin-top:5px;font-style:normal;font-size:.72rem;
  color:var(--muted)}
.shotbar{display:flex;gap:12px;align-items:center;padding:8px 2px;
  font-size:.78rem}
.shotbar .tog{display:flex;gap:6px;align-items:center;cursor:pointer}
.body{padding:18px}
.drop{border:1px dashed var(--rule2);border-radius:5px;padding:12px 14px;
  margin:0 0 12px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-start;
  justify-content:space-between;font-size:.84rem}
.drop.over{border-color:var(--accent);background:var(--accent-soft)}
.drop strong{display:block;margin-bottom:2px}
.drop span{display:block;color:var(--muted);font-size:.78rem;max-width:62ch}
.link{color:var(--accent);cursor:pointer;text-decoration:underline}
.drop .hide{display:none}
.drop #upman{width:100%}
.uplog{font-family:var(--mono);font-size:.74rem;min-width:300px;flex:1}
.uplog .ok{color:var(--ok)} .uplog .bad{color:var(--crit)}
.uplog .warn{color:var(--warn)}
/* A BAR, because "is it doing anything" is the question, and a number that
   changes every few minutes does not answer it as well as a line that
   creeps. */
.upbar{height:6px;border-radius:99px;background:var(--panel2);overflow:hidden;
  border:1px solid var(--rule2)}
.upbar span{display:block;height:100%;background:var(--accent);
  transition:width .3s ease}
.upstat{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:6px}
.upstat strong{font-size:.82rem}
.upstat .btn{padding:2px 8px;font-size:.7rem}
/* WHAT IS HAPPENING RIGHT NOW: three lines at most, one per upload in flight,
   so somebody can see movement without reading five hundred rows. */
.upnow{margin-top:5px;color:var(--muted);display:flex;flex-direction:column}
.upnow span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  max-width:46ch}
.upbad{margin-top:7px;color:var(--crit);max-height:9em;overflow:auto}
.upbad div{padding:1px 0}
.filters .tick{display:flex;align-items:center;gap:5px;font-size:.82rem;
  padding-bottom:4px}
/* A PREVIEW THAT CANNOT BE MISTAKEN FOR A RESULT. The first version said
   "412 would be relabelled" above a button somebody had already scrolled
   past, and it was read as "412 relabelled" -- so the wording says nothing
   has happened and the button that makes it happen is underneath the thing
   it describes. */
.pending{background:var(--accent-soft);border-left:3px solid var(--accent);
  padding:8px 12px;border-radius:3px}
.btn.go{background:var(--accent);color:#fff;border-color:var(--accent);
  font-size:.9rem;padding:7px 14px}
.who-role{display:inline-flex;align-items:center;gap:4px;margin:0 8px 3px 0;
  white-space:nowrap;font-size:.82rem}
.pdesc{margin:0 0 12px;padding:10px 14px;border-left:3px solid var(--rule2);
  color:var(--ink2);font-size:.88rem;white-space:pre-wrap;max-width:78ch}
.issues{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:-6px 0 12px;
  font-size:.8rem}
.issues.none{color:var(--muted)}
.issues .lead{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;
  font-size:.68rem;margin-right:2px}
.issues .chip{font:inherit;font-family:var(--mono);font-size:.74rem;
  padding:3px 9px;border-radius:99px;cursor:pointer;
  border:1px solid var(--rule2);background:var(--panel);color:var(--ink2)}
.issues .chip:hover{border-color:var(--crit);color:var(--crit)}
.issues .chip.on{background:var(--crit);border-color:var(--crit);color:#fff}
.issues .chip.clear{color:var(--muted)}
.sum{display:flex;flex-wrap:wrap;gap:1px;background:var(--rule);border:1px solid var(--rule);
  border-radius:4px;overflow:hidden;margin-bottom:14px}
.sum>div{flex:1 1 150px;background:var(--panel);padding:9px 12px;min-width:0}
.sum dt{font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.sum dd{margin:2px 0 0;font-size:1.05rem;font-family:var(--mono);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sum small{display:block;font-size:.7rem;color:var(--muted);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.panel{border:1px solid var(--rule);border-radius:4px;background:var(--panel);
  margin-bottom:14px;overflow:hidden}
.panel h3{margin:0;padding:9px 14px;border-bottom:1px solid var(--rule);
  font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.panel .tw{border:0;border-radius:0}
.panel table.facts th{padding:5px 10px 5px 0}
.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}
.filters input,.filters select{font:inherit;font-size:.83rem;padding:5px 8px;
  border:1px solid var(--rule2);border-radius:3px;background:var(--panel);color:var(--ink)}
.filters label{display:flex;align-items:center;gap:5px;font-size:.75rem;color:var(--muted)}
.count{font-family:var(--mono);font-size:.74rem;color:var(--muted)}
th.sortable{cursor:pointer;user-select:none}
th.sortable:hover{color:var(--accent-ink)}
.tw{border:1px solid var(--rule);border-radius:4px;overflow:auto;background:var(--panel)}
table{border-collapse:collapse;width:100%}
th{text-align:left;font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted);font-weight:600;padding:8px 12px;border-bottom:1px solid var(--rule);
  background:var(--panel2);position:sticky;top:0}
td{padding:8px 12px;border-bottom:1px solid var(--rule);vertical-align:middle}
tr:last-child td{border-bottom:0}
tr.pick{cursor:pointer}
tr.pick:hover{background:var(--accent-soft)}
td img{width:72px;height:52px;object-fit:cover;border-radius:3px;background:var(--panel2);
  display:block}
td.m{font-family:var(--mono);font-size:.8rem;font-variant-numeric:tabular-nums}
.who{font-size:.78rem}
.who small{display:block;color:var(--muted);font-size:.68rem}
.empty{padding:56px 20px;text-align:center;color:var(--muted)}
.modal{position:fixed;inset:0;background:rgba(9,14,20,.62);display:flex;
  align-items:center;justify-content:center;padding:24px;z-index:20}
.sheet{background:var(--panel);border-radius:6px;max-width:1080px;width:100%;
  max-height:92vh;overflow:auto;border:1px solid var(--rule2)}
.sheet header{display:flex;align-items:center;gap:12px;padding:11px 16px;
  border-bottom:1px solid var(--rule);position:sticky;top:0;background:var(--panel)}
.sheet h3{margin:0;font-size:.95rem}
.sheet .sub{color:var(--muted);font-family:var(--mono);font-size:.74rem}
.sheet .in{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(260px,1fr);
  gap:16px;padding:16px}
@media(max-width:760px){.sheet .in{grid-template-columns:1fr}}
/* A checkerboard, because these cut-outs have their background removed and a
   transparent instance on a grey panel looks like a badly lit one. */
.sheet img.big{width:100%;border-radius:4px;
  background-color:var(--panel2);
  background-image:
    linear-gradient(45deg,var(--rule) 25%,transparent 25%,transparent 75%,var(--rule) 75%),
    linear-gradient(45deg,var(--rule) 25%,transparent 25%,transparent 75%,var(--rule) 75%);
  background-size:18px 18px;background-position:0 0,9px 9px}
td img{background-image:
    linear-gradient(45deg,var(--rule) 25%,transparent 25%,transparent 75%,var(--rule) 75%),
    linear-gradient(45deg,var(--rule) 25%,transparent 25%,transparent 75%,var(--rule) 75%);
  background-size:10px 10px;background-position:0 0,5px 5px}
.facts{width:100%;border-collapse:collapse;font-size:.82rem}
.facts th{width:38%;text-transform:none;letter-spacing:0;font-size:.78rem;
  background:transparent;position:static;color:var(--muted);vertical-align:top}
/* Wrap between words, not inside them: `break-all` turned "project_member"
   into "project_memb / er" and a row of counts into a puzzle. Ids still break
   anywhere, because they have nowhere else to break. */
.facts td{font-family:var(--mono);font-size:.78rem;overflow-wrap:anywhere;
  word-break:normal}
.btn{font:inherit;padding:5px 11px;border-radius:3px;border:1px solid var(--rule2);
  background:var(--panel);color:var(--accent-ink);cursor:pointer}
.gate{max-width:520px;margin:12vh auto;background:var(--panel);border:1px solid var(--rule2);
  border-radius:6px;padding:22px}
.gate h2{margin:.2rem 0 .6rem;font-size:1.05rem}
.gate p{color:var(--muted);font-size:.85rem}
.gate input{width:100%;font:inherit;padding:7px 9px;border:1px solid var(--rule2);
  border-radius:3px;background:var(--panel);color:var(--ink);font-family:var(--mono)}

/* A RANGE WITH TWO HANDLES, which the platform does not have.
   TWO <input type=range> OVERLAID IS THE USUAL TRICK AND IT DOES NOT WORK.
   The one on top has a full-width invisible track, so it swallows every
   pointer event: the lower handle is unreachable except in the sliver where
   the upper one is not, which reads exactly as "only the right cursor
   works". No amount of z-index fixes it, because whichever is on top has the
   same problem in the other direction.
   So: a track, two handles, and pointer capture. The handles are the only
   things that take events, they are focusable, and they say where they are
   while being dragged -- which is the question somebody is asking as they
   move one. */
.range{position:relative;width:210px;height:26px;touch-action:none;
  user-select:none}
.range .bar{position:absolute;left:0;right:0;top:11px;height:4px;
  border-radius:3px;background:var(--rule2)}
/* THE BAND IS DRAGGABLE, so it says so under the pointer. A control that
   can be swept but looks like a decoration is a control nobody sweeps. */
.range .fill{position:absolute;top:11px;height:4px;border-radius:3px;
  background:var(--accent);cursor:grab}
.range .fill:active{cursor:grabbing}
/* AND A WIDER REACH THAN IT LOOKS. A four-pixel bar is a hard thing to hit
   with a mouse; this gives it the height of the track without changing what
   is drawn. */
.range .fill::after{content:"";position:absolute;left:0;right:0;top:-8px;
  bottom:-8px}
.range .grip{position:absolute;top:5px;width:16px;height:16px;margin-left:-8px;
  border-radius:50%;background:var(--panel);border:2px solid var(--accent);
  cursor:grab;box-shadow:0 1px 3px rgba(0,0,0,.28)}
.range .grip:active{cursor:grabbing}
.range .grip:focus-visible{outline:2px solid var(--accent-ink);outline-offset:2px}
.range .tag{position:absolute;top:-9px;transform:translateX(-50%);
  font-size:.66rem;font-family:var(--mono);color:var(--muted);
  white-space:nowrap;pointer-events:none}

/* WHILE THE CABINET IS ANSWERING. Clicks during a search were not merely
   ignorable -- each one started another search, and a slow answer could land
   after a newer one and overwrite it, so the picture ended up showing the
   result of a question nobody asked last.
   The picture and the mosaic stop taking clicks and say so; the controls
   above them do not, because stopping a search or changing the mode are
   exactly what somebody waiting might want. */
.busy{position:relative;opacity:.55;pointer-events:none;
  transition:opacity .12s ease}
.busy::after{content:"";position:absolute;left:50%;top:50%;width:26px;
  height:26px;margin:-13px 0 0 -13px;border-radius:50%;
  border:3px solid var(--rule2);border-top-color:var(--accent);
  animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.busy::after{animation:none;
  border-top-color:var(--rule2)}}

/* THE FILTERS, IN GROUPS. A wall of controls that does not say which subject
   each one is about is a wall nobody reads twice: "in June" is a question
   about an image and "over 10 mm²" is a question about the thing on it. */
.filterbar{align-items:flex-start;gap:14px}
.fgroup{display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  padding:8px 10px 8px 0;border-right:1px solid var(--rule);position:relative}
.fgroup:last-of-type{border-right:0}
.flabel{font-size:.62rem;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted);align-self:center;padding-right:2px}
