/* Command Navy — shared shell chrome and components (Phase 1).
 *
 * Linked only by base_navy.html (see navy/tokens.css for the scoping
 * contract). Class prefix `nx-`. Everything uses logical properties so RTL
 * mirrors for free; Latin figures/tags inside Arabic text ride `.lw-numeric`
 * or explicit dir="ltr" spans (utilities from tokens/base.css, which the navy
 * shell also links).
 *
 * Interaction contract (handoff §5): hover lightens borders to --nx-outline;
 * focus is a visible cyan ring; selected is orange border + tint; disabled is
 * ABSENT, not greyed; no animation is part of any locked design — transitions
 * stay at border-color/color only, and motion is never meaning.
 */

/* --- Base ------------------------------------------------------------- */
body.nx-body {
  margin: 0;
  background: var(--nx-canvas);
  color: var(--nx-body);
  font-family: var(--nx-font-body);
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;
}

/* :where() keeps the base link rule at the lowest specificity, so every
   component class below (.nx-btn, .nx-tab, .nx-row, .nx-identity…) wins by
   plain source order — cyan is for LINKS, not for everything an <a> renders. */
.nx-body :where(a) {
  color: var(--nx-cyan);
  text-decoration: none;
}
.nx-body :where(a:hover) { color: var(--nx-link-hover); }

.nx-body ::selection { background: var(--nx-gold); color: var(--nx-canvas); }

.nx-body h1, .nx-body h2, .nx-body h3 {
  font-family: var(--nx-font-display);
  font-weight: 700;
  color: var(--nx-text);
  margin: 0;
}

/* Form controls inherit the page's font. Without this, every <option> (448 of
   them on the planner) and any button no component rule reaches rendered in
   the UA default — Arial — which on an Arabic page meant a different, wrong
   face mid-form (fresh-eyes review, 2026-08-19). Element-level, so every
   component rule that sets its own font still wins. */
.nx-body button, .nx-body input, .nx-body select,
.nx-body textarea, .nx-body option { font: inherit; }

/* Arabic is cursive and connected: any tracked utility below zeroes its
   letter-spacing under RTL, the same contract tokens/base.css enforces for
   the lw-* utilities. */
.nx-kicker {
  font: 600 10.5px var(--nx-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nx-muted);
}
[dir="rtl"] .nx-kicker { letter-spacing: 0; }

.nx-mono { font-family: var(--nx-font-mono); }

/* --- Accessibility ----------------------------------------------------- */
.nx-body .visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.nx-body .skip-link {
  position: absolute;
  inset-inline-start: 8px;
  top: -48px;
  z-index: 40;
  background: var(--nx-text);
  color: var(--nx-canvas);
  font: 600 13px var(--nx-font-body);
  padding: 10px 16px;
  border-radius: 8px;
  /* A real target when it appears: the floor has no exceptions (2026-09-06). */
  display: inline-flex;
  align-items: center;
  min-height: var(--nx-touch);
  box-sizing: border-box;
}
.nx-body .skip-link:focus { top: 8px; color: var(--nx-canvas); }

.nx-body :focus-visible {
  outline: 2px solid var(--nx-focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* --- Shell width ------------------------------------------------------- */
.nx-shell {
  max-width: var(--nx-public-max);
  margin: 0 auto;
  padding-inline: var(--nx-gutter);
}
.nx-shell--tool { max-width: var(--nx-tool-max); }

/* --- Wordmark -----------------------------------------------------------
   .nx-mark ITSELF NOW LIVES IN tokens/brand.css. It moved because a SECOND
   shell renders it: the Desert Storm editor takes navy's token layer but not
   this sheet (components.css describes page furniture the editor does not
   render), and copying the mark's geometry into the editor's own stylesheet
   would have made two definitions of the brand. tokens/brand.css is reached by
   both shells through lw-tokens.css and is where the product's brand assets
   already live. */

.nx-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--nx-text);
  /* The brand link measured 22px tall on every navy page; the hit area rides
     the masthead's height now, with no change to the mark or the wordmark. */
  min-height: var(--nx-touch);
}
.nx-brand:hover { color: var(--nx-text); }
.nx-brand-text {
  font: 700 15px var(--nx-font-display);
  letter-spacing: 0.05em; /* Latin-only wordmark, carried dir="ltr" */
  color: var(--nx-text);
  white-space: nowrap;
}
.nx-brand-text--sm { font-size: 12px; }

.nx-masthead-signin {
  font: 500 13.5px var(--nx-font-body);
  min-height: var(--nx-touch);
  /* A short word ("登入", "로그인") is still a 44px target on both axes. */
  min-inline-size: var(--nx-touch);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.nx-body a.nx-masthead-signin { color: var(--nx-text); }
.nx-body a.nx-masthead-signin:hover { color: var(--nx-ivory-hover); }

/* --- Masthead (public + authenticated) --------------------------------- */
.nx-masthead {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(16, 24, 32, 0.94);
  backdrop-filter: blur(8px);
  border-block-end: 1px solid var(--nx-border-2);
}
.nx-masthead-inner {
  display: flex;
  align-items: center;
  gap: 22px;
  min-height: 58px;
  padding-block: 6px;
  /* A flex item's automatic minimum is its content, so the row could not
     shrink below the natural width of the wordmark, the four nav links and the
     end block together — about 790px. Between 561px (where the compact phone
     masthead takes over) and roughly 800px that made the MASTHEAD, not the
     page under it, scroll the whole document sideways: 37px at 768x1024 on
     every authenticated page, the tree included. Found while closing the
     "the document must never scroll horizontally" criterion on the technology
     workstation; the cause was never the workstation.

     AND IT MUST BE ABLE TO WRAP (owner defect, 2026-08-29). `min-width: 0`
     lets the row shrink; it does not let it BREAK. Below roughly 320px of
     usable width the wordmark and the end block together still need more room
     than there is, and a flex row with nowhere to go simply overflows:
     measured, the masthead held the document at a constant 319px scrollWidth
     at 150%, 200%, 250% and 300% browser zoom on every authenticated page,
     which is the whole of the 26-172px sideways document scroll found on the
     technology calculator. The tree was never responsible for it, and neither
     was the bottom tab bar - that is `position: fixed` with `inset-inline: 0`,
     so it FOLLOWS the layout viewport the masthead had already widened and can
     never be the cause of it.

     Wrapping rather than clipping: nothing is hidden, the identity chip and
     the way out both stay reachable, and the masthead simply becomes two short
     rows at a width where it cannot be one. It is inert above that width -
     there is nothing to wrap - so no approved layout moves. NOT `overflow-x:
     hidden` on html/body, which would leave the row exactly as wide and merely
     put the part of it past the edge out of reach.

     THE WRAP IS SCOPED, because flexbox decides a wrap from an item's CONTENT
     size and therefore BEFORE flex-shrink is applied. Left unscoped it fired
     at widths where the row had always fitted by shrinking: measured against
     the previous commit, the Arabic masthead at 320px grew from 75px to 109px
     and pushed every page's content down 34px, and English at 768px grew from
     71px to 117px. The media query at the end of this file is where it is
     turned on, and why that width. */
  min-width: 0;
}
.nx-masthead-sep {
  width: 1px;
  height: 16px;
  background: var(--nx-border);
  flex: none;
}
.nx-masthead-nav {
  display: flex;
  align-items: center;
  gap: 20px;
  font: 500 13.5px var(--nx-font-body);
  /* THE NAV IS WHAT YIELDS, AND IT LOSES NOTHING BY YIELDING. Where the row
     is too narrow for everything, the four links scroll inside their own band
     rather than pushing the page sideways: every link stays present and stays
     reachable by swipe, by trackpad and by Tab (focus scrolls its container
     into view). Nothing is hidden, and at every width where the masthead
     already fitted this changes nothing at all. */
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.nx-masthead-nav::-webkit-scrollbar { width: 0; height: 0; display: none; }
.nx-masthead-nav a { flex: none; }
/* The public masthead's anchor links are targets like any other: the floor on
   both axes (closure Correction 5, 2026-09-06 -- "Products" measured 62x16). */
.nx-masthead-nav a { display: inline-flex; align-items: center; min-block-size: var(--nx-touch); min-inline-size: var(--nx-touch); }
.nx-masthead-nav a { color: var(--nx-body); }
.nx-masthead-nav a:hover { color: var(--nx-text); }
.nx-masthead-end {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: 14px;
  /* Itself a flex row of three intrinsic-width chips whose automatic minimum
     is their own content. Shrinking the end block, like shortening the wordmark,
     is scoped with the wrap - see the media query at the end of this file. */
}

/* Authenticated tabs: orange underline marks the CURRENT tab (selection is
   orange, never cyan, never gold). */
.nx-tabs {
  display: flex;
  align-items: stretch;
  align-self: stretch;
  gap: 4px;
  font: 500 13.5px var(--nx-font-body);
  /* THE TABS ARE NOT WHAT YIELDS. (owner defect, 2026-08-30, correcting the
   * commit that made them yield.)
   *
   * They were given `min-width: 0` and an internal scroll band, on the
   * reasoning that "every tab stays present and stays reachable by swipe, by
   * trackpad and by Tab (focusing an off-screen tab scrolls its container to
   * it)". Measured on the running server, that last clause is false. Pressing
   * real Tab keys until the trailing tab holds focus:
   *
   *              900    820    768    700    640
   *     EN       ok     CUT    CUT    ok     CUT
   *     AR       ok     CUT    CUT    ok     CUT
   *
   * Chrome only scrolls a focused element into view when it is COMPLETELY
   * outside the scroll port; a partly visible one it leaves where it is. So
   * between roughly 601px and 899px a keyboard user could focus Account and
   * never see it or its focus ring — and nothing on screen said the strip
   * scrolled at all, because `scrollbar-width: none` hides the one affordance
   * it had. In Arabic it was worse by a pixel: the RTL scroll range rounds
   * down, so even at the extreme 1px of the tab stayed clipped, at every
   * width, permanently.
   *
   * WHAT YIELDS INSTEAD IS THE ROW, WHICH BREAKS - see the wrap rule at the
   * end of this file. Wrapping was always the doctrine here ("nothing is
   * hidden"); the tabs were the one place it was not applied.
   *
   * AND THE BELT WRAPS TOO, rather than scrolling. A scroll port clips
   * whatever leaves it, and `:focus-visible` draws its ring 2px outside the
   * box at a 2px offset - so with `overflow-x: auto` here the ring on the
   * FIRST and LAST tab was cut at every width in both languages, measured, on
   * all four routes. A belt that damages the focus ring on two of four tabs
   * always, to guard a locale that does not exist yet, is the wrong belt.
   * These tabs wrap: if labels ever grow wider than the line they are given,
   * they take a second line and the masthead gets taller. Nothing is clipped,
   * nothing scrolls behind a hidden scrollbar, and the document still never
   * widens. At every width measured it is inert - the tabs get their full
   * width and stay on one line. */
  flex-wrap: wrap;
  min-width: 0;
}
.nx-tabs > * { flex: none; }
.nx-tab {
  display: flex;
  align-items: center;
  padding-inline: 14px;
  min-height: var(--nx-touch);
  color: var(--nx-support);
  border-block-end: 2px solid transparent;
}
.nx-tab:hover { color: var(--nx-text); }
.nx-tab[aria-current="page"] {
  color: var(--nx-text);
  font-weight: 600;
  border-block-end-color: var(--nx-orange);
}
.nx-tab-locked {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-inline: 14px;
  color: var(--nx-faint);
}

/* Identity chip (authenticated masthead). */
.nx-identity {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--nx-border);
  background: var(--nx-surface);
  border-radius: 9px;
  padding: 5px 10px;
  color: var(--nx-text);
  font: 600 12.5px var(--nx-font-body);
  min-height: 36px;
}
.nx-identity:hover { border-color: var(--nx-outline); color: var(--nx-text); }
.nx-identity-avatar {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--nx-text);
  color: var(--nx-canvas);
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 11px var(--nx-font-display);
  flex: none;
}

