/* os-band.css — THE MASTHEAD BAND, shared with the apex.
   ============================================================================
   Loaded AFTER design-tokens.css and BEFORE space.css / admin.css, so a space
   can still override band geometry locally but inherits the band's palette.

   WHAT THIS IS, STATED HONESTLY. This is not a byte-for-byte relocation of the
   apex's `header.os` rules. The apex band and this band carry different markup
   (header.os/.hbar/.hbrand/.hq/.ctl there, .net-bar__* here across 10 files),
   and renaming ten files' worth of selectors to fake a shared file would have
   been a large blast radius bought for nothing visible. What IS shared is the
   part that decides what a reader sees: the two band tokens, the geometry, the
   zone dividers, the control chrome, and the mobile collapse. Those are ported
   from ~/noo/network/src/css/os.css and pinned to it by tests/band_test.php,
   which reads the apex file and fails if the values drift apart. The comment on
   templates/admin/_nav.php used to claim the old bar was "byte-identical to the
   eZine + apex net-bar"; it was not, and had not been since 2026-07-25. A test
   that can fail is worth more than a comment that cannot.

   WHY IT WAS REBUILT (2026-08-07). The old .net-bar was `background:var(--ink)`
   with every child painted `var(--paper)` — the inversion trick the apex threw
   out on 2026-07-25, for a reason that applied here verbatim and went unnoticed
   for six weeks. os.css:1258 records it:

       "It did invert. It did not do it cleanly: on the dark theme --ink
        resolves to pure white, so the band rendered a white slab across the
        top of an all-black page."

   Spaces shipped exactly that. Every dark-mode reader of all 39 live spaces got
   a white slab over a black page, and the 2026-08-07 palette swap (cream -> the
   apex's white/black) sharpened it from #1a1a1a-on-cream to #000-on-white: the
   maximum-contrast version of the wrong idea. The bar also set a background but
   inherited its text colour, so any element added up here without its own color
   was invisible — the same latent trap the apex names.

   The fix is the apex's fix, not a new one. Two explicit tokens:
     --band     the surface, one faint step off the page so it reads as a band
     --on-band  text on it, which is just the page's normal ink
   The band now tracks the page in BOTH themes and the seam is carried by the
   rule and the stuck-state shadow, which is what a seam is for.
   ========================================================================= */

/* ── Band tokens ───────────────────────────────────────────────────────────
   --band / --on-band / --hh are ported from os.css:1276 and design-tokens.css
   :root (apex). The rest are apex token names that spaces-svc does not define;
   rather than invent values, each falls back to this property's equivalent so
   the band renders identically on both sides:
     --soft        -> --muted       (secondary text)
     --line-strong -> --rule-hard   (the stuck-state seam)
     --hover       -> a 5% ink tint, the apex's own formula
   Declared with var(x, fallback) so that IF spaces-svc ever adopts the apex
   names outright, those win automatically and this block stops mattering. */
:root {
  --band:    color-mix(in srgb, var(--ink) 5%, var(--paper));
  --on-band: var(--ink);
  --hh:      56px;   /* apex ships 64px; spaces has no sidenav button, so the band is shorter */
  /* The band's own display face, and the reason it is NOT var(--disp).
     space.css lets a member restyle their space's type: data-space-type="terminal"
     sets --disp to Space Mono, "book" sets it to Lora, "plain" to Figtree. Those
     are choices about THEIR publication, and they were reaching the band and
     restyling the NETWORK wordmark with them — on joe.nicheof.one the brand slot
     rendered in Space Mono while the apex rendered Anton, which is precisely the
     "they look like completely different places" this band exists to fix.
     The band is the landlord's chrome. It is not themeable, so it holds its own
     token that no per-space preset can reach. Same stack as the apex's --disp. */
  --band-disp: 'Anton', 'Arial Narrow', 'Poppins', 'Poppins fallback', 'Arial Black', sans-serif;
  --band-gap: clamp(8px, 1vw, 14px);
}

/* ── The band ──────────────────────────────────────────────────────────────
   Full-bleed out of body's centred measure, then sticky so it survives the
   scroll the way the apex's fixed band does.

   position:sticky, not fixed. The apex band is fixed and pays for it with
   --hh scroll-padding math and a body offset; a space has no sidenav and no
   PWA standalone mode, so sticky gets the same result with no offset to keep
   in sync. NB html{overflow-x:hidden} sits above this: per spec the root's
   overflow propagates to the viewport and the root itself computes to visible,
   so it does NOT become the sticky containing block. Sticky works. Do not
   "fix" that by moving overflow-x onto body — that WOULD break it. */
