
.ec-hub, .ec-hub * { box-sizing: border-box; }
.ec-hub {
  min-width: 0;
  color: var(--nilla-text);
  /* Stated rather than inherited, exactly as tier-board.tsx and dossier-rail.tsx
     do it, so a hub surface rendered outside the shell still reads in the app's
     own face. The Tailwind `font-mono` utility is deliberately NOT used: it
     resolves --font-mono, which globals.css points at --font-geist-mono, which
     nothing in this app defines (see the same finding in site-nav.tsx). */
  font-family: var(--font-geist-mono), ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
}

/* ── the plate: the outer element IS the border, as on the card ─────────── */

.ec-hub-panel {
  position: relative;
  padding: 1px;
  background: var(--nilla-border);
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
.ec-hub-panel__in {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--theme-panel-bg);
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
/* A recessed variant for readouts that should read as inset rather than raised. */
.ec-hub-panel[data-tone="well"] .ec-hub-panel__in { background: var(--nilla-bg); }
/* The ONE panel that answers rather than sits: the archive-damage notice. The
   plate goes accent-2 because it is the app answering a condition the operator
   has to act on, not because it is decoration. */
.ec-hub-panel[data-tone="alert"] { background: var(--theme-accent-2); }

/* ── type ───────────────────────────────────────────────────────────────── */

.ec-hub-ml {
  font-size: 10px; font-weight: 700; line-height: 1.2;
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--theme-ink-dim);
}
.ec-hub-ml[data-accent="true"] { color: var(--nilla-accent); }
.ec-hub-h {
  margin: 0;
  font-size: 14px; font-weight: 700; line-height: 1.2;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--nilla-text);
}
.ec-hub-h[data-sub="true"] { font-size: 11px; letter-spacing: 0.18em; }
.ec-hub-p {
  margin: 0;
  font-size: 11px; line-height: 1.55; letter-spacing: 0.02em;
  color: var(--theme-ink-dim);
}
.ec-hub-p strong { color: var(--nilla-text); font-weight: 700; }
.ec-hub-n {
  font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.08em;
  color: var(--nilla-text);
}
/* Spoken, not printed. Namespaced rather than Tailwind's `sr-only` because
   nothing else in this app uses that utility, and a unit that silently stopped
   being hidden would print itself eight times down a 272px rail. The hub owns
   its own stylesheet; it can own this too. */
.ec-hub-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; white-space: nowrap; border: 0; clip-path: inset(50%);
}

/* ── the wall ───────────────────────────────────────────────────────────── */

/* THE CENTRE, ONE SURFACE. Same change as the rails and for the same reason:
   a header floating above a separate scrolling area, both on the page
   background, read as a broken-up middle rather than as the app's main
   container. The wall is now a single framed plate the full height of the
   well, with the header flush inside it above a hairline. */