.nx-signout button {
  background: none;
  border: 0;
  color: var(--nx-muted);
  font: 500 12.5px var(--nx-font-body);
  cursor: pointer;
  padding: 8px 6px;
  min-height: var(--nx-touch);
}
.nx-signout button:hover { color: var(--nx-text); }

/* --- Mobile bottom tab bar (authenticated, phone tier) ------------------ */
.nx-tabbar {
  display: none;
  position: fixed;
  bottom: 0;
  inset-inline: 0;
  z-index: 10;
  background: rgba(16, 24, 32, 0.97);
  border-block-start: 1px solid var(--nx-border-2);
  padding-block: 0 max(10px, env(safe-area-inset-bottom));
}
.nx-tabbar a {
  flex: 1;
  /* A flex item's automatic minimum is its own word, so four tab labels were a
     133px floor under a 110px viewport - and the bar is fixed to both inline
     edges, so that floor was the document's too. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
  font: 500 10.5px var(--nx-font-body);
  color: var(--nx-muted);
  padding-block: 12px 6px;
  border-block-start: 2px solid transparent;
  min-height: var(--nx-touch);
}
.nx-tabbar a[aria-current="page"] {
  color: var(--nx-text);
  font-weight: 600;
  border-block-start-color: var(--nx-orange);
}

/* --- Footer ------------------------------------------------------------- */
.nx-footer {
  border-block-start: 1px solid var(--nx-border-2);
  background: var(--nx-deep);
  margin-block-start: 0;
}
.nx-footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: center;
  justify-content: space-between;
  padding-block: 26px;
}
.nx-footer-identity {
  font: 400 11.5px/1.7 var(--nx-font-body);
  color: var(--nx-muted);
  max-width: 640px;
  margin: 0;
}

/* --- Language selector (shared partial, restyled) ------------------------ */
.nx-body .language-menu { position: relative; }
.nx-body .language-menu summary {
  list-style: none;
  cursor: pointer;
  font: 500 12.5px var(--nx-font-body);
  color: var(--nx-support);
  border: 1px solid var(--nx-border);
  background: var(--nx-surface);
  border-radius: 8px;
  /* A 40px desktop icon button: no inherited label width or padding may push
     the globe off centre. The phone rule below increases only the touch box. */
  box-sizing: border-box;
  inline-size: 40px;
  block-size: 40px;
  min-height: 40px;
  min-width: 40px;
  padding: 0;
  display: inline-grid;
  place-items: center;
  line-height: 1;
}
.nx-body .language-menu summary::-webkit-details-marker { display: none; }
.nx-body .language-menu-icon { display: block; width: 22px; height: 22px; }
.nx-body .language-menu summary:hover { border-color: var(--nx-outline); background: var(--nx-elevated); color: var(--nx-text); }
.nx-body .language-menu[open] summary { border-color: var(--nx-outline); color: var(--nx-text); }
.nx-body .language-menu ul {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + 6px);
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--nx-elevated);
  border: 1px solid var(--nx-border);
  border-radius: 10px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.45);
  /* 172px OR THE VIEWPORT, whichever is smaller. The menu is anchored to the
     inline end of a chip that is itself near the edge, so a flat 172px is a
     flat 172px of document overflow once the viewport is narrower than that. */
  min-width: min(172px, calc(100vw - 16px));
  max-width: calc(100vw - 16px);
  max-height: min(60vh, 420px, calc(100dvh - 80px));
  overflow-y: auto;
  z-index: 30;
}
.nx-body .language-menu form { margin: 0; }
.nx-body .language-menu ul button {
  width: 100%;
  text-align: start;
  background: none;
  border: 0;
  border-radius: 7px;
  color: var(--nx-body);
  font: 400 13px var(--nx-font-body);
  padding: 9px 10px;
  /* Ten locale rows at 38px each were ten targets under the floor. The
     floor is the LAST min-height in this block: a later `38px` once won the
     cascade over it while the guard only looked for the token. */
  min-height: var(--nx-touch);
  cursor: pointer;
}
.nx-body .language-menu ul button:hover { background: var(--nx-surface); color: var(--nx-text); }
.nx-body .language-menu summary:focus-visible,
.nx-body .language-menu ul button:focus-visible { outline: 2px solid var(--nx-focus); outline-offset: 2px; }
.nx-body .language-menu ul button.is-active {
  color: var(--nx-text);
  font-weight: 600;
  border-inline-start: 2px solid var(--nx-orange);
  background: var(--nx-surface);
}

/* --- Buttons ------------------------------------------------------------ */
/* The ivory button is the signature primary action; orange fill CTAs are NOT
   used (handoff §1). */
.nx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--nx-text);
  color: var(--nx-canvas);
  font: 600 15px var(--nx-font-body);
  padding: 13px 26px;
  min-height: var(--nx-touch);
  border-radius: 9px;
  border: 0;
  cursor: pointer;
  text-decoration: none;
  /* BORDER-BOX, because the phone rule below sets `width: 100%`.
     There is no global box-sizing reset in this stylesheet, so a content-box
     button asked for 100% width measured 100% PLUS its own 26px of horizontal
     padding on each side — 52px wider than the container it was told to fill.
     Every full-width button on a phone therefore pushed its page 15px sideways
     inside a padded card, which is a horizontal document scroll on a surface
     that has no business having one. Found on the rebuilt error page at 390px
     and fixed here rather than there, because the arithmetic was never that
     page's: it applies to every `.nx-btn` the product renders below 560px.
     Nothing that already fitted changes -- a button whose width is `auto` is
     laid out identically either way. */
  box-sizing: border-box;
}
.nx-btn:hover { background: var(--nx-ivory-hover); color: var(--nx-canvas); }

