/* ob3y.net — portal UI
   Monochrome instrument panel. One accent (error red). All Plex Mono.

   Ported from ob3y.me. The original design is preserved exactly — same
   colours, same type scale, same square corners. Changes made on port:
     - hardcoded colours (#fcfcfb #d8d8d5 #ececeb, the badge hues) promoted
       into :root so modules can reuse them
     - a spacing scale added (the original had none; 22px was the gutter)
     - IBM Plex Mono self-hosted instead of loaded from the Google CDN
     - portal components appended: nav, tables, forms, buttons, alerts,
       the permission matrix, job states */

/* ---------- self-hosted IBM Plex Mono ----------
   A private site should not phone a third-party CDN on every page load.
   See assets/fonts/README.md for the one-time fetch. */
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
  src:url('fonts/IBMPlexMono-Regular.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
  src:url('fonts/IBMPlexMono-Medium.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:700;font-display:swap;
  src:url('fonts/IBMPlexMono-Bold.woff2') format('woff2');
}

:root{
  /* surfaces & ink — unchanged from ob3y.me */
  --paper:#f5f5f4;
  --ink:#17181a;
  --mid:#77797d;
  --label:#4c4e52;
  --card:#ffffff;
  --hair:#d3d3d1;
  --trace:#8a1a2b;
  --err:#a32133;
  --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;

  /* promoted from hardcoded values in the original */
  --field:#fcfcfb;        /* input / toolbar idle background */
  --field-focus:#ffffff;  /* input background on focus */
  --field-edge:#d8d8d5;   /* input / toolbar border */
  --code:#ececeb;         /* inline code background */

  /* badge hues promoted — reused for status chrome across modules */
  --hue-arc:#9a7b2e;   --hue-arc-t:#8a6d24;
  --hue-disk:#3f6d8c;  --hue-disk-t:#345f7d;
  --hue-doc:#5a6270;   --hue-doc-t:#4c5460;
  --hue-img:#4a7a55;   --hue-img-t:#3d6b48;
  --hue-media:#7a4a6a; --hue-media-t:#6b3d5c;

  --ok:var(--hue-img-t);
  --warn:var(--hue-arc-t);
  --info:var(--hue-disk-t);

  /* spacing scale — 22px stays the gutter, as in the original */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:22px;
  --s6:32px; --s7:48px; --s8:64px;
  --gutter:22px;

  /* border weights — the original's implicit 3-step scale, named */
  --bw-hair:1px; --bw-chrome:1.5px; --bw-strong:2px;

  /* chrome type — uppercase, letter-spaced, small */
  --ls-tight:.14em; --ls-mid:.26em; --ls-wide:.36em;
}
*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  background:var(--paper);
  color:var(--ink);
  font-family:var(--mono);
  font-size:15px;
  line-height:1.6;
  min-height:100svh;
}
::selection{background:var(--ink);color:var(--paper)}
:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
a{color:var(--ink)}

/* faint corner hatch echo of the logo */
body::before{
  content:"";position:fixed;right:-140px;top:-140px;z-index:0;
  width:420px;height:420px;border-radius:50%;
  border:52px solid transparent;
  border-color:rgba(0,0,0,.045);
  pointer-events:none;
}

/* ---------- logo ---------- */
.ob3y-logo{
  display:block;width:100%;height:auto;
  filter:drop-shadow(0 18px 14px rgba(0,0,0,.16));
}
.ob3y-logo .tr{transition:stroke-dashoffset 2.4s cubic-bezier(.6,0,.25,1),fill-opacity .9s ease}
.ob3y-logo.err-shake{animation:obshake .45s cubic-bezier(.36,.07,.19,.97)}
@keyframes obshake{
  10%,90%{transform:translateX(-2px)}
  20%,80%{transform:translateX(4px)}
  30%,50%,70%{transform:translateX(-7px)}
  40%,60%{transform:translateX(7px)}
}

/* ---------- login layout ---------- */
.gate{
  min-height:100svh;position:relative;z-index:1;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:8vh var(--gutter) calc(6vh + env(safe-area-inset-bottom,0px));
}
.gate .logo-wrap{width:min(74vw,320px);margin-bottom:2.8rem}
.gate .eyebrow{
  font-size:.66rem;letter-spacing:.34em;text-transform:uppercase;
  color:var(--mid);margin-bottom:2rem;text-align:center;
}
.gate .eyebrow b{color:var(--ink);font-weight:500}

