
/* ==========================================================================
   DEKLO tenant dashboard.

   Design thesis: PROVENANCE OVER REASSURANCE. This repo's honesty rules say
   every number carries a basis and a date, and that fidelity limits travel
   with the headline. A console that renders green checkmarks over that would
   be lying by omission, so the visual system is built to show where each
   fact came from and how far it can be trusted.

   Three rules the whole sheet obeys. Break one and the system stops meaning
   anything:

   1. COLOUR IS STATE. There is no brand accent. Saturated colour appears only
      inside a status token or a hatch. Buttons, links and headings are ink.
      The payoff: when something goes amber, it is the only amber on screen.
      (--focus is the single exception -- a keyboard focus ring must be
      visible and must not be mistaken for a status.)
   2. MONO IS THE CHROME. Labels, values, identifiers and metadata are
      monospace with tabular figures. Sans is for prose only. The subject's
      own vernacular is digests, CIDRs, ports and DSNs.
   3. RULES, NOT CARDS. Structure comes from hairlines and spacing. A raised
      surface is used only where content is genuinely nested (code, logs).

   Signature: HATCHED UNCERTAINTY (.unverified). Anything the platform cannot
   vouch for -- an unbacked-up database, an allowlist open to the internet, a
   port nobody declared, a measurement never taken -- carries a diagonal hatch
   on its left edge. Epistemic status becomes something you can see across the
   room, not a sentence you have to read.

   TD7: this used to live inline in one HTML file. TD1 asked for that to be
   revisited past ~1000 lines rather than defended, and TD5.8 recorded the
   threshold as crossed. It is now index.html + app.css + app.js: three static
   files, still NO build step and NO dependencies, and both of these are
   cacheable and gzipped where the inline version was re-sent on every load.

   Fonts stay system: a webfont would be a runtime dependency on a third party
   for a control surface, so character comes from the mono/sans split instead.
   ========================================================================== */

:root{
  color-scheme: light dark;

  /* One warm-neutral grey family throughout. Mixing a warm and a cool grey is
     the fastest way to make an interface look assembled rather than designed. */
  --paper:#F4F3F0; --surface:#FFFFFF; --sunken:#EDEBE6;
  --ink:#191814; --ink-2:#5B584F; --ink-3:#8B8779;
  --rule:#E1DFD8; --rule-2:#CAC7BD;

  /* State. The only colours in the interface. */
  --ok:#1E6B3A; --warn:#8A5A00; --bad:#A32A22;
  --warn-bg:#F6EEDC; --bad-bg:#F7E9E7;
  --focus:#2B5AA8;

  --shadow:0 1px 2px rgba(25,24,20,.05), 0 8px 24px -12px rgba(25,24,20,.16);

  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono","JetBrains Mono","IBM Plex Mono",Menlo,Consolas,monospace;

  --wrap:1140px;
  --z-header:10; --z-toast:40;
}

/* Dark mode at full parity -- same hierarchy, same contrast relationships,
   not a filter over the light theme. Shadows are tinted with the background
   hue rather than pure black, which at this darkness would be invisible. */
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --paper:#141412; --surface:#1B1B18; --sunken:#232320;
  --ink:#EDEBE4; --ink-2:#9C988B; --ink-3:#6F6B60;
  --rule:#2B2B26; --rule-2:#3D3D35;
  --ok:#63C184; --warn:#DFA83F; --bad:#F0796C;
  --warn-bg:#2A2113; --bad-bg:#2C1917;
  --focus:#7FA8EA;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
}}

*{box-sizing:border-box}
/* Must precede any rule setting display on a container that also uses [hidden]:
   the UA stylesheet's [hidden]{display:none} loses to a class or ID selector. */
[hidden]{display:none !important}

html{scroll-behavior:smooth}
body{
  margin:0; background:var(--paper); color:var(--ink);
  font:400 13.5px/1.6 var(--sans);
  -webkit-font-smoothing:antialiased;
  font-variant-numeric:tabular-nums;
}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important}
}

/* Keyboard users must be able to see where they are. Applied globally rather
   than per-component so a control added later cannot forget it. */
:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:3px}

.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);
  padding:10px 16px;z-index:calc(var(--z-header) + 1);font:500 13px/1 var(--sans)}
.skip:focus{left:8px;top:8px}