.nx-btn--ghost {
  background: none;
  border: 1.5px solid var(--nx-outline);
  color: var(--nx-text);
}
.nx-btn--ghost:hover { background: none; border-color: var(--nx-muted); color: var(--nx-text); }

.nx-btn--tool {
  background: none;
  border: 1px solid var(--nx-blue-line);
  color: var(--nx-blue-text);
  font-size: 13.5px;
  padding: 9px 16px;
}
.nx-btn--tool:hover { color: var(--nx-blue-hover); border-color: var(--nx-blue-hover); background: none; }

/* `--sm` is a SMALLER LABEL, not a smaller target. It shipped at 38px and put
   every small Train and Technology action under the floor; the owner ruled
   (2026-09-06) that 44x44 has no exceptions, so the modifier keeps its 13.5px
   label and its tighter padding and takes the touch floor like every other
   button. Recorded as an accessibility correction to the approved goldens,
   not a redesign: nothing about the hierarchy changes, only the hit box. */
.nx-btn--sm { font-size: 13.5px; padding: 9px 18px; min-height: var(--nx-touch); }

/* --- Button states (ported from the design-system layer, 2026-09-06) --------
   Two facts that used to share one word. An action that must not EXIST for
   this account is ABSENT -- never greyed, never explained; that rule is
   unchanged. An action that exists but cannot fire YET (invalid form, nothing
   picked) is DISABLED: it stays visible so the shape of the form survives,
   pairs with `aria-disabled` (never the `disabled` attribute, which hides it
   from a screen reader's tab order) and states its reason beside it.
   A button in flight keeps its full strength and lets the label carry the
   state; it is never greyed and never removed mid-action. */
.nx-btn--busy { cursor: progress; }
.nx-btn--busy::after {
  content: "";
  display: inline-block;
  min-inline-size: 1.2em;
  text-align: start;
  animation: nx-ellipsis 1.2s steps(1, end) infinite;
}
.nx-btn--disabled,
.nx-btn--disabled:hover {
  background: none;
  border: 1.5px solid var(--nx-border-2);
  color: var(--nx-faint);
  cursor: not-allowed;
}
@keyframes nx-ellipsis {
  0% { content: ""; }
  25% { content: "·"; }
  50% { content: "· ·"; }
  75% { content: "· · ·"; }
  100% { content: "· · ·"; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-btn--busy::after { animation: none; content: "…"; }
}

/* --- Chips --------------------------------------------------------------- */
/* Role, not color: neutral fact · live-cyan · selected-orange · signal-gold ·
   danger-red · alliance-blue · personal-olive. Shape/glyph carries meaning
   alongside color (● ▲ ◌), never color alone. */
.nx-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: 500 11px var(--nx-font-body);
  color: var(--nx-support);
  background: var(--nx-canvas);
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius-sm);
  padding: 3px 9px;
  white-space: nowrap;
}
.nx-chip--mono { font: 400 10.5px var(--nx-font-mono); }
.nx-chip--live {
  color: var(--nx-cyan);
  background: var(--nx-cyan-tint);
  border-color: var(--nx-cyan-line);
}
.nx-chip--selected {
  color: var(--nx-orange);
  background: var(--nx-orange-tint);
  border-color: var(--nx-orange);
}
.nx-chip--signal {
  color: var(--nx-gold);
  background: var(--nx-gold-tint);
  border-color: var(--nx-gold-line);
}
.nx-chip--danger {
  color: var(--nx-red-text);
  background: var(--nx-red-tint);
  border-color: var(--nx-red-line);
}
.nx-chip--alliance {
  color: var(--nx-blue-text);
  background: var(--nx-blue-tint);
  border-color: var(--nx-blue-line);
}
.nx-chip--me {
  color: var(--nx-olive-text);
  background: var(--nx-olive-tint);
  border-color: var(--nx-olive-line);
}

/* --- Cards --------------------------------------------------------------- */
.nx-card {
  background: var(--nx-surface);
  border: 1px solid var(--nx-border);
  border-radius: var(--nx-radius-card);
  transition: border-color 0.15s;
}
a.nx-card { display: block; color: inherit; }
a.nx-card:hover, .nx-card--hover:hover { border-color: var(--nx-outline); color: inherit; }
.nx-card--me { border-block-start: 2px solid var(--nx-olive); }
.nx-card--alliance { border-block-start: 2px solid var(--nx-blue); }

/* Context / attention rails. Orange = command decision; gold = governance /
   stated rule; olive = personal; blue = alliance; neutral = stale. */
.nx-rail {
  background: var(--nx-surface);
  border: 1px solid var(--nx-border);
  border-inline-start: 3px solid var(--nx-outline);
  border-radius: var(--nx-radius);
  padding: 10px 14px;
}
.nx-rail--decision { border-inline-start-color: var(--nx-orange); }
.nx-rail--governance {
  border-inline-start-color: var(--nx-gold);
  border-color: var(--nx-gold-line);
  background: var(--nx-gold-tint);
}
.nx-rail--me { border-inline-start-color: var(--nx-olive); }
.nx-rail--alliance { border-inline-start-color: var(--nx-blue); }

.nx-notice {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 400 12.5px/1.6 var(--nx-font-body);
  color: var(--nx-body);
}

/* --- Forms ---------------------------------------------------------------- */
.nx-body label { font: 500 12.5px var(--nx-font-body); color: var(--nx-text); }
.nx-input {
  background: var(--nx-inset);
  border: 1px solid var(--nx-border-2);
  border-radius: 8px;
  color: var(--nx-text);
  font: 400 13.5px var(--nx-font-body);
  padding: 10px 12px;
  min-height: var(--nx-touch);
  width: 100%;
  box-sizing: border-box;
}
.nx-input:focus-visible { outline: none; border: 1.5px solid var(--nx-focus); }
.nx-input[aria-invalid="true"] { border: 1.5px solid var(--nx-red); }
.nx-field-error {
  color: var(--nx-red-text);
  font: 400 12px/1.6 var(--nx-font-body);
  margin-block-start: 4px;
}

/* --- Provenance marks ------------------------------------------------------ */
/* Shape first, color never required (handoff: ■ known · ◐ observed ·
   ◪ projected · ◌ unknown). Inline squares, sized to ride beside text. */
.nx-prov {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2.5px;
  vertical-align: -1px;
  flex: none;
}
.nx-prov--known { background: var(--nx-text); }
.nx-prov--observed {
  border: 1.5px solid var(--nx-gold);
  background: linear-gradient(90deg, var(--nx-gold) 50%, transparent 50%);
  box-sizing: border-box;
}
.nx-prov--projected {
  border: 1.5px solid var(--nx-muted);
  background: repeating-linear-gradient(45deg, var(--nx-faint) 0 2px, transparent 2px 4px);
  box-sizing: border-box;
}
.nx-prov--unknown {
  border: 1.5px dotted var(--nx-muted);
  box-sizing: border-box;
}

/* --- Rows / lists ----------------------------------------------------------- */
.nx-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--nx-surface);
  border: 1px solid var(--nx-border);
  border-radius: var(--nx-radius);
  padding: 11px 14px;
  min-height: var(--nx-touch);
  transition: border-color 0.15s;
}
a.nx-row { color: inherit; }
a.nx-row:hover { border-color: var(--nx-outline); color: inherit; }
.nx-row-title { font: 600 13px var(--nx-font-body); color: var(--nx-text); flex: none; }
.nx-row-caption { font: 400 12px/1.6 var(--nx-font-body); color: var(--nx-support); flex: 1; }
.nx-row-arrow { color: var(--nx-orange); font-weight: 600; flex: none; }
/* The onward glyph points where the row goes: forward is inline-end. */
[dir="rtl"] .nx-row-arrow { display: inline-block; transform: scaleX(-1); }

/* THE ROW STACKS BEFORE IT OVERFLOWS (owner correction, 2026-08-28).
 *
 * Title, caption and arrow sit on one line, and the title is `flex: none` —
 * so a long one cannot shrink and the row simply runs off the page. Stores'
 * "Mall — Packs & Weekly shelves" is 196px of a 256px row; with the 12px gaps,
 * the 14px arrow and the row's own padding it demanded 308px and pushed the
 * DOCUMENT 7px past a 320px phone (5px in Arabic, where the label is shorter).
 *
 * Below 561px the row becomes two lines: what it is and where it goes on the
 * first, what it does underneath. The caption is the natural thing to move —
 * it is the sentence, and it is the child that was being squeezed to nothing.
 * The title can shrink and break a word as a last resort, so no label length
 * can overflow again.
 *
 * INERT WHERE ROWS ALREADY FIT: `flex-wrap` changes nothing until a line
 * overflows, and only four templates render this component — Stores, the
 * Technology index, a plan phase and the Technology directory. All four are
 * measured at ten widths in both locales. */