form.gate-form{
  width:min(90vw,380px);
  background:var(--card);
  border:var(--bw-strong) solid var(--trace);
  padding:clamp(26px,6vw,36px) clamp(20px,5.5vw,30px) clamp(22px,5vw,30px);
  box-shadow:0 22px 34px -16px rgba(0,0,0,.22);
}
.field{margin-bottom:1.6rem}
.field label{
  display:block;font-size:.64rem;letter-spacing:.3em;
  text-transform:uppercase;color:var(--label);margin-bottom:.5rem;
}
.field input,.field select,.field textarea{
  width:100%;background:var(--field);
  border:var(--bw-chrome) solid var(--field-edge);
  font-family:var(--mono);font-size:1.05rem;color:var(--ink);
  padding:13px 12px;border-radius:0;
  transition:border-color .18s,background .18s;
}
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--ink);background:var(--field-focus);outline:none;
}
.field input:-webkit-autofill{
  -webkit-box-shadow:0 0 0 60px var(--field-focus) inset;
  -webkit-text-fill-color:var(--ink);
}
.field .hint{
  display:block;margin-top:.45rem;font-size:.66rem;letter-spacing:.04em;
  color:var(--mid);text-transform:none;
}

.btn-enter{
  width:100%;margin-top:.4rem;
  background:var(--ink);color:var(--paper);
  border:var(--bw-strong) solid var(--ink);
  font-family:var(--mono);font-weight:700;font-size:.8rem;
  letter-spacing:var(--ls-mid);text-transform:uppercase;
  padding:16px 10px;cursor:pointer;
  transition:background .18s,color .18s;
}
.btn-enter:hover,.btn-enter:focus-visible{background:var(--trace);border-color:var(--trace);color:#fff}
.btn-enter:disabled{opacity:.35;cursor:not-allowed}

.gate-msg{
  min-height:1.4rem;margin:-0.3rem 0 1.1rem;
  font-size:.74rem;letter-spacing:.06em;color:var(--err);
}
.gate-foot{
  margin-top:3.4rem;font-size:.62rem;letter-spacing:.24em;
  text-transform:uppercase;color:var(--mid);text-align:center;
}

/* ---------- page shell ---------- */
.wrap{max-width:760px;margin:0 auto;padding:0 var(--gutter) 12vh;position:relative;z-index:1}
.wrap.wide{max-width:1040px}
.tools-head{
  display:flex;align-items:flex-end;justify-content:space-between;gap:16px;
  padding:7vh 0 2.2rem;border-bottom:var(--bw-strong) solid var(--ink);margin-bottom:.4rem;
  flex-wrap:wrap;
}
.tools-head .logo-wrap{width:min(38vw,168px)}
.tools-head .meta{
  font-size:.64rem;letter-spacing:var(--ls-mid);text-transform:uppercase;
  color:var(--mid);text-align:right;
}
.tools-head .meta a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--ink)}
.tools-head .meta a:hover{color:var(--err);border-color:var(--err)}
h1.page-title{
  font-size:.72rem;letter-spacing:var(--ls-wide);text-transform:uppercase;
  font-weight:500;color:var(--mid);margin:1.6rem 0 1.2rem;
}
.file-list{list-style:none}
.file-row{
  display:flex;align-items:baseline;gap:14px;
  padding:15px 4px;border-bottom:1px solid var(--hair);
}
.file-row a.fname{
  text-decoration:none;font-weight:500;word-break:break-all;
  border-bottom:1px solid transparent;
}
.file-row a.fname:hover{border-color:var(--ink)}
.file-row .dots{flex:1;border-bottom:1px dotted var(--hair);transform:translateY(-4px);min-width:24px}
.file-row .fsize{color:var(--mid);font-size:.78rem;white-space:nowrap}
.file-row .get{
  font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;
  text-decoration:none;border:1px solid var(--ink);
  padding:5px 11px;white-space:nowrap;
  transition:background .15s,color .15s;
}
.file-row .get:hover{background:var(--ink);color:var(--paper)}
.empty{color:var(--mid);padding:2.5rem 0;font-size:.85rem}

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce){
  .ob3y-logo .tr{transition:none !important;stroke-dashoffset:0 !important;stroke-dasharray:none !important;fill-opacity:1 !important}
  .ob3y-logo.err-shake{animation:none}
  *{transition-duration:.01ms !important}
}