/* --- type -------------------------------------------------------------- */
/* .lbl is the workhorse: a mono micro-label. Used for field names, eyebrows
   and column headers. Uppercase is confined to THIS class -- uppercase
   headings everywhere is the tell of a templated dashboard. */
.lbl{font:500 10.5px/1.4 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-3)}
.mono{font-family:var(--mono);font-size:12.5px}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums}
.muted{color:var(--ink-2)}
.faint{color:var(--ink-3)}
/* The provenance line. Small, mono, faint, and it sits under the value it
   qualifies -- never beside it, so the value stays scannable. */
.prov{font:400 11.5px/1.5 var(--mono);color:var(--ink-3);margin-top:3px}
.prose{max-width:64ch}
h1,h2,h3{margin:0;font-weight:600;letter-spacing:-.011em;text-wrap:balance}
h1{font-size:21px}
h2{font-size:15.5px}
h3{font-size:13px}

/* --- header ------------------------------------------------------------ */
header{position:sticky;top:0;z-index:var(--z-header);background:var(--paper);
  border-bottom:1px solid var(--rule)}
.hbar{max-width:var(--wrap);margin:0 auto;padding:0 22px;display:flex;align-items:center;
  gap:20px;min-height:54px;flex-wrap:wrap}
.brand{display:flex;align-items:baseline;gap:9px;font:600 14px/1 var(--sans);letter-spacing:-.01em}
.brand em{font-style:normal;font-family:var(--mono);font-size:12px;font-weight:500;color:var(--ink-2)}
/* The tenant slug is an identifier, so it is set as one. */
.tenant{font:500 12px/1 var(--mono);color:var(--ink-2);padding:4px 8px;
  border:1px solid var(--rule);border-radius:5px;background:var(--surface)}

nav{display:flex;gap:2px;margin-left:auto}
nav button{background:none;border:0;color:var(--ink-2);padding:7px 12px;border-radius:6px;
  cursor:pointer;font:500 13px/1 var(--sans);position:relative;transition:color .16s,background .16s}
nav button:hover{color:var(--ink);background:var(--sunken)}
/* Current view is marked by ink weight plus an underline anchored to the header
   rule -- a tab, rather than a coloured pill that would break rule 1. */
nav button.on{color:var(--ink);font-weight:600}
nav button.on::after{content:"";position:absolute;left:10px;right:10px;bottom:-11px;height:2px;
  background:var(--ink)}
.hactions{display:flex;gap:6px;align-items:center}

/* --- layout ------------------------------------------------------------ */
main{max-width:var(--wrap);margin:0 auto;padding:26px 22px 80px}
.pagehead{margin-bottom:22px}
.pagehead .basis{margin-top:5px}

/* Bands separated by hairlines. No border box, no drop shadow, no radius --
   the card look is what makes most dashboards interchangeable. */
.band{border-top:1px solid var(--rule);padding:22px 0}
.band:first-of-type{border-top:0;padding-top:4px}
.bandhead{display:flex;align-items:baseline;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.bandhead .lbl{margin-left:auto}

/* Two columns once there is genuinely room. Below that a single column, in
   source order, which is already the correct reading order. */
.split{display:grid;gap:26px}
@media (min-width:1040px){.split{grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);gap:38px}}

/* --- key/value ---------------------------------------------------------- */
.kv{display:grid;grid-template-columns:minmax(96px,auto) 1fr;gap:11px 18px;align-items:baseline}
.kv>dt{font:500 10.5px/1.7 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-3)}
.kv>dd{margin:0;min-width:0;font-family:var(--mono);font-size:12.5px;word-break:break-word}

/* --- status ------------------------------------------------------------- */
/* One token for every status in the product. A dot carries the colour and the
   word carries the meaning, so it survives colour-blindness and greyscale. */
.st{display:inline-flex;align-items:center;gap:6px;font:500 12px/1 var(--mono);
  white-space:nowrap}
