/* ═══════════════════════════════════════════════════════════════════════════
   ArdaForm portal — the theme root
   ═══════════════════════════════════════════════════════════════════════════

   This file is the ONLY place the ArdaForm identity is written down. Everything
   below it — nux, `modules/global/page.css`, `modules/user/*.css` — reads tokens
   from here and hardcodes no colour of its own. Re-skinning the portal means
   editing this file and nothing else.

   ── why the tokens matter, not just the values ────────────────────────────
   Module CSS is spliced into <head> as <style id="ardadevcss"> AFTER this
   file's <link>, so at equal specificity module CSS WINS. A theme built as
   "global.css overrides page.css" would therefore need a specificity war it
   could only lose one selector at a time. So the direction is inverted: the
   module files read `var(--af-…)`, and this file supplies the values.

   ── the palette is the marketing site's ───────────────────────────────────
   Values are lifted verbatim from /WEBZ/ardaform/website/css/global.css so the
   portal and ardaform.net are the same product. Orange #f37021 on a near-black
   #0a0c0b, square corners, mono labels, no rounded anything.

   ── zero third-party requests ─────────────────────────────────────────────
   A stated selling point on /early-access ("check the source"), and the CSP is
   `default-src 'self'` to match. Roboto is served from /img/ here; the mono
   stack is system fonts only, exactly as on the site. Nothing is fetched.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
	font-family: "Roboto";
	src: url("/img/roboto-variable.woff2") format("woff2");
	font-weight: 100 900;
	font-style: normal;
	font-display: swap;
}

:root {
	/* ═══ 1. ArdaForm brand tokens ═══════════════════════════════════════
	   Verbatim from website/css/global.css:7-34. Anything the portal needs
	   that the marketing site never did is marked "portal only" below. */
	--af-orange:      #f37021;
	--af-orange-lt:   #ff8f4d;
	--af-orange-dk:   #b8500f;
	--af-orange-rgb:  243, 112, 33;

	--af-green:       #76dc4d;
	--af-green-rgb:   118, 220, 77;
	--af-blue:        #63c7f5;
	--af-blue-rgb:    99, 199, 245;

	/* ⚠️ portal only. The marketing site has NO red — it renders even form
	   errors in orange, because on a sales page "attention" and "brand" are
	   the same signal. An admin screen cannot do that: a destructive action
	   and the accent must not look alike. So the portal adds one red, and
	   uses it for exactly two things — danger, and locked. */
	--af-red:         #ff5a5a;
	--af-red-rgb:     255, 90, 90;
	--af-red-dk:      #ad2626;
	/* ⚠️ portal only, and the awkward one: the site's warning yellow sits
	   next to the accent on the hue wheel. Pushed to a clean yellow so a
	   warning never reads as branding. */
	--af-yellow:      #ffc861;
	--af-yellow-rgb:  255, 200, 97;

	/* surfaces, darkest first */
	--af-ink:         #0a0c0b;   /* the page */
	--af-ink-2:       #0d100e;   /* raised: cards, tiles, forms */
	--af-panel:       #111512;   /* panel */
	--af-panel-2:     #151a17;   /* panel hover */
	--af-panel-3:     #1b211d;   /* portal only — one step further, for the top bar */

	/* hairlines, faintest first */
	--af-line:        #262b28;
	--af-line-2:      #363d38;
	--af-line-3:      #4a534d;

	/* text, brightest first */
	--af-text:        #edeae4;
	--af-text-2:      #c3c7c2;
	--af-muted:       #9aa19b;
	--af-dim:         #757d77;

	/* The site is mono-first: every heading, label, kicker, tag, button and
	   table header is mono; only running body copy is sans. No webfont is
	   loaded for it — these are system faces, same as ardaform.net. */
	--af-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, Consolas, "Liberation Mono", monospace;
	--af-sans: "Roboto", Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;

	/* The identity is SQUARE. website/css/global.css declares --radius: 3px
	   and then uses it exactly zero times; the only border-radius on the
	   whole site is 50% on status dots. Keep it that way. */
	--af-radius: 2px;

	/* the uppercase mono label recipe, from website/css/global.css:155-157 */
	--af-label-tracking: .1em;


	/* ═══ 2. nux seeds ═══════════════════════════════════════════════════
	   ⚠️ These MUST be bare `r, g, b` triplets, never hex. Every nux rule
	   wraps them — `rgb(var(--nux-accent-color))`, `rgba(var(--…), .5)` — so
	   a hex here makes ~137 declarations invalid-at-computed-value and they
	   fall back to inherit, silently, with no console error.

	   nux derives essentially its whole palette from these three by
	   color-mix: surfaces mix text INTO background, dim text mixes text
	   TOWARD background, every state mixes accent into one or the other. Set
	   the seeds and the framework re-skins itself. */
	--nux-accent-color:     243, 112, 33;   /* --af-orange */
	--nux-background-color: 10, 12, 11;     /* --af-ink */
	--nux-text-color:       237, 234, 228;  /* --af-text */
	--nux-shadow-color:     0, 0, 0;

	/* ── the four gains, one per CSS property family ──────────────────────
	   Each scales the per-rule weight nux hardcodes. 1 = as designed.

	   ⚠️ The version of this file inherited from Portal 7 pinned
	   --nux-block-bg-1..4 and the whole --nux-field-* set to literal greys,
	   which left the multipliers doing NOTHING and every panel stranded at
	   #2c2c2e — a grey island once the page went near-black. They are
	   deliberately absent below. Do not put them back; tune these four
	   instead, and the entire elevation ladder moves together. */
	/* ⚠️ The clamp points are LOWER than they look, and they are what sets the
	   ceiling on each of these — not taste. color-mix percentages clamp to
	   [0,100], so a gain of G saturates every weight W where W×G ≥ 100:

	     accent   heaviest weight 85%  → clamps at G = 1.176
	     contrast heaviest weight 90%  → clamps at G = 1.111

	   Past those, distinct states resolve to the SAME colour. At 1.25 (where
	   both started) nux-tab:hover and nux-tab[nux-selected] were both exactly
	   #edeae4, and nux-modalbtn[nux-primary] rest (84%×1.25 → clamped) sat
	   1.08:1 from its own hover (72%×1.25) — the primary confirm button in
	   every modal had no visible hover state at all. Both are now safely under.
	   If you raise either, do the multiplication first. */
	--nux-surface-mult:  2.5;   /* fills. The page is much darker than the
	                               charcoal this theme came from, so surfaces
	                               need more lift to separate at all: at 1.8
	                               the four block levels were ~8 8-bit steps
	                               apart (1.08:1) and a nested panel did not
	                               read as nested. 2.5 → #191a19 / #252624 /
	                               #313230 / #3d3e3b. */
	--nux-border-mult:   2.2;   /* hairlines. ⚠️ Not cosmetic, and it is why
	                               this one is so far above 1: nux marks a text
	                               field with its 18% bottom rule and NOTHING
	                               else, and at 1 that rule was 1.40:1 against
	                               the field fill — every input on the page had
	                               no perceivable boundary at rest. 2.2 takes
	                               the field rule to 2.79:1 and nux's 20% block
	                               hairline to 3.42:1. Higher starts to read as
	                               a wireframe on a ground this dark. */
	--nux-accent-mult:   1.1;   /* every focus/hover/active/selected state */
	--nux-contrast-mult: 1.08;  /* dim text */

	/* Shadows read as mud on a true near-black — see the note under
	   §3 for what replaces them. Geometry is kept small and the darkness
	   low; separation comes from hairlines instead. */
	--nux-shadow-mult:     .35;
	--nux-shadows-opacity: .55;

	/* ⚠️ nux reads this in exactly ONE rule — `.nux-container { color-scheme:
	   var(--nux-color-scheme, dark) }` — and this app never adds that class:
	   the frame builds bare custom tags and nux.min.js does not add it either.
	   So the token alone does nothing, and native widgets (scrollbars, the
	   number spinner on the profile Weight field, date pickers, autofill
	   yellow) would render LIGHT on a near-black page. It is kept because nux
	   documents it, and `color-scheme` is set directly below where it actually
	   takes effect. */
	--nux-color-scheme: dark;
	color-scheme: dark;

	/* ── geometry ─────────────────────────────────────────────────────────
	   ⚠️ --nux-gap is not only spacing: it also feeds --nux-button-height
	   and the nested-block radius taper. Moving it moves button height. */
	--nux-radius:             var(--af-radius);
	--nux-gap:                .65rem;
	--nux-block-border-width: .0625rem;
	--nux-underline-width:    .125rem;
	--nux-font:               var(--af-sans);

	/* elevation ladder: block-n = base + step*(n-1) percent of text mixed
	   into background, scaled by --nux-surface-mult */
	--nux-block-level-base: 2.5%;
	--nux-block-level-step: 2%;

	/* ── semantic tones ───────────────────────────────────────────────────
	   Two parallel sets: the plain one drives buttons and list booleans, the
	   -deep- one drives progress bars, toasts and error modals. */
	--nux-success-color:      var(--af-green-rgb);
	--nux-info-color:         var(--af-blue-rgb);
	--nux-warning-color:      var(--af-yellow-rgb);
	--nux-danger-color:       var(--af-red-rgb);
	--nux-success-deep-color: var(--af-green-rgb);
	--nux-info-deep-color:    var(--af-blue-rgb);
	--nux-warning-deep-color: var(--af-yellow-rgb);
	--nux-danger-deep-color:  var(--af-red-rgb);

	/* ⚠️ nux's tooltip is the one island that ignores every seed: it ships a
	   literal cream balloon (#e7d7b0 on #2b2317). The permission grid leans
	   on tooltips heavily — a locked row has no other way to explain itself
	   — so on an orange-on-black page an un-themed tooltip would be the
	   loudest thing on screen. These two are literal colours, not triplets. */
	--nux-tooltip-bg:    #1b211d;
	--nux-tooltip-color: #edeae4;

	/* the page ground, read by body */
	--page-bg: var(--af-ink);
}


