/*
 * GeoShield portal stylesheet.
 *
 * Hand-written in Tailwind's utility vocabulary rather than generated by a
 * Tailwind build. The class names are the same ones a Tailwind project would
 * use, so swapping in a real build needs no template change — but there is no
 * CDN request, no build step, and no external origin, which is what lets the
 * portal run on an air-gapped compliance box under a Content-Security-Policy
 * that permits nothing off-host.
 *
 * No `style="..."` attribute appears anywhere in the templates. The CSP sets
 * `style-src 'self'`, which blocks inline style attributes; anything dynamic is
 * drawn with SVG presentation attributes instead, which CSP does not restrict.
 */

/*
 * The vocabulary below is a *mapping*, not a palette. Every hex the portal
 * shows lives in `ui/styling.py` and arrives as a `--gs-*` custom property from
 * `/theme.css`, which every page loads before this file. Re-skinning the portal
 * is one dictionary in one Python module; nothing here needs to change.
 *
 * This is now literally true. The file used to carry about thirty `rgba(30,41,
 * 59,.32)`-shaped literals for hover fills, banner tints, tooltip shadows and
 * graph nodes — each one a colour that `ui/styling.py` did not know about, and
 * collectively the reason the portal could not have a light scheme: a token can
 * be redefined under `prefers-color-scheme`, a literal cannot. They are tokens
 * now, and `--hover`, `--header-bg`, `--shadow` and `--on-accent` exist for that
 * reason rather than because a stylesheet needs four more names.
 *
 * The fallbacks after each `var()` are Tailwind's own palette, deliberately
 * *not* this theme's values. Restating the theme's hexes here would leave the
 * file looking identical while quietly reintroducing what
 * `test_the_stylesheet_reads_the_tokens_rather_than_repeating_them` forbids: a
 * band colour that lives in two files and can be changed in one.
 * They exist so the stylesheet renders sanely on its own — a saved page, a diff preview — rather
 * than collapsing to unstyled black on black if `/theme.css` is missing.
 */