.st::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
.st-ok{color:var(--ok)} .st-bad{color:var(--bad)}
.st-mute{color:var(--ink-3)}
.st-mute::before{background:none;box-shadow:inset 0 0 0 1.5px currentColor}
/* Work in progress reads as motion, not as a colour. */
.st-run{color:var(--ink-2)}
.st-run::before{background:none;border:1.5px solid currentColor;border-top-color:transparent;
  width:9px;height:9px;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* --- the signature: hatched uncertainty --------------------------------- */
/* Reserved for things the platform cannot vouch for. Overuse would destroy it,
   so it appears in exactly four places: no backup, an open allowlist, a port
   nobody declared, and a measurement never taken. */
.unverified{position:relative;padding-left:15px}
.unverified::before{content:"";position:absolute;left:0;top:1px;bottom:1px;width:4px;border-radius:1px;
  background:repeating-linear-gradient(135deg,var(--warn) 0 2px,transparent 2px 5px)}
.unverified.grave::before{background:repeating-linear-gradient(135deg,var(--bad) 0 2px,transparent 2px 5px)}

/* --- notes -------------------------------------------------------------- */
/* A note states a fact and what to do about it. It is not a mood: no icon, no
   exclamation mark, no apology. */
.note{padding:12px 14px;border-radius:8px;background:var(--sunken);font-size:13px}
.note.warn{background:var(--warn-bg)}
.note.bad{background:var(--bad-bg)}
.note b{font-weight:600}
.note p{margin:0}
.note p+p{margin-top:6px}

/* --- buttons ------------------------------------------------------------ */
/* Ink, not colour (rule 1). Hierarchy comes from fill vs hairline vs bare. */
.btn{font:500 12.5px/1 var(--sans);padding:8px 13px;border-radius:7px;cursor:pointer;
  border:1px solid var(--rule-2);background:var(--surface);color:var(--ink);
  transition:border-color .16s,background .16s,color .16s,transform .06s}
.btn:hover{border-color:var(--ink-3);background:var(--sunken)}
.btn:active{transform:translateY(1px)}
.btn:disabled{opacity:.42;cursor:not-allowed;transform:none}
.btn-primary{background:var(--ink);border-color:var(--ink);color:var(--paper);font-weight:600}
.btn-primary:hover{background:var(--ink);border-color:var(--ink);opacity:.88}
/* Danger is the one place a control borrows a state colour, because the
   consequence IS the state. */
.btn-danger{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,transparent)}
.btn-danger:hover{background:var(--bad);border-color:var(--bad);color:#fff}
.btn-primary.btn-danger{background:var(--bad);border-color:var(--bad);color:#fff}
.btn-sm{padding:5px 9px;font-size:11.5px}
/* A bare text control, for tertiary actions that would otherwise add noise. */
.btn-bare{border-color:transparent;background:none;color:var(--ink-2);padding:5px 7px;
  font-size:11.5px;text-decoration:underline;text-underline-offset:3px;
  text-decoration-color:var(--rule-2)}
.btn-bare:hover{background:none;border-color:transparent;color:var(--ink);
  text-decoration-color:var(--ink-3)}
.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:15px}

a{color:var(--ink);text-underline-offset:3px;text-decoration-thickness:1px}
a:hover{text-decoration-color:var(--ink-3)}

/* --- forms -------------------------------------------------------------- */
.field{display:block;margin-bottom:13px}
.field>.lbl{display:block;margin-bottom:5px}
.field .hint{display:block;margin-top:5px;font:400 11.5px/1.5 var(--sans);color:var(--ink-2);
  max-width:60ch}
/* Every text-ish input, listed rather than assumed. `email` was missing until the
   auth pages started sharing this sheet (TD7) -- the workspace has no email field,
   so it fell back to the UA default and rendered as a differently-shaped box on the
   one page a new customer sees first. */
input[type=text],input[type=password],input[type=email],input[type=url],
input[type=number]{width:100%;padding:9px 11px;border:1px solid var(--rule-2);
  border-radius:7px;background:var(--surface);color:var(--ink);
  font:400 12.5px/1.4 var(--mono);transition:border-color .16s}
input[type=text]:hover,input[type=password]:hover,input[type=email]:hover,
input[type=url]:hover,input[type=number]:hover{border-color:var(--ink-3)}
input::placeholder{color:var(--ink-3)}
.err{color:var(--bad);font:500 12px/1.5 var(--sans);margin-top:8px}
.err:empty{display:none}

/* --- code / logs -------------------------------------------------------- */
/* The one place a sunken surface earns its keep: content that is verbatim
   machine output and should read as pasted in, not authored. */
.code{display:block;background:var(--sunken);border-radius:8px;padding:11px 13px;
  font:400 12px/1.6 var(--mono);overflow-wrap:anywhere;color:var(--ink)}
.log{max-height:340px;overflow:auto;white-space:pre-wrap;word-break:break-word}
.copyrow{display:flex;gap:7px;align-items:center;margin-top:9px;flex-wrap:wrap}

/* --- disclosures -------------------------------------------------------- */
/* Progressive disclosure: show what is needed to decide, and keep set-up-once
   material one click away rather than repeated down the page. */
details.disc{border-top:1px solid var(--rule);padding:14px 0 0}
details.disc[open]{padding-bottom:16px}
details.disc>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;
  font:500 12.5px/1 var(--sans);color:var(--ink-2);padding:3px 0;transition:color .16s}
