/* ===========================================================================
   THE PLATFORM'S APP SHELL — one stylesheet, served by the hub, linked by every
   app. Ruled 22 Aug 2026 by Deepesh:

       "Keep the same ProLane logo and branding as the app hub on all apps.
        Apps should not decide on the header branding, the hub does."

   Before this, each app rendered its own header markup with its own CSS. That is
   four places to change a wordmark, four button styles that drift, and — the
   reason it is a rule rather than a preference — an app deciding how the
   PLATFORM identifies itself to a customer. Brand is already externalised for
   the trademark process (ADR 0003/AD-81) and realm-scoped (A18): a realm swaps
   its brand pack and every surface must follow. That is only true if the header
   comes from one place.

   Consumed at the app's OWN origin (`/brand/app-shell.css`), which each app
   already proxies from the platform — so no CORS, no cross-origin stylesheet,
   and no cache that outlives a brand publish.

   CONTAINS NO PRODUCT NAME AND NO ASSET PATH. Marks come from `/api/brand` at
   runtime; this file is layout and treatment only. AD-84 holds: nothing here
   needs to change when the platform is renamed.
   =========================================================================== */

/* THE TOKENS ARE THE HUB'S, VALUE FOR VALUE — copied from `web/app.css`, which
   implements the TTM App Hub UX design handoff. They are NOT re-derived here:
   an approximate second palette is a second design system wearing the first
   one's name, and that is the defect this file exists to remove.
   `--hub-*` aliases follow each token so the hub's own stylesheet is governed
   by this file rather than by a duplicate of the same numbers. */
:root {
  --tsnz-ink:            #242423;
  --tsnz-navy:           #0E2A47;
  /* THE ACTION FILL, separate from `--tsnz-navy` ON PURPOSE.
     `--tsnz-navy` is a TEXT colour in dark mode (it lightens), so a
     primary button using it as a FILL rendered near-white text on a
     near-white fill — one token doing two opposite jobs. A fill and
     the ink on it always travel as a pair. */
  --tsnz-action:         #0E2A47;
  --tsnz-action-hover:   #1B3D61;
  --tsnz-action-pressed: #081D33;
  --tsnz-on-action:      #FFFFFF;
  --tsnz-navy-hover:     #1B3D61;
  --tsnz-navy-pressed:   #081D33;
  --tsnz-muted:          #6B7280;   /* slate-600 */
  --tsnz-line-strong:    #A8B6C6;   /* slate-300 */
  --tsnz-cone:           #FF7400;   /* LOGO MARK ONLY — never body text */
  --tsnz-canvas:         #F5F7F9;
  --tsnz-surface:        #FFFFFF;
  --tsnz-surface-sunken: #EEF1F5;
  --tsnz-line:           #DFE5EC;
  --tsnz-locked-surface: #F5F7F9;
  --tsnz-locked-fg:      #6B7280;
  --tsnz-requested-surface: #E9EEF4;
  --tsnz-requested-fg:   #0E2A47;
  --tsnz-danger:         #B42318;
  --tsnz-ink-2:          #6B7280;
  --tsnz-accent:         #FF7400;
  --tsnz-radius:         8px;       /* radius-control */
  --tsnz-radius-modal:   12px;
  --tsnz-header-h:       56px;      /* topbar-height */
  --tsnz-focus-ring:     0 0 0 2px #FFFFFF, 0 0 0 5px #35648F;
  --tsnz-motion-fast:    120ms;
  --tsnz-motion-standard: 180ms;
  --tsnz-motion-ease:    cubic-bezier(0.2, 0, 0, 1);
  --tsnz-motion:         120ms cubic-bezier(0.2, 0, 0, 1);
  --tsnz-font-brand:     'Archivo', 'Inter', sans-serif;
  --tsnz-font-ui:        'Inter', 'Segoe UI', sans-serif;
  --tsnz-font-mono:      'JetBrains Mono', ui-monospace, monospace;

  /* Aliases — one definition, both vocabularies. */
  --hub-ink: var(--tsnz-ink);
  --hub-navy: var(--tsnz-navy);
  --hub-navy-hover: var(--tsnz-navy-hover);
  --hub-navy-pressed: var(--tsnz-navy-pressed);
  --hub-slate-600: var(--tsnz-muted);
  --hub-slate-300: var(--tsnz-line-strong);
  --hub-cone: var(--tsnz-cone);
  --hub-canvas: var(--tsnz-canvas);
  --hub-surface: var(--tsnz-surface);
  --hub-surface-sunken: var(--tsnz-surface-sunken);
  --hub-border: var(--tsnz-line);
  --hub-border-strong: var(--tsnz-line-strong);
  --hub-danger: var(--tsnz-danger);
  --hub-radius-control: var(--tsnz-radius);
  --hub-radius-modal: var(--tsnz-radius-modal);
  --hub-topbar-height: var(--tsnz-header-h);
  --hub-focus-ring: var(--tsnz-focus-ring);
  --hub-font-brand: var(--tsnz-font-brand);
  --hub-font-ui: var(--tsnz-font-ui);
  --hub-font-mono: var(--tsnz-font-mono);

  /* Which wordmark is shown. Flipped by the dark blocks below; the assets
     themselves are emitted in pairs by the renderer. */
  --tsnz-wm-light-display: block;
  --tsnz-wm-dark-display:  none;
}