:root {
  --slate-950: var(--gs-base, #020617);
  --slate-900: var(--gs-base-navy, #0b1220);
  --slate-850: var(--gs-surface, #101a2e);
  --slate-800: var(--gs-line, #1e293b);
  --slate-700: var(--gs-line, #334155);
  --slate-500: var(--gs-muted, #64748b);
  --slate-400: var(--gs-body, #94a3b8);
  --slate-200: var(--gs-text, #e2e8f0);
  --sky-400: var(--gs-action, #38bdf8);
  --sky-500: var(--gs-action, #0ea5e9);
  --emerald: var(--gs-secure, #10b981);
  --yellow: var(--gs-warn, #eab308);
  --orange: var(--gs-elevated, #f97316);
  --rose: var(--gs-critical, #f43f5e);
  --surface-raised: var(--gs-surface-raised, #1e293b);
  --line-soft: var(--gs-line-soft, #172033);
  --hover: var(--gs-hover, rgba(255, 255, 255, .045));
  --header-bg: var(--gs-header, rgba(2, 6, 23, .86));
  --shadow: var(--gs-shadow, rgba(0, 0, 0, .55));
  --focus: var(--gs-focus, rgba(14, 165, 233, .4));
  --on-accent: var(--gs-on-accent, #041220);
  --radius: var(--gs-radius, 10px);
  --radius-sm: var(--gs-radius-sm, 7px);
  --radius-lg: var(--gs-radius-lg, 22px);
  /* Depth materials. Alpha is the point in all six — they are drawn over
     whatever is behind them — so the fallbacks are alphas of Tailwind's
     equivalents rather than of this theme's, same rule as the block above. */
  --glow-action: var(--gs-glow-action, rgba(56, 189, 248, .30));
  --glow-critical: var(--gs-glow-critical, rgba(244, 63, 94, .26));
  --glow-warm: var(--gs-glow-warm, rgba(249, 115, 22, .20));
  --glass: var(--gs-glass, rgba(255, 255, 255, .035));
  --glass-edge: var(--gs-glass-edge, rgba(255, 255, 255, .09));
  --lift: var(--gs-lift, rgba(0, 0, 0, .45));
  /* The sticky header's height, so the section rail can stick *below* it and an
     anchor can scroll to a heading rather than under one. Measured, not
     guessed: 107.5px — 108 — at 1141, 1440 and 2560, where the header does not
     wrap. Re-measured at all three after the `padding-block` fix below and the
     44px tap targets, and unchanged by either: on the desktop band the
     shorthand collision was already winning with the right vertical value and
     the wrong horizontal one, and the minimum target size is scoped under 768.
     It is the *narrow* header that moved — 185px to 261px at 390, 32 of that
     the vertical padding the collision had been eating — and below 1141 the
     rail is not sticky, so nothing reads this constant there.
     1141 and not 861 because the three navigation labels come to 519px in
     Georgian: brand, search and nav need about 1063px before anything has to
     give, and the old 861 left the header overflowing its own page between the
     two.
     Below that it wraps and the rail stops being sticky, which is why this is
     a constant rather than something read at runtime — the alternative is
     JavaScript writing an inline style, and the Content-Security-Policy that
     keeps this portal honest forbids exactly that.

     It is a *banded* constant rather than a single one because something below
     1141 does now read it: `scroll-padding-top` below. See the ladder under
     this block. */
  --header-height: 108px;
  --mono: var(--gs-font-mono, ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace);
}

/*
 * The same measurement, at the widths where the header wraps.
 *
 * 108 is right from 1141 up and wrong everywhere below it: the header is 145.5
 * at 900, 176 at 740, 267.8 at 600 and 334.9 at 340 — it grows a band every
 * time the brand, the search field and the three navigation labels stop fitting
 * on one line. While nothing below 1141 read this token that did not matter.
 * `scroll-padding-top` reads it on every page, so it does.
 *
 * Four bands, on breakpoints this stylesheet already uses (1140, 767, 720,
 * 520) rather than four new ones. Written as a descending chain of `max-width`
 * so several match at once and the narrowest wins; no `min-width` pairs to keep
 * in step.
 *
 * Each value is the *tallest* the header measured anywhere in its band across
 * all three shipped languages — Georgian, Russian and English, swept every 20px
 * from 320 to 1140 on the entity page:
 *
 *      band        ka / ru / en worst      value    worst over-shoot
 *      >= 1141     107.5                   108      0.5px
 *      768-1140    145.5                   146      0.5px
 *      721-767     176.0                   177      1.0px
 *      521-720     267.8 (at 540-600)      269      93px (at 700-720)
 *      <= 520      334.9 (at 320-360)      336      79px (at 390-520)
 *
 * Worst case, not average, because the two directions are not symmetrical. Too
 * large and the heading you jumped to sits lower on the screen than it needed
 * to; too small and it is *behind the header*, which is the defect this fixes.
 * The 79px at the 390 phone reference buys the 320-360 band, where Georgian and
 * Russian need a third header row and 257 would hide the heading by 78px.
 *
 * Every value clears its band by at least half a pixel, and that margin is not
 * decoration. The browser rounds the final scroll offset to whole device
 * pixels, so a band set to exactly its measured worst lands fractionally short:
 * 176 against a 176.0 header left 0.3px of the target under it at 721, 740 and
 * 767, and 268 against 267.8 did the same at 521-560. The two bands whose
 * measurement happens to end in .5 had the margin already; the three that came
 * out on whole pixels have a pixel added.
 *
 * These are three languages' worth of wrapping behaviour, so a new translation
 * with longer navigation labels can push a band taller. That is a re-measure,
 * not a redesign: raise the number for the band it moved.
 */
@media (max-width: 1140px) { :root { --header-height: 146px; } }
@media (max-width: 767px)  { :root { --header-height: 177px; } }
@media (max-width: 720px)  { :root { --header-height: 269px; } }
@media (max-width: 520px)  { :root { --header-height: 336px; } }

/*
 * One rule for every in-page anchor in the portal.
 *
 * `.site-header` is `position: sticky; top: 0`, so the top of the viewport is
 * not where content becomes visible — the header's bottom edge is. Without
 * this, every fragment jump lands its target underneath the header: the skip
 * link, which is the first tab stop on every page, put `#main` 108px behind it
 * at 1440 and 257px behind it at 390, so a keyboard reader's first action hid
 * the subject's name and risk band. The landing page's "how it works" link
 * covered 107.5px of a 193.6px section — the reader arrived mid-sentence inside
 * step one at 1440 and at step three at 390.
 *
 * `scroll-padding-top` on the scrollport rather than `scroll-margin-top` on
 * each target: the property being asserted is "nothing in this portal scrolls
 * to underneath the header", and a per-target list is a list somebody will add
 * an anchor to and forget — exactly the reason `:focus-visible` below is one
 * rule too. It also composes: scroll-padding on the scrollport and
 * scroll-margin on the target add, so a target that needs *extra* clearance
 * still asks for only the extra (see `.panel[id]`).
 */
html { scroll-padding-top: var(--header-height); }

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--gs-font-sans, "Segoe UI", system-ui, -apple-system, "Noto Sans Georgian", sans-serif);
  font-size: 15px;
  line-height: 1.55;
  background: var(--slate-950);
  color: var(--slate-200);
  /* Georgian is a dense script and the interface is read for hours. The two
     OpenType features below cost nothing and are the difference between a
     column of identification codes that lines up and one that does not. */
  font-variant-numeric: tabular-nums;
  text-rendering: optimizeLegibility;
}

.bg-slate-950 { background: var(--slate-950); }
.text-slate-200 { color: var(--slate-200); }

/*
 * Every focusable control shows where the keyboard is.
 *
 * There was no rule for this at all: the browser's own ring is drawn in the
 * user agent's colour against a near-black page, and on the accent buttons it
 * was invisible. One rule rather than one per component, because the property
 * being asserted is "nothing in this portal can be reached without being seen",
 * and a per-component list is a list somebody will add a control to and forget.
 */
:focus-visible {
  outline: 2px solid var(--sky-400);
  outline-offset: 2px;
  border-radius: 3px;
}
/* A control that draws its own ring does not also need the default one. */
.searchbar input:focus-visible { outline: none; }

/* 1600, not 1240. This is an investigation console: the analyst reading it has
   a 1440 or 2560 monitor, and at 2560 the old cap left roughly half the display
   empty while four fields in the profile card sat truncated inside the column.
   Prose still needs a measure — `.lede` and the landing copy carry their own
   `max-width` — but a two-column layout carrying a graph canvas and a table
   does not, and it was paying for a rule written for the paragraph. */
/* `padding-inline`, not `padding: 0 24px`. The gutter is the only thing this
   rule has an opinion about, but the shorthand also asserts `padding-block: 0`
   — and `.header-main` and `.header-context` are `.wrap` too and carry their
   own vertical padding at the same specificity, so whichever rule was written
   later silently won the whole property. Naming one axis leaves the other to
   the element that knows about it. */
.wrap { max-width: 1600px; margin: 0 auto; padding-inline: 24px; }
.flex { display: flex; }
.items-center { align-items: center; }
.gap-6 { gap: 24px; }

a { color: var(--sky-400); }
.link { color: var(--sky-400); text-decoration: none; }
.link:hover { text-decoration: underline; }

/*
 * A link inside a data table is not an action.
 *
 * Every subject name, every counterparty and every source in the dossier was
 * accent blue, so a screen whose whole purpose is to show four risk colours
 * showed a fifth on almost every row and the eye had nowhere to rest. In a
 * table the name is the content; the link is how you follow it. So it reads as
 * text with a quiet underline and takes the accent on hover, which is where an
 * affordance is actually needed. Outside a table — prose, the lede, the
 * footer — a link is a genuine departure from the page and keeps the accent.
 */
.table .link, .table a {
  color: var(--slate-200);
  text-decoration: underline;
  /* The muted grey, not the border grey: a hairline the same colour as the row
     rule is not an affordance, it is a coincidence. Measured at 1.3:1 against
     the panel in the light scheme, which is invisible. */
  text-decoration-color: var(--slate-500);
  text-underline-offset: 3px;
}
.table .link:hover, .table a:hover {
  color: var(--sky-400);
  text-decoration-color: currentColor;
}
/* `.trunc-cell` is `display: block`, so an underline on the anchor wrapping one
   is drawn across the whole 44ch clamp rather than under the name — a rule
   across the row, next to the dotted rule the truncation tooltip already draws.
   Shrink-to-fit here, which clamps exactly the same way and lets the underline
   end where the text does. */
.table a > .trunc-cell { display: inline-block; vertical-align: bottom; }

/*
 * And the same correction for a heading, where it was worse.
 *
 * `.trunc-cell` is `display: block` and `.tip` draws a dotted rule along its
 * bottom edge — the affordance that says "this may be clipped, hover for the
 * whole string". In a table cell the clamp is roughly the column, so the rule
 * is roughly under the text. In an `<h1>` the clamp is `.trunc-wide`'s 44ch,
 * which measured 692px against a subject name of 250: a dotted line ran 440px
 * past the end of the company's name and read as a heading rule somebody had
 * drawn deliberately. Both the console's subject bar and the entity header
 * carried it.
 *
 * Shrink-to-fit ends the rule where the text ends and keeps the clamp for the
 * names that genuinely need it. It does not fix the deeper thing — the
 * affordance is drawn whether or not the string was actually clipped, which
 * CSS cannot know — but it stops the untruncated case from inventing a rule.
 */
h1 > .trunc-cell,
h2 > .trunc-cell { display: inline-block; vertical-align: bottom; max-width: 100%; }

/*
 * And the dotted rule itself comes off a heading.
 *
 * Shrink-to-fit above ends it at the text; it is still drawn under a name that
 * was never clipped, which is an affordance advertising something that did not
 * happen. Under a 28px company name it reads as a deliberate underline — the
 * one thing a reader is entitled to take from a rule under a heading is that
 * somebody meant to put it there.
 *
 * The honest signal for a heading that *is* clipped is the ellipsis, which
 * `.trunc-cell` already renders. It says "there is more" exactly when there is
 * more, which the rule cannot.
 *
 * Only the rule goes. `m.trunc` writes `data-full` and `tabindex="0"`, and
 * `.tip::after` builds the tooltip from that attribute — so the full string is
 * still one hover or one Tab away on a heading, which is the part of the
 * affordance that was doing real work. In a table cell the rule stays too: a
 * column of names at 13px has no room for a second cue, and the reader there
 * is scanning rather than reading.
 */
h1 > .tip, h2 > .tip { border-bottom: 0; }

/* A submit button that has to read as a link. The example questions on /ask
   were anchors, and an anchor can only issue a GET — which put the question,
   names and all, into the URL. They are one-field POST forms now; this makes
   that invisible to the reader. `text-align: left` because a button centres
   its label and the surrounding table cells do not. */
.inline-form { display: inline; margin: 0; }
.linklike {
  background: none; border: 0; padding: 0; cursor: pointer;
  text-align: left; font: inherit; color: var(--sky-400);
}
.linklike:hover { text-decoration: underline; }

.muted { color: var(--slate-400); font-size: 13px; }
.mono { font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.trunc { display: inline-block; max-width: 240px; overflow: hidden;
         text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.empty { color: var(--slate-400); padding: 16px 0; }

/* A small caps label above a heading or beside a figure. The interface says
   what kind of thing you are looking at before it says which one. */
.eyebrow {
  font-size: 10.5px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--slate-500);
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/*
 * Skip to the content.
 *
 * The header is eight controls before the first word of the subject, and a
 * keyboard or screen-reader user met all eight on every page. Off-screen until
 * focused, which is the point: it costs a sighted mouse user nothing and saves
 * everyone else the whole navigation on every single page.
 */
.skip-link {
  position: absolute; left: 12px; top: -60px; z-index: 60;
  background: var(--slate-850); color: var(--slate-200);
  border: 1px solid var(--sky-400); border-radius: var(--radius-sm);
  padding: 9px 16px; font-size: 13.5px; font-weight: 600; text-decoration: none;
  transition: top .12s ease;
}
.skip-link:focus-visible { top: 12px; }

/* ---------------------------------------------------------------- header */

/*
 * Two bands, not one wrapped row.
 *
 * The header held eight items in a single `flex-wrap` row, so the browser
 * decided where the break fell: the language switcher, the role and the graph
 * status wrapped underneath at whatever width was left, in the same weight and
 * on the same background as the brand and the search field. They are not the
 * same kind of thing. Band one is what you *do* — search, ask, upload. Band two
 * is the state you are doing it in — which language, which role, which graph,
 * how old. Making that a separate strip is a template change of two `<div>`s
 * and it is what turns a wrapped row into a deliberate one.
 */
.site-header {
  position: sticky; top: 0; z-index: 20;
  background: var(--header-bg);
  /* `saturate` as well as `blur`: a translucent bar over a page carrying four
     risk colours drains them to grey without it, and the header is the one
     surface in the portal that has something coloured moving underneath it. */
  backdrop-filter: blur(16px) saturate(140%);
  border-bottom: 1px solid var(--line-soft);
  /* A hairline alone leaves the bar and the page looking like one surface with
     a rule drawn on it. The drop is 24px of spread at a quarter opacity — it
     reads as depth while scrolling and is invisible at rest. */
  box-shadow: 0 14px 34px -26px var(--shadow);
}
/*
 * The lit edge.
 *
 * `-1px` and `position: absolute`, so it lies *over* the border rather than
 * adding to the box: `--header-height` is a measured constant that five bands
 * of this stylesheet depend on, and a decorative pixel that changed
 * `offsetHeight` would move every fragment link in the portal by one.
 */
.site-header::after {
  content: "";
  position: absolute; inset: auto 0 -1px; height: 1px;
  background: linear-gradient(90deg,
    transparent, var(--glow-action) 28%, var(--glow-action) 72%, transparent);
  pointer-events: none;
}
/*
 * `padding-block`, not `padding`.
 *
 * Both bands are `.wrap` as well, and `.wrap` sets `padding: 0 24px`. The
 * shorthand here re-declared all four sides at equal specificity, so exactly
 * one of the two rules survived and which one depended on source order: at
 * 1440 these rules come last and won, which zeroed the inline padding and put
 * `.brand` at x=0 and `.status-pill`'s right edge at x=1440 — the header flush
 * against both viewport edges and 24px left of the H1, the banner, every panel
 * and the footer. Below 1140 the collision flipped, because `.wrap`'s narrow
 * override is written after `.header-main`'s: the sides survived and the
 * vertical padding was the casualty, so `.brand` rendered at y=0 against the
 * top of a 390px screen. `padding-block` sets only the axis this rule is about
 * and leaves `.wrap`'s inline padding alone, so there is nothing left to win.
 */
.header-main { display: flex; align-items: center; gap: 18px; padding-block: 12px; }
.header-context {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding-block: 7px; border-top: 1px solid var(--line-soft);
  font-size: 12px;
}
/* The graph status is state about the machine, not about the investigation:
   it sits at the far end of the strip rather than in the reading order. */
.header-context .status-pill { margin-left: auto; }

.brand { display: flex; align-items: center; gap: 11px; text-decoration: none; color: inherit; }
/*
 * The mark.
 *
 * This rule used to size a bordered chip around the letters "GS", and the note
 * on it argued about whether the chip should be a gradient. Both were answers
 * to the wrong question: the portal had no mark, it had a placeholder, and a
 * placeholder is what a header reads as no matter how it is filled.
 *
 * What is drawn now is a shield with an ownership chain inside it — two holders
 * above, the subject below — and one of the two lit in `--red`, which is the
 * exact colour every sanctioned party on every other page is drawn in. That is
 * the product in twelve shapes, and it means the logo cannot drift from the
 * risk palette: it reads the same tokens.
 *
 * No box around it. The chip existed to give two letters somewhere to sit; a
 * shape does not need one, and removing it is what stops the brand looking like
 * a badge stuck onto the header. The bloom behind it is `--glow-action` at a
 * strength that is atmosphere in the dark scheme and almost nothing in the
 * light one, which is what those tokens are for.
 */
.brand-mark {
  position: relative;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  flex: none;
}
.brand-mark::before {
  content: "";
  position: absolute; inset: -6px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--glow-action), transparent 68%);
  opacity: .85;
  pointer-events: none;
}
.brand-glyph { position: relative; width: 32px; height: 32px; overflow: visible; }
/* Fill and stroke in CSS rather than as presentation attributes, for the reason
   the landing hero gives: an attribute cannot follow a media query, so the mark
   would keep the dark palette on a light workstation. */
.brand-shield {
  fill: var(--glass);
  stroke: var(--sky-400);
  stroke-width: 1.35;
  stroke-linejoin: round;
}
.brand-link { stroke: var(--slate-500); stroke-width: 1.2; stroke-linecap: round; }
.brand-node { fill: var(--slate-950); stroke: var(--sky-400); stroke-width: 1.2; }
/* The one saturated thing in the header, and it is the same red as a listed
   party. Deliberate: the mark states what the portal is looking for. */
.brand-node-hot { fill: var(--rose); stroke: var(--rose); }
/* The mark answers the pointer, so the brand reads as the link it has always
   been — there was no hover state on it at all. The bloom is what moves and not
   the stroke: `--sky-400` and `--sky-500` are the same token underneath
   (`--gs-action`), so a "brighter blue" here would be a rule that changes
   nothing, which is worse than no rule. */
.brand:hover .brand-mark::before { opacity: 1; }
.brand-mark::before { transition: opacity .18s ease; }
@media (prefers-reduced-motion: reduce) {
  .brand-mark::before { transition: none; }
}
/* `flex-shrink: 0` and no wrapping: the product name is three words and the
   subtitle four, and in a row that also holds a search field the browser will
   happily reflow both into a six-line block 90px wide before it takes a pixel
   off the input. Measured at 800px, where the brand stacked over five lines. */
.brand-text { display: flex; flex-direction: column; line-height: 1.25; flex-shrink: 0; }
.brand-text strong { font-size: 13.5px; letter-spacing: -.005em; white-space: nowrap; }
.brand-text small { color: var(--slate-500); font-size: 11px; white-space: nowrap; }

.searchbar { display: flex; gap: 8px; flex: 1 1 320px; min-width: 260px; }
/*
 * The field is a field now, not a text box.
 *
 * A bare rectangle beside a filled button is what a search control looked like
 * before search controls had a shape, and it is the single thing that dated
 * this header most. The pill and the glyph are the two conventions a reader
 * recognises without reading anything, and neither costs a control.
 *
 * The border stays on the `<input>` rather than moving to this wrapper.
 * `.searchbar input:focus` is what draws the focus ring, and a ring around a
 * box the reader is not typing into is a ring in the wrong place — the wrapper
 * exists only to give the glyph something to be positioned against.
 */
.searchbar-field {
  position: relative;
  display: flex; align-items: center;
  flex: 1; min-width: 0;
}
.searchbar-glyph {
  position: absolute; left: 13px;
  width: 15px; height: 15px;
  fill: none; stroke: var(--slate-500); stroke-width: 1.7;
  stroke-linecap: round;
  pointer-events: none;
  transition: stroke .15s ease;
}
.searchbar-field:focus-within .searchbar-glyph { stroke: var(--sky-400); }
@media (prefers-reduced-motion: reduce) { .searchbar-glyph { transition: none; } }
.searchbar input {
  flex: 1; min-width: 0;
  background: var(--slate-900);
  border: 1px solid var(--slate-800);
  border-radius: 999px;
  /* 36 on the left is the 13px inset plus the 15px glyph plus an 8px gap. The
     right side keeps 15 rather than 13 because a pill's corner eats the last
     couple of pixels a rectangle did not. */
  padding: 8px 15px 8px 36px;
  color: var(--slate-200);
  font: inherit; font-size: 14px;
}
.searchbar input::placeholder { color: var(--slate-500); }
.searchbar input:focus {
  outline: none; border-color: var(--sky-500);
  box-shadow: 0 0 0 3px var(--focus);
}
.searchbar-lg { max-width: 720px; margin-top: 24px; }
/* A pill, like the header's field above and the console's `-xl` below. It was
   the 10px panel radius, which was consistent with everything until the header
   became a pill — and a portal whose three search fields are three different
   shapes reads as three different products. */
.searchbar-lg input { padding: 13px 18px; font-size: 15.5px; border-radius: 999px; }
.searchbar-lg .btn { border-radius: 999px; padding-inline: 22px; }

/*
 * The console's field, at the size of the page's purpose.
 *
 * `-xl` and not a bigger `-lg`, because `-lg` is still the right size on the
 * pages where the field is one control among several. Here it is the page: the
 * pill shape and the 17px placeholder are what carry "start typing" without a
 * sentence saying so, and the focus ring is a bloom rather than the 3px halo
 * the small fields take, because at this scale the halo reads as a border
 * someone forgot to remove.
 */
.searchbar-xl { max-width: 780px; margin-top: 26px; }
.searchbar-xl input {
  padding: 18px 24px;
  font-size: 17px;
  border-radius: 999px;
  background: var(--glass);
  border-color: var(--glass-edge);
}
.searchbar-xl input:focus {
  border-color: var(--sky-500);
  box-shadow: 0 0 0 4px var(--focus), 0 14px 40px -14px var(--glow-action);
}
.searchbar-xl .btn-lg { border-radius: 999px; padding: 15px 30px; }

/*
 * The hero's field is allowed to take a second row.
 *
 * The input is `flex: 1 1 0` against a button that is `flex: 0 1 auto`, so the
 * button takes whatever its label needs and the field gets the remainder. In
 * Georgian the label is a phrase: measured at 390px, "მოძებნეთ" and its
 * siblings left the button 217x52 and the input 133x52 — the one control on the
 * page you are meant to type into got 38% of its own row, and the placeholder
 * that explains what to type into it truncated to nothing. On /ask the same row
 * is 248/101 because "Ask" is short, which is the tell: the split is decided by
 * the button's language, not by the layout.
 *
 * So the button drops below the field and both get the full width. 520px rather
 * than the 480 the layout audit proposed, because 520 is already a breakpoint
 * in this file and 480 would be a tenth one for no measured reason: at 520 the
 * padded wrap leaves 488px, the button still needs 217 of it, and a 263px field
 * is already the wrong side of half a row.
 */
.hero .searchbar { flex-wrap: wrap; }

.btn {
  border: 1px solid var(--slate-800);
  background: var(--slate-850);
  color: var(--slate-200);
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  font: inherit; font-size: 13.5px; font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.btn:hover { border-color: var(--slate-500); }
.btn-primary {
  background: var(--sky-500); border-color: var(--sky-500); color: var(--on-accent);
}
.btn-primary:hover { filter: brightness(1.08); border-color: var(--sky-400); }
/* The header's own navigation. Borderless until pointed at: three outlined
   buttons beside the search field made every destination look like a command. */
.btn-ghost {
  background: transparent; border-color: transparent; color: var(--slate-400);
  font-weight: 500;
}
.btn-ghost:hover { background: var(--hover); border-color: transparent; color: var(--slate-200); }
.btn-lg { padding: 12px 22px; font-size: 14.5px; border-radius: var(--radius); }

/*
 * The header's destinations, as one group rather than three loose controls.
 *
 * A pane with an edge, at the strength `--glass` is defined for: enough to say
 * "these three belong together and the search field is not one of them", not
 * enough to become a fourth surface on a page that already has four. The pill
 * matches the field beside it, which is the whole reason it is a pill.
 *
 * `flex-wrap` because below 1140 this group is what wraps, and it has to be
 * able to take a second line inside itself rather than pushing the whole band
 * down — see the header-height ladder at the top of this file, which is
 * measured against exactly this behaviour.
 */
.header-nav {
  display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
  padding: 3px;
  border-radius: 999px;
  background: var(--glass);
  border: 1px solid var(--glass-edge);
}
/*
 * The segments give back exactly what the track takes.
 *
 * `padding-block: 4` rather than the `.btn` default of 8, because the track
 * around them is 3px of padding and a 1px border on each side. A 39px button in
 * a 47px group made `.header-main` the tallest thing in the header and pushed
 * the measured `--header-height` from 107.5 to 114.9 at every width above 1141
 * — which is every fragment link in the portal landing 7px behind the header,
 * bought with a decorative border. The group is 39px again, and it is the
 * segments that are inset, which is what a segmented control looks like anyway.
 */
.header-nav .btn-ghost { border-radius: 999px; padding-block: 4px; }
/*
 * Where you already are.
 *
 * This header could not answer that question at all: three identical ghost
 * buttons, one of which was the page under them. `aria-current="page"` is the
 * assertion a screen reader gets and this is the same fact drawn — a filled
 * pill, not an underline, because an underline on a control that is not a link
 * to anywhere is the same ambiguity in a quieter form.
 */
.btn-ghost[aria-current="page"] {
  background: var(--surface-raised);
  color: var(--slate-200);
  font-weight: 600;
}
.btn-ghost[aria-current="page"]:hover { background: var(--surface-raised); }

/* The commit control on /upload. Its own class rather than `.dossier-form`,
   which is laid out for the entity page's header row and indented the button
   into the middle of the panel here. */
.commit-form { margin-top: 16px; }
.commit-form form { margin: 0 0 8px; }

/*
 * The file picker on /upload.
 *
 * `<input type="file">` is drawn by the operating system: a grey Win32 "Choose
 * File" button and the words "No file chosen" in the browser's own face, which
 * on this page sat beside a 40px accent button and was the only control in the
 * portal that did not belong to it. Styling the *element* and its
 * `::file-selector-button` is the whole fix — the input keeps its type, its
 * `accept` list and its keyboard behaviour, so nothing about how the upload
 * works changes. No `appearance: none`, which would take the button away
 * entirely on engines that do not implement the pseudo-element.
 */
/*
 * The upload control is a drop zone now.
 *
 * The old rule sized a native `<input type="file">` to match the button beside
 * it — 48px tall, dashed, with a styled `::file-selector-button`. The whole of
 * that paragraph of measurement went away with the element it measured: the
 * input is `.sr-only` inside the label, so the browser draws none of it and
 * there is nothing to line up. What replaced it is a target the size of the
 * gesture it accepts, which is the point of a drop zone.
 *
 * The input keeps its place in the tab order — `.sr-only`, not `display:
 * none`, which would remove it — so the zone lights on `:focus-within` and a
 * keyboard reader sees the same state a pointer reader does.
 */
.upload-form { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }

.dropzone {
  flex: 1 1 340px; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 26px 24px;
  border: 1.5px dashed var(--slate-700);
  border-radius: var(--radius-lg);
  background: var(--glass);
  color: var(--slate-400);
  cursor: pointer;
  text-align: center;
  transition: border-color .2s ease, background-color .2s ease;
}
.dropzone:hover, .dropzone:focus-within { border-color: var(--sky-400); }
/* Solid, not dashed, while a file is over it: the dashes say "this is empty
   and takes something"; a live target under the cursor is not that. */
.dropzone.is-over {
  border-style: solid;
  border-color: var(--sky-400);
  background: var(--gs-action-tint);
}
.dropzone-glyph { font-size: 20px; line-height: 1; color: var(--slate-500); }
.dropzone-prompt { font-size: 14.5px; font-weight: 600; color: var(--slate-200); }
/* Empty in the markup and filled by `portal.js` only after the drop handler is
   bound — see the note in `upload.html`. An empty span reserves no space, so a
   page that never runs the script shows the two-line control it always had. */
.dropzone-hint { font-size: 12.5px; color: var(--slate-500); }
.dropzone-name {
  font-family: var(--mono); font-size: 12px;
  color: var(--sky-400);
  max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dropzone.has-file { border-style: solid; border-color: var(--sky-400); }
.dropzone.has-file .dropzone-name { margin-top: 6px; }

@media (prefers-reduced-motion: reduce) { .dropzone { transition: none; } }

/* Language switcher. The active entry is text, not a link to itself. */
.lang-switch { display: flex; align-items: center; gap: 10px; font-size: 12px; }
.lang-switch .lang-active { color: var(--slate-200); font-weight: 600; }
.lang-switch a { color: var(--slate-500); text-decoration: none; }
.lang-switch a:hover { color: var(--sky-400); text-decoration: underline; }

/*
 * The appearance switch.
 *
 * Three segments in one track, which is the shape a reader already reads as
 * "pick one of these" — the same shape as the currency switch on the financial
 * tables, deliberately, because it is the same kind of choice.
 *
 * Sized in pixels and not in text, so its width is identical in English,
 * Georgian and Russian. That is not tidiness: this strip wraps, and a control
 * that is 43px wide in one language and 96px in another adds a header row in
 * one language and not the other, which is a `--header-height` band that is
 * right for some readers and wrong for the rest. The words are in `.sr-only`
 * spans and in `title`; only the *drawing* is language-independent.
 *
 * `[hidden]` needs saying because `display: flex` would otherwise beat it, and
 * this control is hidden in the template until `portal.js` reveals it.
 */
/*
 * 1px of inset, not 2, and the reason is a measurement.
 *
 * The tallest thing in this strip is the role pill at 27.81px, and that number
 * is what `--header-height: 108` was measured against. At 2px of padding this
 * control came to 28.0 — 0.19px taller — which made it the tallest item in the
 * band, pushed the header to 107.69, and left the pinned constant with 0.31px
 * of margin where the ladder at the top of this file requires half a pixel.
 * Nineteen hundredths of a pixel of decoration is not worth a fragment link
 * landing under the header, so the track is thinner and 26px is the total.
 */
.theme-switch {
  display: flex; align-items: center; gap: 2px;
  padding: 1px;
  border-radius: 999px;
  background: var(--glass);
  border: 1px solid var(--glass-edge);
}
.theme-switch[hidden] { display: none; }
.theme-btn {
  display: grid; place-items: center;
  width: 26px; height: 22px;
  padding: 0;
  border: 0; border-radius: 999px;
  background: transparent;
  color: var(--slate-500);
  cursor: pointer;
  transition: background-color .14s ease, color .14s ease;
}
.theme-btn:hover { color: var(--slate-200); background: var(--hover); }
/*
 * The current segment, drawn the same way the current page is in the nav above:
 * a filled pill on the raised surface. One idiom for "this one".
 *
 * Keyed on `data-current` and not on `aria-pressed`, which is the same fact but
 * not always present: below 768 this control is one cycling button rather than
 * three segments, and `aria-pressed` is wrong on a button that advances instead
 * of toggling — `portal.js` takes it off there. A stylesheet that hung the
 * "which one is on" state off the ARIA attribute would have lost the highlight
 * on exactly the layout where it is the only thing left to look at.
 */
.theme-btn[data-current] {
  background: var(--surface-raised);
  color: var(--sky-400);
}
.theme-glyph {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The moon is a silhouette, not an outline: a 14px crescent drawn as a stroke
   is two hairlines that close up into a smudge at this size. */
.theme-glyph-fill { fill: currentColor; stroke: none; }
@media (prefers-reduced-motion: reduce) { .theme-btn { transition: none; } }

.status-pill {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--slate-800); border-radius: 999px;
  padding: 4px 12px; font-size: 11.5px; color: var(--slate-400);
  background: var(--slate-900);
}
.status-sep { color: var(--slate-700); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--emerald); }
.status-degraded .dot { background: var(--orange); }
.status-degraded { border-color: var(--orange); }

/* --------------------------------------------------------------- banners */

/*
 * A standing condition, not an alert.
 *
 * The development-instance notice is on every page of every non-operator
 * deployment — which is every page an engineer ever sees — and it was a filled
 * orange block four lines deep above the content. A notice that never goes away
 * and shouts every time teaches the reader to skip the place where the real
 * warnings appear. It keeps its border, its tint and its full text; what it
 * loses is the shout. The accent moves to the first phrase, which is the part
 * that says which kind of notice this is.
 */
.banner {
  border-radius: var(--radius-sm); padding: 10px 14px; margin: 16px 0;
  font-size: 12.5px; line-height: 1.5; border: 1px solid; color: var(--slate-400);
  border-left-width: 3px;
}
.banner code { font-family: var(--mono); font-size: 11.5px; color: var(--slate-200); }
.banner strong, .banner em { color: var(--slate-200); font-style: normal; }
.banner-warn { background: var(--gs-elevated-tint); border-color: var(--slate-800); border-left-color: var(--orange); }
.banner-warn strong { color: var(--orange); }
.banner-info { background: var(--gs-action-tint); border-color: var(--slate-800); border-left-color: var(--sky-400); }
.banner-info strong { color: var(--sky-400); }
/*
 * The third kind. `banner-warn` is "read this"; this one is "the thing you
 * configured is not in effect", which is the only banner in the portal that
 * reports a broken state rather than a caveat, and it should not look like the
 * one an engineer sees on every development page.
 */
.banner-danger { background: var(--gs-critical-tint); border-color: var(--slate-800); border-left-color: var(--gs-critical); }
.banner-danger strong { color: var(--gs-critical); }

/* -------------------------------------------------- case-file form controls
 *
 * The portal was read-only until the customer file, so the only styled input
 * in this sheet was `.searchbar`'s — one large field on a dark stage. A
 * determination form needs a select, a textarea and a short text input sitting
 * inside a panel, and unstyled controls there render as the browser's own grey
 * on a near-black card: unreadable in dark mode, and the one place in the
 * product where an officer types something that goes into a compliance record.
 */
.field { display: flex; flex-direction: column; gap: 4px; margin: 12px 0; max-width: 72ch; }
.field > span { font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--slate-500); }
/*
 * `--slate-850`, the field surface `.btn` sits on, and *not* `--gs-elevated`,
 * which this rule carried from the day it was written. `--gs-elevated` is a
 * risk band — the orange a HIGH finding prints in — so every text field, select
 * and textarea on the portal rendered as a solid orange block with its
 * placeholder unreadable inside it. Nothing failed: the tokens test only asks
 * that the stylesheet reads tokens rather than repeating hexes, and this read
 * one. It read the wrong one, which is a question only a rendered page answers.
 */
.input {
  background: var(--slate-850); color: var(--slate-200);
  border: 1px solid var(--slate-800); border-radius: var(--radius-sm);
  padding: 8px 10px; font: inherit; font-size: 13px; width: 100%;
}
.input:focus { outline: none; border-color: var(--sky-400); }
.input::placeholder { color: var(--slate-500); }
textarea.input { resize: vertical; min-height: 64px; }
.stack-form { margin: 12px 0 0; }
/* Several controls on one line — "screen now" beside "end the relationship",
 * a note field beside its button. `.inline-form` is `display: inline` for a
 * single control, which cannot lay out a row. */
.inline-form + .inline-form { margin-left: 8px; }
div.inline-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 12px 0 0; }
form.inline-form { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
form.inline-form .input { width: auto; min-width: 220px; flex: 1 1 220px; }
/* An ended relationship, a lapsed suppression, a reviewed alert: still on the
 * page because the record is the product, but not competing with live rows. */
.row-muted { opacity: .55; }
.bullets { margin: 8px 0 0; padding-left: 18px; color: var(--slate-400); font-size: 12.5px; }
.bullets li { margin: 2px 0; }
.crumbs { font-size: 12px; color: var(--slate-500); margin: 0 0 8px; }

/* ------------------------------------------------------------------ hero */

/*
 * The block rhythm, and the one scale it is allowed to use.
 *
 * Measured before it was touched: the entity page put 15 distinct gaps between
 * adjacent siblings into the 0–26px range — 10, 11, 12, 13, 14, 16, 18 and 19
 * among them — and /welcome added 20 and 28. The worst instance was this hero
 * on /ask: 10px from the H1 to the lede, 20px to the search field, 13px to the
 * paragraph under it and 14px to the banner. Four different separations inside
 * 200 vertical pixels, none of them meaning anything different from its
 * neighbour, and none of them chosen — 13 is a `<p>`'s own 1em at 13px type and
 * 14 is `.banner`'s margin collapsing over it.
 *
 * Every block margin in this file that lands in that range is now one of
 * 4 / 8 / 12 / 16 / 24. Separations wider than 26px — the hero's own 44px of
 * top padding, the footer's 42px rule, the landing page's 36px and 60px — are
 * deliberate section breaks and are left where they are; the complaint was
 * never that the page has large gaps, it is that it has nine small ones.
 */
.hero { padding: 44px 0 24px; }
.hero h1 { font-size: 30px; margin: 0 0 8px; letter-spacing: -.02em; line-height: 1.2; }
.lede { color: var(--slate-400); max-width: 72ch; margin: 0; font-size: 14.5px; }
/*
 * And the paragraph *under* the lede gets the lede's measure.
 *
 * `.lede` is capped at 72ch = 563px at 1440. The paragraph 12px below it had no
 * cap at all and ran the full 1392px of the wrap — `p.muted` on /ask and /,
 * `ul.legend` on /console — so two pieces of body copy 47px apart were set to
 * line lengths 2.5x apart. A measure is a property of prose, not of one class.
 *
 * The child combinator, and `.banner` exempted below: a notice is a component
 * with a border and a tint, it appears full-width in `.wrap` on every other
 * page, and 563px of it under a 1392px panel reads as a broken box rather than
 * as a column. The rule is about paragraphs; the banner is not one.
 */
.hero > p, .hero > ul { max-width: 72ch; margin: 12px 0 0; }
.hero > p.lede { margin: 0; }
.hero > .banner { max-width: none; margin: 16px 0 0; }

/* ---------------------------------------------------------------- panels */

.panel {
  background: var(--slate-900);
  border: 1px solid var(--slate-800);
  border-radius: var(--radius);
  padding: 18px 20px;
  margin: 16px 0;
}
/* The login panel: narrow and centred. Was an inline style attribute on the
   element, which the portal's own style-src 'self' CSP (no 'unsafe-inline')
   blocks, so the login form rendered full-bleed and uncentred in any enforcing
   browser. */
.panel-narrow {
  max-width: 26rem;
  margin: 3rem auto;
}
.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 12px;
}
.panel-head h2 { font-size: 15px; margin: 0; letter-spacing: -.005em; font-weight: 600; }
.panel-head p { margin: 0; }

/*
 * The prose inside a section, on the same scale as everything else.
 *
 * These are the blocks that carry no layout class, so they kept the user
 * agent's own `1em` margin — which makes a paragraph's separation a function of
 * the type it happens to be set in and nothing else. Measured on the live
 * pages: `p.muted` at 13px opened 13px under a panel head, under a table and
 * between two of itself; `p.money-note` at 11px opened 11px under the money
 * table; the dossier's classless `<h3>` opened 17.5 and its `<h2>` 18.7. Four
 * separations expressing one relationship, and the reason the entity page had
 * 13px, 11px and 18px gaps that no rule in this file asked for.
 */
p.muted, p.money-note, p.no-print { margin: 12px 0 0; }
.panel > h3 { margin: 16px 0 8px; }
.dossier-frame > h2 { margin: 0 0 8px; }

/*
 * A section whose whole body is its empty state.
 *
 * The sentence stays. "No ownership edge reaches this subject in the loaded
 * graph" is a finding — absence of a record is not absence of a check, and this
 * interface is deliberate about the difference everywhere else. What it does
 * not need is 22px of padding, a 17px heading and a paragraph explaining how to
 * read a table that is not on the page: on the entity screenshot, "Beneficial
 * ownership chain" and "Officers" each spent a full card to say nothing was
 * there, between two sections that had something to say.
 *
 * `:has()` rather than a modifier class on six sections in two templates: the
 * condition really is "this panel's body is the empty state", which is what the
 * selector says and what no class can keep true on its own. An engine without
 * `:has()` ignores the block and renders exactly what it rendered before.
 */
.panel:has(> p.empty) { padding: 12px 18px; margin: 12px 0; }
.panel:has(> p.empty) .panel-head { margin-bottom: 0; }
.panel:has(> p.empty) .panel-head h2 { font-size: 14px; }
/* The explanatory prose in the head describes the table. There is no table. */
.panel:has(> p.empty) .panel-head p { display: none; }
.panel:has(> p.empty) > p.empty { padding: 4px 0 0; }

/* ---------------------------------------------------------------- tables */

/* A wide table scrolls inside its own box. Without this the officers table —
   seven columns, one of them a source URL — pushes the whole document sideways
   and the sticky header scrolls off with it. */
.table-scroll { overflow-x: auto; }

/*
 * And it has to *look* scrollable.
 *
 * On a 390px phone the subjects table showed one column and a clean vertical
 * edge; there was nothing to say six more columns were to the right of it, and
 * an overlay scrollbar only appears once you are already scrolling. Two
 * shadows, revealed by the scroll position itself: the `local` pair are
 * gradients anchored to the *content*, so they slide out of view at each end,
 * and the `scroll` pair are opaque covers anchored to the *box* that hide them
 * there. No JavaScript, no scroll listener, no inline style — the effect is
 * entirely a consequence of where the content is.
 */
.table-scroll {
  background:
    linear-gradient(to right, var(--slate-900) 30%, transparent) left / 28px 100% no-repeat local,
    linear-gradient(to left, var(--slate-900) 30%, transparent) right / 28px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--shadow), transparent) left / 12px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--shadow), transparent) right / 12px 100% no-repeat scroll;
}
/* The container is also a place the keyboard stops — it carries `tabindex="0"`,
   `role="region"` and an `aria-labelledby` naming its panel heading, because a
   scroller no key can reach is a set of columns no keyboard user can read, and
   these hide 309–631px of themselves at 390. No rule is needed here for the
   ring: the one `:focus-visible` above draws it at 15–375 on a 390px screen,
   measured, so the scroll box's own edges are what lights up. */
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th {
  text-align: left; font-size: 10.5px; text-transform: uppercase;
  letter-spacing: .09em; color: var(--slate-500);
  padding: 7px 12px; border-bottom: 1px solid var(--slate-800); font-weight: 600;
  white-space: nowrap;
}
.table td { padding: 10px 12px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
/*
 * The first column starts where the section title starts.
 *
 * Measured at 1440 on every page that has a table: the `.panel-head h2` text
 * begins at x=45 — 24 of wrap, 1 of border, 20 of panel padding — and the `th`
 * and `td` text of the column under it began at x=57, because the cells carry
 * `padding: 7px 12px`. A heading and the column it names, 12px out of step, on
 * every table in the portal.
 *
 * The pull is on the scroll box rather than a `padding-left: 0` on the first
 * cell, which was the layout audit's proposal and which was measured too: it
 * moves the text to 45 but leaves the row's hover fill and its bottom rule
 * starting at 45 as well, so the first character of every row sits flush on the
 * edge of its own highlight — and on `.row-ubo`, which is a tinted band rather
 * than a 4.5% wash, that reads as a clipped cell. Pulling the table 12px into
 * the panel's padding instead moves the *text* to 45 and leaves the band 12px
 * wider on each side, which is what a table set inside a card should look like.
 * 8px of the panel's 20px padding is still between the band and the border.
 */
.panel > .table-scroll { margin-inline: -12px; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--hover); }
.row-ubo { background: var(--gs-elevated-tint); }

/*
 * Column weights, measured rather than declared and left alone. On the entity
 * page at 1440px the "Derived from" column rendered 391px wide — the *widest*
 * of the four, wider than either column carrying the finding it explains.
 *
 * The provenance stays. Being able to say `node.high_risk_proximity, set by
 * RiskEngine.flag_sanction_proximity()` beside a HIT is most of what separates
 * this from a dashboard, and an auditor asking "where does that come from"
 * gets an answer without leaving the page. What it should not be is the
 * heaviest thing in the row: it is the footnote, not the finding.
 */
.table-matrix td { width: 30%; }
.table-matrix .cell-label { width: 15%; font-weight: 600; }
.cell-basis { width: 25%; font-size: 11.5px; line-height: 1.45; }
.cell-state {
  display: inline-block; font-size: 10px; font-weight: 700;
  letter-spacing: .09em; padding: 2px 7px; border-radius: 5px; margin-bottom: 4px;
  background: var(--slate-850); color: var(--slate-400);
}
.cell-detail { display: block; color: var(--slate-200); }
.cell-links { display: block; margin-top: 4px; font-size: 12.5px; }
.cell-clear .cell-detail { color: var(--slate-500); }
.cell-hit { background: var(--hover); }
.cell-hit.sev-critical .cell-state { background: var(--gs-critical-tint); color: var(--rose); }
.cell-hit.sev-high .cell-state { background: var(--gs-elevated-tint); color: var(--orange); }
.cell-hit.sev-medium .cell-state { background: var(--gs-warn-tint); color: var(--yellow); }
.cell-hit.sev-low .cell-state { background: var(--gs-action-tint); color: var(--sky-400); }

/* ------------------------------------------------------------------ tags */

.tag {
  display: inline-block; font-size: 10.5px; font-weight: 600;
  padding: 2px 8px; border-radius: 5px; margin-right: 5px;
  border: 1px solid transparent; white-space: nowrap; letter-spacing: .01em;
}
.tag-critical { background: var(--gs-critical-tint); color: var(--rose); }
.tag-high { background: var(--gs-elevated-tint); color: var(--orange); }
.tag-medium { background: var(--gs-warn-tint); color: var(--yellow); }
.tag-low { background: var(--gs-action-tint); color: var(--sky-400); }
.tag-ok { background: var(--gs-secure-tint); color: var(--emerald); }
.tag-neutral { background: var(--slate-850); color: var(--slate-400); border-color: var(--slate-800); }

/* --------------------------------------------------------- subject header */

/*
 * The subject's header is a stage carrying three things: who this is, what the
 * verdict is, and how to take the document away.
 *
 * A grid and not `flex … space-between`. The flex version put the name and the
 * export block at opposite ends of a row and let the browser decide what
 * happened in between; with a third region there is nothing sensible for it to
 * decide, and a Georgian company name is long enough that "whatever is left"
 * is not a column width. `minmax(0, …)` on the first track for the same reason
 * the landing hero needs it — a grid item's default minimum is its content's.
 */
.subject-head {
  position: relative;
  isolation: isolate;
  overflow: clip;
  margin-top: 20px;
  padding: clamp(22px, 2.4vw, 34px) clamp(20px, 2.2vw, 32px);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 28px;
  align-items: start;
}
/* Light from the verdict's own corner. It is the same ambient device the
   landing and console stages use, placed where the band block sits so the two
   read as one gesture rather than as a panel with a coloured box in it. */
.subject-head::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -70% -10% auto 30%;
  height: 200%;
  background: radial-gradient(34% 40% at 74% 26%, var(--glow-action), transparent 70%);
  filter: blur(10px);
  opacity: .8;
}
.subject-head-who { min-width: 0; }
.subject-head h1 {
  font-size: clamp(24px, 2.4vw, 34px);
  margin: 6px 0 12px;
  letter-spacing: -.028em;
  line-height: 1.14;
}
.crumbs { font-size: 11.5px; color: var(--slate-500); display: flex; gap: 8px; }
.tagrow { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/*
 * The verdict, at the size of a verdict.
 *
 * `band-*` supplies `color` and `background`; everything here is geometry, so
 * a band added to `ui/styling.py` needs no rule of its own. The border is
 * `currentColor` for the same reason — it is the band's colour by definition
 * rather than by a fifth declaration somebody has to remember to change.
 */
.subject-head-verdict {
  display: flex; flex-direction: column; gap: 3px;
  padding: 14px 20px 13px;
  border: 1px solid currentColor;
  border-radius: var(--radius);
  /* 215, not 190. The basis line is "Confidence 0.00 · source coverage 0 of 6"
     and at 190 it broke between "source" and "coverage" — a two-word term
     split across lines, in the sentence whose job is to qualify the word above
     it. Measured in all three languages at the widths this block renders. */
  min-width: 215px;
  /*
   * And a ceiling, which is the other half of removing the basis's own cap.
   *
   * This block sits in an `auto` grid track beside a `minmax(0, 1fr)` one, and
   * an `auto` track takes its item's *max-content* width. With nothing
   * limiting the basis, that became the whole line — measured at 674px of a
   * 736px row on the console at 768px — and `minmax(0, 1fr)` obediently
   * collapsed the other track to **zero**. The subject's name was not
   * truncated at that width, it was gone, at every viewport from 721 to 1080.
   *
   * 340 because the block measures 247 in English, 282 in Russian and 309 in
   * Georgian at 1440: the cap is above all three, so nothing wraps where it
   * used to fit, and below it the basis takes the second line it must.
   */
  max-width: 340px;
}
.verdict-label {
  font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase;
  opacity: .85;
}
.verdict-word {
  font-size: clamp(22px, 2vw, 30px);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
}
/* `color: inherit` with an opacity, not `--slate-500`: this text sits on the
   band's own tint, and the grey ramp is measured against the page surface. On
   the crimson tint it lands at 2.6:1.

   No `max-width`. It had `26ch`, and `ch` is the width of the *Latin* zero —
   the measure this stylesheet has been caught by twice already. Worse here
   than usual: the cap was narrower than the block's own `min-width`, so
   raising the block did nothing and the line broke between "source" and
   "coverage" in English and between «охват» and «источников» in Russian —
   a two-word term split across lines inside the sentence whose only job is to
   qualify the word above it. The block is `min-width` plus shrink-to-fit, so
   letting the basis size it is what keeps the phrase together in all three
   languages at once. */
.verdict-basis { font-size: 11.5px; line-height: 1.4; opacity: .82; }
/* The caveat keeps the band's colour rather than the link accent. A crimson
   verdict with a blue sentence under it reads as two unrelated statements, and
   this one is a property *of* the verdict. */
.verdict-caveat {
  margin-top: 5px; font-size: 12px; font-weight: 600;
  color: inherit; text-decoration: underline; text-underline-offset: 3px;
}

.dossier-form { max-width: 300px; text-align: right; }
.dossier-form .muted { margin: 8px 0 0; text-align: left; font-size: 11.5px; line-height: 1.5; }
.dossier-form code { font-family: var(--mono); font-size: 11.5px; color: var(--slate-400); }

/*
 * One breakpoint, and it is where three columns stop fitting rather than a
 * device width. Measured with the Georgian labels: below 1080 the export
 * block's 300px and the verdict's 190px leave the name under 400px, which is
 * two words of a Georgian company name per line.
 */
@media (max-width: 1080px) {
  .subject-head { grid-template-columns: minmax(0, 1fr); }
  .subject-head-verdict { flex-direction: row; align-items: baseline; flex-wrap: wrap; gap: 10px 14px; }
  .verdict-basis { max-width: none; }
  .dossier-form { max-width: none; text-align: left; }
}

/* ---------------------------------------------------------- section rail */

/*
 * The dossier is twelve sections and about 4,800 pixels tall. An analyst
 * checking one thing — is there a PEP, what did the engine find — scrolled
 * past everything else to reach it and scrolled past it again to get back, and
 * on the way lost track of what was above. A rail of the section names, stuck
 * under the header, is the whole fix.
 *
 * Anchors, not tabs: every section stays on the page and stays printable, the
 * browser's own back button undoes a jump, and it works with JavaScript off —
 * which matters here, because a compliance workstation that blocks scripts
 * still has to be able to read a dossier.
 *
 * Sticky only where the header's height is known. Below 861px the header wraps
 * to whatever the content needs, and a rail pinned to a stale offset would
 * either float in the middle of the page or hide the first line of every
 * section it scrolled to.
 */
.section-rail {
  display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none;
  margin: 16px 0 0; padding: 4px;
  border: 1px solid var(--slate-800); border-radius: var(--radius);
  /* Eleven sections do not fit on any screen, so the rail scrolls — and says
     so with the same content-anchored shadows the tables use, because a row of
     chips ending flush at a border reads as the last chip. */
  background:
    linear-gradient(to right, var(--slate-900) 40%, transparent) left / 26px 100% no-repeat local,
    linear-gradient(to left, var(--slate-900) 40%, transparent) right / 26px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--shadow), transparent) left / 10px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--shadow), transparent) right / 10px 100% no-repeat scroll,
    var(--slate-900);
}
.section-rail::-webkit-scrollbar { display: none; }
.section-rail a {
  flex: 0 0 auto;
  padding: 6px 12px; border-radius: var(--radius-sm);
  font-size: 12.5px; font-weight: 500; color: var(--slate-400);
  text-decoration: none; white-space: nowrap;
  transition: background-color .12s ease, color .12s ease;
}
.section-rail a:hover { background: var(--hover); color: var(--slate-200); }
/*
 * Where the reader is, set by `portal.js`'s IntersectionObserver.
 *
 * Distinct from `.panel:target` below, which marks where they *jumped* — the
 * target survives until the address changes, so after scrolling on from a jump
 * the rail kept pointing at a section three thousand pixels above the screen.
 * With scripting off the class never appears and the rail is exactly what it
 * was, which is why this is an addition rather than a replacement.
 */
.section-rail a.is-current {
  background: var(--gs-action-tint);
  color: var(--sky-400);
  font-weight: 600;
}

@media (min-width: 1141px) {
  .section-rail {
    position: sticky; top: var(--header-height); z-index: 15;
    background: var(--header-bg); backdrop-filter: blur(10px);
    /*
     * And it wraps rather than scrolls, once there is room for two rows.
     *
     * Measured at 1440 on the entity page: clientWidth 1390 against a
     * scrollWidth of 2616 — the rail showed 6 of its 11 chips and hid the other
     * five inside a 43px bar that reads, correctly, as a complete table of
     * contents. A reader who cannot see that five sections exist does not scroll
     * a strip sideways to look for them; the whole point of the rail is that the
     * dossier's shape is visible without scrolling anything.
     *
     * Only here, because this is where it is sticky and where the sideways
     * gradients are already replaced by a flat background. Below 1140 the rail
     * keeps scrolling: two rows of chips on a phone is a navigation block taller
     * than the first section it points at.
     */
    flex-wrap: wrap;
  }
  /* Air under the rail, or the heading it jumped to sits against the rail's own
     bottom border. Measured with the eleven Georgian chips now that they all
     show: 112px and three rows at 1141, 77px and two from 1391 up, where the
     `.wrap` cap at 1600 stops it changing again — it was 43px and one row, of
     which five chips were off-screen. 131 is the *tallest* of those plus the
     19px of air the old 62 left over its own 43: one constant, and the one that
     cannot hide a heading. The alternative is a second breakpoint at 1391,
     which is not a width — it is where eleven translated labels happen to stop
     needing a third row, and the next translation moves it.

     131 and not `header + 131` any more: `html`'s `scroll-padding-top` already
     contributes the header, and the scrollport's padding and the target's
     margin add. Same 239px of total clearance as before, measured — the header
     term simply moved to the one place that states it once. */
  .panel[id] { scroll-margin-top: 131px; }
}
@media (max-width: 1140px) {
  /* Below 1141 the rail is not sticky, so there are no chips to clear — only
     air between the header and the heading. This was the whole offset, and it
     assumed a viewport top the sticky header does not leave free: every one of
     the eleven headings landed 226px behind the header at 390. It is now the
     air on top of `scroll-padding-top`, which is what 12px always read as. */
  .panel[id] { scroll-margin-top: 12px; }
}

/* Which section you jumped to, marked for as long as the address says so.
   `:target` rather than a class: the browser already knows, and no script has
   to be kept in step with the scroll position. */
.panel:target { border-color: var(--sky-400); }

/* ----------------------------------------------------------------- gauge */

.risk-grid { display: grid; grid-template-columns: 360px 1fr; gap: 32px; align-items: start; }

/* The band, its scale and its meaning. Replaces the radial dial on screen —
   about 110px tall where the dial was 350, which is what puts the evidence
   beside the band instead of below the fold. */
.band-meter { min-width: 0; }
/* 420px, and a label sized in the meter's own units. The shared `.meter-label`
   is 9px inside a 520-unit viewBox, which is right on paper and 7px on screen
   at 340px wide — measured, and unreadable. Scoped rather than raised globally:
   the dossier prints the same SVG at a different size. */
.band-meter .meter { display: block; width: 100%; max-width: 420px; height: auto; margin: 0 0 8px; }
.band-meter .meter-label { font-size: 14px; letter-spacing: .04em; }
.band-meaning { margin: 0; max-width: 46ch; color: var(--slate-400); }
.gauge { max-width: 100%; height: auto; }
.gauge-tick { font-size: 10px; font-weight: 700; letter-spacing: .06em;
              font-family: var(--mono); }
.band-line { font-size: 23px; font-weight: 700; letter-spacing: .02em; margin: 0 0 4px; }
.band-low { color: var(--emerald); }
.band-medium { color: var(--yellow); }
.band-high { color: var(--orange); }
.band-critical { color: var(--rose); }

.drivers-cell h3 { font-size: 10.5px; text-transform: uppercase; letter-spacing: .1em;
                   color: var(--slate-500); margin: 0 0 8px; font-weight: 600; }
.drivers { margin: 0 0 16px; padding-left: 20px; }
.drivers li { margin-bottom: 4px; }

/* 16px, with `.cap-grid` and `.qa-list`. Measured across the three pages that
   carry a card grid: `.metric-grid` 0, `.qa-list` 10, `.metrics` 12,
   `.cap-grid` 16, `.risk-grid` 32 — five gaps for one idea. The 0 is the
   landing page's segmented panel, where the cards share dividers instead of
   air, and it stays; the rest are the same card on three pages and are 16. */
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
/* Label above figure, not below it. The figure is the answer and the label is
   the question; a reader scanning three tiles reads the questions first. */
.metric {
  background: var(--slate-850); border: 1px solid var(--slate-800);
  border-radius: var(--radius-sm); padding: 11px 13px;
  display: flex; flex-direction: column; gap: 4px;
}
.metric-label { font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
                color: var(--slate-500); font-weight: 600; }
.metric-value { font-size: 20px; font-weight: 600; font-family: var(--mono);
                letter-spacing: -.02em; }

/* ----------------------------------------------------------------- graph */

.graph-controls { display: flex; gap: 8px; }
/*
 * The map gets the whole column; the inspector sits under it.
 *
 * It was `1fr 300px`, which left the canvas 486px wide inside the console's
 * right column. The server lays a 3-hop network out at 1264x266 user units, so
 * `preserveAspectRatio="…meet"` scaled it to 0.385 to fit that width — node
 * labels reached the screen at 5px, and 81% of the pane's height was empty
 * because a 4.75:1 drawing was being fitted into a 0.87:1 box. Measured, not
 * inferred: the first attempt at this was a rewrite of `fit()`, and the probe
 * showed the transform had almost nothing to do with it.
 *
 * At the 1600 wrap the canvas is now ~1000px wide, which is 0.8 scale and a
 * legible label. The inspector loses nothing by being a strip: it holds one
 * record at a time and was mostly empty space.
 */
.graph-wrap { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 16px; }
.graph-canvas {
  border: 1px solid var(--slate-800); border-radius: var(--radius);
  background:
    radial-gradient(circle at 1px 1px, var(--slate-800) 1px, transparent 0) 0 0/26px 26px,
    var(--slate-950);
  overflow: hidden; cursor: grab; position: relative;
}
.graph-canvas.is-panning { cursor: grabbing; }
/*
 * The pane has no height of its own; it takes the drawing's shape.
 *
 * Two fixed heights lived here — 520px, and 300px for the "flat" class
 * `graph.js` sets when the drawn content is wider than 2.5:1. Both were the
 * *only* thing setting the scale, because the `viewBox` is fixed by
 * `ui/graph_layout.py` and `preserveAspectRatio="xMidYMid meet"` scales by
 * `min(boxW/vbW, boxH/vbH)`. A constant in one of those two terms is a
 * constant scale, and the box it was measured against is not the box it gets:
 *
 *   /entity at 1440 — a 952x358 viewBox in a 1348x298 box. Height bound the
 *   fit at 0.8324, so the drawing spanned 713px of 1348 and left 318px blank
 *   on each side — 47.2% of the pane. The same graph in the console's 812px
 *   column drew the identical 183px node box: 536px of extra width bought
 *   nothing, because width was never what the scale was reading.
 *
 *   Either page at 390 — the same 298px against a 314px-wide box. Now *width*
 *   bound the fit, at 0.3298, and the drawing was 83px tall inside 298px of
 *   SVG: 72.2% of the pane's height was blank, and `.graph-wrap` spent 400px
 *   of a phone screen on it.
 *
 * `height: auto` on an outermost `<svg>` that carries a `viewBox` and no
 * width/height attributes resolves against the viewBox's own aspect ratio, so
 * the box the fit is measured against *is* the drawing's shape and the two
 * terms of that `min()` agree by construction. Nothing here names 952, 358 or
 * any other graph's numbers, and no rule needs to know which shape arrived.
 *
 * `max-height` is the one constant left, and it is a ceiling rather than a
 * height: a network taller than it is wide would otherwise make a 1348px-wide
 * pane 1348px tall. Past it the fit goes back to being height-bound with blank
 * at the sides, which for a tall drawing in a wide column is the truth.
 *
 * `graph.js` still toggles `.canvas-flat`. It no longer needs a rule — a strip
 * gets a strip-shaped pane because it is a strip, not because a class said so
 * — and it is left alone rather than reached into from here.
 */
.graph-canvas svg {
  width: 100%; height: auto;
  /* The floor is the drag, not the drawing. `graph_layout.py` lays a chain of
     hops one node deep out at 9:1 — 35px of pane on a 390px screen if the
     shape is taken literally, and a 12.6:1 probe measured 25px. Both are under
     the 44px a finger can catch, and panning is the only way a network wider
     than the screen is read at all. */
  min-height: 120px; max-height: 520px;
  display: block; touch-action: none;
}
/* A pane with no drawing in it still holds the drawing's place.
 *
 * The height comes from the `<svg>`, and until the fetch resolves there is no
 * `<svg>`: the console's container holds the "Loading the relationship map…"
 * sentence and the entity page's is empty. Measured with the fetch stalled,
 * `.graph-canvas` came back 2px tall on /entity, so the whole page below the
 * map jumped 507px when it landed. Keyed on the absence of the drawing rather
 * than on the placeholder, because those are two different markups for the
 * same state — and the failure note `graph.js` writes instead is a third. */
.graph-canvas:not(:has(> svg)) { min-height: 300px; }
.graph-inspector {
  border: 1px solid var(--slate-800); border-radius: var(--radius);
  background: var(--slate-850); padding: 15px; font-size: 13px;
  overflow-y: auto; max-height: 520px;
}
.graph-inspector h3 { margin: 0 0 4px; font-size: 14px; }
.graph-inspector dl { margin: 12px 0 0; }
.graph-inspector dt { font-size: 10px; text-transform: uppercase;
                      letter-spacing: .1em; color: var(--slate-500); margin-top: 8px;
                      font-weight: 600; }
.graph-inspector dd { margin: 4px 0 0; word-break: break-word; }

.gnode { cursor: pointer; }
.gnode-box { fill: var(--slate-850); stroke: var(--slate-700); stroke-width: 1.25; }
/* A person is a different kind of node, and the difference has to survive both
   schemes — so it is the raised surface and a muted rim, not a hand-mixed navy
   that turns into a bruise on a white background. */
.gnode-person .gnode-box { fill: var(--surface-raised); stroke: var(--slate-500); }
.gnode-flagged .gnode-box { stroke: var(--orange); stroke-width: 2; }
.gnode-subject .gnode-box { fill: var(--gs-action-tint); stroke: var(--sky-400); stroke-width: 2.25; }
.gnode.is-selected .gnode-box { stroke: var(--slate-200); stroke-width: 2.75; }
.gnode-label { fill: var(--slate-200); font-size: 13px; font-weight: 600; }
.gnode-sub { fill: var(--slate-500); font-size: 10.5px; font-family: var(--mono); }
.gnode-flag { fill: var(--orange); font-size: 9.5px; font-weight: 700; letter-spacing: .05em; }

.gedge { fill: none; stroke: var(--slate-700); stroke-width: 1.4; stroke-dasharray: 5 4; }
.gedge-own { stroke: var(--sky-500); stroke-width: 2; stroke-dasharray: none; }
.gedge.is-selected { stroke: var(--slate-200); stroke-width: 3; }
.gedge-hit { stroke: transparent; stroke-width: 14; fill: none; cursor: pointer; }
.gedge-label { fill: var(--slate-500); font-size: 10px; font-family: var(--mono); }
/* The caption's backing plate and the three arrowheads. `graph.js` used to
   write these as hex attributes; they are the page's own surfaces and accents,
   so they belong to the theme and follow it into the light scheme. */
.gedge-label-bg { fill: var(--slate-950); }
.arrowhead-plain { fill: var(--slate-700); }
.arrowhead-own { fill: var(--sky-500); }
.arrowhead-selected { fill: var(--slate-200); }

.legend { display: flex; flex-wrap: wrap; gap: 16px; list-style: none;
          padding: 0; margin: 16px 0 0; font-size: 12px; color: var(--slate-400); }
.legend li { display: flex; align-items: center; gap: 7px; }
.swatch { width: 12px; height: 12px; border-radius: 3px; border: 1.5px solid; }
.swatch-subject { background: var(--gs-action-tint); border-color: var(--sky-400); }
.swatch-org { background: var(--slate-850); border-color: var(--slate-700); }
.swatch-person { background: var(--surface-raised); border-color: var(--slate-500); }
.swatch-flag { background: var(--slate-850); border-color: var(--orange); }
.line { width: 22px; height: 0; border-top: 2px solid var(--sky-500); }
.line-other { border-top: 1.5px dashed var(--slate-700); }

/* -------------------------------------------------------------- findings */

.finding {
  border: 1px solid var(--slate-800); border-left-width: 3px;
  border-radius: var(--radius-sm); padding: 13px 16px; margin-bottom: 12px;
  background: var(--slate-850);
}
.finding.sev-critical { border-left-color: var(--rose); }
.finding.sev-high { border-left-color: var(--orange); }
.finding.sev-medium { border-left-color: var(--yellow); }
.finding.sev-low { border-left-color: var(--sky-400); }
.finding header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.finding p { margin: 0 0 8px; }
.evidence { margin: 0; padding-left: 20px; font-size: 12.5px; color: var(--slate-400); }
.evidence li { margin-bottom: 8px; }
.evidence-nodes { display: block; margin-top: 4px; }

.conf { display: inline-flex; align-items: center; gap: 7px; }
/* The confidence bar's two rects. Attributes in the template until now, which
   put a `#1e293b` track on a white page in the light scheme. */
.conf-track { fill: var(--surface-raised); }
.conf-fill { fill: var(--sky-400); }

.route { font-family: var(--mono); font-size: 11.5px; color: var(--slate-400);
         max-width: 460px; word-break: break-word; }

/* -------------------------------------------------------------- identity */

.identity { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
            gap: 16px; margin: 0 0 16px; }
.identity dt { font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
               color: var(--slate-500); font-weight: 600; }
.identity dd { margin: 4px 0 0; word-break: break-word; }

/* ---------------------------------------------------------------- footer */

.site-footer {
  border-top: 1px solid var(--slate-800); margin-top: 42px;
  padding: 24px 0 40px; color: var(--slate-500); font-size: 12px;
}
.site-footer p { margin: 0 0 8px; max-width: 96ch; }
.site-footer code { font-family: var(--mono); font-size: 11px; }
.footer-warn { color: var(--orange); }

/* ------------------------------------------------------------ responsive */

@media (max-width: 1080px) {
  .risk-grid { grid-template-columns: 1fr; }
  .graph-wrap { grid-template-columns: 1fr; }
  .graph-inspector { max-height: none; }
}

/*
 * The header on a narrow screen.
 *
 * Measured before it was touched: 315px tall at 390px wide — a third of a phone
 * screen, above the fold, on every page, spent on a brand block that had
 * stopped fitting on one line and three navigation buttons each on a line of
 * its own. The order of the fix is the order of what the reader needs: search
 * first and full width, then the destinations as one compact row, then the
 * state strip. The subtitle under the product name is the first thing to go —
 * it is the least load-bearing text in the interface.
 */
@media (max-width: 1140px) {
  /* `padding-block` for the same reason as the desktop rule above: `.wrap`'s
     narrow `padding: 0 16px` is written *below* this one, so a shorthand here
     lost the whole declaration and the band had no vertical padding at all. */
  .header-main { flex-wrap: wrap; gap: 10px 12px; padding-block: 10px; }
  .brand { order: 1; }
  .searchbar { order: 3; flex: 1 0 100%; }
  /* The `order` moved from the buttons to the group that now holds them: they
     are no longer children of `.header-main`, so an `order` on them would be a
     declaration on a non-flex-item and the destinations would have dropped
     below the full-width search field. */
  .header-nav { order: 2; }
  /* The track comes off below 1141. Up here it groups three controls that sit
     in a row with a search field and a brand; down there the destinations are
     already alone on their own line, so the pane groups them against nothing
     and its 8px of inset is the difference between the header taking a row and
     not. Same rule as everywhere else in this stylesheet: decoration that costs
     a measured constant is not decoration, it is a regression. */
  .header-nav { padding: 0; border-width: 0; background: none; }
  .btn-ghost { padding: 6px 10px; font-size: 12.5px; }
  .header-context { gap: 10px; padding-block: 6px; }
  .header-context .status-pill { margin-left: 0; }
  .brand-text small { display: none; }
  .wrap { padding-inline: 16px; }
  .hero { padding: 26px 0 18px; }
  .hero h1 { font-size: 24px; }
  .subject-head h1 { font-size: 22px; }
}
/*
 * A target you can actually hit with a thumb.
 *
 * Measured at 390px, on every page, in the chrome that every page carries:
 * `.lang-switch a` 19px tall, `.header-main .btn` 31px, `.section-rail a` 31px.
 * Between 13 and 25px short of the 44px that both WCAG 2.5.5 and the platform
 * guidelines put the floor at, on the controls a reader uses most and the ones
 * that sit closest together — the two language links are 10px apart, so a miss
 * on one is a hit on the other and the page reloads in the wrong language.
 *
 * `min-height` and not `height`: `.btn-lg` on the landing page carries a whole
 * Georgian sentence and is allowed to be two lines tall. `inline-flex` with
 * `align-items: center` because a min-height on an inline-block only moves the
 * box's bottom edge and leaves the label sitting at the top of it.
 *
 * Under 768 only. A 44px `.btn` in the desktop header would make the search
 * row 12px taller than the field it sits beside, to solve a problem a mouse
 * does not have.
 */
@media (max-width: 767px) {
  .btn, .lang-switch a, .section-rail a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* The appearance segments are the smallest controls in the portal — 26x22 by
     construction, because the width has to be language-independent — and they
     sit 2px apart, which is the miss-and-hit-the-neighbour geometry this rule
     was written for. Widened rather than only heightened: a 26px-wide 44px-tall
     target is still half a thumb.
     `display: grid` is kept; the rule above switches `.btn` to inline-flex to
     re-centre a label, and these have no label to re-centre. */
  .theme-btn { min-width: 44px; min-height: 44px; }
  /* And the track comes off here too, for the same reason it comes off the nav
     group below 1141: a 44px target inside 2px of padding and a 1px border is a
     50px control on a strip whose row height is 44. `border-width` and not
     `border-color: transparent`, which was the first attempt — a transparent
     border is still a border, and the control measured 46px with it. */
  .theme-switch { padding: 0; border-width: 0; background: none; }
  /*
   * Three segments become one button.
   *
   * Three 44px targets are 132px wide, and that is the difference between the
   * context strip fitting on one line and taking two. Measured: at 740 in
   * Georgian the strip needs 718px of a 708px row with the switch present and
   * 576 without it, and at 320 the language links and the switch come to 304 in
   * a 288px row. One extra row is 44px of header on the narrowest screen this
   * portal supports, in the two languages whose words are longest — a 389px
   * header on a 320px phone, which is not a header, it is the page.
   *
   * So the current appearance is the only one drawn, and pressing it advances
   * to the next. `portal.js` decides which behaviour to run by counting how
   * many segments the browser is actually drawing, so this breakpoint stays
   * written down once, here, and the script never has to agree with a number it
   * cannot see.
   */
  .theme-btn { display: none; }
  .theme-btn[data-current] { display: grid; }
}

@media (max-width: 720px) {
  .metrics { grid-template-columns: 1fr; }
  .subject-head { flex-direction: column; }
  .dossier-form { text-align: left; max-width: none; }
  /* A table is the one thing that cannot reflow, so on a phone it gets the full
     width of the screen to scroll in rather than the panel's inner width. The
     inner padding is 8 rather than 20 so the first column's text still lands on
     the heading's left edge: -20 of margin plus 8 of padding plus the cell's own
     12 puts it back at the panel's content edge, which is the same 12px pull the
     desktop rule makes. */
  .panel > .table-scroll { margin: 0 -20px; padding: 0 8px; }
  /* "Fit" and "Reset layout" come to 355px in Georgian and the panel's inner
     width at 390 is 318, so the pair pushed the whole document 2px sideways —
     which is a horizontal scrollbar on every page of the phone. */
  .graph-controls { flex-wrap: wrap; }
  .graph-controls .btn { padding: 6px 10px; font-size: 12.5px; }
  /* `.btn` is `white-space: nowrap` so a label never breaks mid-phrase in a
     toolbar. The two large call-to-action buttons carry whole Georgian
     sentences — "მოითხოვეთ პორტალის დანერგვა / უსაფრთხო სადემონსტრაციო წვდომა"
     is 600px of unbreakable text — and on a 360px screen that one button was
     dragging the entire landing page sideways. A big button is allowed to be
     two lines tall; a page is not allowed to scroll sideways. */
  .btn-lg { white-space: normal; }
  .qa-chip { overflow-wrap: anywhere; }
}
@media (max-width: 520px) {
  .brand-text strong { font-size: 12.5px; }
  .lang-switch { gap: 8px; }
  .status-pill { padding: 3px 9px; font-size: 11px; }
  /* The three destinations on one line rather than three.
   *
   * This said 336px, and the copy has moved since: measured on the running
   * page at 390px the labels now come to 118 + 159 + 66 = 343, plus two 8px
   * gaps = 359 against the 358 the padded wrap leaves. **One pixel over**, and
   * the cost of that pixel was a whole third row — 52px of header on the
   * narrowest screen, pushing the landing page's own picture off the fold.
   *
   * So the numbers below buy headroom rather than fit exactly: 6px gaps and a
   * half-point smaller label leave about 29px of slack, which is enough to
   * survive the next time a nav label is reworded. A layout that depends on
   * one pixel is a layout that will break again on a Tuesday. */
  .header-main { gap: 6px; }
  .btn-ghost { padding: 5px 6px; font-size: 11.5px; }
  /* The hero's submit takes its own row — see `.hero .searchbar` above. The
     field then gets the whole 358px the padded wrap leaves at 390 instead of
     the 133px it was left with after the Georgian label had taken 217. */
  .hero .searchbar > .btn { flex: 1 0 100%; }
}

/* ==========================================================================
 * Enterprise layer — landing page, investigation console, compliance dossier,
 * and the four data-handling rules that apply to every page:
 *   1. Georgian strings truncate rather than break a grid, and say so on hover
 *   2. money prints right-aligned, monospaced, in both currencies
 *   3. the dossier prints to paper without the dark palette
 *   4. a control the active role may not use is visibly locked, not missing
 * ======================================================================== */

h1, h2, h3, .metric-value, .doc-title { color: var(--gs-text, #e2e8f0); }

/* ------------------------------------------------- 1. overflow shielding */

/*
 * Georgian is a dense script with long compounds — the foreign-influence
 * register's own name runs to 58 characters — and one such string in a table
 * cell will otherwise widen a column until the grid stops being readable. Cells
 * clamp; the full value stays available on hover and to a screen reader.
 */
.trunc-cell {
  display: block;
  max-width: 26ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.trunc-wide { max-width: 44ch; }
/* 24ch, not 16. `trunc-narrow` carries the source URL, and 16 characters is
   `https://exampl…` — a column whose every row is the scheme and nothing else.
   Still below `.trunc-cell`'s 26, which is the ordering the width test pins. */
.trunc-narrow { max-width: 24ch; }

/*
 * The one column where the clamp costs more than it buys: the signal matrix's
 * "derived from". Measured at 1440 and at 390, in both schemes, all eight cells
 * overflowed — 296px of cell against 428–756px of text, the worst of them
 * hiding 460px, 61% of the string — and nothing brought the rest back. The
 * tooltip below is painted *inside* a box that is `overflow: hidden`, so it is
 * clipped by the very element it belongs to and never appears; `title` is not
 * set; and the `.table-scroll` around the table would clip it even if the cell
 * did not. A clamp whose escape hatch does not open is a deletion.
 *
 * So this column wraps rather than clamping. It keeps the whole identifier —
 * an auditor reads `organization.is_registered_foreign_influence` or reads
 * nothing — inside the 344px the table already grants the column, spending row
 * height, which the row has, instead of characters, which the evidence does
 * not. `anywhere` because these are dotted machine identifiers with no spaces
 * to break at: `break-word` alone leaves the line overflowing exactly as
 * before. The column cannot widen while it does this — `.table-matrix td` is
 * `width: 30%` and the break opportunity keeps the min-content contribution
 * small — so the grid is as safe as the clamp made it.
 *
 * The clamp stays everywhere else. This is the column of longest strings and
 * the only one whose value *is* the string.
 */
.cell-basis .trunc-cell {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Nothing is hidden here any more, so the cell must stop advertising that
   something is: no dotted underline promising a tooltip, no help cursor over
   text that is already all there. `data-full` and the tab stop stay — they come
   from the shared macro and cost the reader nothing. */
.cell-basis .tip { border-bottom: 0; cursor: auto; }

/*
 * The same argument, on the three columns it was not applied to.
 *
 * "A clamp whose escape hatch does not open is a deletion" was written above
 * about one column and is true of the portal: `.tip::after` is painted inside
 * a box that is `overflow: hidden` and is its own containing block, so it is
 * clipped by the element it belongs to and appears nowhere; `title` is never
 * set. Repairing the tooltip is a different change. Until it is made, any cell
 * that clamps has simply lost the rest of its value, and these three were
 * losing evidence:
 *
 *   `td.mono`          the source URL beside every sanction and watchlist hit.
 *                      7 rows, 176px of cell against 374px of URL — 53% gone,
 *                      and the clamp was the binding constraint, not the
 *                      column: the cell's own content box is 223px at 1440.
 *   `.evidence .mono`  the citation under the CRITICAL finding. 322 vs 374 at
 *                      1440, 260 vs 374 at 390 — both ending `…/sandbo…`,
 *                      which names no document.
 *   `.profile-list dd` the registry profile. At 390 the `dd` is 166px and the
 *                      address (235), the Georgian legal name (194) and the
 *                      publisher (210) all overflowed it.
 *
 * The last of those is why the fix is wrapping rather than a bigger `ch`
 * number. `ch` is the width of the Latin zero and Georgian is the default
 * language, so the caps are loose where the script is wide and tight where it
 * is narrow — but at 390 the caps are not what binds at all. The `dd` is 166px
 * because the column is 166px. No cap fits 235px of address into it. The only
 * thing that does is a second line.
 *
 * A URL is the harder case and the answer is the same one. It has no spaces, so
 * `overflow-wrap: anywhere` is doing the breaking, and a URL broken across two
 * lines is uglier than a URL on one. It is not uglier than no URL: the reader's
 * alternative here is `https://example.inva…` and no way to see the rest.
 * `anywhere` and not `break-word` because these have no break opportunity at
 * all — `break-word` leaves the line overflowing exactly as before — and
 * because `anywhere` is what keeps the min-content contribution small, so a
 * wrapped cell cannot widen the table it sits in. No hyphen is inserted, so
 * nothing is added to a string that has to be copied exactly.
 *
 * The clamp stays on every other cell: `.trunc-cell`, `.trunc-narrow` and
 * `.trunc-wide` keep their bounds and their ordering, and a cell that fits is
 * not touched by any of this — wrapping only engages where a line does not fit.
 */
td.mono > .trunc-cell,
.evidence .mono > .trunc-cell,
.profile-list dd > .trunc-cell {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
}
/*
 * The floor the table needs and the other two do not.
 *
 * `overflow-wrap: anywhere` drops a cell's min-content contribution to one
 * character, and a table with automatic layout believes it. Measured with the
 * rule above and without this one: the source column collapsed to 64px and the
 * row grew to 234px — the URL rendered as an eleven-line ribbon four characters
 * wide, on the entity page at 390 and on the console at 1440, which is a worse
 * read than the clamp was. This is the floor the clamp used to be: 24ch, 175.8px
 * in the monospace stack these cells are set in, where `ch` is exactly one
 * character because the font is fixed-pitch and the value is an ASCII URL. So
 * the column is never narrower than it was, and the URL wraps to three lines
 * instead of losing 53% of itself.
 *
 * Table cells only. `.profile-list dd` is 166px at 390 and a 176px floor inside
 * it is a page that scrolls sideways; block containers hand their child a width
 * rather than competing for one, so they never collapse this way.
 */
td.mono > .trunc-cell { min-width: 24ch; }
/* And, as on `.cell-basis`: nothing is hidden here any more, so the cell stops
   advertising that something is. No dotted underline promising a tooltip that
   does not open, no help cursor over text that is already all there. */
td.mono > .trunc-cell.tip,
.evidence .mono > .trunc-cell.tip,
.profile-list dd > .trunc-cell.tip { border-bottom: 0; cursor: auto; }

/* The tooltip is a CSS pseudo-element fed by a data attribute: no JavaScript,
   no inline style, and it survives the Content-Security-Policy untouched. */
.tip { position: relative; cursor: help; border-bottom: 1px dotted var(--slate-700); }
.tip::after {
  content: attr(data-full);
  position: absolute;
  left: 0;
  top: calc(100% + 6px);
  z-index: 40;
  display: none;
  max-width: 460px;
  width: max-content;
  padding: 8px 11px;
  border: 1px solid var(--slate-700);
  border-radius: var(--radius-sm);
  background: var(--slate-900);
  color: var(--slate-200);
  font-size: 12.5px;
  line-height: 1.45;
  white-space: normal;
  word-break: break-word;
  box-shadow: 0 10px 30px var(--shadow);
}
.tip:hover::after, .tip:focus-visible::after { display: block; }
.tip:focus-visible { outline: 2px solid var(--gs-action); outline-offset: 2px; }

/* Georgian script, wherever a value is known to be in it. */
.ka { font-family: var(--gs-font-georgian, inherit); }

/* -------------------------------------------------- 2. money / ledger fmt */

.num, .money { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
th.num, td.num, th.money, td.money { text-align: right; white-space: nowrap; }
.money-primary { display: block; color: var(--slate-200); }
.money-secondary { display: block; font-size: 11.5px; color: var(--slate-500); }
.money-note { display: block; font-size: 11px; color: var(--slate-500); text-align: right; }

/* The toggle switches which line is shown; with JavaScript off, both are. */
[data-currency="GEL"] .money-secondary { display: none; }
[data-currency="USD"] .money-primary { display: none; }
[data-currency="USD"] .money-secondary { color: var(--slate-200); font-size: inherit; }

.currency-toggle { display: inline-flex; gap: 0; border: 1px solid var(--slate-800);
                   border-radius: var(--radius-sm); overflow: hidden; }
.currency-toggle button {
  border: 0; background: transparent; color: var(--slate-400);
  font: inherit; font-size: 11.5px; font-weight: 600; padding: 5px 11px; cursor: pointer;
}
.currency-toggle button + button { border-left: 1px solid var(--slate-800); }
.currency-toggle button[aria-pressed="true"] { background: var(--gs-action-tint); color: var(--gs-action); }

/* ------------------------------------------------------ 4. role isolation */

.role-pill {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--slate-800); border-radius: 999px;
  padding: 4px 11px; font-size: 11.5px; color: var(--slate-400);
  background: var(--slate-900);
}
.role-officer { border-color: var(--sky-400); color: var(--gs-action); }
.role-analyst { border-color: var(--slate-700); }

.locked { position: relative; opacity: .45; }
.locked button, .locked input, .locked select { cursor: not-allowed; }
.lock-note {
  display: block; margin-top: 8px; font-size: 12px; color: var(--slate-400);
  border-left: 2px solid var(--gs-warn); padding-left: 10px;
}
.lock-badge {
  display: inline-flex; align-items: center; gap: 6px; font-size: 10.5px;
  letter-spacing: .09em; text-transform: uppercase; color: var(--gs-warn);
}

/* ----------------------------------------------- console subject bar
 *
 * What replaces the console's search stage once a subject is loaded. The stage
 * is a search page's opening — a title, a sentence and the three shapes the
 * field accepts — and with a company on screen it was 330px of instructions
 * between the analyst and the verdict, which started 1,140px down the page.
 *
 * Three parts: who it is, what the band is, and a field for the next lookup.
 *
 * It is a stage now rather than a row with a rule under it, and the reason is
 * consistency rather than decoration: `/entity` states the same verdict about
 * the same subject on a lit surface, and this page stated it on the page
 * background with a hairline. Two surfaces of one product describing one
 * finding at two levels of seriousness is the thing that makes a compliance
 * tool feel assembled rather than designed.
 *
 * The band block itself is no longer styled here at all — `.subject-head-
 * verdict` is, once, beside the header it was written for, and this page uses
 * those classes. `.subject-bar-band` and its two children are gone with the
 * markup that carried them.
 */
.subject-bar {
  position: relative;
  isolation: isolate;
  overflow: clip;
  margin-top: 20px;
  padding: clamp(20px, 2.2vw, 30px) clamp(18px, 2vw, 28px);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px 24px;
  align-items: center;
}
.subject-bar::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -80% -10% auto 35%;
  height: 220%;
  background: radial-gradient(32% 40% at 76% 24%, var(--glow-action), transparent 70%);
  filter: blur(10px);
  opacity: .75;
}
.subject-bar-who { min-width: 0; }
.subject-bar h1 {
  font-size: clamp(21px, 1.9vw, 28px); margin: 0 0 9px; letter-spacing: -.022em;
  line-height: 1.16;
}
.subject-bar .tagrow { margin: 0; }

/* The five bands, in the tokens the gauge and the dossier already use, so a
   colour means the same thing on every surface that shows one. */
.band-low { color: var(--gs-secure); background: var(--gs-secure-tint); }
.band-medium { color: var(--gs-warn); background: var(--gs-warn-tint); }
.band-high { color: var(--gs-elevated); background: var(--gs-elevated-tint); }
.band-critical { color: var(--gs-critical); background: var(--gs-critical-tint); }

/* Its own row: the field is a *next* action, not part of the identity, and
   putting it beside a Georgian company name leaves neither enough width. */
.subject-bar-search { grid-column: 1 / -1; max-width: 640px; }

@media (max-width: 720px) {
  .legend-rich { grid-template-columns: 1fr; }
}

/*
 * 1080, the same width `.subject-head` stacks at, and not 720.
 *
 * The two bars carry the same three things and stacked at two different
 * widths, so between 721 and 1080 the entity header was one column while the
 * console's was still two — and two columns is exactly where the name-versus-
 * verdict fight above happens. One breakpoint for one layout.
 */
@media (max-width: 1080px) {
  .subject-bar { grid-template-columns: minmax(0, 1fr); }
}

/* --------------------------------------------------------- landing page */

/*
 * The page a first-time reader meets, rebuilt.
 *
 * What it was: a stack of flat boxes with a hairline around each, 13px copy,
 * a 40px title and no depth anywhere. Every fact on it was true and the whole
 * thing read as an internal status document, because the only visual devices
 * in the file were `border: 1px solid` and `background: var(--slate-900)`.
 * Nothing was *wrong*, which is exactly why an audit loop that admits only
 * measurable defects ran for five hours over it and changed nothing visible.
 *
 * What it is: one composed stage rather than a stack of panels. Three things
 * carry it, and each is a token, not a literal:
 *
 *   - ambient light (`--glow-*`), so the surface has a direction and the page
 *     has a subject before a word is read;
 *   - glass (`--glass`, `--glass-edge`), so a card sits *on* the page rather
 *     than being drawn onto it;
 *   - motion that is the argument, not decoration — the graph draws itself
 *     edge by edge and the sanctioned node keeps pulsing, which is the exact
 *     sequence the product performs.
 *
 * Type is the other half. `clamp()` throughout rather than a fixed size and a
 * breakpoint: the title is 34px on a 390 phone and 62px on a 1440 laptop and
 * every step between, and it is the same rule, so there is no width at which
 * it is briefly wrong. The measures are in px and not `ch` for the reason the
 * old file already gave — `ch` is the Latin zero, and ქართული is wider.
 *
 * Everything animated is behind `prefers-reduced-motion`, in one block at the
 * end of this section rather than per rule, so a rule added later is covered
 * by default and an exception has to be written deliberately.
 */

/* ------------------------------------------------- the hero stage */

/*
 * A stage, not a band. The hero used to be a bordered region the width of the
 * text column with a rule under it; it is now a single rounded surface that
 * clips its own light. `overflow: clip` rather than `hidden` — it establishes
 * no scroll container, so the glow can sit outside the box in the layer below
 * without giving the page a scrollbar (`test_landing_ui` asserts the document
 * does not scroll horizontally, and `hidden` here is how that regresses).
 *
 * `isolation: isolate` so the negative z-index on the light layers stays
 * behind the hero's own content and in front of the page background. Without
 * it a `z-index: -1` child paints behind `body` and disappears entirely.
 */
.lp-hero {
  position: relative;
  isolation: isolate;
  overflow: clip;
  margin-top: 24px;
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
  padding: clamp(36px, 5vw, 72px) clamp(22px, 3.6vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr);
  gap: clamp(28px, 3.6vw, 56px);
  align-items: center;
}

/*
 * The ambient light: three accents bloomed to the size of the box.
 *
 * Azure at the reading start, crimson where the sanctioned node lands, warm
 * orange low and behind — the same three meanings the diagram uses, so the
 * light is not arbitrary. `blur()` on top of the gradients rather than instead
 * of them: a radial gradient alone bands visibly on an 8-bit display at this
 * size, and six pixels of blur is enough to break the rings without costing a
 * separate compositor layer.
 *
 * The drift is 26 seconds and 3% of the box. It reads as "alive" in
 * peripheral vision and is not perceptible if you look straight at it, which
 * is the only setting a background animation is allowed to have on a page
 * someone reads.
 */
.lp-hero::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -45% -12% auto -22%;
  height: 165%;
  background:
    radial-gradient(38% 46% at 20% 34%, var(--glow-action), transparent 70%),
    radial-gradient(32% 40% at 80% 16%, var(--glow-critical), transparent 70%),
    radial-gradient(42% 46% at 60% 92%, var(--glow-warm), transparent 72%);
  filter: blur(6px);
  animation: lp-drift 26s ease-in-out infinite alternate;
}

/*
 * A survey grid, faded out at the edges.
 *
 * Two hairline gradients at 56px, masked to an ellipse so it never reaches a
 * border and never has to be aligned to one. It is the cheapest way to say
 * "this is a measuring instrument" without adding an image, and it costs one
 * pseudo-element and no request.
 */
.lp-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(var(--glass-edge) 1px, transparent 1px),
    linear-gradient(90deg, var(--glass-edge) 1px, transparent 1px);
  background-size: 56px 56px;
  mask-image: radial-gradient(74% 74% at 50% 42%, black, transparent 78%);
  opacity: .55;
  pointer-events: none;
}

.lp-hero-copy { min-width: 0; }

/*
 * The eyebrow is a status pill now, with a live dot.
 *
 * It said the same words before, set in 11px letterspaced caps against
 * nothing. As a pill with a pulsing emerald dot it also carries the one thing
 * the words did not: that this instance is running and answering. The dot is
 * `--gs-secure` — the token that means "a check ran" everywhere else — so it
 * is not a new colour with a new meaning.
 */
.lp-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 22px;
  padding: 7px 15px 7px 12px;
  border: 1px solid var(--glass-edge);
  border-radius: 999px;
  background: var(--glass);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--slate-200);
}
.lp-eyebrow::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--emerald);
  box-shadow: 0 0 0 4px var(--gs-secure-tint);
  animation: lp-blink 2.6s ease-in-out infinite;
}

/*
 * 34px to 62px, fluid.
 *
 * The old 40px fixed size was the same on a phone and a 2560 monitor, so it
 * was too large on one and too small on the other, and the max-width was 800px
 * on a title that now sets much larger. 760px is three lines of ქართული at the
 * top of the clamp and two at the bottom, measured rather than assumed.
 *
 * `text-wrap: balance` because a three-line Georgian headline whose last line
 * is one word is the most common way a large title looks accidental. Chromium
 * caps it at a handful of lines, which is all a headline has.
 */
.lp-title {
  /* Capped at 54, not 62. The copy column is ~620px at 1440 — the hero's own
     padding and gap take 168 of the 1392 available and the figure takes the
     rest — so the cap is what the *column* can hold, not what the viewport
     can. At 62 the Georgian headline set four lines and pushed the primary
     action to y=855 of a 900px screen; at 54 it clears by 60px. English is two
     lines either way, which is why this had to be measured in ქართული. */
  font-size: clamp(32px, 4vw, 54px);
  line-height: 1.06;
  letter-spacing: -.035em;
  font-weight: 700;
  margin: 0 0 20px;
  max-width: 760px;
  text-wrap: balance;
}

/*
 * The clause that names the finding, in the three risk hues.
 *
 * Guarded by `@supports`, and the guard is not ceremony: `background-clip:
 * text` with `color: transparent` renders *nothing at all* where it is not
 * supported, and the thing it would erase is the subject of the sentence. So
 * the transparency is only ever declared inside the query that establishes the
 * clip works.
 */
.lp-accent { color: var(--sky-400); }
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .lp-accent {
    background-image: linear-gradient(96deg, var(--sky-400), var(--orange) 54%, var(--rose));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.lp-sub {
  font-size: clamp(15px, 1.25vw, 18px);
  line-height: 1.62;
  color: var(--slate-400);
  max-width: 620px;
  margin: 0;
}

.lp-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  margin-top: 30px;
}
/* Scoped to the hero: these are the page's two largest targets and the only
   pill-shaped controls in the portal. Everything inside the console keeps the
   10px panel radius, so the shape still distinguishes "marketing surface" from
   "instrument". */
.lp-hero .btn-lg {
  padding: 15px 30px;
  font-size: 15px;
  font-weight: 600;
  border-radius: 999px;
  transition: transform .28s cubic-bezier(.2, .8, .3, 1), box-shadow .28s, filter .2s;
}
.lp-hero .btn-primary { box-shadow: 0 12px 34px -10px var(--glow-action); }
.lp-hero .btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 18px 42px -10px var(--glow-action);
}
.lp-quiet-link {
  font-size: 14.5px;
  font-weight: 500;
  padding: 12px 6px;
}

/* ------------------------------------------------- the hero graph
 *
 * The product, drawn: two owners, the subject, an offshore parent, and a
 * sanctioned party two hops out. Colours are the band tokens the risk gauge
 * uses, so red here and red on a dossier are the same statement.
 *
 * Every fill and stroke is CSS rather than an SVG presentation attribute. Not
 * a preference: `style-src 'self'` forbids the inline `style=` this would
 * otherwise want, and a presentation attribute cannot follow a media query, so
 * the light scheme would keep the dark palette. Same reasoning that took the
 * hardcoded hexes out of `graph.js`.
 *
 * It sits in a glass card now rather than floating on the page, because a
 * diagram with no frame beside a headline reads as an illustration and the
 * claim being made is that it is a screen.
 */
.lp-figure {
  margin: 0;
  min-width: 0;
  position: relative;
  padding: 20px 20px 16px;
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--glass);
  backdrop-filter: blur(16px);
  box-shadow: 0 30px 60px -34px var(--lift);
}
/* No `overflow: visible`. Everything is inside the viewBox by construction now,
   and letting the box paint outside itself is what put a label off the page. */
.lp-graph { width: 100%; height: auto; display: block; }
.lp-figure figcaption {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--glass-edge);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--slate-500);
}