/* ---------- breadcrumb + toolbar + badges ---------- */
.crumbs{
  font-size:.72rem;letter-spacing:var(--ls-tight);color:var(--mid);
  margin:.2rem 0 1.6rem;
}
.crumbs a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--hair)}
.crumbs a:hover{border-color:var(--trace);color:var(--trace)}
.crumbs .sep{margin:0 .5em;color:var(--hair)}
.crumbs .here{color:var(--trace)}

.toolbar{
  display:flex;gap:14px;align-items:center;justify-content:space-between;
  flex-wrap:wrap;margin-bottom:1.4rem;
}
#filter,.input-inline{
  flex:1 1 220px;min-width:180px;
  background:var(--field);border:var(--bw-chrome) solid var(--field-edge);
  font-family:var(--mono);font-size:.9rem;color:var(--ink);
  padding:11px 13px;border-radius:0;
  transition:border-color .18s,background .18s;
}
#filter:focus,.input-inline:focus{border-color:var(--ink);background:var(--field-focus);outline:none}
#filter::placeholder,.input-inline::placeholder{color:var(--mid)}
.sorts{display:flex;gap:0;border:var(--bw-chrome) solid var(--field-edge)}
.sorts button{
  font-family:var(--mono);font-size:.66rem;letter-spacing:var(--ls-tight);
  text-transform:uppercase;color:var(--mid);
  background:var(--field);border:none;border-left:var(--bw-chrome) solid var(--field-edge);
  padding:10px 13px;cursor:pointer;transition:background .15s,color .15s;
}
.sorts button:first-child{border-left:none}
.sorts button:hover{color:var(--ink)}
.sorts button.on{background:var(--ink);color:var(--paper)}

/* badges */
.badge{
  flex:0 0 auto;width:42px;text-align:center;
  font-size:.58rem;font-weight:700;letter-spacing:.08em;
  padding:4px 0;border:1px solid var(--hair);color:var(--mid);
  text-transform:uppercase;user-select:none;
}
.badge-dir{border-color:var(--trace);color:var(--trace);font-weight:700}
.badge-exe{border-color:var(--ink);color:var(--ink)}
.badge-arc{border-color:var(--hue-arc);color:var(--hue-arc-t)}
.badge-disk{border-color:var(--hue-disk);color:var(--hue-disk-t)}
.badge-doc{border-color:var(--hue-doc);color:var(--hue-doc-t)}
.badge-img{border-color:var(--hue-img);color:var(--hue-img-t)}
.badge-media{border-color:var(--hue-media);color:var(--hue-media-t)}
.badge-data{border-color:var(--hue-doc);color:var(--hue-doc-t)}

/* row tweaks for the richer layout */
.file-row{gap:12px;align-items:center}
.file-row .fname{align-self:center}
.file-row.updir .fname{color:var(--mid)}
.file-row .fdate{
  color:var(--mid);font-size:.72rem;white-space:nowrap;
  min-width:82px;text-align:right;
}
.file-row .get-slot{width:42px;flex:0 0 auto}
@media(max-width:560px){
  .file-row .fdate{display:none}
  .badge{width:38px}
}

.noresult{color:var(--mid);padding:2rem 0;font-size:.85rem;text-align:center}
.tools-foot{
  margin-top:3.5rem;padding-top:1.4rem;border-top:1px solid var(--hair);
  font-size:.62rem;letter-spacing:.24em;text-transform:uppercase;color:var(--mid);
}
code{font-family:var(--mono);background:var(--code);padding:1px 5px;font-size:.9em}


/* ============================================================
   PORTAL COMPONENTS — new for ob3y.net
   Same vocabulary: square corners, letter-spaced caps chrome,
   one crimson accent, ink for weight.
   ============================================================ */