/* ---------------------------------------------------------------- dark ---
   THREE STATES, not two. An explicit choice stamps `data-theme` on the root;
   the DEFAULT setting stamps nothing, and only `prefers-color-scheme`
   separates light from dark there. A rule written for one state leaves the
   other with a light wordmark on a dark bar — which is exactly the defect this
   block fixes.

   Every token is DEFINED in the light `:root` above and only REDEFINED here.
   A colour whose only definition lives inside a dark block is invisible in
   light mode, which is the standard way this pattern breaks. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tsnz-ink:            #F2F4F7;
    --tsnz-ink-2:          #C3C9D4;
    --tsnz-muted:          #A8B6C6;   /* the endorsement, and it MUST lighten */
    --tsnz-line:           #2A3340;
    --tsnz-line-strong:    #3A4553;
    --tsnz-surface:        #15171B;
    --tsnz-surface-sunken: #0F1115;
    --tsnz-navy:           #E8EDF4;   /* text-on-dark, not the navy fill */
    --tsnz-navy-hover:     #FFFFFF;
    --tsnz-navy-pressed:   #D5DEEA;
    --tsnz-canvas:         #0F1115;
    --tsnz-action:         #2C4E77;
    --tsnz-action-hover:   #386191;
    --tsnz-action-pressed: #24405F;
    --tsnz-on-action:      #FFFFFF;
    --tsnz-wm-light-display: none;
    --tsnz-wm-dark-display:  block;
    --hub-ink: var(--tsnz-ink);
    --hub-navy: var(--tsnz-navy);
    --hub-slate-600: var(--tsnz-muted);
    --hub-canvas: var(--tsnz-canvas);
    --hub-surface: var(--tsnz-surface);
    --hub-surface-sunken: var(--tsnz-surface-sunken);
    --hub-border: var(--tsnz-line);
    --hub-border-strong: var(--tsnz-line-strong);
  }
}

/* The toggle wins in BOTH directions — dark when chosen on a light OS, and
   light when chosen on a dark OS (that second case is what the media query
   above cannot express on its own). */
:root[data-theme="dark"] {
  --tsnz-ink:            #F2F4F7;
  --tsnz-ink-2:          #C3C9D4;
  --tsnz-muted:          #A8B6C6;
  --tsnz-line:           #2A3340;
  --tsnz-line-strong:    #3A4553;
  --tsnz-surface:        #15171B;
  --tsnz-surface-sunken: #0F1115;
  --tsnz-navy:           #E8EDF4;
  --tsnz-navy-hover:     #FFFFFF;
  --tsnz-navy-pressed:   #D5DEEA;
  --tsnz-canvas:         #0F1115;
  --tsnz-action:     #2C4E77;
  --tsnz-action-hover:   #386191;
  --tsnz-action-pressed: #24405F;
  --tsnz-on-action:    #FFFFFF;
  --tsnz-wm-light-display: none;
  --tsnz-wm-dark-display:  block;
  --hub-ink: var(--tsnz-ink);
  --hub-navy: var(--tsnz-navy);
  --hub-slate-600: var(--tsnz-muted);
  --hub-canvas: var(--tsnz-canvas);
  --hub-surface: var(--tsnz-surface);
  --hub-surface-sunken: var(--tsnz-surface-sunken);
  --hub-border: var(--tsnz-line);
  --hub-border-strong: var(--tsnz-line-strong);
}

