: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;
/* THREE NAMES THAT WERE USED AND NEVER DEFINED. An undefined custom property
   makes the whole declaration invalid at computed-value time -- so the rule is
   dropped and the property inherits, silently and plausibly. The toast set
   `color:var(--paper)` on `background:var(--ink)` and inherited `--ink` from
   the body: black on black, for as long as it has existed.
   `--line` (18 uses) and `--bg` (5) did the same to borders and backgrounds
   in the point cloud and the mosaic -- there, inheriting gave something
   readable, which is why nobody saw it.
   ALIASED RATHER THAN RENAMED AT EVERY SITE: one place to be wrong instead of
   twenty-four, and the names read as what their users meant. */
--paper:var(--panel);--line:var(--rule);--bg:var(--panel)}
@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}

/* -- picking images, and the one destructive button in the place ----------- */
/* A COLUMN NARROW ENOUGH TO READ AS A MARGIN. Wider and the table looks like
   it is about selection; this one is about images, and the boxes are an
   affordance a person goes looking for. */
th.cbx,td.cbx{width:30px;padding-right:0}
th.cbx input,td.cbx input{width:15px;height:15px;cursor:pointer;
  accent-color:var(--accent)}
tr.taken{background:var(--accent-soft)}
/* THE BAR APPEARS ONLY WITH A SELECTION and sits above the table rather than
   floating over it: a control that covers a row of the thing it acts on is a
   control people mis-aim. */
.pickbar{display:flex;align-items:center;gap:12px;margin:0 0 8px;padding:8px 12px;
  background:var(--panel);border:1px solid var(--rule);border-radius:4px}
/* AND `hidden` HAS TO WIN. The browser's own `[hidden]{display:none}` is a
   user-agent rule of the same specificity as the class above, so the author
   rule beats it and an empty bar sits there permanently. Said here, once. */
.pickbar[hidden]{display:none}
.pickbar .n{font-weight:600}
.pickbar .grow{flex:1}
/* RED, AND ONLY HERE. Nothing else in this interface deletes anything, so the
   colour is unambiguous rather than one severity among several. */
.btn.danger{border-color:var(--crit);color:var(--crit);background:transparent}
.btn.danger:hover{background:var(--crit);color:#fff}
.btn.danger[disabled]{opacity:.45;cursor:not-allowed}
.btn.danger[disabled]:hover{background:transparent;color:var(--crit)}
/* THE TALLY IN THE CONFIRMATION, where the number is the message. */
.cost{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:12px 0;
  font-size:.9rem}
.cost dt{font-family:var(--mono);font-variant-numeric:tabular-nums;
  text-align:right;font-weight:600}
.cost dd{margin:0;color:var(--ink2)}
.cost .heavy dt,.cost .heavy dd{color:var(--crit)}

/* -- the look-alike strip, and picking out of it ----------------------------- */
/* TILES THAT CAN BE TICKED. The checkbox sits ON the thumbnail rather than
   under it: a column of boxes below a row of pictures makes somebody count
   across to find which box belongs to which animal, and at 92 px they miscount. */
.nearbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:0 0 8px}
/* `.tick` is only styled inside `.filters`, and this bar is not one. */
.nearbar .tick{display:flex;align-items:center;gap:5px;font-size:.82rem}
.ntiles{display:flex;flex-wrap:wrap;gap:8px}
.ntile{position:relative;width:92px;border-radius:5px}
/* THE WHOLE TILE SAYS SO, not just the box. A tick 14 px across is not
   readable as state from the distance somebody looks at a grid of these. */
.ntile.taken{outline:2px solid var(--accent);outline-offset:2px}
.ntick{position:absolute;top:3px;left:3px;z-index:2;display:flex;padding:2px;
  background:var(--panel);border:1px solid var(--rule2);border-radius:3px;
  cursor:pointer;opacity:.75}
.ntile:hover .ntick,.ntile.taken .ntick{opacity:1}
.ntick input{margin:0;cursor:pointer}
.nopen{all:unset;cursor:pointer;display:block;width:92px;text-align:center}
.nopen[disabled]{cursor:default}
.nopen img{width:92px;height:92px;object-fit:contain;border-radius:4px;
  background:var(--panel);border:1px solid var(--rule)}