details.disc>summary::-webkit-details-marker{display:none}
details.disc>summary:hover{color:var(--ink)}
details.disc>summary::before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .16s;flex:none}
details.disc[open]>summary::before{transform:rotate(45deg)}
details.disc>summary>.lbl{margin-left:auto}
.disc-body{margin-top:14px}

/* --- tables ------------------------------------------------------------- */
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
table{border-collapse:collapse;width:100%;min-width:460px;font-family:var(--mono);font-size:12px}
th{text-align:left;padding:0 14px 7px 0;font:500 10.5px/1.4 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-3);border-bottom:1px solid var(--rule)}
td{padding:9px 14px 9px 0;border-bottom:1px solid var(--rule);vertical-align:middle}
tr:last-child td{border-bottom:0}
td:last-child,th:last-child{padding-right:0;text-align:right}

/* --- app rows ----------------------------------------------------------- */
.app{border-top:1px solid var(--rule);padding:20px 0}
.approw{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:13px}
.appname{font:600 16px/1.2 var(--sans);letter-spacing:-.012em}
.appurl{font:500 12.5px/1.4 var(--mono);margin-left:auto;text-decoration:none;
  border-bottom:1px solid var(--rule-2);padding-bottom:2px;overflow-wrap:anywhere;max-width:100%}
.appurl:hover{border-bottom-color:var(--ink)}
.appbody{max-width:740px}

/* --- stats -------------------------------------------------------------- */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1px;
  background:var(--rule);border-block:1px solid var(--rule)}
.stat{background:var(--paper);padding:14px 16px 15px}
.stat .v{display:block;font:500 25px/1.1 var(--mono);letter-spacing:-.02em;margin-top:6px}
.stat .v.none{color:var(--ink-3);font-size:19px}

/* --- skeleton ----------------------------------------------------------- */
/* Shaped like the content it replaces, so the layout does not jump when real
   data lands. A centred spinner would tell the reader nothing about what is
   coming. */
.sk{border-radius:5px;height:11px;background:var(--rule);position:relative;overflow:hidden}
.sk::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--paper) 70%,transparent),transparent);
  animation:sweep 1.4s ease-in-out infinite}
.sk.w1{width:34%} .sk.w2{width:62%} .sk.w3{width:46%}
.sk+.sk{margin-top:11px}
@keyframes sweep{to{transform:translateX(100%)}}

/* --- empty states ------------------------------------------------------- */
/* An invitation to act, not a shrug. */
.empty{padding:34px 0 10px;max-width:60ch}
.empty h2{margin-bottom:7px}

/* --- dialog ------------------------------------------------------------- */
dialog{border:1px solid var(--rule-2);border-radius:12px;background:var(--surface);color:var(--ink);
  padding:0;max-width:480px;width:calc(100% - 32px);box-shadow:var(--shadow)}
dialog::backdrop{background:rgba(20,20,18,.5);backdrop-filter:blur(2px)}
.dpad{padding:20px}
.drow{display:flex;gap:8px;justify-content:flex-end;margin-top:18px}

/* --- toast -------------------------------------------------------------- */
/* Replaces window.alert(). Non-blocking, dismissible, and it does not steal
   focus from whatever the reader was doing. */
#toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);z-index:var(--z-toast);
  display:flex;flex-direction:column;gap:8px;align-items:center;width:max-content;
  max-width:min(520px,calc(100% - 32px));pointer-events:none}
.toast{background:var(--ink);color:var(--paper);padding:11px 15px;border-radius:9px;
  font:500 12.5px/1.45 var(--sans);box-shadow:var(--shadow);pointer-events:auto;
  animation:rise .18s ease-out}