.net-bar {
  position: sticky; top: 0; z-index: 60;
  /* Full-bleed out of body's centred measure, by MARGIN not by offset.
     The old rule was the classic `left:50%; width:100vw; margin-left:-50vw`,
     which works only under position:relative. Under position:sticky, `left` is
     a STICKY CONSTRAINT (where to stop when scrolled horizontally), not a
     relative offset — so it silently stops contributing and the bar renders
     shifted and clipped instead of full width. Negative inline margins need no
     positioning at all and behave the same under every position value. */
  margin-inline: calc(50% - 50vw);
  background: var(--band);
  color: var(--on-band);
  border-bottom: 1px solid var(--rule);
  transition: border-color .3s, box-shadow .3s;
}
/* JS adds .stuck once the page has scrolled past the band's resting position. */
.net-bar.stuck {
  border-color: var(--rule-hard);
  box-shadow: 0 2px 16px rgb(0 0 0 / .30);
}

/* --measure/--gutter are the READER's tokens and live in space.css; the dashboard
   never defines them. Both carry fallbacks so this file is valid in either context
   — without them the whole declaration would be dropped as invalid-at-computed-value
   in admin.css and the band would lose its padding entirely. admin.css narrows the
   max-width to the studio's 1120px canvas. */
.net-bar__inner {
  max-width: calc(var(--measure, 700px) + var(--gutter, 24px) * 2);
  margin: 0 auto;
  padding: 8px var(--gutter, 24px);
  min-height: var(--hh);
  display: flex; align-items: center; gap: var(--band-gap);
  flex-wrap: nowrap;
}

/* ── Identity ──────────────────────────────────────────────────────────────
   The mark is black artwork (jf.png). On the OLD inverted band it was flipped
   white by default and un-flipped in dark mode. The band now tracks the page,
   so both rules invert: no filter on a light band, invert on a dark one.
   Getting this backwards is invisible in light mode and paints a black mark on
   a black band in dark mode, so it is asserted in tests/band_test.php. */
.net-bar__brand {
  display: inline-flex; align-items: center; gap: 11px;
  flex: none; min-width: 0; text-decoration: none;
}
.net-bar__mark {
  width: 26px; height: 26px; flex: none;
  background: url('/assets/jf.png') center/contain no-repeat;
  filter: none;
}
.net-bar__name {
  font-family: var(--band-disp); font-synthesis-weight: none;
  font-weight: 800; font-size: 1.06rem; letter-spacing: .005em;
  color: var(--on-band); white-space: nowrap;
}
/* A member's space name is arbitrary text, not the 13 characters of "NICHE OF ONE".
   It gets an ellipsis instead of a nowrap row that shoves the search and Follow off
   the right edge — the two controls a reader needs are worth more than the tail of a
   long title, and the full name is in the masthead 60px below anyway. min-width:0 on
   both the flex item and its parent, or the ellipsis never triggers inside a flex
   row: the default min-width:auto refuses to shrink below the content. */
.net-bar__brand { min-width: 0; }
.net-bar__name--space {
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
  max-width: 42vw;
}
/* .net-bar__avatar is RETIRED (2026-08-08). It used to stand in for the network
   mark so a guest's band read as the writer's site; the crumb below replaced that
   job with words, and a member avatar next to the network mark was two icons doing
   one thing in the tightest row on the page. The rule is left deleted rather than
   commented so nothing re-adopts it by accident.

   ── Slot 2 + 2b: brand, then crumb ──────────────────────────────────────────
   The band reads "NICHE OF ONE / Midnight Mass": the network, a separator, the
   room. Two links, not one — the wordmark goes to the apex and the space name goes
   to the space home — which is what makes it a breadcrumb rather than decoration.
   The separator is muted and aria-hidden: it is punctuation, and a screen reader
   announcing "slash" between two labelled links helps nobody. */
.net-bar__home, .net-bar__space {
  display: inline-flex; align-items: center; gap: 11px;
  min-width: 0; text-decoration: none; color: inherit;
}
.net-bar__home:hover .net-bar__name, .net-bar__space:hover .net-bar__name { text-decoration: underline; }
.net-bar__sep {
  flex: none; color: var(--soft, var(--muted));
  font-family: var(--band-disp); font-size: 1.06rem; line-height: 1;
  opacity: .75; margin-inline: 2px;
}

/* Slot 1: the drawer toggle. Same square, same optical weight as the search chip
   on the other side, so the band is bookended rather than lopsided. */
.net-bar__nav {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  background: none; border: 0; border-radius: 8px;
  color: var(--on-band); cursor: pointer;
}
.net-bar__nav:hover { background: var(--hover, color-mix(in srgb, var(--ink) 5%, transparent)); }
.net-bar__nav:focus-visible { outline: 2px solid var(--on-band); outline-offset: 2px; }

/* ── Zone dividers ─────────────────────────────────────────────────────────
   Ported from os.css:1289. A 1px hairline centred in the flex gap, carried as
   a ::before on the zone it precedes — so it disappears WITH its zone at small
   widths instead of orphaning. The apex learned the mobile half of this the
   hard way (os.css:1294): once .hq is display:none, the next divider is
   separating something from nothing and reads as a clipped edge. */