/* --------------------------------------------------------------- buttons ---
   THE UNDERLINE RULE. Every "button" that is really an anchor — and the lobby's
   Open buttons, the console's Sign out, and the app entry links all are —
   inherits `text-decoration: underline` from the user agent. It read as a link
   pretending to be a button. Killed here for every app at once, including the
   states, because `:hover` re-introduces it in several UA stylesheets.

   `:where()` keeps specificity at zero so an app can still override a single
   button deliberately, but gets the platform treatment by default. */
:where(a.btn, a.button, button, .btn, .button, [role="button"]),
:where(a.btn, a.button, button, .btn, .button, [role="button"]):hover,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):focus,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):active,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):visited {
  text-decoration: none;
}

:where(.btn, .button, a.btn, a.button) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border-radius: var(--tsnz-radius);
  border: 1px solid transparent;
  background: var(--tsnz-surface);
  color: var(--tsnz-navy);
  font: inherit;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--tsnz-motion), border-color var(--tsnz-motion);
}

:where(.btn.primary, .button.primary) {
  background: var(--tsnz-action); color: var(--tsnz-on-action);
}
:where(.btn.primary, .button.primary):hover { background: var(--tsnz-action-hover); }
:where(.btn.primary, .button.primary):active { background: var(--tsnz-action-pressed); }

:where(.btn.secondary, .button.secondary) {
  background: var(--tsnz-surface);
  border-color: var(--tsnz-line-strong);
  color: var(--tsnz-navy);
}
:where(.btn.secondary, .button.secondary):hover { background: var(--tsnz-surface-sunken); }

:where(.btn.ghost, .button.ghost) { background: none; color: var(--tsnz-navy); }
:where(.btn.ghost, .button.ghost):hover { background: var(--tsnz-surface-sunken); }

:where(.btn.danger, .button.danger) { background: var(--tsnz-danger); color: #fff; }

:where(.btn, .button):disabled,
:where(.btn, .button)[aria-disabled="true"] {
  background: var(--tsnz-line);
  color: var(--tsnz-ink-2);
  cursor: default;
}

/* Keyboard focus stays visible everywhere — WCAG 2.1 AA, and the one thing a
   `text-decoration: none` sweep is most likely to quietly remove. */
:where(a, button, .btn, .button, [role="button"]):focus-visible {
  outline: 2px solid var(--tsnz-accent);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- header ---
   ONE header for the platform and every app. The app contributes its TITLE and
   its right-hand actions; it contributes no marks, no colours and no layout.

   Markup contract:
     <header class="tsnz-header">
       <a class="tsnz-brand" href="{platform}"><img alt=""></a>   <- from /api/brand
       <span class="tsnz-app-title">{app name}</span>              <- app's only say
       <div class="tsnz-header-actions"> … </div>
     </header>                                                                */
.tsnz-header,
.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--tsnz-header-h);
  padding: 0 20px;
  background: var(--tsnz-surface);
  border-bottom: 1px solid var(--tsnz-line);
  position: sticky;
  top: 0;
  z-index: 20;
}

/* THE BRAND SLOT — the platform's identity, rendered identically everywhere.
   These are the ruled sizes (A4, A5), and the odd numbers are load-bearing:
   wordmark 18 -> 21.6 -> 25.92 -> 31.104px (+20% three times); endorsement
   -20% once (8.5 -> 6.8px) and held there.

   `.brand-*` are the hub's original class names, kept as aliases so the hub's
   own header is governed by THIS file rather than a second copy of the same
   numbers. One definition, four surfaces. */