.ec-hub-wall {
  position: relative;
  display: flex; flex-direction: column;
  height: 100%; min-height: 0;
  padding: 1px;
  background: var(--nilla-border);
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
.ec-hub-wall__in {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0;
  background: var(--theme-panel-bg);
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
.ec-hub-head {
  flex: 0 0 auto;
  display: flex; flex-direction: column; gap: 6px;
  padding: 12px;
  border-bottom: 1px solid var(--nilla-border);
}
.ec-hub-head__row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.ec-hub-figs { display: flex; gap: 14px; flex-wrap: wrap; margin-left: auto; }
.ec-hub-fig { display: flex; gap: 6px; align-items: baseline; }
/* THE SCROLLER. The shell's content well is overflow-hidden (app-shell.tsx:478),
   so a wall rendered as ordinary flow content is clipped at the bottom with no
   scrollbar and no cue — it reads as a layout bug rather than a scroll bug. The
   grid's wrapper owns the overflow so the header stays put while the wall moves. */
.ec-hub-scroll {
  flex: 1 1 auto; min-height: 0; overflow-x: hidden; overflow-y: auto;
  /* The wall's own inset, now that the frame owns the outer edge. */
  padding: 12px;
}
.ec-hub-stack { display: flex; flex-direction: column; gap: 10px; }

/* ── controls ───────────────────────────────────────────────────────────── */

.ec-hub-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 26px; padding: 5px 12px;
  background: var(--nilla-bg); border: 1px solid var(--nilla-border);
  color: var(--nilla-text);
  font: inherit; font-size: 10px; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase; text-decoration: none;
  cursor: pointer;
  transition: box-shadow 140ms ease;
}
.ec-hub-btn:hover { box-shadow: inset 0 0 0 1px var(--theme-accent-2); }
.ec-hub-btn:focus-visible { outline: 2px solid var(--theme-accent-2); outline-offset: -2px; }

/* Density and sort. Two-state toggles in a named group rather than a radiogroup:
   a radiogroup wants a roving tabindex, and these are four and three ordinary
   buttons where the pressed one is simply pressed. */
.ec-hub-seg { display: flex; gap: 2px; }
.ec-hub-seg > button {
  flex: 1 1 0; min-width: 0; min-height: 26px; padding: 5px 2px;
  background: var(--nilla-bg); border: 1px solid var(--nilla-border);
  color: var(--theme-ink-dim);
  font: inherit; font-size: 10px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
  cursor: pointer;
  transition: color 140ms ease, box-shadow 140ms ease;
}
.ec-hub-seg > button:hover { color: var(--nilla-text); box-shadow: inset 0 0 0 1px var(--theme-accent-2); }
/* Declared after :hover so hovering the pressed item keeps the pressed mark
   instead of dropping to the hover ring. Answer light on the perimeter, ink at
   full strength — the same resolution reaction-controls.tsx uses for a vote. */
.ec-hub-seg > button[aria-pressed="true"] {
  color: var(--nilla-text);
  box-shadow: inset 0 -2px 0 0 var(--theme-accent-2);
}
.ec-hub-seg > button:focus-visible { outline: 2px solid var(--theme-accent-2); outline-offset: -2px; }

/* ── rails ──────────────────────────────────────────────────────────────── */

/* ── the rail: ONE frame, full height, sections flush inside ────────────── */
/* This used to be a COLUMN OF PLATES: every section wore its own chamfered
   panel and the column put 10px of air between them, so a rail read as four
   cards floating in a gap rather than as one instrument. The armory's
   RailFrame is the reference — a single bordered aside, one background, and
   content sitting flush with hairlines between sections.

   The outer element IS the border, the same one-pixel-plate trick the cards
   and the old panels use, so the chamfer language is unchanged. What changed
   is that there is now exactly ONE of them per rail. */
.ec-hub-rail {
  position: relative;
  display: flex; flex-direction: column;
  height: 100%; min-height: 0;
  padding: 1px;
  background: var(--nilla-border);
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
.ec-hub-rail__scroll {
  display: flex; flex-direction: column;
  /* FLUSH. The divider below carries the separation; a gap here would put the
     rail's own background between sections and undo the whole change. */
  gap: 0;
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  /* NO SCROLLBAR, AT THE OPERATOR'S DIRECTION. A rail is 248px wide and a
     scrollbar takes 6-8 of them for a control nobody aims at — and it appears
     and disappears as content changes, so the rail's usable width moved under
     the reader. The scroll itself is untouched: wheel, trackpad, drag, arrow
     keys, Page Up/Down and programmatic scrollIntoView all still work, and the
     edge auto-scroll below covers the one thing the bar was really for, which
     is reaching the far end without a wheel.

     Both properties are needed and neither is redundant: scrollbar-width is
     the standard and is what Firefox reads, the ::-webkit- rule is what
     Chromium reads. Overflow stays auto and never hidden, which would take
     the keyboard and the wheel with it. */
  scrollbar-width: none;
  background: var(--theme-rail-bg, var(--theme-panel-bg));
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
  /* Positioned FOR THE LATTICE: RailLattice mounts inside this surface at
     z-index -1, and the clip-path above already makes this a stacking
     context, so the lattice paints over this background and under every
     static section. Without position the layer would anchor to the outer
     rail plate instead. */
  position: relative;
}
.ec-hub-rail__scroll::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* A section inside the frame. Padding moved here from the old panel, so the
   inset from the rail edge is identical to what it was — only the border and
   the air between sections have gone. */
.ec-hub-sec {
  display: flex; flex-direction: column; gap: 6px;
  padding: 12px;
  border-bottom: 1px solid var(--nilla-border);
}
/* No trailing rule against the frame's own bottom edge: two lines 1px apart
   is a seam, not a division. */
.ec-hub-sec:last-child { border-bottom: 0; }
/* The two toned sections keep their meaning without going back to being
   plates. A well is RECESSED — it reads as inset in the frame rather than
   raised out of it. An alert is the app answering a condition the operator has
   to act on, so it takes the answering light, carried as a spine down the
   inside edge rather than as a filled slab: a full accent-2 panel inside a
   rail is the loudest thing on screen for a message that is only a warning. */
.ec-hub-sec[data-tone="well"] { background: var(--nilla-bg); }
.ec-hub-sec[data-tone="alert"] {
  box-shadow: inset 2px 0 0 var(--theme-accent-2);
  background: color-mix(in srgb, var(--theme-accent-2) 7%, transparent);
}
.ec-hub-list { display: flex; flex-direction: column; gap: 1px; margin: 0; padding: 0; list-style: none; }
.ec-hub-link {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  min-height: 26px; padding: 5px 8px 5px 10px;
  color: var(--theme-ink-dim);
  font-size: 10px; font-weight: 700; line-height: 1.2;
  letter-spacing: 0.18em; text-transform: uppercase; text-decoration: none;
  transition: color 140ms ease, background 140ms ease;
}
/* A spine, not a fill. Twelve accent slabs in a rail leaves no light in reserve
   for the thing actually answering the pointer. */
.ec-hub-link::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: var(--theme-accent-2);
  transition: width 140ms ease;
}
.ec-hub-link:hover { color: var(--nilla-text); background: var(--theme-panel-bg); }
.ec-hub-link:hover::before { width: 1px; }
.ec-hub-link[aria-current="page"] { color: var(--nilla-text); background: var(--theme-panel-bg); }
.ec-hub-link[aria-current="page"]::before { width: 2px; }
.ec-hub-link:focus-visible { color: var(--nilla-text); outline: 2px solid var(--theme-accent-2); outline-offset: -2px; }
.ec-hub-link__n { margin-left: auto; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; }

/* ── TOPIC ROWS AND THE FAVOURITE STAR ─────────────────────────────
   A subject row is NOT a link (there is no subject-level destination yet —
   boards carry a category, not a subject), so it gets none of .ec-hub-link's
   affordances: no spine, no pointer, no aria-current. It is a label with one
   control in it. */
.ec-hub-topic {
  position: relative;
  display: flex; align-items: center; gap: 6px;
  min-height: 26px; padding: 4px 4px 4px 10px;
  color: var(--theme-ink-dim);
  font-size: 10px; font-weight: 700; line-height: 1.2;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.ec-hub-topic:hover { color: var(--nilla-text); background: var(--theme-panel-bg); }
.ec-hub-topic__l { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A favourite carries its category, because the label alone does not say which
   of fifteen buckets it came from and two categories may hold the same word. */
.ec-hub-topic__c {
  flex: 0 0 auto; font-size: 7.5px; letter-spacing: 0.16em;
  color: color-mix(in srgb, var(--theme-ink-dim) 76%, transparent);
}
.ec-hub-topic__link {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 7px;
  color: inherit; text-decoration: none;
}
.ec-hub-topic__link:hover { color: var(--nilla-text); }
.ec-hub-topic__link:focus-visible { outline: 2px solid var(--theme-accent-2); outline-offset: -2px; }

/* THE STAR IS ALWAYS IN THE LAYOUT AND ONLY EVER FADED. Revealing it with
   display or width would reflow the row at the moment the pointer is arriving
   at it, which moves the target out from under the hand.

   REVEALED BY :hover AND :focus-within, never hover alone — a control a
   pointer can reach and a keyboard cannot is not a control. A row whose
   subject is already starred keeps it visible at all times: the star is then
   STATE, not an affordance, and hiding state until hover means the rail cannot
   be read at a glance. */
.ec-hub-fav {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 22px; height: 22px; padding: 0; cursor: pointer;
  font: inherit; font-size: 11px; line-height: 1;
  color: var(--theme-ink-dim); background: transparent; border: 0;
  opacity: 0;
  transition: opacity 120ms ease, color 120ms ease;
}
/* THE STARRED STATE IS READ OFF THE BUTTON, NOT OFF ITS ROW — one element, no
   combinator, the control owning its own state. The row keeps its own
   data-starred because the ROW's colour still reads it.

   A NOTE FOR WHOEVER VERIFIES THIS HEADLESS, because it cost an hour: in a
   non-compositing tab (visibilityState hidden, which is what the preview
   harness gives you) Chrome does not re-run style matching when an attribute
   MUTATES. getComputedStyle keeps returning the old value while matches()
   already reports the new selector as matching, so a working rule looks broken.
   An element rendered with the attribute FROM BIRTH styles correctly, which is
   the tell — and is also why a page reload shows the right thing while a click
   appears to do nothing. Do not rewrite a selector to chase this; check it in a
   real window first. */
.ec-hub-topic:hover .ec-hub-fav,
.ec-hub-topic:focus-within .ec-hub-fav { opacity: 1; }
.ec-hub-fav[data-on="1"] { opacity: 1; color: var(--nilla-accent); }
.ec-hub-fav:hover { color: var(--theme-accent-2); }
.ec-hub-fav:focus-visible { opacity: 1; outline: 2px solid var(--theme-accent-2); outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) { .ec-hub-fav { transition: none; } }
@media (forced-colors: active) {
  /* Opacity is not a colour, so forced colours cannot rescue a hidden star.
     Everything stays visible and the state is carried by the glyph itself,
     which is a filled star against an outlined one. */
  .ec-hub-fav { opacity: 1; }
}

.ec-hub-legend { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
.ec-hub-legend li {
  display: flex; align-items: center; gap: 8px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--theme-ink-dim);
}
/* Band colours are DATA and exempt from the two-light rule — they are the
   operator's own classification scheme, not the app's voice. */
.ec-hub-swatch { flex: none; width: 12px; height: 12px; clip-path: polygon(3px 0, 100% 0, 100% calc(100% - 3px), calc(100% - 3px) 100%, 0 100%, 0 3px); }
.ec-hub-legend__n { margin-left: auto; color: var(--nilla-text); font-variant-numeric: tabular-nums; }

/* The standing storage line. Full ink plus an accent-2 spine: never a dimmed
   text alpha, never a red — this app has two lights and neither of them is a
   fault colour. */
.ec-hub-warn { display: flex; gap: 8px; }
.ec-hub-warn::before { content: ""; flex: none; width: 2px; align-self: stretch; background: var(--theme-accent-2); }
.ec-hub-warn p { color: var(--nilla-text); }

@media (prefers-reduced-motion: reduce) {
  .ec-hub-link, .ec-hub-link::before, .ec-hub-seg > button, .ec-hub-btn { transition: none; }
}
/* Forced-colors strips author colour, so every mark that carried meaning has to
   be restated in a system colour or it disappears. */
@media (forced-colors: active) {
  .ec-hub-link[aria-current="page"]::before { background: CanvasText; }
  .ec-hub-seg > button[aria-pressed="true"] { box-shadow: none; outline: 2px solid CanvasText; outline-offset: -3px; }
}