.net-bar__right { position: relative; }
.net-bar__right::before {
  content: ""; position: absolute; top: 50%;
  left: calc(var(--band-gap) / -2 - .5px);
  transform: translateY(-50%);
  width: 1px; height: 26px;
  background: color-mix(in srgb, var(--on-band) 44%, transparent);
  pointer-events: none;
}
.net-bar__right {
  display: inline-flex; align-items: center; gap: 14px;
  flex: none; margin-left: auto;
}

/* ── Control chrome ────────────────────────────────────────────────────────
   Quiet by default — the apex's rule is that the chrome never competes with
   the page's one loud button. */
.net-bar__cta {
  display: inline-flex; align-items: center;
  min-height: 40px; box-sizing: border-box;
  font-family: var(--mono); font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--on-band); opacity: .85;
  padding: 0 4px; border-radius: var(--btn-radius);
  text-decoration: none; white-space: nowrap;
  transition: opacity .15s ease, background .15s ease;
}
.net-bar__cta:hover { opacity: 1; background: color-mix(in srgb, var(--on-band) 8%, transparent); }
.net-bar__cta:focus-visible { outline: 2px solid var(--on-band); outline-offset: 2px; }
/* the owner's primary door reads as the one filled control up here */
.net-bar__cta--own {
  opacity: 1; padding: 0 12px;
  border: 1.5px solid var(--on-band);
  background: var(--on-band); color: var(--band);
}
.net-bar__cta--own:hover { background: var(--on-band); color: var(--band); filter: brightness(1.15); }

/* ── Search trigger ────────────────────────────────────────────────────────
   Stays for EVERY viewer, owner or not: searching this space is a feature of
   this space, not landlord signage. Shaped like the apex's .hq — a quiet field
   tinted off the band rather than a boxed button. */
.net-bar__search {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 36px; box-sizing: border-box;
  padding: 0 11px; border-radius: var(--btn-radius);
  font-family: var(--mono); font-size: .78rem;
  background: color-mix(in srgb, var(--on-band) 11%, transparent);
  border: 1px solid color-mix(in srgb, var(--on-band) 20%, transparent);
  color: var(--on-band); cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.net-bar__search:hover {
  border-color: color-mix(in srgb, var(--on-band) 42%, transparent);
  background: color-mix(in srgb, var(--on-band) 16%, transparent);
}
.net-bar__search:focus-visible { outline: 2px solid var(--on-band); outline-offset: 2px; }
.net-bar__search-icon { flex: none; opacity: .7; }
.net-bar__search-kbd {
  font-family: var(--mono); font-size: .68rem; opacity: .6;
  border: 1px solid color-mix(in srgb, var(--on-band) 30%, transparent);
  border-radius: 4px; padding: 1px 4px; line-height: 1.4;
}

/* ── Dark ──────────────────────────────────────────────────────────────────
   Only the mark's filter flips; --band and --on-band are defined in terms of
   --ink/--paper, so they track the palette on their own. Both selectors are
   needed and neither is redundant: the media query covers OS-dark with no
   cookie, the attribute covers the explicit cookie choice on a light OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .net-bar__mark { filter: invert(1); }
}
:root[data-theme="dark"] .net-bar__mark { filter: invert(1); }

/* ── Mobile ────────────────────────────────────────────────────────────────
   The divider goes first (nothing left of it once the brand collapses), then
   the search label, then the kbd hint. The bar must never wrap: a two-row
   sticky band eats a phone's whole viewport. */
@media (max-width: 720px) {
  .net-bar__right::before { display: none; }
}
@media (max-width: 720px) {
  /* The crumb collapses to "◉ / Midnight Mass". The mark still says which network
     this is; the room name is what the reader actually needs, so the 13 characters
     of the wordmark are the first thing to go and the space name is the last. */
  .net-bar__home .net-bar__name { display: none; }
  .net-bar__home { gap: 0; }
}
@media (max-width: 560px) {
  .net-bar__search-label, .net-bar__search-kbd { display: none; }
  .net-bar__search { padding: 0 9px; }
  .net-bar__inner { gap: 10px; }
  .net-bar__right { gap: 8px; }
  .net-bar__cta { font-size: .68rem; letter-spacing: .06em; }
}
@media (max-width: 400px) {
  /* last thing to go is the secondary CTA; the owner keeps their dashboard door */
  .net-bar__cta:not(.net-bar__cta--own):not(.net-bar__search) { display: none; }
}

/* Touch targets: the apex's 44px floor applies to anything tappable up here. */
@media (pointer: coarse) {
  .net-bar__cta, .net-bar__search { min-height: 44px; }
  .net-bar__nav { min-width: 44px; min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .net-bar { transition: none; }
}