.toast.bad{background:var(--bad);color:#fff}
@keyframes rise{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}

/* --- narrow screens ---------------------------------------------------- */
@media (max-width:720px){
  .hbar{gap:10px;padding:8px 16px 0;min-height:0}
  nav{order:3;width:100%;margin-left:0;overflow-x:auto;padding-bottom:2px;
    scrollbar-width:none}
  nav::-webkit-scrollbar{display:none}
  nav button{padding:9px 10px}
  nav button.on::after{bottom:0;left:8px;right:8px}
  .hactions{margin-left:auto}
  main{padding:20px 16px 64px}
  .kv{grid-template-columns:minmax(0,1fr);gap:0}
  .kv>dt{margin-top:13px}
  .kv>dd{margin-top:3px}
  .approw{gap:8px}
  .appurl{margin-left:0;width:100%}
  .band:first-of-type{padding-top:0}
}

/* View transitions: one orchestrated entry, not scattered effects. */
.view{animation:enter .2s ease-out}
@keyframes enter{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}


/* ==========================================================================
   TD7 -- information architecture.

   Three places, not three-and-an-action: Overview, Applications, Database.
   "Performance" was a top-level tab that only ever measured the database, so
   it is a section inside Database now -- it was an action wearing the costume
   of a place.

   An application is a PLACE (#/apps/<name>), with its six concerns split into
   four sections instead of crammed into one row. The components below exist to
   make position legible: a breadcrumb saying where you are, a subnav saying
   which part of it, and a step rail for anything multi-step.
   ========================================================================== */

/* --- breadcrumb --------------------------------------------------------- */
/* Only ever rendered on a detail page. On a top-level view there is nothing
   to say and a one-item breadcrumb is noise. */
.crumb{display:flex;align-items:center;gap:7px;font:400 12px/1 var(--mono);
  color:var(--ink-3);margin-bottom:9px;flex-wrap:wrap}
.crumb a{color:var(--ink-2);text-decoration:none;border-bottom:1px solid transparent}
.crumb a:hover{color:var(--ink);border-bottom-color:var(--rule-2)}
.crumb span[aria-current]{color:var(--ink)}
.crumb .sep{color:var(--rule-2)}

/* --- section subnav ----------------------------------------------------- */
/* Sits under a detail page's title. Same visual grammar as the top nav -- ink
   weight plus a rule -- so "which section" reads the same way as "which view"
   rather than inventing a second idiom. */
.subnav{display:flex;gap:2px;border-bottom:1px solid var(--rule);margin-bottom:20px;
  overflow-x:auto;scrollbar-width:none}
.subnav::-webkit-scrollbar{display:none}
.subnav a{padding:8px 12px;font:500 12.5px/1 var(--sans);color:var(--ink-2);
  text-decoration:none;position:relative;white-space:nowrap;transition:color .16s}
.subnav a:hover{color:var(--ink)}
.subnav a.on{color:var(--ink);font-weight:600}
.subnav a.on::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;height:2px;
  background:var(--ink)}

/* --- list rows ---------------------------------------------------------- */
/* The applications index. A table would be defensible, but every row here is a
   link to a place, and a row of links reads better than a table with a chevron
   column bolted on. */
.rows{border-top:1px solid var(--rule)}
.row-link{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;
  align-items:center;padding:15px 4px 15px 0;border-bottom:1px solid var(--rule);
  text-decoration:none;color:inherit;transition:background .16s}
.row-link:hover{background:var(--sunken)}
.row-link .name{font:600 15px/1.3 var(--sans);letter-spacing:-.012em}
.row-link .meta{font:400 11.5px/1.5 var(--mono);color:var(--ink-3);
  grid-column:1;overflow-wrap:anywhere}
.row-link .right{grid-row:1;grid-column:2;display:flex;align-items:center;gap:12px}
.row-link .chev{color:var(--ink-3);font:400 14px/1 var(--mono)}

/* --- attention list ----------------------------------------------------- */
/* The Overview's first block. Reuses .unverified rather than inventing a new
   alarm language: everything listed here is something the platform cannot
   vouch for, which is exactly what the hatch already means. */