/* ═══ 3. app frame tokens ════════════════════════════════════════════════
   What `modules/global/page.css` and `modules/user/*.css` read. Each one
   replaces a literal that used to be hardcoded in those files — the
   inherited frame was plum/magenta dropdowns on a slate-blue left menu,
   which against orange-on-black read as two products fighting.

   Every module rule carries these as `var(--af-x, <old literal>)`, so the
   frame still renders if this file is ever missing. */
:root {
	/* — the top bar — */
	--af-top-bg:            var(--af-panel-3);
	--af-top-border:        var(--af-line-2);
	/* ⚠️ on a true near-black a drop-shadow does almost nothing; the top bar
	   and the dropdowns separate on a hairline instead. Kept faint and real
	   rather than deleted, so a bar overlapping a light table still lifts. */
	--af-top-shadow:        rgba(0, 0, 0, .55);

	/* — dropdown menus under the top bar — */
	--af-menu-bg:           var(--af-panel);
	--af-menu-border:       var(--af-line-2);
	/* ⚠️ TRANSLUCENT ON PURPOSE. This token is used on two different grounds —
	   a dropdown row (--af-menu-bg) and the top-bar item wash (--af-top-bg) —
	   and an opaque panel colour cannot serve both: --af-panel-2 was 1.05:1 on
	   the first (invisible) and DARKER than the second, so hovering a top-bar
	   item punched a hole in the bar. A white wash lightens whatever it lands
	   on, which is the only thing that works for both. */
	--af-menu-hover-bg:     rgba(255, 255, 255, .07);
	--af-menu-selected-bg:  rgba(var(--af-orange-rgb), .16);
	--af-menu-search-bg:    var(--af-ink-2);
	--af-menu-search-text:  var(--af-orange);
	--af-menu-match:        var(--af-orange);
	--af-menu-ink:          var(--af-muted);   /* chevron + magnifier mask ink */
	--af-scroll-thumb:      var(--af-line-3);
	--af-scroll-thumb-hover: #6b756e;

	/* — the left menu — */
	--af-leftmenu-bg:       var(--af-ink-2);
	/* the brightest hairline in the ArdaForm vocabulary, and this one earns it:
	   the column's fill is 1.03:1 against the page, so the border is the ONLY
	   thing that makes it a column. --af-line was 1.33:1 — no edge at all. */
	--af-leftmenu-border:   var(--af-line-3);
	/* .035 was below the threshold of visibility on this ground, and the left
	   nav's hover has no other cue — no border, no colour change, unlike
	   lmitem[selected]. */
	--af-leftmenu-hover-bg: rgba(255, 255, 255, .07);
	--af-leftmenu-sel-bg:   rgba(var(--af-orange-rgb), .1);
	--af-leftmenu-ink:      var(--af-muted);

	/* — status LEDs. The green/red pair is semantic and stays; "internet"
	     used to be #3dabff, which after the re-skin was the last blue pixel
	     on the page. It is a neutral now, and the meaning is carried by the
	     other two.
	     ⚠️ "off" is the state that matters most — no connection at all — so it
	     is the one that must not disappear. --af-line-3 put it at 2.06:1 on
	     the bar; --af-dim clears 3:1. — */
	--af-led-off:           var(--af-dim);
	--af-led-online:        var(--af-text-2);
	--af-led-server:        var(--af-green);
	--af-led-error:         var(--af-red);

	/* — infopills — */
	--af-pill-bg:           var(--af-panel-2);
	--af-pill-inner-bg:     rgba(0, 0, 0, .35);
	/* ⚠️ #8a6a15 carried --af-text at 4.21:1 — the only one of the four under
	   AA. The pills and the chips share this value, so it was failing twice. */
	--af-pill-warning:      #7a5c10;
	--af-pill-info:         #1d5b7d;
	--af-pill-ok:           #1f6b1a;
	--af-pill-danger:       var(--af-red-dk);

	/* — licon status chips —
	   ⚠️ COLOUR IS THE SECOND SIGNAL HERE, NOT THE FIRST. Each chip carries a
	   distinct mask glyph (tick / cross / padlock / key) and that is what
	   actually names the state: enabled-green, disabled-red and pwreset-amber
	   all collapse toward the same olive under red-green colour blindness, and
	   no palette fixes that — a red/amber/green triad cannot be made
	   CVD-distinct. Keep the glyphs distinct and never add a fifth state that
	   is colour-only.
	   ⚠️ `locked` used to be the exact same value as `disabled` — 1.00:1, zero
	   rgb distance — so two of the four chips were literally one swatch. It is
	   deeper now: still the red family (both mean "cannot log in") but
	   separated by luminance as well as by glyph. */
	--af-licon-bg:          var(--af-panel-2);
	--af-licon-ink:         var(--af-text);
	--af-licon-enabled:     #1f6b1a;
	--af-licon-disabled:    var(--af-red-dk);
	--af-licon-locked:      #6e1414;
	--af-licon-pwreset:     #7a5c10;

	/* — list row states — */
	--af-row-locked:        var(--af-red-dk);
	--af-row-locked-text:   #ff9b9b;

	/* — the permission grid —
	     ⚠️ Deny / Allow / Inherit are three STATES, not three brand
	     colours. They have to stay three obviously different things, so
	     Inherit keeps a blue: painting it orange would collapse it into the
	     accent and make "no opinion" look like an active choice. The order
	     below is load-bearing — user.css binds glyph and pad colour to
	     :nth-of-type(), and web_useradmin.js:441 says so out loud. */
	/* ⚠️ The pad strip is RECESSED, not raised, and that is what makes the
	   three states legible. An unlit pad sits at the page ground while the
	   block sits above it, so "which of the three is lit" is a step UP out of
	   a well. It matters numerically: against the old --af-panel-2 pad ground,
	   Deny cleared only 2.70:1 and Inherit 2.79:1 — "which one is set" read
	   reliably only when the answer was Allow. Against --af-ink all three
	   clear 3:1 (Deny 3.01, Inherit 3.10, Allow 4.84).
	   ⚠️ Honest limit: Deny and Inherit are near-equiluminant, so in greyscale
	   or under a heavy colour deficiency they separate by hue and glyph rather
	   than by brightness. The glyphs are ✕ / ✓ / →| and are the primary
	   signal; do not add a fourth state that relies on colour alone. */
	--af-perm-block-bg:     var(--af-panel);
	--af-perm-block-border: var(--af-line-2);
	--af-perm-rule:         var(--af-line-2);
	--af-perm-pad-bg:       var(--af-ink);
	--af-perm-ink:          var(--af-text);
	--af-perm-deny:         #a43939;
	--af-perm-allow:        #199229;
	--af-perm-inherit:      #2f5fa8;
	/* ⚠️ OPAQUE, not an alpha wash. This REPLACES the rest fill rather than
	   layering over it, so a translucent value composited against the block
	   behind it instead of the button — 1.16:1, no hover at all. */
	--af-perm-scope-bg:     var(--af-panel-2);
	--af-perm-scope-hover:  #5a3a1c;

	/* — error messages — */
	--af-error-bg:          rgba(var(--af-red-rgb), .09);
	--af-error-border:      rgba(var(--af-red-rgb), .55);
	--af-error-text:        #ffc9c9;
}