/* `--gs-muted`, and the reason is worth writing down: `--slate-600` does not
   exist. The vocabulary this stylesheet maps onto the palette is 200/400/500/
   700/800/900/950 — there is no 300 and no 600 — so `stroke: var(--slate-600)`
   fell back to nothing, computed to `stroke: none`, and every edge in the hero
   graph was invisible. The percentages floated beside a diagram with no lines
   in it. Caught by asking the running page for the computed value rather than
   by reading the rule. */
.lp-edges line { stroke: var(--gs-muted); stroke-width: 1.5; }
.lp-edges .lp-edge-alert { stroke: var(--gs-critical); stroke-width: 2.5; }
.lp-edge-labels text {
  fill: var(--slate-400); font-family: var(--mono); font-size: 11px;
  text-anchor: middle;
}

.lp-node circle { stroke-width: 2; }
/* `--gs-body`, not `--slate-300`: these are labels a reader has to read, and
   the first version set them two steps down the grey ramp where they sat at
   the edge of legible against the surface behind them. */
.lp-node text {
  font-size: 12.5px; text-anchor: middle; fill: var(--gs-body);
}
.lp-node .lp-label-end { text-anchor: end; }
.lp-node-person circle { fill: var(--gs-surface-raised); stroke: var(--gs-muted); }
.lp-node-subject circle { fill: var(--gs-action-tint); stroke: var(--gs-action); }
.lp-node-subject text { fill: var(--gs-text); font-weight: 600; }
.lp-node-offshore circle { fill: var(--gs-elevated-tint); stroke: var(--gs-elevated); }
.lp-node-critical circle { fill: var(--gs-critical-tint); stroke: var(--gs-critical); }
.lp-node-critical text { fill: var(--gs-critical); font-weight: 600; }

