/* aiplat portal stylesheet.
   THIS IS A FILE, NOT AN INLINE <style>, AND THAT IS THE WHOLE POINT.
   The portal sends `default-src 'self'` with no `style-src`. That blocks an
   inline <style> element and every style= attribute, silently -- no error, no
   log, just an unstyled page. It always had; the previous design used system
   fonts and Canvas/CanvasText, so a blocked stylesheet rendered almost exactly
   like an applied one and the breakage was invisible for months. Served from
   /static it is 'self', so it loads under the existing policy and the CSP did
   NOT have to be widened with 'unsafe-inline'.
   Cache-bust by bumping the ?v= in the <link>; serve_static ignores the query. */
/* SAMA branding. Palette taken from www.samaconsulting.co.uk: brand yellow #FAE605
   on near-black #1E1E22, white ground, Oswald headings over Inter body.

   THE FONTS ARE SELF-HOSTED (/static/*.woff2) AND MUST STAY THAT WAY. This portal
   sends `default-src 'self'` with no font-src, style-src or img-src override, so a
   <link> to fonts.googleapis.com is not a slow load -- it is silently BLOCKED, and
   the page renders in Times with no error anywhere. The same is true of hotlinking
   a logo from the parent site. Widening the CSP to put a third party into the login
   page's render path is not a trade worth making for a typeface; and the parent site
   was observed refusing connections on 2026-09-01, which is exactly the outage that
   would have taken the portal's branding down with it.

   Yellow is a ~1.2:1 contrast against white: it is a BLOCK colour (bar fills, button
   grounds, rules, the focus ring) and never body text on a light ground. */
@font-face{font-family:Inter;src:url(/static/inter.woff2) format("woff2");
 font-weight:100 900;font-style:normal;font-display:swap}
@font-face{font-family:Oswald;src:url(/static/oswald.woff2) format("woff2");
 font-weight:200 700;font-style:normal;font-display:swap}