.nopen:focus-visible img{outline:2px solid var(--accent);outline-offset:1px}
.ncap{font-size:.66rem;color:var(--muted);margin-top:2px;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}

/* -- stepping through the set a sheet was opened from ----------------------- */
/* THE SAME CONTROL ON BOTH SHEETS. An image sheet and an instance sheet walk
   different sets, but "‹ 4 of 100 ›" is one gesture and looking like two
   would make somebody learn it twice. */
.stepper{display:inline-flex;gap:5px;align-items:center;margin-right:8px}
.stepper .at{color:var(--muted);font-size:.74rem;white-space:nowrap}
.stepper .btn[disabled]{opacity:.4;cursor:default}

/* -- a picture that is not there, and which of the two reasons it is -------- */
/* THE SAME SIZE AS A THUMBNAIL, so a page of these does not reflow into
   something that looks like a different table. */
.nothumb{width:72px;height:52px;border-radius:3px;background:var(--panel2);
  display:flex;align-items:center;justify-content:center;
  font-size:.62rem;color:var(--muted);text-align:center;cursor:help;
  border:1px dashed var(--rule2)}
.nothumb.gone{color:var(--warn);border-style:solid}
.sum a{cursor:pointer;text-decoration:underline}

/* -- what the last batch did, after its sheet has been closed --------------- */
/* THE SHEET IS THE PLACE TO WATCH A BATCH AND THE WRONG PLACE TO KEEP ITS
   RESULT: it is closed, and closing it used to be all it took to lose which
   files did not go up. This is the same report, on the page, until it is
   dismissed or another batch replaces it. */
.uplog .upsum{display:flex;gap:8px;align-items:center;flex-wrap:wrap;
  font-family:var(--sans);font-size:.78rem}
.uplog .upsum .btn{padding:2px 8px;font-size:.7rem}
.uplog .quiet{color:var(--muted)}
.uplog .upbadline{padding:1px 0;color:var(--crit);
  overflow-wrap:anywhere;max-width:62ch}
.uplog .upbadline b{color:var(--ink2)}
/* AND THE ONE WORD IN THE SHEET THAT HAD TO BE RED WAS NOT. `.uplog .bad` was
   the only rule for it, and the count moved into `.upstat` without one. */
.upstat .bad{color:var(--crit);font-weight:600}
.upstat .ok{color:var(--ok)}

/* -- a machine whose clock disagrees with ours ------------------------------ */
/* Neither good nor bad: a fact about the machine that explains timings which
   look wrong everywhere else, which is why it is a chip and not an error. */