/*
 * The diagram performs the search.
 *
 * Order is the point and it is the order the engine actually works in: the
 * subject you typed appears first, its two shareholders resolve, the edges to
 * them draw, the offshore parent one hop further, and only then — 1.5 seconds
 * in, after everything a name-by-name check would also have found — the
 * sanctioned party and the red edge. A reader who watches it once has been
 * told what the product does without reading the caption.
 *
 * `stroke-dasharray: 120` covers the longest edge (108 units, owner to
 * subject); a dash longer than the path simply draws the whole path, so one
 * value serves all four rather than four measured ones that would need
 * re-measuring every time the geometry moves.
 *
 * `backwards` on every fill mode, because without it each node is painted at
 * full opacity for the length of its own delay and then jumps to zero to start
 * animating — which is worse than no animation at all.
 */
.lp-edges line {
  stroke-dasharray: 120;
  stroke-dashoffset: 120;
  animation: lp-draw .75s ease forwards;
}
.lp-edges line:nth-child(1) { animation-delay: .20s; }
.lp-edges line:nth-child(2) { animation-delay: .28s; }
.lp-edges line:nth-child(3) { animation-delay: .46s; }
.lp-edges .lp-edge-alert { animation-delay: .74s; }

.lp-edge-labels text { animation: lp-fade .4s ease .58s backwards; }