:root{color-scheme:light dark;
 --brand:#fae605;--brand-ink:#1e1e22;
 --bg:#fff;--surface:#fff;--surface-2:#f6f6f4;--ink:#1e1e22;--mute:#65656d;
 --line:#e4e4e1;--bar:#1e1e22;--bar-ink:#fff;--danger:#c22;--up:#2e9e4f;--down:#9a9a9a;
 --shadow:0 1px 2px #1e1e2210,0 8px 24px #1e1e220a;
 /* --up/--danger are BLOCK colours (the dot, a bar fill). --up-ink and --warn are
    the same two signals as TEXT on the page ground, which the block colours cannot
    be: --up is ~3.1:1 on white and brand yellow is ~1.2:1 and is a block colour
    only, per the palette note at the top of this file. --danger (#c22, ~5.9:1)
    already works as text, so it needs no ink twin. */
 --up-ink:#1c7a3c;   /* ~5.0:1 on #fff */
 --warn:#8a6d00;     /* ~5.4:1 on #fff -- the amber signal as text; the brand yellow
                        stays a block fill and never becomes body text */
}
@media (prefers-color-scheme:dark){:root{
 --bg:#161619;--surface:#1e1e22;--surface-2:#26262b;--ink:#f2f2f0;--mute:#a2a2aa;
 --line:#34343c;--bar:#0e0e10;--bar-ink:#fff;--danger:#ff7a7a;--up:#3fb950;
 --up-ink:#56d364;--warn:#e3c008;
 --shadow:0 1px 2px #0006,0 8px 24px #0004}}
*{box-sizing:border-box}
body{font:16px/1.6 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
 margin:0;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}
.wrap{max-width:58rem;margin:0 auto;padding:0 1.25rem 4rem}
/* THE DASHBOARD, AND ONLY THE DASHBOARD, GETS THE WIDER MEASURE. .wrap is 58rem
   because that is a readable line length for the prose pages (restore, new task,
   every notice). A card grid is not prose: at 58rem it fits two 21rem cards and
   leaves a third column of empty screen on any normal monitor. :has() scopes the
   widening to the one page that has a card grid, with no app.py change and no
   script -- and a browser too old for :has() simply keeps 58rem and two columns,
   which is still the fix for the single-column stack. */
.wrap:has(.projects){max-width:76rem}
/* Brand bar. FULL WIDTH because <header> is a block in <body>, OUTSIDE .wrap -- not
   because of negative margins. The previous version lived inside .wrap and used
   `margin:0 -1.25rem` to break out, which made it wider than its own container and
   gave the whole document a horizontal scrollbar on any viewport under 58rem. The bar
   spans the screen; .bar-in centres its contents on exactly the same axis as .wrap.

   The parent site ships its header logo VERTICALLY (200x518). Rotated 270deg here, at
   Spencer's preference, because at bar height the upright form is ~13px wide. Yellow
   on transparency, so it needs the dark ground. The roundel remains the favicon. */
header{background:var(--bar);color:var(--bar-ink);
 border-bottom:3px solid var(--brand);margin-bottom:2rem}
header .bar-in{max-width:58rem;margin:0 auto;padding:1rem 1.25rem 0;
 display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
header .mark{display:flex;align-items:center;gap:.5rem;
 font:600 1.2rem/1 Oswald,ui-sans-serif,system-ui,sans-serif;
 letter-spacing:.09em;text-transform:uppercase;color:var(--bar-ink);
 padding-bottom:1rem;white-space:nowrap}
header .mark img{display:block;width:57px;height:22px}
header .mark-sep{width:1px;height:18px;background:#ffffff40;display:inline-block}
h1{font:500 1.15rem/1.3 Oswald,ui-sans-serif,system-ui,sans-serif;letter-spacing:.05em;
 text-transform:uppercase;margin:0;padding-bottom:1rem;color:var(--bar-ink)}
header .sub{color:#ffffffb0;font-size:.8125rem;padding-bottom:1.05rem;letter-spacing:.02em}
header .right{margin-left:auto;padding-bottom:.7rem}
header button{margin:0;padding:.3rem .75rem;font-size:.8125rem;
 background:transparent;color:var(--bar-ink);border-color:#ffffff40}
header button:hover{background:#ffffff1a;border-color:#ffffff80}
/* Everything below the bar sits on the page ground. */
.sub{color:var(--mute);font-size:.875rem}
h2{font:500 1rem/1.3 Oswald,ui-sans-serif,system-ui,sans-serif;letter-spacing:.05em;
 text-transform:uppercase;margin:0 0 .5rem}
a{color:var(--ink);text-decoration-color:var(--brand);text-decoration-thickness:2px;
 text-underline-offset:3px}
a:hover{background:var(--brand);color:var(--brand-ink);text-decoration-color:transparent}
label{display:block;margin:.9rem 0 .3rem;font-size:.8125rem;font-weight:600;
 letter-spacing:.02em;color:var(--mute)}
/* textarea IS IN THIS LIST DELIBERATELY. It was omitted, and because nothing else
   in this file mentions textarea, the task prompt box rendered at the browser
   default of ~20 columns inside a 24rem card -- about a quarter of a wide screen.
   A control that takes a paragraph of instructions for an unattended agent is the
   last one that should be the smallest box on the page. */
input[type=text],input[type=password],select,textarea{width:100%;padding:.55rem .65rem;
 border:1px solid var(--line);border-radius:4px;background:var(--surface);
 color:var(--ink);font:inherit}
textarea{display:block;min-height:11rem;line-height:1.55;resize:vertical}
input:focus-visible,select:focus-visible,button:focus-visible,a:focus-visible{
 outline:3px solid var(--brand);outline-offset:1px}
/* Primary action: brand ground, near-black text -- 15:1, and the one place the yellow
   carries weight. Header/secondary buttons opt out via their own rules above. */
button{margin-top:1.1rem;padding:.5rem 1.1rem;border:1px solid transparent;border-radius:4px;
 background:var(--brand);color:var(--brand-ink);font:600 .9375rem/1.4 inherit;
 letter-spacing:.02em;cursor:pointer}
button:hover{background:#ffef4d}
form.card,div.card{background:var(--surface);border:1px solid var(--line);
 border-radius:6px;padding:1.25rem 1.4rem;box-shadow:var(--shadow)}
form.card{max-width:24rem}
div.card{max-width:30rem;margin-top:2rem}
/* THE WIDER TREATMENT IS OPT-IN, AND ONLY THE FULL-PAGE FORMS OPT IN.
   24rem is right for a login box or a one-field confirmation sitting on a page
   with other content. It is wrong for a form that IS the page -- /tasks/new and
   /projects/new each have their own <header> and nothing else -- and it is what
   made the task prompt's textarea about a quarter of a wide screen. Adding the
   class to those two pages leaves every other form.card on the site at 24rem. */
form.card.wide,div.card.wide{max-width:44rem}
.err,pre.err{background:#c2222214;border:1px solid #c2222255;border-left:4px solid var(--danger);
 padding:.7rem .9rem;border-radius:4px;margin:1rem 0;font-size:.9rem;color:var(--ink)}
/* Classes replacing the style= attributes that used to be written into the markup.
   A style attribute is blocked by `default-src 'self'` exactly as an inline <style>
   is, and unlike a stylesheet no hash can rescue it -- CSP is explicit that "hashes
   do not apply to ... style attributes". So they live here instead. */
.inline{display:inline}
.gap{margin-top:2rem}
.tight{margin:.25rem 0 0}

/* ---------------------------------------------------------------------------
   Project cards. These replaced a six-column table that overflowed a narrow
   viewport and whose Actions cell was a <button> in a form followed by three bare
   <a>s -- three different shapes for three things of equal weight, which is what
   made the row look broken. Here every control is .act: same box, same height,
   whether it is a link or a form submit. Only two things break out of that --
   .act-primary (Terminal, the point of the platform) and .act-danger (Destroy).
   --------------------------------------------------------------------------- */
.projects{display:grid;gap:1rem;align-items:start;
 grid-template-columns:repeat(auto-fill,minmax(21rem,1fr))}
.proj{background:var(--surface);border:1px solid var(--line);border-radius:6px;
 padding:1rem 1.15rem;box-shadow:var(--shadow)}
.proj-head{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
.proj-head h3{font:500 1.05rem/1.3 Oswald,ui-sans-serif,system-ui,sans-serif;
 letter-spacing:.05em;text-transform:uppercase;margin:0}
.proj-head .sp{flex:1 1 auto}
.proj-head .state{font-size:.8125rem;color:var(--mute)}
/* app.py has emitted <span class="dot up|down"> since the card redesign; the rule
   was never written, so the dot has been invisible. */
.dot{width:.6rem;height:.6rem;border-radius:50%;display:inline-block;
 flex:0 0 auto;background:var(--down)}
.dot.up{background:var(--up)}
.dot.down{background:var(--down)}
/* An OUTLINE chip, never a yellow-on-white one: brand yellow is ~1.2:1 on white
   and is a block colour only. The signal is carried by the border and the ink
   token, both of which have dark-mode values already. */
.health{font:600 .6875rem/1 Oswald,ui-sans-serif,system-ui,sans-serif;
 letter-spacing:.08em;text-transform:uppercase;padding:.25rem .5rem;
 border-radius:999px;border:1px solid transparent;white-space:nowrap}
.h-ok{color:var(--up-ink);border-color:var(--up-ink)}
.h-warn{color:var(--warn);border-color:var(--warn)}
.h-bad{color:var(--danger);border-color:var(--danger)}
.h-off{color:var(--mute);border-color:var(--line);border-style:dashed}
/* Definition list, so each fact keeps its label when the card gets narrow and the
   columns collapse to stacked pairs. */
.proj-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem .9rem;
 margin:.9rem 0 0;font-size:.875rem;align-items:baseline}
.proj-meta dt{color:var(--mute);font:600 .75rem/1.6 Oswald,ui-sans-serif,sans-serif;
 letter-spacing:.08em;text-transform:uppercase}
.proj-meta dd{margin:0;min-width:0;overflow-wrap:anywhere}
.acts{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:1rem;
 padding-top:.85rem;border-top:1px solid var(--line)}
/* One shape for every control, <a> and <button> alike. */
.act,.acts form{display:inline-flex}
.act{align-items:center;gap:.3rem;margin:0;padding:.32rem .7rem;
 border:1px solid var(--line);border-radius:4px;background:transparent;
 color:var(--ink);font:600 .8125rem/1.4 Inter,ui-sans-serif,system-ui,sans-serif;
 text-decoration:none;cursor:pointer;white-space:nowrap}
.act:hover{background:var(--brand);color:var(--brand-ink);border-color:var(--brand)}
.act-primary{background:var(--brand);color:var(--brand-ink);border-color:var(--brand)}
.act-primary:hover{background:#ffef4d}
.act-danger{color:var(--danger);border-color:#c2222255}
.act-danger:hover{background:var(--danger);color:#fff;border-color:var(--danger)}
/* Not a <button disabled>: there is nothing to press. It states why the door is shut. */
.act-off{color:var(--mute);border-style:dashed;cursor:default}
.act-off:hover{background:transparent;color:var(--mute);border-color:var(--line)}
@media (max-width:30rem){.proj-meta{grid-template-columns:1fr;gap:.1rem}
 .proj-meta dd{margin-bottom:.5rem}}
/* The "Task containers" section heading. A task section sits directly under the
   projects grid, and without this the heading butts against the last card and reads
   as part of it -- which is the one thing it must not do, because the sentence under
   it says closing DELETES and that sentence is false about everything above it. */
.sect{margin-top:2.5rem;margin-bottom:.25rem}
/* The card's live half. Three fixed rows, one grid, so Mem/CPU/PIDs line up
   across every card in the row regardless of how long the values are. */
.res{display:grid;grid-template-columns:auto 1fr auto;gap:.35rem .6rem;
 align-items:center;margin:.85rem 0 0;font-size:.8125rem}
.res-k{font:600 .6875rem/1.6 Oswald,ui-sans-serif,sans-serif;letter-spacing:.08em;
 text-transform:uppercase;color:var(--mute)}
.res-v{text-align:right;color:var(--ink);font-variant-numeric:tabular-nums;
 white-space:nowrap}
.res-v .sub{font-size:.75rem}
.res-none{margin:.85rem 0 0}
.bar{display:block;height:.5rem;min-width:3rem;border-radius:999px;
 background:var(--surface-2);border:1px solid var(--line);overflow:hidden}
.bar-fill{display:block;height:100%;border-radius:999px}
/* Block colours: a bar fill is exactly the case brand yellow is FOR. */
.lvl-ok{background:var(--up)}
.lvl-warn{background:var(--brand)}
.lvl-hot{background:var(--danger)}
/* QUANTIZED WIDTHS, because `default-src 'self'` blocks style="width:42%" silently
   -- see the header of this file. 5% steps, plus a 2% sliver so a small but
   non-zero value is still visibly non-zero. */
.fill-0{width:0}
.fill-2{width:2%}
.fill-5{width:5%}
.fill-10{width:10%}
.fill-15{width:15%}
.fill-20{width:20%}
.fill-25{width:25%}
.fill-30{width:30%}
.fill-35{width:35%}
.fill-40{width:40%}
.fill-45{width:45%}
.fill-50{width:50%}
.fill-55{width:55%}
.fill-60{width:60%}
.fill-65{width:65%}
.fill-70{width:70%}
.fill-75{width:75%}
.fill-80{width:80%}
.fill-85{width:85%}
.fill-90{width:90%}
.fill-95{width:95%}
.fill-100{width:100%}
@media (max-width:30rem){.res{grid-template-columns:auto 1fr auto;gap:.25rem .45rem}}
/* The task-name prefix chip. app.py has emitted <div class="pfx"> since the task
   form shipped; the rule was never written, so the prefix read as a stray word
   above the field instead of as part of the control. */
.pfx{display:flex;align-items:stretch}
.pfx>span{display:flex;align-items:center;padding:.55rem .5rem;
 border:1px solid var(--line);border-right:0;border-radius:4px 0 0 4px;
 background:var(--surface-2);color:var(--mute);white-space:nowrap;
 font:600 .9rem/1 ui-monospace,SFMono-Regular,Menlo,monospace}
.pfx>input[type=text]{border-radius:0 4px 4px 0}

/* ---------------------------------------------------------------------------
   ITEM 1 -- the action toolbar. Three bare controls at the TOP of the dashboard,
   replacing three cards of prose that sat under the grid. Same .act shape every
   other control on this site uses, so the doors read as controls and not as content.
   --------------------------------------------------------------------------- */
.toolbar{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.1rem}
.toolbar .act{padding:.42rem .9rem;font-size:.875rem}
@media (max-width:34rem){.toolbar .act{flex:1 1 100%;justify-content:center}}

/* ---------------------------------------------------------------------------
   ITEM 3 -- two strips between the toolbar and the grid, both reusing .sysline's
   layout. .hostline is the VPS itself (load/mem/disk/uptime, read directly by the
   portal process -- see host_health() in app.py) and sits FIRST, above .sysline,
   which is the docker/project aggregate ("Containers"). Neither is computed from a
   second broker call: the host figures come from a few local file reads, and the
   container figures come from data the page already had. .sysline also carries the
   degraded flag, which is why the old degraded paragraph is deleted from app.py:
   never both.
   --------------------------------------------------------------------------- */
.sysline{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem .9rem;
 background:var(--surface-2);border:1px solid var(--line);border-radius:6px;
 padding:.55rem .9rem;margin:0 0 .5rem;font-size:.875rem}
.sysline .sp{flex:1 1 auto}
.sys-k{font:600 .6875rem/1.6 Oswald,ui-sans-serif,sans-serif;letter-spacing:.08em;
 text-transform:uppercase;color:var(--mute)}
.sys-v{color:var(--ink);white-space:nowrap}
.sys-v b{font-variant-numeric:tabular-nums;font-weight:700}
/* --warn is the amber signal AS TEXT (~5.4:1 on white, and it has a dark-mode value).
   The brand yellow stays a block colour and never becomes body text -- see the
   palette note at the top of this file. */
.sys-v.sys-hit{color:var(--warn)}
.sys-flag{color:var(--warn);font-size:.8125rem}
#sys-updated{font-size:.75rem}
/* .hostline's meters reuse .bar/.bar-fill verbatim (see the per-project resource
   block below) but need a fixed width here -- inside a flex row .bar's own
   min-width:3rem would otherwise let three of them stretch unevenly. .hostline
   itself needs no other rule: it is a .sysline, so the flex layout, background,
   border and margin are already inherited from that class. */
.sys-bar{width:5rem;flex:0 0 auto}

/* ITEM 4 -- both grids now have a heading, and they weigh the same. .sect already
   exists and is unchanged; .sect-top is the first heading on the page, which sits
   right under the strip and needs less air above it than a mid-page one. */
.sect-top{margin-top:1.25rem}

/* ---------------------------------------------------------------------------
   ITEM 2 -- the native <dialog>. No library and no inline anything: showModal() and
   <form method="dialog"> are the platform's own primitives, so the CSP did NOT have
   to be widened for this either.
   --------------------------------------------------------------------------- */
dialog.dlg{border:1px solid var(--line);border-radius:8px;background:var(--surface);
 color:var(--ink);padding:0;width:100%;max-width:46rem;box-shadow:var(--shadow)}
dialog.dlg::backdrop{background:#1e1e22b3}
.dlg-in{padding:.6rem 1.4rem 1.5rem;max-height:84vh;overflow-y:auto}
.dlg-head{display:flex;align-items:center;gap:.75rem;position:sticky;top:0;z-index:1;
 background:var(--surface);padding:.8rem 0 .6rem;
 border-bottom:1px solid var(--line);margin-bottom:.8rem}
.dlg-head h2{margin:0}
.dlg-x{margin-left:auto;display:inline}
.dlg-x button{margin-top:0}
/* A card inside a dialog is a box inside a box. The dialog IS the card, so flatten
   the fragment's own .card chrome and let a rule separate the form from the
   explanatory block that follows it. */
.dlg .card{border:0;box-shadow:none;background:transparent;padding:0;max-width:none;
 margin-top:0}
.dlg .card+.card{margin-top:1.4rem;padding-top:1.2rem;border-top:1px solid var(--line)}

/* THE RULE THAT MAKES node.hidden = true ACTUALLY HIDE. The UA sheet's
   [hidden]{display:none} loses to any class rule that sets a display, and this file
   sets display on .res (grid), .sysline (flex) and .toolbar (flex). Without this,
   portal.js hiding a meter grid would do nothing at all -- silently, which is the
   same failure shape this file's header comment already warns about for inline
   styles. Everything portal.js toggles is toggled through this one rule. */
[hidden]{display:none!important}

/* Header controls that are LINKS, not buttons (the Admin door). .act paints itself
   with --ink, which is near-black in light mode and therefore invisible on the dark
   brand bar; `header button` already had its own opt-out and this is the same one. */
header .right .hdr-act{color:var(--bar-ink);border-color:#ffffff40;
 background:transparent;padding:.3rem .75rem;font-size:.8125rem;margin-right:.4rem;
 text-decoration:none}
header .right .hdr-act:hover{background:#ffffff1a;color:var(--bar-ink);
 border-color:#ffffff80}

/* ---------------------------------------------------------------------------
   ITEMS 6 and 7 -- the admin account list and the audit log. Tables, not cards:
   these are rows of the same shape to be scanned down a column, which is the one
   case the card grid replaced a table FOR the wrong reasons would be.
   --------------------------------------------------------------------------- */
/* The audit table is wide. Same trick .wrap:has(.projects) already uses to widen the
   one page that needs it, with no app.py change and no script. */
.wrap:has(.tbl){max-width:76rem}
.tblwrap{overflow-x:auto}
.tbl{width:100%;border-collapse:collapse;font-size:.875rem;margin-top:.5rem}
.tbl th{text-align:left;padding:.4rem .6rem;border-bottom:1px solid var(--line);
 font:600 .6875rem/1.6 Oswald,ui-sans-serif,sans-serif;letter-spacing:.08em;
 text-transform:uppercase;color:var(--mute);white-space:nowrap}
.tbl td{padding:.5rem .6rem;border-bottom:1px solid var(--line);vertical-align:top;
 overflow-wrap:anywhere}
.tbl tr:hover td{background:var(--surface-2)}
.tbl .num{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--mute)}
.u-on{color:var(--up-ink);font-weight:600}
.u-off{color:var(--mute)}
/* Same outline-chip treatment as .health, and for the same reason: the signal is the
   border and the ink token, never a yellow ground under text. */
.u-admin{display:inline-block;font:600 .6875rem/1 Oswald,ui-sans-serif,sans-serif;
 letter-spacing:.08em;text-transform:uppercase;padding:.22rem .5rem;
 border:1px solid var(--up-ink);color:var(--up-ink);border-radius:999px}
.rowacts{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center}
.rowacts form{display:inline-flex;gap:.3rem;align-items:center;margin:0}
.rowacts input[type=password]{width:11rem;padding:.3rem .45rem;font-size:.8125rem}
.rowacts button{margin-top:0}
.o-ok{color:var(--up-ink)}
.o-refused{color:var(--warn)}
.o-failed{color:var(--danger)}
.pager{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin-top:1.1rem;
 font-size:.875rem}