.tsnz-brand, .brand-slot {
  display: flex;
  align-items: center;
  text-decoration: none;
}

/* Centred on each other, not flush left: the tracked endorsement sets the wider
   box (~103px against the wordmark's ~95px), so left-aligning them reads as a
   mistake. */
.tsnz-brand-text, .brand-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
}

/* The text rule is the no-image fallback for the same slot, so it scales with
   the image and an app with no marks yet still looks like the platform. */
.tsnz-brand-wordmark, .brand-wordmark {
  font: 700 29.376px var(--tsnz-font-brand);
  color: var(--tsnz-navy);
}
.tsnz-brand-wordmark img, .brand-wordmark img {
  height: 31.104px;
  width: auto;
  /* NO `display` HERE. It used to say `display: block`, and this selector is
     (0,1,1) while the swap rule below is (0,1,0) — so the sizing rule WON and
     forced BOTH wordmarks visible at once. `display` is the swap's business
     and belongs only where the swap is decided; the pair rules give every
     image in this slot a display value, so nothing is left unset. */
  vertical-align: middle;
}

/* The light/dark wordmark pair. Both are in the DOM; the tokens decide which is
   painted, so a theme change is instant and needs no script, no listener and no
   re-render. `.brand-wordmark img` above sets the size for both.

   DELIBERATELY NOT SCOPED TO A CONTAINER CLASS. These rules used to require a
   `.tsnz-brand-wordmark` ancestor, which meant the swap silently did nothing on
   any surface whose wordmark sat in a differently-named wrapper — and two hub
   pages had exactly that, so they painted the LIGHT wordmark on a dark ground:
   an all-but-invisible logo, which is the one element a customer notices. The
   swap is a property of the IMAGE, not of where it happens to live, so the
   class on the image is the whole selector and any surface that emits the pair
   is themed correctly with no further styling. */
.tsnz-wm-light { display: var(--tsnz-wm-light-display); }
.tsnz-wm-dark  { display: var(--tsnz-wm-dark-display); }

/* ------------------------------------------------------- the theme toggle ---
   Every surface gets one, and they all behave identically: three states in a
   cycle — system, light, dark — because a two-state toggle silently overrides
   the OS preference the first time it is touched and never gives it back.
   The chosen value is written to the root as `data-theme` and remembered; the
   absence of the attribute IS the system state. */
.tsnz-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--tsnz-line);
  border-radius: var(--tsnz-radius);
  background: var(--tsnz-surface);
  color: var(--tsnz-ink-2);
  cursor: pointer;
  line-height: 1;
}
.tsnz-theme-toggle:hover { background: var(--tsnz-surface-sunken); }
.tsnz-theme-toggle svg { width: 18px; height: 18px; display: block; }

/* One icon per state; the toggle shows exactly the one that applies. */
.tsnz-theme-toggle .ic-system,
.tsnz-theme-toggle .ic-light,
.tsnz-theme-toggle .ic-dark { display: none; }
:root:not([data-theme]) .tsnz-theme-toggle .ic-system { display: block; }
:root[data-theme="light"] .tsnz-theme-toggle .ic-light { display: block; }
:root[data-theme="dark"]  .tsnz-theme-toggle .ic-dark  { display: block; }

/* ------------------------------------------------------------- page ground ---
   The body must paint an explicit token. A transparent body borrows whatever
   the host paints behind it, which in dark mode is how you get dark text on a
   dark ground — legible in neither theme. */
body {
  background: var(--tsnz-surface-sunken);
  color: var(--tsnz-ink);
}

/* letter-spacing appends a trailing space after the final Z, which sits inside
   the box being centred and would shove the glyphs ~1px left. A negative margin
   of exactly one track pulls the box back onto the visible text. */
.tsnz-brand-endorse, .brand-endorse {
  font: 600 6.8px var(--tsnz-font-brand);
  letter-spacing: 0.24em;
  color: var(--tsnz-muted);
  text-transform: uppercase;
  margin-top: 2px;
  margin-right: -0.24em;
  text-align: center;
  white-space: nowrap;
}