.attn{display:flex;flex-direction:column;gap:11px}
.attn-item{display:block;padding-left:15px;text-decoration:none;color:inherit}
.attn-item strong{font-weight:600}
.attn-item:hover .attn-what{text-decoration:underline;text-underline-offset:3px}
.attn-what{display:block;font-size:13.5px}
.calm{display:flex;align-items:baseline;gap:9px;font-size:13.5px;color:var(--ink-2)}

/* --- summary rows (Overview -> a place) --------------------------------- */
.sum{display:grid;gap:0;border-top:1px solid var(--rule)}
.sum-link{display:flex;align-items:baseline;gap:14px;padding:15px 4px 15px 0;
  border-bottom:1px solid var(--rule);text-decoration:none;color:inherit;
  flex-wrap:wrap;transition:background .16s}
.sum-link:hover{background:var(--sunken)}
.sum-link .what{font:600 14.5px/1.3 var(--sans);min-width:130px}
.sum-link .detail{font:400 12.5px/1.5 var(--mono);color:var(--ink-2);flex:1;min-width:0}
.sum-link .chev{color:var(--ink-3);font:400 14px/1 var(--mono);margin-left:auto}

/* --- multi-step ---------------------------------------------------------- */
/* A rail, not a progress bar. A bar implies a percentage; these are named
   stages and the names are the useful part -- you should be able to see what
   is still coming before you commit to the first field.
   The current step is ink; done steps stay legible and clickable (going back
   must never mean losing what you typed); future steps are faint and inert. */
.steps{display:flex;align-items:center;gap:0;margin-bottom:22px;flex-wrap:wrap;
  border-bottom:1px solid var(--rule);padding-bottom:14px}
.step{display:flex;align-items:center;gap:8px;font:500 12.5px/1 var(--sans);
  color:var(--ink-3);background:none;border:0;padding:4px 0;cursor:default}
.step .n{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;
  border-radius:50%;border:1px solid var(--rule-2);font:500 11px/1 var(--mono);flex:none}
.step.done{color:var(--ink-2);cursor:pointer}
.step.done .n{border-color:var(--ink-3);color:var(--ink-2)}
.step.done:hover{color:var(--ink)}
.step.now{color:var(--ink);font-weight:600}
.step.now .n{background:var(--ink);border-color:var(--ink);color:var(--paper)}
.step-sep{width:26px;height:1px;background:var(--rule-2);margin:0 11px;flex:none}
.wizard{max-width:460px}
.wizard-nav{display:flex;gap:8px;margin-top:20px;align-items:center}
.wizard-nav .spacer{flex:1}

/* The review step. Values are quoted back in the same mono the form used, so
   the reader is checking the same characters they typed. */
.review{border-top:1px solid var(--rule)}
.review-row{display:grid;grid-template-columns:minmax(92px,auto) 1fr;gap:10px 16px;
  padding:11px 0;border-bottom:1px solid var(--rule);align-items:baseline}
.review-row dt{font:500 10.5px/1.5 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-3);margin:0}
.review-row dd{margin:0;font-family:var(--mono);font-size:12.5px;overflow-wrap:anywhere}
.review-row dd.unset{color:var(--ink-3);font-style:normal}

/* --- narrow screens for the new components ------------------------------ */
@media (max-width:720px){
  .row-link{grid-template-columns:minmax(0,1fr)}
  .row-link .right{grid-row:auto;grid-column:1;margin-top:2px}
  .sum-link .what{min-width:0}
  .sum-link .chev{margin-left:0}
  .steps{gap:0}
  .step-sep{width:14px;margin:0 7px}
  .step .lbltext{display:none}
  .wizard{max-width:none}
}


/* ==========================================================================
   TD8 -- motion.

   The rule, and it is the same rule as the colour system: MOTION ENCODES
   SOMETHING TRUE. Work in progress, data arriving, direction of travel. A
   control surface is read under pressure -- somebody is here because something
   is broken -- so decorative movement is not neutral, it is noise competing
   with the status it sits next to.

   No animation library. Lottie was considered and measured: 76 KB gzipped
   (46 KB for lottie_light) against an entire application of 14.7 KB, as a
   third-party runtime dependency on a control plane. It is also the wrong
   tool -- Lottie renders illustration, and nothing here is illustration.
   Everything below is CSS, and every one of it is disabled wholesale by the
   prefers-reduced-motion block near the top of this file.
   ========================================================================== */