@media (max-width: 560px) {
  .nx-row { flex-wrap: wrap; }
  .nx-row-title { flex: 0 1 auto; min-width: 0; overflow-wrap: break-word; }
  /* ORDER, not DOM order: the caption is the second child, so a full-basis
   * caption would push the ARROW onto a third line of its own. The arrow
   * joins the title on the first line and the caption takes the second, while
   * the markup — and so the reading order — is untouched. */
  .nx-row-arrow { order: 1; margin-inline-start: auto; }
  .nx-row-caption { order: 2; flex: 1 0 100%; min-width: 0; }
}

/* Section label + divider line. */
.nx-section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.nx-section-line { flex: 1; height: 1px; background: var(--nx-border-2); min-width: 40px; }

/* --- Access / empty states --------------------------------------------------- */
/* An access state is a FACT row: what is true, no upsell, no disabled controls. */
.nx-access-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 400 12.5px/1.7 var(--nx-font-body);
  color: var(--nx-support);
  background: var(--nx-surface);
  border: 1px solid var(--nx-border);
  border-radius: var(--nx-radius);
  padding: 11px 14px;
}
.nx-access-row svg { flex: none; color: var(--nx-muted); }

.nx-empty {
  border: 1px dashed var(--nx-border);
  border-radius: var(--nx-radius-card);
  padding: 18px;
  color: var(--nx-muted);
  font: 400 12.5px/1.7 var(--nx-font-body);
  text-align: center;
}

/* --- Loading / busy (ported from the design-system layer, 2026-09-06) ------
   A mono muted read-out with a live ellipsis and DELIBERATELY NO FRAME: dashed
   and dotted stay reserved for absence and unknown. Never a spinner, never a
   fake percentage. `role="status"` in the markup. Only where the product
   really exposes a wait -- the calculators keep their last valid answer and
   must not grow one of these. */
.nx-busy {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  font: 500 11.5px var(--nx-font-mono);
  color: var(--nx-muted);
}
.nx-busy::after {
  content: "";
  display: inline-block;
  min-inline-size: 1.4em;
  text-align: start;
  animation: nx-ellipsis 1.2s steps(1, end) infinite;
}
.nx-busy--block {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 18px;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius-card);
  background: var(--nx-surface);
  inline-size: 100%;
  box-sizing: border-box;
}
@media (prefers-reduced-motion: reduce) {
  .nx-busy::after { animation: none; content: "· · ·"; }
}

/* --- The inline failure rail (ported, 2026-09-06) ---------------------------
   `.nx-error` is the rail that appears INSIDE a page the reader is already on:
   live data could not be read, the figures below are the last complete set.
   It interrupts, so it carries `role="alert"` in the markup. The full-page
   request-error surface is a different component (`.nx-error-page`, in
   navy/error.css): the reader navigated there on purpose, so it announces as
   `status`. The two used to share this one name, which is how the ruling that
   "both are correct" came to read as a contradiction. Red is the danger role
   and it is a tint and a line here, never a fill. */
.nx-error {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--nx-red-tint);
  border: 1px solid var(--nx-red-line);
  border-radius: var(--nx-radius);
  padding: 11px 14px;
  color: var(--nx-red-text);
  font: 400 12.5px/1.6 var(--nx-font-body);
}
.nx-error svg { flex: none; margin-block-start: 1px; }
.nx-error-title {
  margin: 0 0 2px;
  font: 600 12.5px var(--nx-font-body);
  color: var(--nx-red-text);
}
.nx-error > p { margin: 0; }

/* --- Role badge (Batch 6 B4, built 2026-09-06) -------------------------------
   WHO SOMEONE IS, not what state something is in. Squared to the small radius,
   a hairline border, no fill, no tint, no dot: the NAME carries the role and
   colour only reinforces it, so "Officer" reads the same in greyscale, to a
   colour-blind reader and to a screen reader. Gold is the rare-signal role and
   Owner is exactly that -- one per alliance; alliance blue for Officer, drawn
   at full saturation on the border because the line token measured 1.26:1
   against the neutral border and was imperceptible; Member takes no colour,
   because most people are members and colouring the majority says nothing.
   Never by size, weight or order: a roster sorts by name and the badge must
   not fight it.

   Distinct on purpose from the legacy `.role-badge` (Compare's aria-hidden
   A/D letter tile in app.css) and `.role-pills`: class selectors match whole
   tokens, so neither legacy rule can reach an element carrying `.role`, and
   this rule cannot reach theirs. */
.role {
  display: inline-flex;
  align-items: center;
  min-block-size: 22px;
  padding: 2px 8px;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius-sm);
  background: none;
  color: var(--nx-support);
  font: 500 11.5px/1.4 var(--nx-font-body);
  white-space: nowrap;
}
.role--owner { border-color: var(--nx-gold); color: var(--nx-gold); }
.role--officer { border-color: var(--nx-blue); color: var(--nx-blue-text); }

/* --- Game art ------------------------------------------------------------------ */
/* The game's own art is semantic recognition: never hue-shifted, never
   cover-cropped (sprites are non-square — tokens/game-art.css contract). */
.nx-art { object-fit: contain; flex: none; }

/* --- Asset frame (V2 shared primitive) --------------------------------------- */
/* One framed tile for a piece of game art, shared by the approved V2 surfaces
   (Product Center area cards, Stores product/setup tiles, the future avatar
   picker). The frame is chrome only: the art inside keeps the .nx-art contract
   above — contained, never cropped, never stretched. Sized as a box so a grid
   of tiles ranks evenly whatever shape each sprite is. */
.nx-asset {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 48px;
  block-size: 48px;
  padding: 4px;
  background: var(--nx-canvas);
  border: 1px solid var(--nx-border-2);
  border-radius: 10px;
  flex: none;
}
.nx-asset > img {
  display: block;
  max-inline-size: 100%;
  max-block-size: 100%;
  object-fit: contain;
}
.nx-asset--lg { inline-size: 64px; block-size: 64px; border-radius: 12px; }
.nx-asset--xl { inline-size: 92px; block-size: 92px; border-radius: 14px; padding: 6px; }
/* The honest gap: art we do not hold renders as a DESIGNED absence — dotted
   frame, muted glyph supplied by the markup (e.g. ◌) — never a broken image
   and never a borrowed sibling's art (fallback contract: a stand-in must not
   misrepresent a different game object). Muted = unknown, per the color
   semantics; nothing here claims liveness or selection. */
.nx-asset--empty {
  border-style: dotted;
  border-color: var(--nx-border);
  color: var(--nx-muted);
  font: 600 16px var(--nx-font-body);
}

/* --- Responsive ------------------------------------------------------------------ */
@media (max-width: 1024px) {
  .nx-shell { padding-inline: 22px; }
}

/* THE PHONE MENU of the public masthead (Homepage V3, 2026-09-10). Hidden
   wherever the masthead nav itself is shown; below 560px it is the one route
   from the shell to Nexus Pro, Alliance Core, Pricing and sign-in. A details
   element: no script, and the language menu beside it works the same way. */
.nx-masthead-menu { display: none; position: relative; }
.nx-masthead-menu > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  min-width: 44px;
  padding: 0 10px;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius-sm);
  background: var(--nx-surface);
  color: var(--nx-text);
  font: 600 13px/1 var(--nx-font-body);
  cursor: pointer;
}
.nx-masthead-menu > summary::-webkit-details-marker { display: none; }
.nx-masthead-menu-icon { display: inline-flex; flex-direction: column; gap: 3px; width: 16px; }
.nx-masthead-menu-icon i { display: block; height: 2px; border-radius: 1px; background: currentColor; }
.nx-masthead-menu-list {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(100% + 8px);
  min-inline-size: 220px;
  display: flex;
  flex-direction: column;
  padding: 6px;
  border: 1px solid var(--nx-border);
  border-radius: var(--nx-radius-card);
  background: var(--nx-elevated);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
  z-index: 30;
}
.nx-masthead-menu-list a {
  display: block;
  min-height: 44px;
  padding: 11px 14px;
  border-radius: var(--nx-radius-sm);
  color: var(--nx-text);
  font: 600 14px/1.5 var(--nx-font-body);
  text-decoration: none;
}
.nx-masthead-menu-list a:hover { background: var(--nx-surface); }
.nx-masthead-menu-list a:focus-visible { outline: 2px solid var(--nx-focus); outline-offset: -2px; }