/* ---------- primary nav ---------- */
.nav{
  display:flex;gap:0;flex-wrap:wrap;
  border-bottom:1px solid var(--hair);margin-bottom:var(--s5);
}
.nav a{
  font-size:.66rem;letter-spacing:var(--ls-mid);text-transform:uppercase;
  color:var(--mid);text-decoration:none;
  padding:var(--s3) var(--s4) var(--s3) 0;margin-right:var(--s5);
  border-bottom:var(--bw-strong) solid transparent;
  transition:color .15s,border-color .15s;
}
.nav a:hover{color:var(--ink)}
.nav a.on{color:var(--trace);border-bottom-color:var(--trace)}

/* Section-level nav (admin: users / roles / system / audit). Deliberately
   quieter than the primary strip — it is a second level, and two rules of
   equal weight stacked on top of each other read as a mistake. */
.subnav{
  border-bottom:0;margin-top:calc(-1 * var(--s3));margin-bottom:var(--s4);
}
.subnav a{
  font-size:.6rem;padding:var(--s2) var(--s3) var(--s2) 0;margin-right:var(--s4);
  border-bottom-width:1px;
}

/* ---------- section headings ---------- */
.sect{
  font-size:.66rem;letter-spacing:var(--ls-wide);text-transform:uppercase;
  font-weight:500;color:var(--mid);
  margin:var(--s7) 0 var(--s3);padding-bottom:var(--s2);
  border-bottom:1px solid var(--hair);
}
.sect:first-child{margin-top:0}

/* ---------- alerts ---------- */
.alert{
  border-left:var(--bw-strong) solid var(--mid);
  background:var(--card);padding:var(--s3) var(--s4);
  margin-bottom:var(--s4);font-size:.85rem;
}
.alert-err{border-left-color:var(--err);color:var(--err)}
.alert-ok{border-left-color:var(--ok);color:var(--ok)}
.alert-warn{border-left-color:var(--warn);color:var(--warn)}
.alert-info{border-left-color:var(--info);color:var(--info)}