/* Staggered entry. Rows arrive in sequence rather than all at once, which
   gives the eye an order to read them in. Capped at 8 -- past that the last
   row waits long enough to feel broken rather than considered. */
.view .band,.view .row-link,.view .sum-link,.view .attn-item{
  animation:rise .34s cubic-bezier(.22,.61,.36,1) backwards}
.view .band:nth-child(1),.view .row-link:nth-child(1),
.view .sum-link:nth-child(1),.view .attn-item:nth-child(1){animation-delay:.02s}
.view .band:nth-child(2),.view .row-link:nth-child(2),
.view .sum-link:nth-child(2),.view .attn-item:nth-child(2){animation-delay:.06s}
.view .band:nth-child(3),.view .row-link:nth-child(3),
.view .sum-link:nth-child(3),.view .attn-item:nth-child(3){animation-delay:.10s}
.view .band:nth-child(4),.view .row-link:nth-child(4),
.view .sum-link:nth-child(4),.view .attn-item:nth-child(4){animation-delay:.14s}
.view .row-link:nth-child(n+5),.view .attn-item:nth-child(n+5){animation-delay:.18s}
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* A status that means "something is happening right now" gets a halo that
   breathes. Only st-run -- a steady state must stay steady, or the eye cannot
   tell which of the two is the one to watch. */
.st-run::before{box-shadow:0 0 0 0 currentColor;animation:spin .8s linear infinite,
  halo 1.9s ease-out infinite}
@keyframes halo{
  0%{box-shadow:0 0 0 0 color-mix(in srgb,currentColor 45%,transparent)}
  70%,100%{box-shadow:0 0 0 7px transparent}}

/* Indeterminate progress, for a build. Genuinely indeterminate: a build's
   duration is not known ahead of time, so a percentage bar would be a lie. */
.prog{height:2px;border-radius:2px;background:var(--rule);overflow:hidden;margin-top:10px}
.prog::after{content:"";display:block;width:38%;height:100%;border-radius:2px;
  background:var(--ink-3);animation:slide 1.25s cubic-bezier(.45,0,.55,1) infinite}
@keyframes slide{0%{transform:translateX(-110%)}100%{transform:translateX(310%)}}

/* Direction of travel: the chevron leans toward where the click goes. */
.row-link .chev,.sum-link .chev{transition:transform .18s cubic-bezier(.22,.61,.36,1)}
.row-link:hover .chev,.sum-link:hover .chev{transform:translateX(3px)}

/* A value that has just been measured settles into place, so a re-measure is
   visibly a NEW reading rather than the same number sitting there. */
.stat .v.fresh{animation:settle .5s cubic-bezier(.22,.61,.36,1)}
@keyframes settle{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}

/* Buttons and rows get weight rather than glow. */
.btn{will-change:transform}
.row-link{transition:background .16s,box-shadow .16s}

/* Toast arrives with a little overshoot -- it is the one element allowed to be
   slightly playful, because it is transient and never carries state. */
.toast{animation:pop .26s cubic-bezier(.2,.9,.3,1.25)}
@keyframes pop{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:none}}

/* The step rail advances rather than cutting. */
.step .n{transition:background .22s,border-color .22s,color .22s}
.step-sep{transition:background .22s}


/* --- repository picker (AL10) ------------------------------------------- */
/* A list of radios, because a private repository is CHOSEN from what the
   installation grants rather than typed. Styled as rows rather than a <select>
   so the private/public marker is visible without opening anything. */
.repo-list{margin-top:6px;border:1px solid var(--rule-2);border-radius:8px;
  max-height:210px;overflow-y:auto;background:var(--surface)}
.repo-pick{display:flex;align-items:center;gap:10px;padding:9px 12px;cursor:pointer;
  border-bottom:1px solid var(--rule);transition:background .14s}
.repo-pick:last-child{border-bottom:0}
.repo-pick:hover{background:var(--sunken)}
.repo-pick input{accent-color:var(--ink);flex:none;margin:0}
.repo-pick:has(input:checked){background:var(--sunken)}
.repo-pick .st{margin-left:auto}

/* --- environment variables (AL13) ---------------------------------------- */
/* The scope column is load-bearing, so it is given room rather than squeezed:
   a reader who does not notice it ships a build-time value to the container
   and gets a silently broken app. */