@media (max-width: 560px) {
  .nx-shell { padding-inline: 16px; }
  .nx-masthead-inner { gap: 12px; min-height: 52px; }
  .nx-masthead-nav, .nx-masthead-sep { display: none; }
  .nx-masthead-menu { display: block; }
  .nx-masthead-menu-word { display: none; }
  /* 6px keeps the icon controls and sign-in action within a 375px masthead. */
  .nx-masthead-end { gap: 6px; min-width: 0; }
  .nx-masthead-end .nx-btn { display: none; }
  .nx-masthead-end .language-menu-word,
  .nx-masthead-end .language-menu-sep { display: none; }
  /* The desktop icon is 40px; the phone touch box is 44px without changing
     the centered 22px globe. */
  .nx-masthead-end .language-menu summary,
  .nx-masthead-end .nx-masthead-signin,
  .nx-masthead-end .nx-identity,
  .nx-signout button { min-height: var(--nx-touch); }
  /* A FLEX ROW ONLY SHRINKS IF ITS CHILDREN ARE ALLOWED TO. Each item's default
     min-width is auto -- the width of its own text -- so a long label makes the
     row wider than the masthead instead of making itself narrower, and the page
     scrolls sideways. That is what "Português (Brasil)" and "Se déconnecter"
     each did, in one locale apiece and in no other. min-width: 0 lets the row
     shrink; the ellipsis is what the reader sees when it has to. */
  .nx-masthead-end > * { min-width: 0; }
  .nx-masthead-end .language-menu { flex: 0 0 44px; }
  .nx-masthead-end .language-menu > summary {
    inline-size: 44px;
    block-size: 44px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
  }
  /* The form is the flex item, the button is inside it, and a flex item does
     not shrink below the intrinsic width of its own contents unless BOTH are
     told they may. Constraining only the button left French 5px over. */
  .nx-masthead-end .nx-signout { min-width: 0; overflow: hidden; }
  /* THE LABEL WRAPS INSTEAD OF BEING CUT. Measured at 375px on all ten launch
     locales: French was 24px short of its own text, Brazilian Portuguese 9 and
     Spanish 8, so three of the ten read "Se déconnect…" while the line above
     claimed nothing was abbreviated. Squeezing the padding further only ever got
     French within 3px, because a 216px row already carries a protected autonym
     and the identity chip.

     Two lines fit the button it already has. The button grows 44px -> 59px in
     French and the MASTHEAD DOES NOT MOVE: measured height delta 0 in all ten
     locales, because 44px was never the row's constraint. The ellipsis caps stay
     as the guard for a locale longer than any of these; nothing reaches them
     today. line-height is tightened so the second line lands inside the chrome
     rather than pushing it. */
  .nx-masthead-end .nx-signout button {
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: normal;
    overflow-wrap: break-word;
    line-height: 1.15;
    padding-inline: 6px;
  }
  .nx-btn { width: 100%; }
  .nx-btn--inline { width: auto; }
  /* Authenticated phone navigation: the four-tab bottom bar replaces the
     top tabs (handoff: 4-tab mobile bar). */
  .nx-tabs { display: none; }
  .nx-tabbar { display: flex; }
  /* THE BAR GROWS WITH THE SAFE AREA AND THIS RESERVATION MUST TOO.
     Its height is 1px border + 64px link + max(10px, env(safe-area-inset-bottom)),
     so on a notched phone it is ~99px while a flat 76px reservation leaves the
     last control on every page partly under it. Adding the inset here clears it
     in both cases and never under-reserves.
     A VISIBLE GAP, NOT ONE PIXEL (owner, 2026-09-10). 76px cleared the 75px bar
     by exactly one pixel: the footer's last line sat flush against the bar at
     the end of every page and read as covered. The reservation is the bar
     (75px) plus a 16px gap, so the last card, the last control and the footer
     all end clearly above it once the page is scrolled to its end. */
  .nx-body--tabbar {
    /* THE BAR'S HEIGHT, AS ONE NAME. A page that floats a control of its own
       above the bar (the Spending Planner's cart) offsets from this instead of
       restating the arithmetic; outside this breakpoint the name is unset and
       every var(--nx-tabbar-block, 0px) falls back to nothing. */
    --nx-tabbar-block: calc(65px + max(10px, env(safe-area-inset-bottom, 0px)));
    /* ...AND A PAGE THAT FLOATS A CONTROL OF ITS OWN ABOVE THE BAR names that
       control's height as --nx-page-float on the body (its body_class block),
       so the document scrolls until its last line -- the footer's included --
       clears the bar AND the control. Nothing pads a section for it. */
    padding-block-end: calc(91px + var(--nx-page-float, 0px) + env(safe-area-inset-bottom, 0px));
  }
  .nx-identity-name { display: none; }
}

/* ===== THE MASTHEAD BREAKS ONLY WHERE IT CANNOT FIT ======================
 * (owner defect, 2026-08-30, correcting the previous commit.)
 *
 * `flex-wrap: wrap` on the masthead was right for the case it was added for
 * and wrong everywhere else. Flexbox decides a wrap from an item's CONTENT
 * size, before flex-shrink is applied, so a row that had always fitted by
 * squeezing its sign-out label from 96px to 53px stopped squeezing and broke
 * in two instead. Measured across 20 routes, EN and AR, at 100% zoom:
 *
 *   ar 320  masthead 75px -> 109px, and every page's content 34px lower
 *   ar 375  masthead 75px -> 109px
 *   ar 768  masthead 75px -> 131px
 *   en 320  masthead 71px -> 117px
 *   en 768  masthead 71px -> 117px
 *
 * WHERE THE BOUNDARY IS, measured rather than chosen. On the previous build
 * the masthead clipped itself at exactly three viewport widths - 220px, 187px
 * and 146px, every one of them produced by 200-300% browser zoom - and at none
 * of 320, 375, 440, 560, 768, 1024 or 1440, in either language. No device
 * presents a viewport under 320px; under it the width is the product of zoom,
 * an accessibility zoom, a split view or a dragged window, and the row then
 * genuinely cannot fit however hard it shrinks. 280px sits between the widest
 * width that needs the break (220) and the narrowest that does not (320), so
 * it is inert on every real device and at every normal width in both
 * languages.
 *
 * Everything else that keeps the row inside the page - `min-width: 0` on the
 * row, on the end block and on the wordmark, the wordmark's ellipsis, and the
 * tab bar's shrinking labels - stays unconditional, because shrinking is what
 * the row did correctly all along and none of it moves a layout that fits. */
@media (max-width: 280px) {
  .nx-masthead-inner {
    flex-wrap: wrap;
    row-gap: 4px;
  }
  .nx-masthead-end {
    min-width: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 4px;
  }
  /* AND THE WORDMARK MAY SHORTEN, here and nowhere else. Letting it shrink at
     every width was the same mistake as letting the row wrap at every width:
     the sign-out label had been absorbing the pressure, and once the wordmark
     could shrink it went first. Measured against the parent commit at 100%
     zoom, across all 20 navy routes - "LW Nexus" cut from 109px to 90px in
     Arabic at 320 and 375, and from 115px to 96px in English at 320 and 768.
     A shortened brand on an ordinary phone is not a trade worth making; a
     shortened brand on a 146px viewport, rather than a document that scrolls
     sideways, is. */
  .nx-brand { min-width: 0; }
  .nx-brand-text {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
}

/* ===== THE SIGNED-IN ROW BREAKS RATHER THAN CUTTING A DESTINATION ========
 * (owner defect, 2026-08-30, correcting `2e1dfb9` in the same unpushed range.)
 *
 * The authenticated masthead carries three blocks whose content adds up to a
 * fixed number, measured on the running server and the same in both
 * languages:
 *
 *     wordmark 115 (AR 109) + tabs 293 (AR 311) + end block 386 (AR 374)
 *                           + two 22px gaps  =  838px
 *
 * against an inner width of viewport - 15 (scrollbar) - 44 (shell padding):
 *
 *     900 -> 885  fits        768 -> 753  short 59
 *     899 -> 884  fits        700 -> 685  short 112
 *     820 -> 805  short 33    640 -> 625  short 187
 *                             601 -> 586  short 226
 *
 * The end block is already at its own min-content by 820 (316px: it has given
 * up 70px of sign-out label and identity name and has nothing left), so every
 * pixel of the shortfall was landing on the tabs, and the trailing tab was
 * cut — up to 200% of its width at 640.
 *
 * SO THE ROW BREAKS. Flexbox lays a line out from CONTENT sizes before
 * flex-shrink runs, which is exactly the property that made an unscoped wrap
 * wrong at 320px: there, the row genuinely fitted by shrinking. Here it
 * genuinely does not fit at all, and the same property makes the break land
 * on precisely the right widths without a chosen number — the line holds
 * while brand + tabs + end fit in it and breaks when they do not. That is why
 * this rule names no breakpoint: it is inert at 900 and above and fires from
 * 899 down, because 838 is where the content says it must.
 *
 * AND IT TRACKS THE SIGNED-IN NAME, which a breakpoint could not. The 838
 * includes the reader's own display name; a name six characters longer than
 * the review account's moves the boundary above 900, and this rule follows it
 * while `@media (max-width: 899px)` would have left that reader cut at 1000px.
 *
 * The floor is 561px, where `.nx-tabs` is already `display: none` and the
 * bottom tab bar has taken over: below it the row is the wordmark and the end
 * block alone, that fitted all along, and wrapping it there would be the 320px
 * regression again. The bottom bar is untouched by any of this — it is
 * `position: fixed`, outside this flow entirely.
 *
 * Cost: the masthead is two short rows between 561 and 899, about 44px
 * taller. Nothing is hidden, nothing is truncated, nothing scrolls, no
 * keyboard user loses a focus ring, and no locale needs special handling. */
@media (min-width: 561px) {
  .nx-masthead-inner.nx-shell--tool {
    flex-wrap: wrap;
    row-gap: 4px;
  }
}

/* --- Section bar: a product page's own sections, as real pages -------------
   ONE COMPONENT, TWO PRODUCTS. /heroes/{id} and /drone are each several
   addressable sections now, and both need the same control: a row of real
   links, one per section, the current one clearly held. It lives here rather
   than twice in heroes.css and drone.css because the next product to split
   needs it too, and two copies would drift.

   EVERY TAB IS AN <a href>. Not a button, not a click handler -- it works with
   JavaScript off, it goes in history, it can be copied, bookmarked and opened
   in a new tab. `aria-current="page"` is what says which one you are on; the
   colour only agrees with it.

   ORANGE IS THE SELECTED ROLE (tokens.css). Gold is the Player Progression
   stroke and marks the bar itself; orange marks the one tab you are on. Neither
   is availability and neither is status.

   THE ROW WRAPS, IT NEVER SCROLLS. A scrolling tab rail hides destinations
   behind a gesture with no affordance -- on a phone the third tab simply is not
   there. Wrapping costs a row of height and hides nothing. */
.nx-sectionbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-block: 18px 22px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--nx-border-2);
}
.nx-sectionbar a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-block-size: var(--nx-touch);
  min-inline-size: var(--nx-touch);
  padding: 0.35rem 0.9rem 0.35rem 0.4rem;
  box-sizing: border-box;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius);
  background: var(--nx-inset);
  color: var(--nx-body);
  font: 500 0.88rem/1.25 var(--nx-font-body);
  text-decoration: none;
}
/* The padding is asymmetric because the art sits on the inline-START edge. A
   tab with no art -- Combat Boost, which the game gives no sprite -- pads
   evenly instead, so it sits in the row as a tab rather than as one missing a
   picture. */