/* ---------- buttons ---------- */
.btn{
  display:inline-block;font-family:var(--mono);
  font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;
  text-decoration:none;cursor:pointer;
  background:transparent;color:var(--ink);
  border:1px solid var(--ink);padding:9px 15px;border-radius:0;
  transition:background .15s,color .15s,border-color .15s;
}
.btn:hover{background:var(--ink);color:var(--paper)}
.btn-primary{background:var(--ink);color:var(--paper)}
.btn-primary:hover{background:var(--trace);border-color:var(--trace);color:#fff}
.btn-danger{border-color:var(--err);color:var(--err)}
.btn-danger:hover{background:var(--err);color:#fff}
.btn-quiet{border-color:var(--hair);color:var(--mid)}
.btn-quiet:hover{background:var(--mid);border-color:var(--mid);color:var(--paper)}
.btn:disabled,.btn[aria-disabled=true]{opacity:.35;cursor:not-allowed}
.btn-sm{font-size:.58rem;padding:5px 10px;letter-spacing:.16em}
.btn-row{display:flex;gap:var(--s2);flex-wrap:wrap;align-items:center}

/* ---------- tables ---------- */
.tbl{width:100%;border-collapse:collapse;font-size:.85rem}
.tbl th{
  text-align:left;font-size:.6rem;font-weight:700;
  letter-spacing:var(--ls-tight);text-transform:uppercase;color:var(--mid);
  padding:var(--s2) var(--s3) var(--s2) 0;
  border-bottom:var(--bw-chrome) solid var(--ink);white-space:nowrap;
}
.tbl td{
  padding:var(--s3) var(--s3) var(--s3) 0;
  border-bottom:1px solid var(--hair);vertical-align:middle;
}
.tbl tr:hover td{background:rgba(0,0,0,.018)}
.tbl td.num,.tbl th.num{text-align:right;font-variant-numeric:tabular-nums}
.tbl td.shrink,.tbl th.shrink{width:1%;white-space:nowrap}
.tbl .muted{color:var(--mid);font-size:.78rem}
.tbl-scroll{overflow-x:auto}

/* ---------- status pill ---------- */
.pill{
  display:inline-block;font-size:.56rem;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;
  padding:3px 8px;border:1px solid var(--hair);color:var(--mid);white-space:nowrap;
}
.pill-active,.pill-done,.pill-idle,.pill-enabled{border-color:var(--hue-img);color:var(--hue-img-t)}
.pill-disabled,.pill-cancelled{border-color:var(--hair);color:var(--mid)}
/* a module that is switched on but cannot load — a dependency is missing */
.pill-blocked{border-color:var(--warn);color:var(--warn)}
/* core modules: cannot be disabled, so they read as structural not stateful */
.pill-core{border-color:var(--ink);color:var(--ink)}
.pill-failed,.pill-error{border-color:var(--err);color:var(--err)}
.pill-queued,.pill-pending{border-color:var(--hue-doc);color:var(--hue-doc-t)}
.pill-slicing,.pill-downloading,.pill-uploading,.pill-claimed{border-color:var(--hue-arc);color:var(--hue-arc-t)}
.pill-printing,.pill-sent{border-color:var(--trace);color:var(--trace)}
.pill-sliced{border-color:var(--hue-disk);color:var(--hue-disk-t)}
/* printer telemetry states the agent reports */
.pill-offline,.pill-unknown{border-color:var(--hair);color:var(--mid)}

/* ---------- progress bar ----------
   A queue is read at a glance, so the bar is a rule that fills rather than
   a widget. Crimson while something is happening, grey while it is not. */
.bar{position:relative;height:6px;min-width:90px;background:var(--hair);overflow:hidden}
.bar > i{display:block;height:100%;background:var(--trace);transition:width .4s ease}
.bar.idle > i{background:var(--mid)}
.bar-row{display:flex;align-items:center;gap:var(--s2)}
.bar-row .pct{
  font-size:.68rem;color:var(--mid);min-width:3.4em;text-align:right;
  font-variant-numeric:tabular-nums;
}

/* ---------- one-time secret (agent token) ----------
   Shown once and never again, so it has to look like something you are
   meant to act on now rather than a field you can come back to. */
.secret{
  border:var(--bw-chrome) solid var(--trace);background:var(--card);
  padding:var(--s4);margin-bottom:var(--s5);
}
.secret code{
  display:block;font-size:.8rem;word-break:break-all;
  padding:var(--s3);background:var(--paper);border:1px solid var(--hair);
  margin:var(--s3) 0;
}

/* ---------- permission matrix ----------
   Deny must read as louder than allow — an explicit deny is the
   exception a reader needs to spot at a glance. */
.matrix{width:100%;border-collapse:collapse;font-size:.8rem}
.matrix th{
  font-size:.56rem;letter-spacing:.12em;text-transform:uppercase;
  color:var(--mid);font-weight:700;padding:var(--s2);
  border-bottom:var(--bw-chrome) solid var(--ink);
}
.matrix th.perm{text-align:left;width:auto}
.matrix th.col{text-align:center;width:64px}
.matrix td{padding:var(--s2);border-bottom:1px solid var(--hair)}
.matrix td.perm{text-align:left}
.matrix td.col{text-align:center}
.matrix .pslug{font-weight:500}
.matrix .pdesc{display:block;color:var(--mid);font-size:.7rem;letter-spacing:.02em}
.matrix .modgroup td{
  background:rgba(0,0,0,.03);font-size:.6rem;font-weight:700;
  letter-spacing:var(--ls-tight);text-transform:uppercase;color:var(--label);
}
.matrix input[type=radio],.matrix input[type=checkbox]{accent-color:var(--ink);cursor:pointer}
/* Not scoped to .matrix — the same yes/no vocabulary is used by the module,
   storage and route tables, and it should mean the same thing everywhere. */
.eff-deny{color:var(--err);font-weight:700}
.eff-allow{color:var(--hue-img-t)}
.eff-role{color:var(--mid)}

/* ---------- definition rows (job detail, printer detail) ---------- */
.dl{display:grid;grid-template-columns:minmax(120px,180px) 1fr;gap:var(--s2) var(--s4)}
.dl dt{
  font-size:.6rem;letter-spacing:var(--ls-tight);text-transform:uppercase;
  color:var(--mid);padding-top:3px;
}
.dl dd{font-size:.85rem;word-break:break-word}

/* ---------- card ---------- */
.card{
  background:var(--card);border:1px solid var(--hair);
  padding:var(--s4);margin-bottom:var(--s4);
}
.card-head{
  font-size:.62rem;letter-spacing:var(--ls-mid);text-transform:uppercase;
  color:var(--mid);margin-bottom:var(--s3);
  padding-bottom:var(--s2);border-bottom:1px solid var(--hair);
}

/* ---------- form grid ---------- */
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:0 var(--s5)}
.form-actions{
  display:flex;gap:var(--s3);align-items:center;flex-wrap:wrap;
  margin-top:var(--s5);padding-top:var(--s4);border-top:1px solid var(--hair);
}

/* ---------- one-time secret reveal (agent tokens) ---------- */
.secret{
  display:block;background:var(--code);border:var(--bw-chrome) dashed var(--trace);
  padding:var(--s3);margin:var(--s3) 0;
  font-size:.85rem;word-break:break-all;user-select:all;
}

/* ---------- progress ---------- */
.prog{height:6px;background:var(--code);border:1px solid var(--hair);overflow:hidden}
.prog span{display:block;height:100%;background:var(--trace);transition:width .4s ease}

/* ---------- timeline (job events) ---------- */
.tl{list-style:none;border-left:1px solid var(--hair);margin-left:var(--s2);padding-left:var(--s4)}
.tl li{position:relative;padding:var(--s2) 0;font-size:.8rem}
.tl li::before{
  content:"";position:absolute;left:calc(-1 * var(--s4) - 4px);top:14px;
  width:7px;height:7px;background:var(--paper);border:1px solid var(--mid);
}
.tl li.on::before{background:var(--trace);border-color:var(--trace)}
.tl .when{color:var(--mid);font-size:.68rem;letter-spacing:.04em;margin-right:var(--s2)}

/* ---------- utility ---------- */
.muted{color:var(--mid)}
.right{text-align:right}
.nowrap{white-space:nowrap}

/* ---------- layout utilities ----------
   These exist because the CSP is style-src 'self' with no 'unsafe-inline',
   which blocks style="" attributes outright — a template that reaches for
   one gets it silently dropped by the browser and nothing says why. Keeping
   the policy tight is worth more than the convenience, so the handful of
   one-off adjustments the templates need live here as named classes. */
.inline{display:inline}
.block{display:block}
.grow{flex:1}
.mt2{margin-top:var(--s2)}
.mt3{margin-top:var(--s3)}
.mt4{margin-top:var(--s4)}
.mt5{margin-top:var(--s5)}
.mt6{margin-top:var(--s6)}
.mb0{margin-bottom:0}
.mb3{margin-bottom:var(--s3)}
.mb4{margin-bottom:var(--s4)}
.mb5{margin-bottom:var(--s5)}
.ml2{margin-left:var(--s2)}
.ml3{margin-left:var(--s3)}
.pre{white-space:pre-wrap;word-break:break-word}
.tiny{font-size:.6rem}
.plain{letter-spacing:0;text-transform:none}

/* A label wrapping its own control is clickable; say so. */
label.pick{cursor:pointer}
.matrix td.col.narrow{width:40px}

/* Progress widths in 5% steps. A percentage is genuinely dynamic, so it is
   the one thing that cannot be a static class — bucketing it keeps the bar
   working with JavaScript disabled, which setting .style.width from a data
   attribute would not. */
.w0{width:0}      .w5{width:5%}    .w10{width:10%}  .w15{width:15%}
.w20{width:20%}   .w25{width:25%}  .w30{width:30%}  .w35{width:35%}
.w40{width:40%}   .w45{width:45%}  .w50{width:50%}  .w55{width:55%}
.w60{width:60%}   .w65{width:65%}  .w70{width:70%}  .w75{width:75%}
.w80{width:80%}   .w85{width:85%}  .w90{width:90%}  .w95{width:95%}
.w100{width:100%}

/* ---------- installer ---------- */
.install-wrap{padding-top:6vh}
.install-logo{width:min(52vw,220px);margin:0 auto 2.4rem}
.install-sub{margin:0 0 3rem}
.stack>*+*{margin-top:var(--s3)}
.mono-sm{font-size:.72rem;letter-spacing:.02em}