/* ═══ 4. the page ════════════════════════════════════════════════════════ */
* { box-sizing: border-box; }

html, body {
	height: 100%;
	margin: 0;
}

body {
	background: var(--page-bg);
	color: var(--af-text);
	/* the site's body setting, verbatim: 400 17px/1.65. 17px is deliberate
	   and 11px is the floor anywhere — the design this came from used 8-9px
	   labels and they were all raised. */
	font: 400 17px/1.65 var(--af-sans);
	-webkit-font-smoothing: antialiased;
}

::selection {
	background: rgba(var(--af-orange-rgb), .3);
}

/* ⚠️ THE EXCLUSION IS LOAD-BEARING, and it has to live HERE.
   nux paints its own accent focus state on its widgets, and clears the UA
   outline to do it — with bare type selectors, specificity (0,0,1). An
   unqualified `:focus-visible` is (0,1,0) and beats it, so every one of them
   would wear a second orange ring on top of nux's own treatment.
   The `:where()` keeps the exclusion at zero specificity, so this rule stays
   (0,1,0) and does not start winning fights it should not be in. Putting the
   same exclusion on a downstream rule instead does nothing — it only makes
   THAT rule abstain; it cannot suppress an unqualified one up here.

   ⚠️ The list is DERIVED, not guessed: every nux tag whose own stylesheet
   carries a :focus / :focus-within / :focus-visible rule. Re-derive it after a
   nux upgrade rather than adding to it by hand —

     grep -oE '[^{}]+\{' nux.min.css | grep ':focus' | grep -oE 'nux-[a-z]+' | sort -u

   The symptom when one is missing is not an obvious double ring: several of
   these set `overflow: hidden`, which CLIPS the stray outline down to a sliver.
   On nux-listsearch that sliver appeared as a vertical line between the
   magnifier and the placeholder, which reads as a stray border, not as focus. */