.lp-node { animation: lp-pop .45s cubic-bezier(.2, .9, .3, 1.25) backwards; }
/* `nth-of-type` over `g`, counting the two grouping elements the figure opens
   with: edges (1), edge labels (2), then the five nodes. Written as delays
   rather than as classes because the sequence *is* the geometry's order and a
   class per node would let the two drift apart silently. */
.lp-node:nth-of-type(3) { animation-delay: .16s; }
.lp-node:nth-of-type(4) { animation-delay: .24s; }
.lp-node:nth-of-type(5) { animation-delay: .02s; }
.lp-node:nth-of-type(6) { animation-delay: .44s; }
.lp-node:nth-of-type(7) { animation-delay: .70s; }

/*
 * The sanctioned node keeps pulsing after everything else has settled.
 *
 * It is a second, unfilled circle rather than a `box-shadow` — SVG has none —
 * and it scales rather than growing its radius, because `r` is not a
 * compositable property and animating it repaints the diagram every frame.
 * `transform-box: fill-box` makes `transform-origin: center` mean the circle's
 * own centre instead of the SVG's origin, which is the whole reason the first
 * attempt at this flew off the top-left corner.
 */
.lp-node-critical .lp-halo {
  fill: none;
  stroke: var(--gs-critical);
  stroke-width: 2;
  transform-box: fill-box;
  transform-origin: center;
  animation: lp-halo 3s ease-out 1.1s infinite;
}