.nx-sectionbar a:not(:has(img)) { padding-inline-start: 0.9rem; }
.nx-sectionbar img {
  inline-size: 32px;
  block-size: 32px;
  flex: none;
  border-radius: 6px;
  object-fit: contain;
  display: block;
}
.nx-sectionbar a:hover { border-color: var(--nx-cyan-line); color: var(--nx-text); }
.nx-sectionbar a:focus-visible { outline: 2px solid var(--nx-focus); outline-offset: 2px; }
.nx-sectionbar a[aria-current="page"] {
  border-color: var(--nx-orange);
  background: var(--nx-orange-tint);
  color: var(--nx-text);
  font-weight: 600;
}
/* The held tab also carries a solid edge, so "which section am I on" survives
   greyscale, a colour-blind reader and a bad screen. */
.nx-sectionbar a[aria-current="page"]::before {
  content: "";
  inline-size: 3px;
  align-self: stretch;
  margin-inline-end: 0.15rem;
  border-radius: 2px;
  background: var(--nx-orange);
}
@media (max-width: 560px) {
  .nx-sectionbar { gap: 6px; margin-block: 14px 16px; }
  .nx-sectionbar a { font-size: 0.84rem; padding-inline-end: 0.7rem; }
}

/* ===========================================================================
   THE FOUR THAT ARE BEHAVIOUR (2026-09-03)

   Alliance Core moved to this shell and stopped loading design-system.css and
   app.css, which is the whole point of the shell. Fifty classes lost their
   rules with them. Forty-six are decoration and are being remapped or removed
   separately; these four are not, and they are written here.

   WHY THE LEGACY NAMES AND NOT `nx-` ONES. Every one of these four is rendered
   by markup SHARED with pages still on `base.html` -- `_job_progress.html` is
   included by Compare (navy) and by Analytics (legacy); `status-pill` is named
   in thirty templates, most of them legacy; `table-wrap` in nineteen. Renaming
   the classes in those templates would take the rules away from every legacy
   page that shares them, which is a redesign of pages nobody asked about. So
   the SELECTOR is the markup's and the RULE is navy's: navy tokens, navy
   sizing, navy idiom, and nothing copied from the legacy sheets. When the last
   legacy page moves, these become aliases and can be folded into the `nx-`
   components they already echo.
   =========================================================================== */

/* --- A running job says it is running -------------------------------------
   Compare's whole purpose is to start one, so the card that reports it is not
   decoration. Built as `.nx-card` is built, because that is what it is. */
.lw-job-chip {
  background: var(--nx-surface);
  border: 1px solid var(--nx-cyan-line);
  border-radius: var(--nx-radius-card);
  padding: 14px 16px;
  margin-block: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.lw-job-chip-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px 14px;
  flex-wrap: wrap;
}
.lw-job-chip-head h2,
.lw-job-chip-head h3 {
  margin: 0;
  font: 600 15px var(--nx-font-display);
  color: var(--nx-text);
}
.lw-job-chip-step {
  margin: 0;
  font: 400 12.5px/1.6 var(--nx-font-body);
  color: var(--nx-body);
}
.lw-job-chip-note {
  margin: 0;
  font: 400 11.5px/1.6 var(--nx-font-body);
  color: var(--nx-muted);
}
/* The figures a reader watches move. Mono, because they are counters. */
.lw-job-chip-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  font: 400 12px var(--nx-font-mono);
  color: var(--nx-support);
}
.lw-job-chip-figures .lw-numeric { color: var(--nx-text); }
.lw-job-chip-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* The track. Cyan is this layer's "live" tone -- the same one `.nx-chip--live`
   uses -- so a moving bar and a "running" pill agree without being told to. */
.progress-track {
  position: relative;
  inline-size: 100%;
  block-size: 6px;
  border-radius: 999px;
  background: var(--nx-inset);
  overflow: hidden;
}
.progress-track > span {
  display: block;
  block-size: 100%;
  background: var(--nx-cyan);
  transition: inline-size 0.3s ease;
}

/* --- Numbers keep their strip ---------------------------------------------
   A figure without its tile is loose text: nothing says where the number stops
   and the next label starts. The band wraps rather than scrolls, because these
   are three to six short counters and a phone should stack them. */
.metric-band {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-block: 12px;
}
.metric-tile {
  flex: 1 1 140px;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  background: var(--nx-inset);
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius);
}
.metric-tile-term {
  font: 600 10.5px var(--nx-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nx-muted);
}
[dir="rtl"] .metric-tile-term { letter-spacing: 0; }
.metric-tile-value {
  font: 600 20px var(--nx-font-display);
  color: var(--nx-text);
}
/* THE TONE IS ON THE VALUE, NOT THE WHOLE TILE. The macro's own comment says
   the tone is decoration on top of the label and must never be the only thing
   carrying the meaning, so it colours the figure and leaves the label alone. */
.metric-tile[data-tone="warning"] { border-color: var(--nx-gold-line); }
.metric-tile[data-tone="warning"] .metric-tile-value { color: var(--nx-gold); }
.metric-tile[data-tone="danger"] { border-color: var(--nx-red-line); }
.metric-tile[data-tone="danger"] .metric-tile-value { color: var(--nx-red-text); }
.metric-tile[data-tone="success"] { border-color: var(--nx-olive-line); }
.metric-tile[data-tone="success"] .metric-tile-value { color: var(--nx-olive-text); }

/* --- Three verdicts, three colours ----------------------------------------
   THIS IS `.nx-chip` UNDER THE NAME THE MARKUP SHIPS. Same tokens, same
   geometry, same variants: `is-danger` is `--danger`, `is-warning` is
   `--signal` (gold is this layer's attention tone), `is-selected` is
   `--selected`. It is written out rather than remapped in the templates
   because thirty templates share the name and most are still on base.html.

   One colour for three verdicts is a wrong answer rendered confidently: a
   merge row that says "excluded" must not look like one that says "kept". */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--nx-border-2);
  background: var(--nx-canvas);
  color: var(--nx-support);
  font: 500 11px var(--nx-font-body);
  white-space: nowrap;
}
.status-pill.is-danger {
  color: var(--nx-red-text);
  background: var(--nx-red-tint);
  border-color: var(--nx-red-line);
}
.status-pill.is-warning {
  color: var(--nx-gold);
  background: var(--nx-gold-tint);
  border-color: var(--nx-gold-line);
}
.status-pill.is-selected {
  color: var(--nx-orange);
  background: var(--nx-orange-tint);
  border-color: var(--nx-orange);
}
/* `is-verified` is the fourth verdict and it is NOT the retired tier word: on
   a pill it means completed, active, current -- a job that finished, a link
   that is not revoked, the locale you are reading. Deleting it would flatten
   "completed" into "failed", so it keeps its meaning under this layer's
   positive tone and the WORD is what should change, not the state. */