:focus-visible:not(:where(
	nux-block, nux-block *,
	nux-buttons, nux-buttons *, nux-button,
	nux-checkboxcontrol,
	nux-colorfav, nux-colorfavadd, nux-colorfavdel, nux-colorplane,
	nux-elm, nux-elm *,
	nux-icon,
	nux-listmenuitem, nux-listselect, nux-listselecthead, nux-listviewport,
	nux-listsearch, nux-listsearch *,
	nux-modclose,
	nux-switch, nux-switches, nux-switches *,
	nux-tab,
	nux-toastaction, nux-toastclose
)) {
	outline: 2px solid var(--af-orange);
	outline-offset: 2px;
}

/* nux draws its own accent edge on blocks; the inherited theme switched it
   off wholesale. Left off — an orange rule on every panel in a dense admin
   grid is noise, and the accent is spent better on state. */
nux-block {
	background-image: none;
}


/* ═══ 5. boot splash ═════════════════════════════════════════════════════
   Held invisible for the first 30% of the animation, so a channel that comes
   up quickly replaces it before it was ever seen; only a slow one fades in.
   Modules clear document.body when they render. */
boot {
	position: fixed;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1.25rem;
	animation: bootfade 1.2s ease-in both;
}
boot .bootlogo {
	width: 7rem;
	max-width: 40vw;
}
boot .boot-text {
	margin: 0;
	min-height: 1.2em;
	color: var(--af-dim);
	/* the site's uppercase mono label recipe */
	font: 600 11px/1 var(--af-mono);
	letter-spacing: var(--af-label-tracking);
	text-transform: uppercase;
	text-align: center;
}
@keyframes bootfade {
	0%, 30% { opacity: 0; }
	100%    { opacity: 1; }
}

/* The site's signature depth cue, reused once: a hard offset orange shadow,
   no blur. It is the whole reason the marketing terminals look industrial
   rather than glassy, and it costs nothing here. */
boot::after {
	content: "";
	position: absolute;
	inset: auto auto 12% 50%;
	width: 8rem;
	height: 1px;
	transform: translateX(-50%);
	background: linear-gradient(90deg, transparent, rgba(var(--af-orange-rgb), .55), transparent);
}

@media (prefers-reduced-motion: reduce) {
	/* .001ms rather than 0s, so animationend still fires */
	boot { animation-duration: .001ms; opacity: 1; }
}