@keyframes lp-draw { to { stroke-dashoffset: 0; } }
@keyframes lp-fade { from { opacity: 0; } }
@keyframes lp-pop {
  from { opacity: 0; transform: scale(.72); }
}
@keyframes lp-halo {
  0% { transform: scale(1); opacity: .75; }
  70%, 100% { transform: scale(2.2); opacity: 0; }
}
@keyframes lp-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}
@keyframes lp-drift {
  from { transform: translate3d(-2%, -1.5%, 0) scale(1); }
  to { transform: translate3d(2.5%, 2%, 0) scale(1.08); }
}
/*
 * Translate only. No opacity — and that is the whole design of this keyframe.
 *
 * The first version faded from 0, and a scroll-driven timeline sitting before
 * its own range holds the *from* state: with a full-page capture, which never
 * scrolls, three of the page's four sections rendered at `opacity: 0` and the
 * document was a hero above two thousand pixels of nothing. It is not only a
 * tooling artefact — any reader whose browser stalls the timeline, any
 * print-to-PDF, any archived copy gets the same blank page.
 *
 * A 26px slide has the same effect on a reader who scrolls and no failure mode
 * at all for one who does not: the worst case is content that is 26 pixels low
 * while it is off-screen. Never make the *presence* of content depend on an
 * animation; only its arrival.
 */
@keyframes lp-reveal {
  from { transform: translateY(26px); }
  to { transform: none; }
}