/* THE APP NAME IS A BADGE, not a heading. Ruled 22 Aug 2026 from TMP Studio's
   treatment, which reads correctly for the reason it was chosen: the platform
   wordmark is the identity and the app name is a LABEL on it. Set as a heading
   it competes with the wordmark; set as a badge it sits beside it and stays
   subordinate, which is the true relationship.

   Values are TMP Studio's, measured rather than approximated: 600/12px on
   `--tsnz-surface-sunken`, `--tsnz-muted` ink, 6px radius, 0 8px padding,
   0.01em tracking. The divider is drawn HERE, not in the app, so the
   relationship between the mark and the name stays the platform's decision. */
.tsnz-app-title {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 8px;
  border-radius: 6px;
  background: var(--tsnz-surface-sunken);
  color: var(--tsnz-muted);
  font: 600 12px/1.45 var(--tsnz-font-ui);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* The rule between the mark and the badge. On the badge's container rather than
   the badge itself so the gap is the header's `gap`, not padding smuggled into
   a component. */
.tsnz-app-title::before {
  content: "";
  align-self: center;
  width: 1px;
  height: 20px;
  margin: 0 12px 0 -4px;
  background: var(--tsnz-line);
}

.tsnz-header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
}
.tsnz-header-actions .who { color: var(--tsnz-muted); }

/* ------------------------------------------------------- the account chip ---
   THE STANDARD ACROSS EVERY SURFACE. Ruled 22 Aug 2026, adopted from TMP
   Studio's treatment and measured from the running app rather than
   approximated: 32px pill, hairline border, 24px round avatar, initials at
   700/10px.

   The asymmetric padding (`0 10px 0 4px`) is deliberate and the detail most
   likely to be "tidied": the avatar is a circle sitting inside a pill, so equal
   padding makes the left side read as too wide. 4px on the avatar side and 10px
   on the text side is what makes the two shapes look concentric.

   The avatar fill is `--tsnz-navy`, which means it re-colours with the theme
   along with everything else — it was already the platform navy in the app this
   came from, which is why it looked right there and nowhere else. */
.tsnz-account {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 10px 0 4px;
  border: 1px solid var(--tsnz-line);
  border-radius: 999px;
  background: var(--tsnz-surface);
  color: var(--tsnz-ink);
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--tsnz-motion), background var(--tsnz-motion);
}
.tsnz-account:hover { border-color: var(--tsnz-line-strong); }

.tsnz-account .tsnz-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--tsnz-action);
  color: var(--tsnz-on-action);
  font: 700 10px/1.45 var(--tsnz-font-ui);
  letter-spacing: .02em;
}

/* Name over role, tight. The role is the quiet half — it is context, not
   identity, and at 11px it must use `--tsnz-muted` or it competes. */
.tsnz-account .tsnz-who {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.15;
  text-align: left;
}
.tsnz-account .tsnz-who b { font: 600 12px/1.25 var(--tsnz-font-ui); }
.tsnz-account .tsnz-who span {
  font: 400 11px/1.25 var(--tsnz-font-ui);
  color: var(--tsnz-muted);
}

/* The dropdown it opens. One surface, one radius, one shadow — an app that
   styles its own menu is the same drift as an app that styles its own button. */
.tsnz-menu {
  position: absolute;
  right: 0;
  min-width: 220px;
  padding: 6px;
  border: 1px solid var(--tsnz-line);
  border-radius: var(--tsnz-radius-modal);
  background: var(--tsnz-surface);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .12), 0 1px 4px rgba(0, 0, 0, .08);
  z-index: 60;
}
.tsnz-menu a, .tsnz-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--tsnz-ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.tsnz-menu a:hover, .tsnz-menu button:hover { background: var(--tsnz-surface-sunken); }
.tsnz-menu hr { margin: 6px 4px; border: 0; border-top: 1px solid var(--tsnz-line); }

@media (max-width: 640px) {
  .tsnz-app-title { font-size: 14px; }
  .tsnz-header { padding: 0 12px; gap: 12px; }
}