.status-pill.is-verified {
  color: var(--nx-olive-text);
  background: var(--nx-olive-tint);
  border-color: var(--nx-olive-line);
}

/* --- A wide table scrolls in its own box ----------------------------------
   Not the document. A history table that pushes the page sideways is the
   horizontal-scroll defect rejected on the technology tree, and it is worse
   here: the masthead and the footer go with it.

   `tabindex="0"` belongs on the element in the markup so a keyboard can reach
   the scroll; this gives that focus somewhere visible to land. */
.table-wrap {
  inline-size: 100%;
  max-inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius);
  background: var(--nx-surface);
}
.table-wrap:focus-visible {
  outline: 2px solid var(--nx-focus);
  outline-offset: 2px;
}
.table-wrap > table {
  inline-size: 100%;
  border-collapse: collapse;
}

/* ===========================================================================
   THE REMAPS (2026-09-03)

   Same rule as the four above: the SELECTOR is the markup's, because these
   names are shared with pages still on `base.html`, and the RULE is navy's.
   Nothing here is a new component -- each block is an existing `nx-` one under
   the name the templates ship, so when the last legacy page moves they fold
   into it and disappear.
   =========================================================================== */

/* --- Ten names, one message ------------------------------------------------
   `alert`, `error`, `info`, `success`, `warning`, `notice`, `success-state`
   and `search-status` are eight names for the same object: a line of text
   telling the reader what just happened. `is-danger` and `is-warning` are two
   more, used as modifiers on it. This layer has ONE message component,
   `.nx-notice`, and this is it -- the base is `.nx-notice`'s and the tone is
   `.nx-chip`'s, which is where every other coloured statement in the layer
   gets its red, gold and olive.

   A tone is a border and a colour, never a background wash: a message is
   prose, and prose on a tinted block is harder to read than prose with a
   coloured edge. */
.alert,
.notice,
.success-state,
.search-status {
  /* A COLUMN, NOT A ROW. These carry a `<strong>` title above a `<p>`, or a
     `<ul>` of reasons; laid out as a row the title sits beside its own
     explanation and both wrap into narrow ribbons. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  margin-block: 10px;
  padding: 10px 12px;
  border: 1px solid var(--nx-border-2);
  border-inline-start-width: 3px;
  border-radius: var(--nx-radius);
  background: var(--nx-surface);
  font: 400 12.5px/1.6 var(--nx-font-body);
  color: var(--nx-body);
}
.alert > *, .notice > *, .success-state > * { margin: 0; }

/* The tones. `.error` and `.is-danger` are the same tone under two names
   because the templates use both; likewise `.warning` and `.is-warning`. */
.alert.error, .notice.error, .alert.is-danger, .notice.is-danger,
.error.search-status {
  border-color: var(--nx-red-line);
  border-inline-start-color: var(--nx-red);
  color: var(--nx-red-text);
}
.alert.warning, .notice.warning, .alert.is-warning, .notice.is-warning,
.warning.search-status {
  border-color: var(--nx-gold-line);
  border-inline-start-color: var(--nx-gold);
  color: var(--nx-gold);
}
.alert.success, .notice.success, .success-state {
  border-color: var(--nx-olive-line);
  border-inline-start-color: var(--nx-olive);
  color: var(--nx-olive-text);
}
.alert.info, .notice.info {
  border-color: var(--nx-cyan-line);
  border-inline-start-color: var(--nx-cyan);
  color: var(--nx-body);
}

/* --- Structural, and each one is an `nx-` component under another name -----
   `.panel` is `.nx-card`. `.panel-header` is `.nx-section-head`.
   `.empty-state` is `.nx-empty`. `.section-label` is `.nx-kicker`.
   `.button` is `.nx-btn`, `.ghost` is `.nx-btn--ghost`, `.secondary` is
   `.nx-btn--tool`. Written out, not aliased with `@extend`, because CSS has
   no `@extend` and a comma-joined selector on the original rule would put
   legacy vocabulary inside the `nx-` blocks. */
.panel {
  background: var(--nx-surface);
  border: 1px solid var(--nx-border);
  border-radius: var(--nx-radius-card);
  padding: 14px 16px;
  margin-block: 12px;
}
.panel-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-block-end: 10px;
}
.panel-header h2, .panel-header h3 {
  margin: 0;
  font: 600 15px var(--nx-font-display);
  color: var(--nx-text);
}
.empty-state {
  border: 1px dashed var(--nx-border);
  border-radius: var(--nx-radius-card);
  padding: 18px;
  color: var(--nx-muted);
  font: 400 12.5px/1.7 var(--nx-font-body);
  text-align: center;
}
.section-label {
  display: block;
  font: 600 10.5px var(--nx-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nx-muted);
}
[dir="rtl"] .section-label { letter-spacing: 0; }

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-block-size: var(--nx-touch);
  padding-inline: 18px;
  box-sizing: border-box;
  border: 1.5px solid transparent;
  border-radius: var(--nx-radius);
  background: var(--nx-text);
  color: var(--nx-canvas);
  font: 600 14px var(--nx-font-body);
  text-decoration: none;
  cursor: pointer;
}
.button:hover { background: var(--nx-ivory-hover); }
.button:focus-visible { outline: 2px solid var(--nx-focus); outline-offset: 2px; }
.button.secondary {
  background: none;
  border-color: var(--nx-blue-line);
  color: var(--nx-blue-text);
}
.button.secondary:hover { border-color: var(--nx-blue-hover); color: var(--nx-blue-hover); background: none; }
.button.ghost {
  background: none;
  border-color: var(--nx-outline);
  color: var(--nx-text);
}
.button.ghost:hover { background: none; border-color: var(--nx-muted); }

/* --- Two the templates ship that are pure layout --------------------------
   Not components in either vocabulary: a row of buttons and a row of chips.
   They get the one declaration each that makes them a row, so a phone wraps
   them instead of pushing the page sideways. */
.button-row, .chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

/* --- `is-hidden` IS NOT DECORATION, and it was in the remainder list --------
   It is `display: none`, and four field-error paragraphs on Compare plus the
   job chip's error and success boxes carry it in their default state. Without
   the rule those elements RENDER: an empty error line under every picker
   field, and a "done" panel above a job that has not started. That is a
   defect on a live navy page, not a style question, so it is fixed here
   rather than queued -- the only item lifted out of the remainder.

   `!important` is kept from the legacy declaration on purpose: this utility
   has to beat whatever `display` a component gives the element it lands on,
   which is the entire reason a page can rely on it. */
.is-hidden { display: none !important; }

/* AND `[hidden]` IS THE SAME DEFECT WITHOUT A CLASS (2026-09-07).

   The UA stylesheet's `[hidden] { display: none }` has the specificity of one
   attribute selector and comes before every author rule, so any component that
   sets `display` beats it. `.nx-btn` is `display: inline-flex`, so a button the
   markup ships as `hidden` was on the page: `/desert-storm` showed "Create
   Workspace and Import Roster" as its dominant action before the reader had
   searched for an alliance.

   `design-system.css` has carried this rule since before the second shell
   existed; the navy layer inherited none of it, and four sheets had each
   written their own single-component version of it instead. One rule, here. */
[hidden] { display: none !important; }

/* A count beside a heading. `.nx-chip--mono` is exactly this object, and the
   name is not retired vocabulary: it counts rows, it does not verify them. */
.count-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--nx-border-2);
  background: var(--nx-canvas);
  color: var(--nx-support);
  font: 400 10.5px var(--nx-font-mono);
}

/* ===========================================================================
   ALLIANCE CORE'S FORM CONTROLS (2026-09-03)

   Measured on the running server at Arabic 375, after the shell migration:
   Analyzer and Merge Planner inputs 21px, Compare's 33px, several buttons 30px,
   against the Page Contract's 38px floor. Alliance Data, War Room and Migration
   were clean, because they already use this layer's own components.

   THE LOSS SCANNER COULD NOT SEE THIS. It reads `class=` attributes, and the
   inputs that lost the most carry no class at all -- they were sized by a bare
   `input` selector in design-system.css, which this shell deliberately does not
   load. Same blindness as the missing `<script src=>`, and the same lesson:
   a migration report is only as wide as the thing it greps for.

   Same rule as the four behaviour classes above: the SELECTOR is the markup's,
   the RULE is navy's, and each one is an `nx-` component under another name.
   =========================================================================== */

/* `.secondary` used STANDALONE, which the `.button.secondary` remap above never
   matched -- and that is where the 30px buttons came from. Compare and Analyzer
   write `class="secondary"` with no `.button` beside it, so the compound
   selector was simply never true for them. Scoped to elements that are actually
   controls, so a `.secondary` on a paragraph somewhere is left alone. */