/* `.lp-node` groups animate with `transform`, so they need their own origin
   too or the scale runs from the SVG's top-left corner. */
.lp-node { transform-box: fill-box; transform-origin: center; }

/* ------------------------------------------------- three steps */

.lp-steps { padding: clamp(44px, 5.5vw, 84px) 0 0; }
.lp-steps ol {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(258px, 100%), 1fr));
  gap: 18px;
}
/*
 * Each step is a card with its number set as a large ghosted numeral behind
 * the heading rather than a 28px blue chip beside it. The chip was legible and
 * said nothing; at 60px, fading out downward, the numeral is the thing that
 * makes three separate cards read as one sequence.
 */
.lp-steps li {
  position: relative;
  overflow: clip;
  padding: 24px 24px 26px;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  background: linear-gradient(180deg, var(--glass), transparent 70%);
}
.lp-steps h3 { font-size: 17px; margin: 2px 0 7px; font-weight: 600; letter-spacing: -.01em; }
.lp-steps p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--slate-400); }
.lp-step-no {
  display: block;
  font-family: var(--mono);
  font-size: 58px;
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.06em;
  color: var(--slate-800);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .lp-step-no {
    background-image: linear-gradient(180deg, var(--slate-400), transparent 88%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* ------------------------------------------------- what it looks for */

.lp-section { padding: clamp(44px, 5.5vw, 84px) 0 0; }
.lp-h2 {
  font-size: clamp(21px, 2.1vw, 30px);
  margin: 0 0 22px;
  font-weight: 600;
  letter-spacing: -.025em;
}

.lp-find-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 16px;
}
/*
 * The four things it looks for, each lit by its own risk hue.
 *
 * `--tone` and `--tone-tint` are set once per card by the modifier class and
 * read three times inside — the halo behind the corner, the dot, and the
 * border it takes on hover. One custom property per card rather than three
 * rules per card, so adding a fifth finding is one class and one line.
 */
.lp-find {
  position: relative;
  overflow: clip;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  padding: 24px 22px 26px;
  background: var(--slate-900);
  transition: transform .35s cubic-bezier(.2, .8, .3, 1), border-color .35s, box-shadow .35s;
}
.lp-find::before {
  content: "";
  position: absolute;
  inset: -55% 45% 55% -30%;
  background: radial-gradient(closest-side, var(--tone, transparent), transparent);
  opacity: .16;
  transition: opacity .4s;
  pointer-events: none;
}
.lp-find:hover {
  transform: translateY(-4px);
  border-color: var(--tone, var(--glass-edge));
  box-shadow: 0 26px 52px -30px var(--lift);
}
.lp-find:hover::before { opacity: .3; }
.lp-find h3 { font-size: 16px; margin: 14px 0 7px; font-weight: 600; letter-spacing: -.01em; }
.lp-find p { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--slate-400); }
.lp-dot {
  display: block; width: 11px; height: 11px; border-radius: 50%;
  background: var(--tone);
  box-shadow: 0 0 0 5px var(--tone-tint);
}
.lp-find-action { --tone: var(--gs-action); --tone-tint: var(--gs-action-tint); }
.lp-find-critical { --tone: var(--gs-critical); --tone-tint: var(--gs-critical-tint); }
.lp-find-warn { --tone: var(--gs-warn); --tone-tint: var(--gs-warn-tint); }
.lp-find-elevated { --tone: var(--gs-elevated); --tone-tint: var(--gs-elevated-tint); }
/* The dot classes the template carried before the tone properties existed.
   Kept so a page that still uses one is not left with an invisible marker. */
.lp-dot-action { --tone: var(--gs-action); --tone-tint: var(--gs-action-tint); }
.lp-dot-critical { --tone: var(--gs-critical); --tone-tint: var(--gs-critical-tint); }
.lp-dot-warn { --tone: var(--gs-warn); --tone-tint: var(--gs-warn-tint); }
.lp-dot-elevated { --tone: var(--gs-elevated); --tone-tint: var(--gs-elevated-tint); }

/* ------------------------------------------------- where it runs */

.lp-onprem {
  position: relative;
  overflow: clip;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  padding: clamp(24px, 3vw, 40px);
  background: var(--slate-900);
}
/* One hairline of accent along the top edge, brightest at the left where
   reading starts. It is the only thing separating this panel from the four
   above it, and it is a gradient rather than a solid rule because a solid
   accent rule across 1550px is a banner. */
.lp-onprem::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, var(--sky-400), transparent 62%);
}
.lp-onprem > p { margin: 0; font-size: 15px; line-height: 1.62; color: var(--slate-400); max-width: 760px; }
.lp-install { margin: 20px 0 0; padding-left: 20px; font-size: 14px; line-height: 1.6;
              color: var(--slate-400); max-width: 860px; }
.lp-install li { margin-bottom: 10px; }
.lp-note { font-size: 12.5px; line-height: 1.55; color: var(--slate-500); margin: 26px 0 0; max-width: 780px; }

/* ------------------------------------------------- the three numbers */

/*
 * `gap: 1px` over the container's own background, rather than a border on
 * every child and `:last-child { border-right: 0 }`. The old rule drew a
 * vertical rule to the right of each card, so when the grid wrapped to two
 * columns — which it now does, because the columns are `auto-fit` instead of
 * a hard three — the card at the end of the first row kept a rule pointing at
 * nothing. A gap does not have that failure mode at any column count.
 */
.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(268px, 100%), 1fr));
  gap: 1px;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  overflow: clip;
  margin: 30px 0 0;
  background: var(--slate-800);
}
.metric-card { padding: 26px 24px 28px; background: var(--slate-900); }
.metric-card .figure {
  display: block;
  font-family: var(--mono);
  font-size: clamp(32px, 3.2vw, 44px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.045em;
  color: var(--gs-text, #e2e8f0);
}
.metric-card .caption { display: block; margin-top: 8px; font-size: 13.5px; color: var(--slate-200); }
.metric-card .basis { display: block; margin-top: 10px; font-size: 12px; line-height: 1.5; color: var(--slate-500); }

/*
 * The instance's own four rows, panelled.
 *
 * It was a bare `.table-scroll` directly on the page background, which was
 * invisible when everything around it was also a flat bordered box and became
 * conspicuous the moment they were not: four unenclosed rows floating between
 * two 22px-radius surfaces. It carries the same facts — it just belongs to the
 * metric panel above it now, and reads as its footnote.
 *
 * No `overflow: clip` despite the radius, because `.table-scroll` is a
 * horizontal scroll container and clipping it would be the one thing it exists
 * to prevent. Nothing inside paints a background, so the corners are clean
 * without it.
 */
.lp-instance {
  margin-top: 14px;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
}
.lp-instance .table { margin: 0; }
.lp-instance th, .lp-instance td { padding: 13px 24px; }
.lp-instance tr:last-child th, .lp-instance tr:last-child td { border-bottom: 0; }

/* ------------------------------------------------- narrow */

/*
 * One breakpoint, at 900. Everything else on this page is `clamp()` or
 * `auto-fit`, so there is nothing else to switch: the only thing that cannot
 * be expressed as a continuous function is that a 460-unit diagram beside a
 * 62px headline stops working before either of them individually does.
 */
@media (max-width: 900px) {
  .lp-hero { grid-template-columns: minmax(0, 1fr); }
  .lp-title { max-width: none; }
}

/* ------------------------------------------------- reveal on scroll */

/*
 * Scroll-driven, and therefore CSS.
 *
 * `animation-timeline: view()` does with one declaration what every site on
 * the web does with an IntersectionObserver — and this portal's
 * Content-Security-Policy is the reason that matters: the JavaScript version
 * writes an inline style to fade each section in, which `style-src 'self'`
 * forbids outright. So the modern feature is not a preference here, it is the
 * only implementation that is allowed to exist.
 *
 * `@supports` around it because a browser without it must get the finished
 * state, not a permanently invisible page — and the range ends at `cover 12%`
 * so a section is fully opaque well before it reaches the middle of the
 * screen. A reveal that is still running when you are reading the text is a
 * reveal that is too slow.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .lp-steps, .lp-section {
      animation: lp-reveal linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 12%;
    }
  }
}

/* ------------------------------------------------- motion, off
 *
 * One block for the whole section. Anything animated above is listed here, and
 * the two properties an interrupted animation would leave in a broken state —
 * the edge dash offset and the hero's transform — are restored explicitly,
 * because `animation: none` reverts to the *declared* value and the declared
 * value for an edge is "not drawn".
 */
@media (prefers-reduced-motion: reduce) {
  .lp-hero::before,
  .lp-eyebrow::before,
  .lp-edges line,
  .lp-edge-labels text,
  .lp-node,
  .lp-node-critical .lp-halo,
  .lp-steps,
  .lp-section {
    animation: none;
  }
  .lp-edges line { stroke-dashoffset: 0; }
  .lp-node-critical .lp-halo { opacity: 0; }
  .lp-hero .btn-lg { transition: none; }
  .lp-find { transition: none; }
}

/* ---------------------------------------- the portal's search-first stage */

/*
 * A search-first screen, used by `/console` and `/ask`.
 *
 * Named for what it is rather than for the page it was written on. It began as
 * `.cn-open` on the console; `/ask` is the same shape — a heading, one
 * sentence, one large field and the accepted shapes under it — and two copies
 * of that would have drifted the first time one of them was tuned.
 *
 * The same stage the landing hero uses — one clipped surface, ambient light
 * behind it — at about half the height, because this is the page an analyst
 * opens forty times a day and a full-height hero would be forty times in the
 * way. `text-align: center` on the head and the field only: the chips and the
 * disclosure under them are read, not scanned, and centred prose is harder to
 * return to on the second line.
 */
.search-stage {
  position: relative;
  isolation: isolate;
  overflow: clip;
  margin-top: 22px;
  padding: clamp(30px, 3.6vw, 54px) clamp(20px, 3vw, 44px) clamp(26px, 3vw, 40px);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
  text-align: center;
}
.search-stage::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -60% -20% auto;
  height: 190%;
  background:
    radial-gradient(30% 44% at 50% 26%, var(--glow-action), transparent 68%),
    radial-gradient(26% 36% at 82% 8%, var(--glow-warm), transparent 70%);
  filter: blur(8px);
}
.search-stage-head { max-width: 760px; margin: 0 auto; }
.search-stage h1 {
  font-size: clamp(26px, 2.6vw, 38px);
  line-height: 1.12;
  letter-spacing: -.03em;
  margin: 0 0 10px;
}
.search-stage-sub {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: var(--slate-400);
}
/* The field is a block-level flex row inside a centred column, so it centres
   by margin rather than by `text-align` — which does nothing to a flex item. */
.search-stage .searchbar-xl { margin-inline: auto; }

/*
 * The three accepted shapes, as chips.
 *
 * `auto-fit` with a 240px floor rather than three fixed columns: in Georgian
 * the shape names are phrases ("იურიდიული სახელწოდება") and the meanings run to
 * three lines, so a hard `repeat(3, 1fr)` at 900px gives each chip 260px of
 * width for 5 lines of text. Below the floor they stack, which is what they
 * were doing as a list in the first place.
 */
.search-shapes {
  list-style: none;
  margin: 24px auto 0;
  padding: 0;
  max-width: 900px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 10px;
  text-align: left;
}
.search-shapes li {
  padding: 13px 16px;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius);
  background: var(--glass);
}
.search-shapes strong {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--slate-200);
  margin-bottom: 3px;
}
.search-shapes span { font-size: 12.5px; line-height: 1.5; color: var(--slate-500); }
.search-stage-note {
  margin: 20px auto 0;
  max-width: 720px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--slate-500);
}

/* ------------------------------------------------- the loaded-graph strip */

/*
 * Four numbers about the instance, not about the industry.
 *
 * Same construction as the landing's metric grid — `gap: 1px` over the
 * container's own background rather than a border per child — for the same
 * reason: at `auto-fit` the row wraps, and a right-hand border on the last
 * card of a wrapped row points at nothing.
 *
 * The three risk tiles take their accent as a left rule and their figure in
 * the accent's own colour. Not a filled card: four saturated panels in a row
 * would make the two-of-five subjects that are actually flagged look like the
 * whole graph is on fire, which is the failure mode of every risk dashboard
 * that colours the container instead of the number.
 */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr));
  gap: 1px;
  margin: 18px 0 0;
  border: 1px solid var(--slate-800);
  border-radius: var(--radius-lg);
  overflow: clip;
  background: var(--slate-800);
}
.stat {
  padding: 20px 22px;
  background: var(--slate-900);
  border-left: 3px solid transparent;
}
.stat-figure {
  display: block;
  font-family: var(--mono);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.04em;
  color: var(--gs-text, #e2e8f0);
}
.stat-label {
  display: block;
  margin-top: 5px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--slate-500);
}
.stat-critical { border-left-color: var(--gs-critical); }
.stat-critical .stat-figure { color: var(--gs-critical); }
.stat-warn { border-left-color: var(--gs-warn); }
.stat-warn .stat-figure { color: var(--gs-warn); }
.stat-elevated { border-left-color: var(--gs-elevated); }
.stat-elevated .stat-figure { color: var(--gs-elevated); }

/* ------------------------------------------------- key caps */

/*
 * One rule for every key this interface names. It is `<kbd>`, so it is already
 * monospace in the user agent stylesheet; what is added is the cap.
 */
.kbd {
  display: inline-block;
  min-width: 20px;
  padding: 2px 6px;
  border: 1px solid var(--slate-800);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--slate-850);
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.4;
  color: var(--slate-400);
  text-align: center;
}

/*
 * The trigger, sized so the header does not grow a row.
 *
 * This is the whole reason it has three states rather than one. `--header-
 * height` is a measured constant with five bands, read by `scroll-padding-top`
 * on every page, and adding a control to the header moved three of them: the
 * Georgian header went 177 to 230 at 740px, 146 to 191 at 780, and 108 to
 * 108.2 at 1141. A heading that a fragment link scrolls to lands *behind* the
 * header when that number is short, so a decorative button had put every
 * in-page anchor in the portal off by up to 53 pixels.
 *
 * So the button costs nothing where the header is tight:
 *
 *   < 1240    not rendered at all. There is no Cmd and no Ctrl on a phone, so
 *             the control it advertises cannot be operated there anyway —
 *             which makes this the one breakpoint here that is about the
 *             reader rather than about the layout.
 *   < 1500    the key cap only, ~62px. Enough to say a shortcut exists.
 *   >= 1500   the label as well.
 *
 * The palette itself is reachable at every width by the keyboard; only its
 * advertisement is conditional.
 */
.cmdk-trigger { display: inline-flex; align-items: center; gap: 8px; flex: none; }
/* The cap inside the trigger is flattened, and this is the last 0.2px of the
   paragraph above. A `.kbd` is 22.4px tall — 11px at line-height 1.4, plus 4
   of padding and 3 of border — against the ~20px line box of the buttons
   beside it, so the header measured 108.2 against a pinned 108 in Georgian at
   1240 and every fragment link in the portal landed a fifth of a pixel under
   the header. Inside a button the cap is decoration on a control that is
   already the right height; standing alone in the palette's footer it keeps
   its full relief. */
.cmdk-trigger .kbd { line-height: 1.1; padding: 1px 6px; border-bottom-width: 1px; }
/* The trigger is `hidden` in the template and revealed by `portal.js`. A
   `display` on a class beats the `hidden` attribute's UA rule, so the
   attribute is restated here at higher specificity or the button appears on a
   workstation with scripting off — pointing at a dialog nothing can open. */
.cmdk-trigger[hidden] { display: none; }
@media (max-width: 1499px) { .cmdk-trigger > span { display: none; } }
@media (max-width: 1239px) { .cmdk-trigger { display: none; } }

/* ------------------------------------------------- the command palette */

/*
 * Cmd-K.
 *
 * A `<dialog>`, so the page behind it goes inert, Escape closes it and focus
 * returns to the trigger without a line of JavaScript for any of the three.
 * The element is laid out by the UA as a centred box; these rules override it
 * to sit near the top of the viewport, where every palette on the web sits,
 * because a list that grows downward from the middle of the screen moves its
 * own first row every time you type.
 *
 * `max-height` and not `height`: the card is as tall as its results and no
 * taller, so a two-hit query is a small card rather than a mostly-empty one.
 */
.cmdk {
  width: min(620px, calc(100vw - 32px));
  max-width: none;
  margin: min(12vh, 110px) auto auto;
  padding: 0;
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  background: var(--slate-900);
  color: var(--slate-200);
  box-shadow: 0 40px 90px -30px var(--lift);
  overflow: hidden;
}
.cmdk::backdrop {
  background: var(--gs-header, rgba(2, 6, 23, .86));
  backdrop-filter: blur(3px);
}
.cmdk-box { margin: 0; display: flex; flex-direction: column; min-height: 0; }

.cmdk-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--slate-800);
}
.cmdk-glyph { color: var(--slate-500); font-size: 17px; line-height: 1; }
.cmdk-field input {
  flex: 1; min-width: 0;
  background: none; border: 0; padding: 0;
  color: var(--slate-200);
  font: inherit; font-size: 16px;
}
.cmdk-field input::placeholder { color: var(--slate-500); }
.cmdk-field input:focus { outline: none; }
/* The ring belongs on the field, not the input: the input has no border of its
   own here, so a 2px outline around a borderless box floats in the middle of
   the row. `:focus-within` puts it on the row that reads as the control. */
.cmdk-field:focus-within { border-bottom-color: var(--sky-500); }

.cmdk-list {
  list-style: none;
  margin: 0;
  padding: 6px;
  overflow-y: auto;
  max-height: min(52vh, 440px);
}
.cmdk-group {
  padding: 10px 12px 6px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--slate-500);
}
.cmdk-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
/* `.is-active` and not `:hover`, and both are wired: the keyboard owns the
   selection, and a pointer moving across the list while somebody is arrowing
   through it must not silently change what Enter opens. Hover paints the same
   surface so the row still reads as a target; only the class decides. */