.envtable{min-width:560px}
.envtable input,.envtable select{width:100%;font-family:var(--mono);font-size:12px;
  padding:5px 7px;border:1px solid var(--rule-2);border-radius:5px;background:var(--paper);
  color:var(--ink)}
.envtable input:focus,.envtable select:focus{outline:2px solid var(--ink);outline-offset:-1px}
.envtable td{vertical-align:top}
.envtable td:first-child{width:34%}
.envtable td:nth-child(2){width:26%}
.prov.warn{color:var(--warn)}
.envpaste{border-top:1px solid var(--rule);margin-top:16px;padding-top:12px}
.envpaste>summary{cursor:pointer;font:500 12px/1.4 var(--mono);color:var(--ink-2)}
/* Also the create wizard's Environment step: the same control, so a .env pasted before the
   first build looks and behaves exactly like one pasted afterwards. */
.envpaste textarea{width:100%;margin-top:10px;padding:9px 11px;border:1px solid var(--rule-2);
  border-radius:6px;font-family:var(--mono);font-size:12px;line-height:1.6;background:var(--paper);
  color:var(--ink);resize:vertical}

/* --- build progress (AL22) ------------------------------------------------ */
/* A LIST OF NAMED STEPS, NOT A BAR. kaniko reports no fractional progress, so a
   bar would be an animation timed against a guess. The steps are real, read from
   the pod, and the one in progress is the only one that moves. */
.buildprog{margin-top:10px}
.stages{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px 18px}
.stages li{position:relative;padding-left:16px;font:400 12px/1.6 var(--mono);color:var(--ink-3)}
.stages li::before{content:"";position:absolute;left:0;top:.55em;width:7px;height:7px;
  border-radius:50%;box-shadow:inset 0 0 0 1.5px currentColor}
.stages li.done{color:var(--ink-2)}
.stages li.done::before{background:currentColor;box-shadow:none}
.stages li.now{color:var(--ink);font-weight:500}
.stages li.now::before{background:none;border:1.5px solid currentColor;border-top-color:transparent;
  box-shadow:none;animation:spin .8s linear infinite}
/* AL19. A build failure's reason, in the row. Wraps rather than truncating: the
   useful part of "your build failed, here are the last lines" is the last lines. */
.prov.why{white-space:pre-wrap;max-width:52ch;margin-top:6px;color:var(--ink-2)}

/* --- SQL console (AL24) --------------------------------------------------- */
#sqlbox{width:100%;padding:10px 12px;border:1px solid var(--rule-2);border-radius:7px;
  background:var(--surface);color:var(--ink);font:400 12.5px/1.6 var(--mono);resize:vertical}
#sqlbox:focus{outline:2px solid var(--ink);outline-offset:-1px}
/* Result cells do not wrap: a grid whose rows are all different heights is
   unreadable as a grid. The horizontal scroll on .scroll is the escape hatch,
   and a cell the server already clipped at 4 KB cannot be unboundedly wide. */
.sqlgrid td{white-space:nowrap;max-width:44ch;overflow:hidden;text-overflow:ellipsis}
.sqlgrid th{position:sticky;top:0;background:var(--paper);z-index:1}

/* GitHub source picker (9 Sep 2026). Repositories are grouped by the account they came from --
   a customer with a personal account and an organisation has two installations, and a flat list
   gave them no way to tell which grant a repository arrived through. */
.repo-group { margin-bottom: 10px; }
.repo-group + .repo-group { border-top: 1px solid var(--hair); padding-top: 10px; }
.repo-group-hd {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 6px; font-size: 12px;
}
.repo-group-hd .muted { margin-left: auto; }
.gh-steps { margin: 6px 0 12px; padding-left: 18px; font-size: 13px; line-height: 1.55; }
.gh-steps li { margin-bottom: 5px; }
.gh-steps li b { font-weight: 600; }

/* Account picker (9 Sep 2026) — see ghAccounts() in app.js. */
.gh-accounts { margin: 0 0 12px; }
.gh-accounts-hd { font-size: 12px; margin-bottom: 6px; }
.gh-account {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 0; border-top: 1px solid var(--hair); font-size: 13px;
}
.gh-account a { margin-left: auto; }
.gh-account .st-ok { margin-left: auto; }