button.secondary,
a.secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-block-size: var(--nx-touch);
  padding-inline: 18px;
  box-sizing: border-box;
  border: 1.5px solid var(--nx-blue-line);
  border-radius: var(--nx-radius);
  background: none;
  color: var(--nx-blue-text);
  font: 600 14px var(--nx-font-body);
  text-decoration: none;
  cursor: pointer;
}
button.secondary:hover,
a.secondary:hover { border-color: var(--nx-blue-hover); color: var(--nx-blue-hover); }
button.secondary:focus-visible,
a.secondary:focus-visible { outline: 2px solid var(--nx-focus); outline-offset: 2px; }

/* The shared alliance picker's own field name. `alliance_picker.css` styles only
   its `aria-invalid` state and never sized it, so it fell to the browser default
   at 33px once the legacy sheets went. This is `.nx-input` under the name the
   partial ships, which both pages including that partial are on this shell. */
.lw-picker-field {
  background: var(--nx-inset);
  border: 1px solid var(--nx-border-2);
  border-radius: 8px;
  color: var(--nx-text);
  font: 400 13.5px var(--nx-font-body);
  padding: 10px 12px;
  min-block-size: var(--nx-touch);
  inline-size: 100%;
  box-sizing: border-box;
}
.lw-picker-field:focus-visible { outline: none; border: 1.5px solid var(--nx-focus); }

/* Two of Compare's own controls, named for what they do rather than what they
   are, so neither picked up a button rule. */
.swap-button,
.primary-compare {
  min-block-size: var(--nx-touch);
  box-sizing: border-box;
}

/* `.compact-button` is smaller in LABEL and padding because it sits in dense
   rows of alliance actions -- but not smaller in hit box. It shipped at the old
   38px "contract floor"; the owner ruled (2026-09-06) that 44x44 has no
   exceptions, and the roster's dense rows are the B5 disclosure's problem to
   solve structurally, not this rule's to solve by shrinking a thumb target. */
.compact-button { min-block-size: var(--nx-touch); box-sizing: border-box; }

/* --- Reference navigation (Kit B7, 2026-09-06) ------------------------------
   One component, one prefix, for the long reference pages: a row of jump
   plates to the page's sections and a name filter, with the kit's empty grade
   below the controls when nothing matches. The plates share one min-block-size,
   vertical padding and border-box sizing, so a one-line plate sits at exactly
   the floor and a wrapped name grows past it. A section that exists but holds
   nothing matching is a different fact from a section that does not exist:
   its plate goes DASHED AND UNLINKED (`aria-disabled`, set by
   reference_nav.js) rather than disappearing, so the ladder never shortens.
   At 375 the filter comes first — a reader on a small screen filters far
   more often than jumps. The driver is `static/js/reference_nav.js`
   (`[data-ref-filter]`, `[data-ref-name]`, `[data-ref-group]`,
   `[data-ref-empty]`, `[data-ref-jump]`). */
.nx-refnav { display: grid; gap: 10px; margin-block-end: 16px; }
.nx-refnav-jump { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.nx-refnav-jump a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--nx-touch);
  box-sizing: border-box;
  padding: 9px 14px;
  border: 1px solid var(--nx-border-2);
  border-radius: var(--nx-radius);
  background: var(--nx-surface);
  color: var(--nx-text);
  font: 600 12.5px/1.4 var(--nx-font-body);
  text-decoration: none;
  transition: border-color 0.15s;
}
.nx-refnav-jump a:hover, .nx-refnav-jump a:focus-visible { border-color: var(--nx-outline); color: var(--nx-text); }
.nx-refnav-jump a[aria-disabled="true"] {
  border-style: dashed;
  background: none;
  color: var(--nx-muted);
  pointer-events: none;
}
.nx-refnav-filter label { display: grid; gap: 6px; max-inline-size: 360px; }
.nx-refnav-empty { margin: 0; }
@media (max-width: 560px) {
  .nx-refnav { display: flex; flex-direction: column; }
  .nx-refnav-filter { order: -1; }
  .nx-refnav-filter label { max-inline-size: none; }
}

/* --- Gate ladder (Kit D7, 2026-09-06) ------------------------------------------
   One component, several boards: a threshold you cross, with things behind
   it you can already read. One plate per gate — the label is the host's (a
   stage name, an HQ numeral, a phase name) and the sub-line its span — and
   the ladder WRAPS rather than scrolls at every width, because a scroller
   would put the far end of a long ladder out of sight, which is the whole
   point of a ladder. Plates carry min-block-size, not a height, so a two-line
   Arabic name grows its own plate and keeps the floor.

   Three readiness states, and they are all readiness, never access: `--met`
   takes the filled mark and the olive line; `--here` takes orange and says
   where you are; `--ahead` takes the dotted unknown mark and STAYS A LIVE
   LINK — nothing dims, nothing carries a padlock. The host renders the mark
   (`.nx-prov--known` / `.nx-prov--unknown`) beside the label. A host that
   holds no reader position renders the plates with no state and no mark:
   a mark would be a claim about an account the page cannot see.
   `--nx-gate-rail` lets a host colour a plate's edge with the game's own
   colour for that gate. */
.nx-gates { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; padding: 0; list-style: none; }
.nx-gate { --nx-gate-rail: var(--nx-border-2); display: block; }
.nx-gate > a,
.nx-gate > span {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-block-size: var(--nx-touch);
  box-sizing: border-box;
  padding: 8px 12px;
  border: 1px solid var(--nx-border-2);
  border-inline-start: 3px solid var(--nx-gate-rail);
  border-radius: var(--nx-radius);
  background: var(--nx-surface);
  color: var(--nx-text);
  text-decoration: none;
  transition: border-color 0.15s;
}
.nx-gate > a:hover, .nx-gate > a:focus-visible { border-color: var(--nx-outline); border-inline-start-color: var(--nx-gate-rail); color: var(--nx-text); }
.nx-gate-mark { flex: none; }
.nx-gate-label { font: 600 12.5px/1.4 var(--nx-font-body); }
.nx-gate-sub { font: 400 11.5px/1.4 var(--nx-font-mono); color: var(--nx-support); }
.nx-gate--met > a, .nx-gate--met > span { border-inline-start-color: var(--nx-olive); }
.nx-gate--here > a, .nx-gate--here > span {
  border-color: var(--nx-orange);
  border-inline-start-color: var(--nx-orange);
  background: var(--nx-orange-tint);
  color: var(--nx-orange);
}
html:lang(ar) .nx-gate-sub { font-size: 12px; }

/* --- The destructive action (Batch 3, 2026-09-06) ------------------------------
   Red is the danger role, and it is a BORDER AND A LABEL — never a fill, and
   never the primary. Pairs with `--ghost`; the confirmation that guards a
   destructive form stays where it is (`data-ac-confirm`). The compatibility
   layer never had a rule for the legacy `.button.danger`, which is how War
   Room's Delete rendered as the ivory primary; this is the rule. */
.nx-btn--danger,
.nx-btn--ghost.nx-btn--danger { border-color: var(--nx-red-line-2); color: var(--nx-red-text); background: none; }
.nx-btn--danger:hover, .nx-btn--danger:focus-visible { border-color: var(--nx-red); color: var(--nx-red); background: var(--nx-red-tint); }

/* --- The notice's tones (2026-09-06) ------------------------------------------
   A tone is a BORDER AND A COLOUR, never a wash: the message is prose, and
   prose on a tinted block is harder to read than prose with a coloured edge.
   Framed only when toned -- the bare `.nx-notice` stays the inline line. */
.nx-notice--success, .nx-notice--warning, .nx-notice--info {
  border: 1px solid var(--nx-border); border-inline-start: 3px solid var(--nx-border-2);
  border-radius: var(--nx-radius); padding: 10px 14px;
}
.nx-notice--success { border-color: var(--nx-olive-line); border-inline-start-color: var(--nx-olive); }
.nx-notice--warning { border-color: var(--nx-gold-line); border-inline-start-color: var(--nx-gold); }
.nx-notice--info { border-color: var(--nx-cyan-line); border-inline-start-color: var(--nx-cyan); }

/* --- The navigation prelude: the way out, one link, at the floor -------------
   HERE, not in a product's stylesheet, because `data-page-prelude` is a Page
   Contract element (A1) and fourteen pages across Technology, Desert Storm,
   War Room, Train, Gear, Drone, Survivors and Alliance Data render it. It lived
   in alliance_workspace.css until 2026-09-08, which none of those pages load:
   every one of them showed a bare browser-blue link with no touch target, and
   the ones inside a shell were only saved from the left margin by the shell's
   own padding. */
.ac-prelude { margin: 0 0 4px; }
.ac-prelude a { display: inline-flex; align-items: center; gap: 6px; min-block-size: var(--nx-touch); min-inline-size: var(--nx-touch); color: var(--nx-support); font-size: var(--text-sm); text-decoration: none; }
.ac-prelude a:hover, .ac-prelude a:focus-visible { color: var(--nx-link-hover); text-decoration: underline; }