.cmdk-row:hover { background: var(--hover); }
.cmdk-row.is-active { background: var(--gs-action-tint); }
.cmdk-row-main {
  flex: 1; min-width: 0;
  font-size: 14px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cmdk-row.is-active .cmdk-row-main { color: var(--gs-text); }
.cmdk-row-tags { display: flex; gap: 5px; flex: none; }
.cmdk-row-hint {
  flex: none;
  font-size: 11.5px;
  color: var(--slate-500);
  max-width: 34%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cmdk-empty { padding: 22px 12px; text-align: center; font-size: 13.5px; color: var(--slate-500); }

.cmdk-foot {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0;
  padding: 10px 16px;
  border-top: 1px solid var(--slate-800);
  background: var(--slate-950);
  font-size: 11.5px;
  color: var(--slate-500);
}
.cmdk-foot .kbd { margin-right: 3px; }
/* The privacy line is the first thing to go when the row is narrow: the two
   key hints are how you operate the control and the sentence is a claim about
   it, which the dialog's own `aria-label` and `/api/subjects` both also make. */
.cmdk-note { margin-left: auto; text-align: right; }
@media (max-width: 560px) { .cmdk-note { display: none; } }

@media (max-width: 720px) {
  .cmdk { margin-top: 6vh; }
  .cmdk-row-hint { display: none; }
}

/* The palette is chrome and does not print — the rule for that lives in the
   one `@media print` block at the end of this file, beside every other thing
   that is screen-only. Not here, and the reason is worth the sentence: this
   stylesheet's print rules are read back by `print_media_css()`, which takes
   the *first* `@media print` it finds and balances its braces. A second one
   earlier in the file does not merge with the real one, it hides it — three
   print assertions started reporting that `.trunc-cell` and `.money-primary`
   had no rule at all, when what had happened is that they were no longer in
   the block the helper could see. One print block, at the bottom. */

.cap-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 24px; }
.cap-card { border: 1px solid var(--slate-800); border-radius: var(--radius);
            padding: 22px; background: var(--slate-900); }
.cap-card h3 { font-size: 15px; margin: 0 0 8px; font-weight: 600; }
.cap-card p { margin: 0 0 12px; color: var(--slate-400); font-size: 13px; }
.cap-card .cap-mark { display: inline-flex; width: 30px; height: 30px; border-radius: var(--radius-sm);
                      align-items: center; justify-content: center; margin-bottom: 12px;
                      font-family: var(--mono); font-size: 12px; font-weight: 700;
                      background: var(--gs-action-tint); color: var(--gs-action); }
.cap-card ul { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--slate-500); }
.cap-card li { margin-bottom: 4px; }

.onboard { border: 1px solid var(--slate-800); border-radius: var(--radius);
           padding: 24px; background: var(--slate-900); margin-top: 24px; }
.onboard ol { margin: 12px 0 0; padding-left: 22px; color: var(--slate-400); font-size: 13px; }
.onboard li { margin-bottom: 8px; }

/* ------------------------------------------------------- question catalogue */

/*
 * The shapes /ask accepts.
 *
 * This was a two-column table: the Georgian description of a shape on the left
 * and, on the right, the English query to click. Reading it meant crossing 700
 * pixels of empty cell for every row, the monospace queries wrapped mid-phrase
 * in the narrow column they were given, and the one thing on the page you are
 * meant to *press* was styled to look exactly like a link inside a data table.
 *
 * Each shape is now one card: what it answers, then the button that runs it.
 * Still a POST — see the comment in `ask.html`; the query never enters a URL.
 */
.qa-list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  /* `min(330px, 100%)`, not `330px`. A bare minmax floor is a *minimum*,
     so on a 360px phone each card stayed 330px wide inside a 328px column
     and the page scrolled sideways. A media-query override would have had
     to sit after this rule to win, which is a rule about source order
     rather than about layout. */
  grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr));
  /* 16px, the same air `.cap-grid` and `.metrics` give the same card. */
  gap: 16px;
}
/*
 * Each card is one question, and the *card* is the target.
 *
 * It was a description in grey with a small pill under it, and the pill was
 * the only thing that did anything — so thirteen cards presented thirteen
 * 24px-tall targets inside thirteen 90px boxes, and the obvious click landed
 * on nothing. The card lifts and lights on hover now and the chip fills its
 * width, so the whole box reads as the control it always contained.
 *
 * `:focus-within` carries the same treatment, because the chip is a real
 * `<button>` inside a real `<form>` and a keyboard reader tabbing through
 * thirteen of them needs to see which card they are standing in, not only
 * which button has the ring.
 */
.qa-item {
  display: flex; flex-direction: column; gap: 12px; justify-content: space-between;
  border: 1px solid var(--slate-800); border-radius: var(--radius);
  background: var(--slate-900); padding: 16px 16px 15px;
  transition: transform .28s cubic-bezier(.2, .8, .3, 1), border-color .28s, box-shadow .28s;
}
.qa-item:hover, .qa-item:focus-within {
  transform: translateY(-3px);
  border-color: var(--glass-edge);
  box-shadow: 0 20px 40px -26px var(--lift);
}
.qa-intent { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--slate-400); }
.qa-item .inline-form { display: block; }
.qa-chip {
  display: block; width: 100%; max-width: 100%;
  border: 1px solid var(--slate-800); border-radius: var(--radius-sm);
  background: var(--surface-raised); color: var(--slate-200);
  font-family: var(--mono); font-size: 11.5px; line-height: 1.45;
  padding: 8px 12px; cursor: pointer; text-align: left;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.qa-item:hover .qa-chip, .qa-chip:hover {
  border-color: var(--sky-400); color: var(--sky-400);
  background: var(--gs-action-tint);
}

@media (prefers-reduced-motion: reduce) {
  .qa-item { transition: none; }
  .qa-item:hover, .qa-item:focus-within { transform: none; }
}

/* ---------------------------------------------------- investigation panel */

/* The left column was 360–420px, and the profile's value cells inherit whatever
   is left after the 140px label track — about 260px, which truncated the legal
   name, the Georgian name, the address and the English name on every subject.
   The clamp is meant to stop a 58-character compound widening the grid, not to
   hide an ordinary name in a column that has the room. */
.console-grid { display: grid; grid-template-columns: minmax(440px, 520px) 1fr; gap: 16px; }
.console-col { min-width: 0; }
.console-col > .panel { margin-bottom: 16px; }

.profile-list { margin: 0; }
.profile-list > div { display: grid; grid-template-columns: 140px 1fr; gap: 10px;
                      padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
.profile-list > div:last-child { border-bottom: 0; }
.profile-list dt { font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
                   color: var(--slate-500); padding-top: 3px; font-weight: 600; }
.profile-list dd { margin: 0; min-width: 0; }

.telemetry { max-height: 340px; overflow-y: auto; }
.telemetry-item { display: grid; grid-template-columns: 76px 1fr; gap: 12px;
                  padding: 12px 0; border-bottom: 1px solid var(--line-soft); }
.telemetry-item:last-child { border-bottom: 0; }
.telemetry-when { font-family: var(--mono); font-size: 11px; color: var(--slate-500); }
.telemetry-body strong { display: block; font-size: 13px; }
.telemetry-body p { margin: 4px 0 0; font-size: 12.5px; color: var(--slate-400); }

/* The console's map section may go taller than the 520px ceiling the pane
   carries elsewhere. A *ceiling*, and no longer a 560px floor: the pane is
   `height: auto` now (see `.graph-canvas svg`), so a floor could only add
   blank — which is what the pair of `min-height` rules that stood here did,
   together with a second one cancelling the first for flat networks. Raising
   the maximum says the same thing without reserving anything. */
.canvas-tall .graph-canvas svg { max-height: 560px; }
/*
 * Three columns, not two. Measured on the console at 1440px: the canvas is
 * 380px tall and its legend was **328px** — the key to the picture was nearly
 * as tall as the picture, and it sat between the map and the contracting
 * table so an analyst scrolled past a paragraph-per-colour to reach either.
 *
 * The shape cue stays. It is not decoration: colour is never the only cue in
 * this interface, and `test_the_console_loads_one_entity_into_the_split_screen`
 * asserts "solid line" and "dashed line" reach the page. What tightens is the
 * *meaning* line, which is set smaller and allowed to wrap under its label
 * rather than being given a column of its own.
 */
.legend-rich { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
               gap: 10px 20px;
               list-style: none; padding: 0; margin: 14px 0 0; }
.legend-rich li { display: grid; grid-template-columns: 14px 1fr; gap: 8px;
                  font-size: 12px; color: var(--slate-400); line-height: 1.35; }
.legend-rich strong { color: var(--slate-200); display: block; font-size: 12.5px; }
.legend-rich .cue { display: block; font-size: 11px; color: var(--slate-500); }

/* ------------------------------------------------------ compliance dossier */

.dossier-frame { border: 1px solid var(--slate-800); border-radius: var(--radius);
                 background: var(--slate-900); padding: 26px; margin-bottom: 16px; }
.doc-header { display: flex; justify-content: space-between; gap: 24px;
              border-bottom: 1px solid var(--slate-800); padding-bottom: 18px; flex-wrap: wrap; }
.doc-title { font-size: 21px; margin: 0 0 4px; letter-spacing: -.02em; }
.doc-kicker { font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
              color: var(--gs-action); margin: 0 0 8px; font-weight: 600; }
.doc-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
            gap: 16px; margin: 16px 0 0; }
.doc-meta dt { font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
               color: var(--slate-500); font-weight: 600; }
.doc-meta dd { margin: 4px 0 0; font-family: var(--mono); font-size: 12px; word-break: break-all; }

.integrity {
  margin-top: 24px; border: 1px solid var(--slate-800); border-radius: var(--radius-sm);
  background: var(--slate-950); padding: 16px 18px;
}
.integrity h3 { font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
                color: var(--slate-500); margin: 0 0 12px; font-weight: 600; }
.integrity dl { display: grid; grid-template-columns: 210px 1fr; gap: 8px 16px; margin: 0; }
.integrity dt { font-size: 12px; color: var(--slate-500); }
.integrity dd { margin: 0; font-family: var(--mono); font-size: 12px; word-break: break-all;
                color: var(--slate-200); }
.integrity .verdict-ok { color: var(--gs-secure); }
.integrity .verdict-bad { color: var(--gs-critical); }

.meter-row { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
.meter { max-width: 100%; height: auto; }
.meter-label { font-size: 9px; font-weight: 700; letter-spacing: .08em; }
/* A band the subject is not in. The active one keeps its `fill` attribute,
   which comes from `ui.styling` through Python; the other three were a
   `#64748b` literal in three templates and are the theme's muted grey now. */
.band-idle { fill: var(--slate-500); }
/* Wraps because a Russian band figure cannot be broken: КРИТИЧЕСКИЙ at 24px
   mono is 149px, and with the two labels and two 30px gaps the row's
   min-content is 384px inside a 316px container at the 390px reference —
   31px past the document, in Russian only. The row never wraps at 1440 or
   in print, where it holds 520px on one line. */
.meter-readout { display: flex; flex-wrap: wrap; gap: 30px; }
.readout-figure { font-family: var(--mono); font-size: 24px; font-weight: 600;
                  letter-spacing: -.02em; }
.readout-label { display: block; font-size: 10px; text-transform: uppercase;
                 letter-spacing: .1em; color: var(--slate-500); font-weight: 600; }

/* --------------------------------------------------------- 3. print rules */

/*
 * A dossier printed for a risk committee has to be readable on paper and cheap
 * to produce: a dark background over an A4 page is both unreadable in a bound
 * pack and an entire cartridge. Navigation, buttons and the graph controls are
 * interface, not evidence, so they do not print at all — and each major section
 * starts on its own page so a section is never split across a staple.
 *
 * The screen may already be light — `/theme.css` follows the workstation's
 * `prefers-color-scheme` — but this block cannot rely on that: the reader who
 * prints is whichever reader hits Ctrl+P, and a paper dossier that comes out
 * black because the analyst's laptop is in dark mode is the failure this exists
 * to prevent. Every material token is restated, not only the four surfaces.
 */
@media print {
  :root {
    --slate-950: #fff; --slate-900: #fff; --slate-850: #fff;
    --slate-800: #999; --slate-700: #666; --slate-500: #444;
    --slate-400: #333; --slate-200: #000;
    --surface-raised: #fff; --line-soft: #ccc;
    --hover: transparent; --header-bg: #fff; --shadow: transparent;
    --on-accent: #000;
  }
  html, body { background: #fff !important; color: #000 !important; }
  body { font-size: 11pt; }
  h1, h2, h3, .doc-title, .metric-value { color: #000 !important; }

  /* `.cmdk` is a `<dialog>`, which the UA already hides when it is closed —
     but a reader who prints *while it is open* would otherwise get a card of
     search results across the first page of a dossier. */
  .site-header, .site-footer, .searchbar, .graph-controls, .currency-toggle,
  .dossier-form, .lp-actions, .no-print, form, .telemetry, nav,
  .cmdk, .cmdk-trigger {
    display: none !important;
  }

  .panel, .dossier-frame, .integrity, .cap-card, .metric-card {
    background: #fff !important;
    border: 1px solid #999 !important;
    box-shadow: none !important;
    break-inside: avoid;
  }

  /* Truncation is a screen concern. On paper the full string must be there. */
  .trunc-cell, .trunc-wide, .trunc-narrow {
    max-width: none; white-space: normal; overflow: visible;
  }
  .tip { border-bottom: 0; }
  .tip::after { display: none !important; }

  /* Both currencies always print: the toggle state is not part of the record. */
  .money-primary, .money-secondary { display: block !important; color: #000 !important; }

  /* Order matters: every panel starts a page, except the two that continue the
     document header — and `.print-break` is last so a section that asks for a
     page break gets one even when it is a frame. */
  .panel { break-before: page; page-break-before: always; }
  .dossier-frame, .dossier-frame + .panel { break-before: auto; page-break-before: auto; }
  .print-break { break-before: page; page-break-before: always; }

  a, .link { color: #000 !important; text-decoration: underline; }
  .table .link, .table a { color: #000 !important; text-decoration-color: #000; }
  .tag { border: 1px solid #666 !important; color: #000 !important; background: #fff !important; }
  .cell-hit .cell-state { border: 1px solid #000; }
  .row-ubo { background: #fff !important; }
  table { break-inside: auto; }
  tr { break-inside: avoid; }
  thead { display: table-header-group; }
}

/* ------------------------------------------------- responsive, enterprise */

@media (max-width: 1180px) {
  .console-grid { grid-template-columns: 1fr; }
  .metric-grid, .cap-grid { grid-template-columns: 1fr; }
  .metric-card { border-right: 0; border-bottom: 1px solid var(--slate-800); }
  /* Two, then one. Below 1180 the console is a single column and the legend
     has the full width; below 720 three swatches per row is a word per line. */
  .legend-rich { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .lp-title { font-size: 31px; }
}

/*
 * The hero stacks well before the grid gets tight. 940px, not 720: the picture
 * needs about 400px to stay readable — its node labels are set at 12px in the
 * SVG's own coordinate space and shrink with it — and beside a Georgian title
 * that is a column the copy cannot spare much below this.
 */
@media (max-width: 940px) {
  .lp-hero { grid-template-columns: 1fr; gap: 32px; padding-top: 40px; }
  .lp-figure { max-width: 480px; }
  .lp-steps ol { grid-template-columns: 1fr; gap: 24px; }
}

@media (max-width: 520px) {
  .lp-hero { padding-top: 28px; }
  .lp-title { font-size: 26px; }
  .lp-sub { font-size: 15px; }
  /*
   * The SVG's text is set in *viewBox units*, so it shrinks with the picture.
   * On a 390px phone the figure renders about 342px wide against a 460-unit
   * box — a scale of 0.74 — which took the 12.5-unit labels to 9px on glass
   * and made the one diagram on the page unreadable. 17 units brings them back
   * to roughly 12px rendered. The percentages stay proportionally smaller;
   * they are an aside, and the node names are the part that has to be read.
   */
  .lp-node text { font-size: 17px; }
  .lp-edge-labels text { font-size: 14px; }
  .lp-actions .btn { width: 100%; justify-content: center; }
  .lp-actions .lp-quiet-link { width: 100%; text-align: center; }
}

/* ---------------------------------------------------------------- toasts
   The connectivity notice `portal.js` appends when the browser goes offline.
   Fixed to the bottom-left so it never covers the search field or the graph
   inspector — an analyst reading "connection lost" still needs to see what
   they were looking at. `no-print` because a paper dossier has no network. */
.toast {
  position: fixed;
  left: 1.25rem;
  bottom: 1.25rem;
  z-index: 60;
  max-width: 24rem;
  padding: .7rem 1rem;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  line-height: 1.35;
  border: 1px solid var(--slate-700);
  background: var(--slate-850);
  color: var(--slate-200);
  box-shadow: 0 8px 24px var(--shadow);
}

.toast-offline {
  border-left: 3px solid var(--orange);
  color: var(--slate-200);
}

@media print {
  .toast { display: none !important; }
}

/* While the browser is offline the controls that would mutate state are shown
   as unavailable rather than removed: an analyst who can still read the record
   in front of them is better off than one whose page emptied itself. */
html[data-offline] .currency-toggle button,
html[data-offline] .searchbar button,
html[data-offline] .graph-controls button {
  opacity: .5;
  cursor: not-allowed;
}

/*
 * Motion is an affordance, not an effect. The only transitions in this file are
 * the 120ms colour changes on `.btn`; a reader who has asked their system to
 * stop animating gets none of them.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