.pill.warn{background:#F6ECD5;color:#7A5A12;border-color:transparent}
@media(prefers-color-scheme:dark){.pill.warn{background:#3D2F12;color:#E0BC72}}

/* -- setting a bench up, one step at a time --------------------------------- */
/* THE NUMBERS ARE NOT DECORATION. This is a real sequence -- the second step
   cannot be done before the first, and the third happens on another machine
   -- which is the one case where numbering a list says something true. */
.wstep{display:flex;gap:10px;align-items:flex-start;padding:8px 0;
  border-top:1px solid var(--rule)}
.wstep:first-child{border-top:0}
.wnum{flex:none;width:22px;height:22px;border-radius:99px;display:flex;
  align-items:center;justify-content:center;font-size:.75rem;font-weight:600;
  background:var(--panel2);color:var(--muted);border:1px solid var(--rule2)}
.wstep.done .wnum{background:var(--ok);color:#fff;border-color:transparent}
.wstep > div:last-child{min-width:0;flex:1}
pre.cmd{font-family:var(--mono);font-size:.76rem;background:var(--panel2);
  border:1px solid var(--rule2);border-radius:4px;padding:8px 10px;
  margin:6px 0;overflow-x:auto;white-space:pre}
/* A SECRET ON A SCREEN IS STILL A SECRET. Marked so it is obvious in a
   screenshot what should not have been in one. */
pre.cmd.secret{border-color:var(--warn);background:var(--panel)}

/* -- who is committed to a step -------------------------------------------- */
.commit{margin-top:5px;font-size:.72rem;color:var(--ink2)}
.commit .m{color:var(--muted)}
/* NOTHING COMMITTED IS NOT AN ERROR AND IS NOT NOTHING. Amber: the step is on,
   the queue will fill, and no machine is going to empty it. */
.commit.none{color:var(--warn)}
.commit.clash > div{color:var(--crit)}
.commit .why{margin-top:3px;color:var(--crit);max-width:44ch}
.commit.none .why{color:var(--warn)}

/* -- processing, as a tree -------------------------------------------------- */
/* THE STEP IS THE ROOT AND THE MACHINES ARE UNDER IT. A table of switches said
   what was configured; it never said whether anything would do the work, which
   is the question somebody has when they are looking at it. */
.steptree{padding:4px 16px 14px}
.steproot{border-left:2px solid var(--accent);padding:8px 0 8px 12px;
  margin:10px 0}
.steproot.off{border-left-color:var(--rule2);opacity:.72}
.stephead{display:flex;gap:12px;align-items:baseline;flex-wrap:wrap}
.stepname strong{font-size:.95rem}
.stepname span{display:block;color:var(--muted);font-size:.72rem}
.stepname{min-width:13rem}
.stepalg{min-width:12rem}
.stepq{font-size:.78rem;color:var(--ink2);flex:1;min-width:10rem}
.stepdo{display:flex;gap:5px;flex-wrap:wrap}
.stepdo .btn{padding:2px 8px;font-size:.72rem}
.stepset{margin:6px 0 0;display:flex;gap:8px;align-items:baseline;
  flex-wrap:wrap}
.stepset input{flex:1;min-width:18rem;font-family:var(--mono);font-size:.74rem;
  padding:3px 6px;border:1px solid var(--rule2);border-radius:3px;
  background:var(--panel);color:var(--ink)}
.stepset .hint{color:var(--muted);font-size:.7rem;max-width:38ch}
/* THE BRANCHES. Indented and joined, so a machine is visibly under a step
   rather than beside it. */
.stepkids{margin:6px 0 0 6px;padding-left:12px;
  border-left:1px dashed var(--rule2)}
.kid{font-size:.76rem;padding:2px 0;color:var(--ink2)}
.kid .m{color:var(--muted);margin-left:6px}
.kid.none{color:var(--warn)}
.kid.none span{color:var(--muted)}
.kid.clash strong{color:var(--crit)}
.kid.why{color:var(--crit);max-width:52ch;padding-top:3px}
.kid.none.why{color:var(--warn)}

/* -- the pipeline, drawn ---------------------------------------------------- */
.dagwrap{padding:6px 16px 14px;overflow-x:auto}
.dag{max-width:none}
.dag text{font:600 12px var(--sans);fill:var(--ink)}
.dag text.num{font-weight:400;font-size:11px;fill:var(--muted)}
/* DATA IS ROUNDED AND QUIET; a thing that exists rather than a thing that
   runs, and the eye should be able to tell them apart without reading. */
.dnode rect{fill:var(--panel2);stroke:var(--rule2);stroke-width:1}
.snode rect{fill:var(--panel);stroke:var(--accent);stroke-width:1.5}
.snode.off rect{stroke:var(--rule2);stroke-dasharray:4 3}
.snode.off text{fill:var(--muted)}
/* ON, AND NOTHING COMMITTED TO IT: the queue fills and no machine empties it. */
.snode.idle rect{stroke:var(--warn)}
.snode.bad rect{stroke:var(--crit);stroke-width:2}
.snode .dot{fill:var(--ok)}
.snode.can{cursor:pointer}
.snode.can:hover rect{filter:brightness(1.06)}
.snode.can:hover text{text-decoration:underline}
.kid.howto{color:var(--ink2);padding-top:6px;max-width:62ch}
.kid.howto pre.cmd{margin:5px 0;font-size:.72rem}
.kid.howto div{padding:1px 0}
/* NO MODEL IS NOT A DECISION. A step nobody has registered an algorithm for is
   a stage the pipeline has and cannot run yet -- fainter than off, because off
   is something somebody chose. */
.snode.bare rect{stroke:var(--rule);stroke-dasharray:2 4;fill:var(--panel2)}
.snode.bare text{fill:var(--muted)}

/* -- viewing settings and the scale bar ------------------------------------- */

/* NEUTRAL BY DEFAULT, AND DEFINED. An undefined custom property invalidates
   the whole declaration at computed-value time, so every rule below that uses
   it would be dropped and the images would inherit a filter from an ancestor
   -- the same fault the comment at the top of this file describes. */
:root{--viewfx:none}

/* WHAT COUNTS AS A PICTURE OF A SPECIMEN. Scans, cut-outs, thumbnails and the
   embedding mosaic get the viewing settings; the brand mark and the icons do
   not, because nobody wants the logo gamma-corrected. */
.shot .big,.sheet img.big,td img,.peek img,.nopen img,#mosaic{filter:var(--viewfx)}

/* THE NAV BECOMES A COLUMN so one item can sit at the bottom. It was already
   `height:100vh`, so there is room to push into; `min-height:0` lets the
   scrolling still work when the list of pages is taller than the window. */
.nav{display:flex;flex-direction:column;min-height:0}
.nav .navfill{flex:1 1 auto;min-height:8px}
.nav a.tool{color:var(--muted);font-size:.84rem;
  border-top:1px solid var(--rule);padding-top:10px;padding-bottom:12px}
.nav a.tool:hover{color:var(--ink)}
.nav a.tool svg{flex:0 0 auto}

/* THE IMAGE AND ITS BAR SHARE A BOX, so the bar's percentage width is a
   percentage of the picture rather than of the column beside it. */
.shotwrap{position:relative;display:block}
.shotwrap .big{display:block}

/* OVER THE IMAGE, NOT BESIDE IT. Semi-transparent dark with a light rule and a
   light label so it reads on a pale background and on a dark specimen alike,
   which is the whole difficulty with drawing on somebody else's photograph.
   `pointer-events:none` so it never eats a click meant for the picture. */
.scalebar{position:absolute;left:50%;transform:translateX(-50%);bottom:10px;
  height:9px;border-radius:2px;pointer-events:none;
  background:rgba(12,18,26,.42);border:1px solid rgba(255,255,255,.75);
  box-shadow:0 0 0 1px rgba(12,18,26,.28)}
.scalebar span{position:absolute;left:0;right:0;bottom:11px;text-align:center;
  font-family:var(--mono);font-size:.7rem;line-height:1;color:#fff;
  text-shadow:0 1px 2px rgba(9,14,20,.9),0 0 3px rgba(9,14,20,.7);
  white-space:nowrap}

/* THE SLIDERS. Full width of the cell so the travel is usable, with the value
   beside them -- a slider with no number is guesswork when somebody wants to
   come back to the same setting tomorrow. */
.facts input[type=range]{width:min(260px,100%);vertical-align:middle}
.facts output.m{display:inline-block;min-width:3.2em;text-align:right;
  color:var(--muted);font-size:.76rem}

/* A BASE RULE FOR `.hint`, which until now existed only as `.stepset .hint`.
   Used unscoped in the viewing panel and rendered at body size there, which
   made the explanations shout louder than the controls. The scoped rule is
   more specific and still wins where it applies. */
.hint{color:var(--muted);font-size:.72rem;max-width:46ch;margin:.35em 0 0}

/* THE BAR ON THE HOVER CARD. The card is a flex row, so the picture and its bar
   need a box that neither grows nor shrinks -- without `flex:0 0 auto` the
   wrapper is free to be squeezed by the column of facts beside it, and a
   percentage of a squeezed box is not a percentage of the picture.
   `display:block` on the image because an inline image carries a few pixels of
   descender space under it, which the bar would otherwise sit on top of. */
.peek .shotwrap{flex:0 0 auto}
.peek .shotwrap img{display:block}
/* SMALLER, because 150 px of thumbnail is a sixth of the sheet's cut-out and a
   9 px bar with a .7rem label over it covers the specimen it is measuring. */
.peek .scalebar{height:7px;bottom:7px}
.peek .scalebar span{bottom:9px;font-size:.62rem}
