/* ═══════════════════════════════════════════════════════════════════════
   hub.css — the shared design system for Ben's Hub.

   Loaded by every page (login, menu, dashboard, services, portfolio) so all
   of them resolve to one palette, one type scale, one set of components.

   PALETTE NOTE — 2026 rebuild
   The site had drifted. It was originally designed on near-black (#050505),
   then the base was moved to a warm grey (#1c1c1c) and the tokens here were
   updated — but the several hundred hardcoded hex values inside each page's
   local <style> block were left at their old near-black values. The result
   was two palettes fighting: a card would hover *darker* on the dashboard and
   *lighter* on the menu, and greys picked to read against #050505 turned to
   mud on #1c1c1c.

   This rebuild does three things:
     1. Moves to a genuinely dark, slightly cool base (#0b0d10) so the site
        reads as one deliberate dark theme rather than a washed-out grey.
     2. Fixes elevation direction: surfaces get *lighter* as they come
        forward (inset < base < surface < surface-2 < surface-3), everywhere,
        with no exceptions. Hover always means "raise", never "darken".
     3. Every text token is verified against every surface it can legally sit
        on at WCAG AA 4.5:1 — including the faintest label greys, which
        previously sat as low as 1.3:1 (some were *border* tokens used as
        text colours, which is why they looked invisible).

   Contrast floor, measured, worst case per token across all five surfaces:
     text-bright 13.67   text 12.03   text-dim 7.57
     text-faint   4.89   text-ghost 4.53
     accent 6.47   danger 5.06   warn 7.12   warn-alt 7.95   info 5.55
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* ── ELEVATION ────────────────────────────────────────────────────────
     One ladder, low to high. Anything that comes toward the viewer moves
     UP this list. Nothing in the site may introduce a surface outside it. */
  --hub-inset:     #07080a;  /* wells, inputs, progress tracks — below base */
  --hub-bg-1:      #0b0d10;  /* page base */
  --hub-bg-2:      #0f1114;  /* gradient stops, base ± a few points */
  --hub-bg-3:      #090a0c;
  --hub-bg-4:      #0d0f12;
  --hub-surface:   #131619;  /* the standard card / panel */
  --hub-surface-2: #1b1f24;  /* hover, raised */
  --hub-surface-3: #232830;  /* active, selected, pressed */

  /* ── BORDERS ──────────────────────────────────────────────────────────
     Dividers between adjacent surfaces. Deliberately low-contrast: they
     separate, they do not announce. Meaningful boundaries (focus, error)
     use an accent instead, which is why those clear 3:1 and these need not. */
  --hub-border:        #23272e;
  --hub-border-strong: #2f343c;
  --hub-border-hover:  #424954;

  /* ── TEXT ─────────────────────────────────────────────────────────────
     Brightest to faintest. All five clear 4.5:1 on all five surfaces.
     text-ghost is the floor — nothing fainter than this may hold text. */
  --hub-text-bright: #f4f6f8;  /* headline numerals, the one "white" */
  --hub-text:        #e4e8ec;  /* default body */
  --hub-text-dim:    #b3bac3;  /* secondary copy */
  --hub-text-faint:  #8d95a0;  /* tertiary, meta */
  --hub-text-ghost:  #868f9b;  /* uppercase micro-labels only */

  /* ── ACCENTS ──────────────────────────────────────────────────────────
     Each has a tint (fill) and a line (border) for badges and callouts,
     pre-computed rather than colour-mix()'d so they work everywhere. */
  --hub-accent:      #4ac25e;
  --hub-accent-tint: #0f2417;
  --hub-accent-line: #1e4a2c;
  --hub-accent-mute: #2f7a44;  /* completed bars, de-emphasised fills */

  --hub-danger:      #f56a6e;
  --hub-danger-tint: #2a1113;
  --hub-danger-line: #5a2124;

  --hub-warn:        #e9a825;
  --hub-warn-tint:   #291d06;
  --hub-warn-line:   #5a4410;
  --hub-warn-alt:    #f0b429;  /* Lunes-branded amber, kept distinct */

  --hub-info:        #5aa2f0;
  --hub-info-tint:   #0e1e2e;
  --hub-info-line:   #1e3d5a;

  /* Brand colours that must stay literal (external identity, not ours) */
  --hub-spotify: #1db954;
  --hub-spotify-hover: #1ed760;

  /* ── UPTIME HEATMAP ───────────────────────────────────────────────────
     A sequential scale for the uptime calendar. The old ramp gave the best
     day (>=99%) the *darkest* green, so a perfect month rendered as an
     almost-empty grid and a bad day barely stood out. It ascends now:
     healthy days form a solid green wall, degraded days pop against it. */
  --hub-heat-none: #1b1f24;  /* no data */
  --hub-heat-1:    #b0413f;  /* < 50%  */
  --hub-heat-2:    #9c6b16;  /* < 90%  */
  --hub-heat-3:    #2f7a44;  /* < 99%  */
  --hub-heat-4:    #3d9e52;  /* >= 99% */

  /* ── SCROLLBAR ────────────────────────────────────────────────────── */
  --hub-scroll-track: #08090b;
  --hub-scroll-thumb: #2f343c;
  --hub-scroll-thumb-hover: #424954;

  /* ── RADII ────────────────────────────────────────────────────────── */
  --hub-radius:      12px;
  --hub-radius-sm:    8px;
  --hub-radius-xs:    6px;
  --hub-radius-2xs:   4px;  /* badges, dots, bars */
  --hub-radius-pill: 999px;

  /* ── BUTTONS ──────────────────────────────────────────────────────────
     One shape, three sizes. Before this the site had ten distinct button
     font-sizes (9.6px to 28.8px), six border-radii (0/4/6/8/12/999px) and
     heights from 22px to 49px — every component had invented its own. */
  --hub-btn-radius: var(--hub-radius-sm);

  --hub-btn-fs-sm:  0.68rem;  --hub-btn-h-sm: 28px;  --hub-btn-pad-sm: 0.32rem 0.7rem;
  --hub-btn-fs-md:  0.8rem;   --hub-btn-h-md: 36px;  --hub-btn-pad-md: 0.5rem 1rem;
  --hub-btn-fs-lg:  0.9rem;   --hub-btn-h-lg: 44px;  --hub-btn-pad-lg: 0.7rem 1.25rem;

  /* ── TYPE ─────────────────────────────────────────────────────────────
     One size for the recurring uppercase-label role (tab bars, widget
     headers, section headings) that six components had each independently
     sized between 0.6rem and 0.72rem. */
  --hub-fs-label: 0.65rem;
  --hub-font: 'Segoe UI', system-ui, -apple-system, sans-serif;
  --hub-mono: ui-monospace, 'Cascadia Code', 'Consolas', monospace;

  /* ── MOTION ───────────────────────────────────────────────────────────
     The site had 37 distinct transition declarations, mixing 0.15s / 0.18s /
     0.2s / 0.35s / 0.4s with no shared easing, so two adjacent controls
     responded to the same hover at visibly different speeds. One curve, two
     durations, and a single reusable set for anything interactive. */
  --hub-fast: 0.15s;
  --hub-med:  0.2s;
  --hub-ease: cubic-bezier(0.4, 0, 0.2, 1);
  /* Softer decelerating ease for movement + entrances -- reads as considered
     rather than a hard linear cut. */
  --hub-ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Colour feedback stays instant so it feels responsive; movement and shadow
     ease over a slightly longer beat so lifts feel physical, not jumpy. */
  --hub-transition-interactive:
      background     var(--hub-fast) var(--hub-ease),
      border-color   var(--hub-fast) var(--hub-ease),
      color          var(--hub-fast) var(--hub-ease),
      box-shadow     var(--hub-med)  var(--hub-ease-out),
      transform      var(--hub-med)  var(--hub-ease-out);

  /* One lift distance for every raisable surface (was -2px and -3px). */
  --hub-lift: -2px;

  /* ── PAGE CHROME ANCHOR ───────────────────────────────────────────────
     One corner for the session controls on every page. They used to differ:
     the dashboard pinned them absolutely inside its hero (top 1.5rem /
     right 2rem, so they scrolled away with the header) while the menu and
     portfolio used fixed positioning at 1.25rem / 1.5rem. Same buttons,
     three positions, two different scroll behaviours. */
  --hub-chrome-top:   1rem;
  --hub-chrome-right: 1.25rem;
  --hub-chrome-w:     88px;    /* matches the chrome button min-width */
  --hub-chrome-gap:   0.5rem;

  /* ── STACKING ─────────────────────────────────────────────────────────
     Named so a future floating element has somewhere deliberate to land
     instead of another anonymous magic number. */
  --hub-z-chrome:   100;
  --hub-z-lightbox: 200;
  --hub-z-totop:    400;
  --hub-z-tooltip:  500;
  --hub-z-modal:    900;
}

/* ── RESET ──────────────────────────────────────────────────────────────
   Every page carried its own copy of this line. It lives here now. */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Form controls do not inherit type by default; without this, every input
   and button on the site silently falls back to the UA font. Each page was
   patching this per-component with `font-family: inherit`. */
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: none; }

img, svg, video { max-width: 100%; }

/* ── SCROLLBAR ──────────────────────────────────────────────────────────
   `overflow-y: scroll` keeps the bar present even on short pages. That is
   deliberate: without it, switching from the tall Dashboard tab to the short
   Services tab removes the scrollbar, the viewport gains ~11px of width, and
   the whole layout jumps sideways. Reserving the gutter costs nothing. */
html {
  overflow-y: scroll;
  scrollbar-width: thin;
  scrollbar-color: var(--hub-scroll-thumb) var(--hub-scroll-track);
  background: var(--hub-bg-1);   /* no white flash before body paints */
  scroll-behavior: smooth;
}

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--hub-scroll-track); }
::-webkit-scrollbar-thumb {
  background: var(--hub-scroll-thumb);
  border-radius: var(--hub-radius-xs);
  border: 3px solid var(--hub-scroll-track);
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--hub-scroll-thumb-hover);
  background-clip: content-box;
}
::-webkit-scrollbar-corner { background: var(--hub-scroll-track); }

/* ── SHARED PAGE CHROME ─────────────────────────────────────────────────
   .hub-body is the one place the page background is defined. Pages used to
   each re-declare this gradient plus their own @keyframes bgshift, which is
   how they drifted apart in the first place. */

.hub-body {
  background: transparent;   /* the gradient lives on ::before, see below */
  color: var(--hub-text);
  font-family: var(--hub-font);
  min-height: 100vh;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The gradient sits on a fixed, viewport-sized layer rather than on <body>.
   It used to be painted directly on the body with `background-size: 400% 400%`
   — and because that percentage resolves against the *element*, the gradient
   was scaled to the page's content height. The Dashboard tab is ~5660px tall
   and the Services tab ~940px, so the same gradient was stretched over 22,600px
   on one and 3,700px on the other: the two tabs genuinely rendered different
   background colours, and every page had a slightly different backdrop
   depending on how much content it happened to have.

   Anchoring it to the viewport makes it identical everywhere, and being a
   fixed layer it also stops repainting on scroll. */
.hub-body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(-45deg,
    var(--hub-bg-1), var(--hub-bg-2), var(--hub-bg-3), var(--hub-bg-4));
  background-size: 400% 400%;
  animation: hub-bgshift 20s ease infinite;
}

@keyframes hub-bgshift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* ── FOCUS ──────────────────────────────────────────────────────────────
   A 1px outline on a dark background is barely perceptible, and it was the
   only focus affordance on the site. 2px + a ring makes keyboard position
   obvious without affecting mouse users (:focus-visible, not :focus). */
:focus-visible {
  outline: 2px solid var(--hub-accent);
  outline-offset: 2px;
  border-radius: var(--hub-radius-2xs);
}

/* Anchored jumps (#services) landed with the heading flush against the
   viewport edge, hidden under nothing but visually cramped. */
[id] { scroll-margin-top: 1.5rem; }

/* Skip link — first tab stop on every page. */
.hub-skip {
  position: absolute; left: -9999px; top: 0; z-index: var(--hub-z-modal);
  background: var(--hub-surface-3); color: var(--hub-text);
  padding: 0.6rem 1rem; border-radius: var(--hub-radius-sm);
  border: 1px solid var(--hub-border-hover);
  font-size: 0.8rem; text-decoration: none;
}
.hub-skip:focus { left: 1rem; top: 1rem; }

/* ── TOP BAR ──────────────────────────────────────────────────────────── */

.hub-topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin-bottom: 1.5rem;
}

.hub-navlink, .hub-logout {
  /* justify-content matters here: these carry a min-width, so the box is
     wider than the label and the text would otherwise sit hard left. */
  display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem;
  background: none; border: none;
  color: var(--hub-text-ghost);
  font-size: 0.68rem;
  letter-spacing: 0.08em; text-transform: uppercase;
  text-decoration: none; padding: 0.45rem 0.6rem;
  border-radius: var(--hub-radius-xs);
  cursor: pointer; transition: color var(--hub-fast), background var(--hub-fast);
}
.hub-navlink:hover { color: var(--hub-text); background: var(--hub-surface-2); }
.hub-logout:hover  { color: var(--hub-danger); background: var(--hub-surface-2); }
.hub-navlink svg   { width: 12px; height: 12px; }

/* ── TABS ─────────────────────────────────────────────────────────────── */

.hub-tabs {
  display: flex; gap: 0.35rem;
  margin: 0 auto 2.25rem; padding: 0.3rem;
  background: var(--hub-surface);
  border: 1px solid var(--hub-border);
  border-radius: var(--hub-radius);
  width: fit-content;
}

.hub-tab {
  appearance: none; background: none; border: none;
  border-radius: var(--hub-radius-sm);
  color: var(--hub-text-faint);
  font-size: var(--hub-fs-label); font-weight: 500;
  --hub-btn-ls: 0.1em; text-transform: uppercase;
  padding: 0.55rem 1.4rem; cursor: pointer;
  transition: color var(--hub-fast), background var(--hub-fast);
}
.hub-tab:hover { color: var(--hub-text-dim); background: var(--hub-surface-2); }
.hub-tab.is-active { background: var(--hub-surface-3); color: var(--hub-text); }

/* Only one view is ever in the layout. Height differences between them are
   absorbed by the always-present scrollbar gutter above. */
.hub-view { display: none; }
.hub-view.is-active { display: block; }

/* ── SEGMENTED CONTROL ────────────────────────────────────────────────── */

.hub-seg {
  display: inline-flex; gap: 2px; padding: 2px;
  background: var(--hub-inset);
  border: 1px solid var(--hub-border);
  border-radius: var(--hub-radius-sm);
}
.hub-seg button {
  appearance: none; background: none; border: none;
  border-radius: var(--hub-radius-xs);
  color: var(--hub-text-faint);
  font-size: var(--hub-fs-label);
  --hub-btn-ls: 0.06em; text-transform: uppercase;
  padding: 0.3rem 0.6rem; cursor: pointer;
  transition: color var(--hub-fast), background var(--hub-fast);
}
.hub-seg button:hover { color: var(--hub-text-dim); background: var(--hub-surface-2); }
.hub-seg button.is-active { background: var(--hub-surface-3); color: var(--hub-text); }

/* ── SCROLL TO TOP ──────────────────────────────────────────────────────
   Bottom-left so it never sits under a scrollbar or a right-hand tooltip. */

.hub-totop {
  position: fixed; left: 1.25rem; bottom: 1.25rem; z-index: var(--hub-z-totop);
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  background: var(--hub-surface-2);
  border: 1px solid var(--hub-border-strong);
  border-radius: var(--hub-radius);
  color: var(--hub-text-faint);
  cursor: pointer;
  opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity var(--hub-med), transform var(--hub-med),
              visibility var(--hub-med), color var(--hub-fast),
              border-color var(--hub-fast), background var(--hub-fast);
}
.hub-totop.is-visible { opacity: 1; visibility: visible; transform: none; }
.hub-totop:hover {
  background: var(--hub-surface-3);
  border-color: var(--hub-border-hover);
  color: var(--hub-accent);
}
.hub-totop svg { width: 17px; height: 17px; }

/* ── SHARED PRIMITIVES ──────────────────────────────────────────────────
   Available to any page that wants them; the existing pages keep their own
   class names and inherit cohesion through the tokens above. */

.hub-card {
  background: var(--hub-surface);
  border: 1px solid var(--hub-border);
  border-radius: var(--hub-radius);
  padding: 1rem 1.1rem;
}
a.hub-card, .hub-card--link {
  display: block; text-decoration: none; color: inherit;
  transition: border-color var(--hub-fast), background var(--hub-fast),
              transform var(--hub-fast);
}
a.hub-card:hover, .hub-card--link:hover {
  background: var(--hub-surface-2);
  border-color: var(--hub-border-hover);
  transform: translateY(-2px);
}

.hub-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 0.4rem;
  background: var(--hub-surface-2);
  border: 1px solid var(--hub-border-strong);
  border-radius: var(--hub-radius-sm);
  color: var(--hub-text-dim);
  font-size: 0.8rem; padding: 0.55rem 1rem;
  cursor: pointer;
  transition: background var(--hub-fast), border-color var(--hub-fast),
              color var(--hub-fast);
}
.hub-btn:hover:not(:disabled) {
  background: var(--hub-surface-3);
  border-color: var(--hub-border-hover);
  color: var(--hub-text);
}
.hub-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.hub-btn--accent { color: var(--hub-accent); border-color: var(--hub-accent-line); }
.hub-btn--accent:hover:not(:disabled) { background: var(--hub-accent-tint); }
.hub-btn--danger { color: var(--hub-danger); border-color: var(--hub-danger-line); }
.hub-btn--danger:hover:not(:disabled) { background: var(--hub-danger-tint); }

.hub-input {
  width: 100%;
  background: var(--hub-inset);
  border: 1px solid var(--hub-border);
  border-radius: var(--hub-radius-sm);
  padding: 0.65rem 0.85rem;
  color: var(--hub-text);
  font-size: 0.9rem;
  outline: none;
  transition: border-color var(--hub-fast), background var(--hub-fast);
}
.hub-input:hover { border-color: var(--hub-border-strong); }
.hub-input:focus { border-color: var(--hub-accent); background: var(--hub-bg-1); }
.hub-input::placeholder { color: var(--hub-text-ghost); }
.hub-input:disabled { opacity: 0.5; cursor: not-allowed; }

.hub-badge {
  display: inline-block;
  font-size: var(--hub-fs-label);
  padding: 0.15rem 0.45rem;
  border-radius: var(--hub-radius-2xs);
  background: var(--hub-surface-2);
  color: var(--hub-text-faint);
  border: 1px solid var(--hub-border);
}
.hub-badge--ok     { color: var(--hub-accent); background: var(--hub-accent-tint); border-color: var(--hub-accent-line); }
.hub-badge--danger { color: var(--hub-danger); background: var(--hub-danger-tint); border-color: var(--hub-danger-line); }
.hub-badge--warn   { color: var(--hub-warn);   background: var(--hub-warn-tint);   border-color: var(--hub-warn-line); }
.hub-badge--info   { color: var(--hub-info);   background: var(--hub-info-tint);   border-color: var(--hub-info-line); }

/* Anything still loading, everywhere on the site. */
.hub-loading { color: var(--hub-text-faint); font-size: 0.85rem; }

/* ═══════════════════════════════════════════════════════════════════════
   BUTTON NORMALISATION LAYER

   Every page grew its own button styles independently, so labels were
   centred in some and flush-left in others (`display:flex` with the default
   `justify-content: normal` was the usual culprit — it looks centred only
   while the button is exactly as wide as its text, then drifts the moment a
   min-height or a flex row stretches it).

   These rules are prefixed with `.hub-body` purely for specificity: they need
   to beat the single-class rules still living in each page's <style> block,
   so the geometry is decided in exactly one place. Pages keep their own
   class names; only the numbers move.
   ═══════════════════════════════════════════════════════════════════════ */

/* Every selector in this layer is deliberately kept at the same specificity
   (0,2,0) so that source order alone decides the winner. An attribute
   selector such as `a[class*="-btn"]` here would score 0,2,1 and silently
   outrank the exceptions further down — which is exactly how the pill and
   square-icon overrides got lost the first time. Anchor-based buttons are
   therefore listed by class instead. */
.hub-body :is(button, .hub-btn, .contact-btn, .hub-navlink, .social-link) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  text-align: center;
  font-family: inherit;
  white-space: nowrap;

  /* VERTICAL CENTRING - do not set this to 1.
     `line-height: 1` makes the line box exactly the font-size (12.8px on a
     medium button) while the font's own box is taller (17px for Segoe UI at
     that size). The overflow becomes negative half-leading, and the rounding
     lands every label ~0.9px ABOVE the button's true centre. Tiny per button,
     but identical in direction on all of them - which is precisely what reads
     as "the text is off centre everywhere".
     `normal` makes the line box match the font box and measures exactly 0.
     Heights are unaffected because min-height still governs every size. */
  line-height: normal;

  /* HORIZONTAL CENTRING - nothing to compensate. Letter-spacing does append
     a trailing gap after the final character, but Chrome already accounts for
     it when centring: measured, labels land at 0.00px from centre untracked
     and +0.31px if you "correct" them with a half-letter-space text-indent.
     Tracking is declared per group via --hub-btn-ls purely to keep the values
     in one place. */
  letter-spacing: var(--hub-btn-ls, normal);
  cursor: pointer;
  border-radius: var(--hub-btn-radius);
  border: 1px solid transparent;
  transition: var(--hub-transition-interactive);
}

/* One press response for every button on the site. Buttons deliberately do
   not lift on hover — that gesture is reserved for cards, so the two read as
   different kinds of thing. */
.hub-body :is(button, .hub-btn, .contact-btn, .hub-navlink,
               .social-link):active:not(:disabled) {
  transform: translateY(1px);
}

/* Cards share the buttons' curve and duration, and now a single lift
   distance — the menu tiles used to travel 3px while every other card
   travelled 2px. */
.hub-body :is(.choice, .project, .site, .widget, .server-card, .card,
               #view-services .svc, .cert-card, .tz-card, .svc-status-card) {
  transition: var(--hub-transition-interactive);
}
.hub-body :is(.choice, .project, a.card, #view-services .svc):hover {
  transform: translateY(var(--hub-lift));
}

/* The wiki page list is a sidebar of file names, not a row of buttons —
   centring those would be actively worse to scan. */
.hub-body .wiki-page-btn {
  justify-content: flex-start;
  text-align: left;
  white-space: nowrap;
}

/* ── SIZE: small — dense actions that live inside a card ─────────────── */
.hub-body :is(.card-action-btn, .svc-restart-btn, .range-btn, .alert-count-btn,
               .lunes-login-btn, .wiki-new-btn, .wiki-save-btn, .wiki-page-btn,
               .incident-note-btn, .hub-seg button,
               .wiki-del-btn, .dl-clear-all, .dl-job-act, .incident-add-note,
               .todo-del) {
  font-size: var(--hub-btn-fs-sm);
  min-height: var(--hub-btn-h-sm);
  padding: var(--hub-btn-pad-sm);
  border-radius: var(--hub-radius-xs);
}

/* ── SIZE: medium — dialog, form and navigation actions ──────────────── */
.hub-body :is(.confirm-cancel, .confirm-ok, .todo-add-btn, .dl-btn, .hub-tab,
               .contact-btn, .spotify-connect-btn, .social-link) {
  font-size: var(--hub-btn-fs-md);
  min-height: var(--hub-btn-h-md);
  padding: var(--hub-btn-pad-md);
}

/* ── CHROME BUTTONS ──────────────────────────────────────────────────────
   Sign out, Menu, and the portfolio's Ben?/Menu button. All three are
   floating page-chrome controls pinned to a top corner, so they get one
   identical treatment rather than the three different ones they had (the
   portfolio's was a 36px solid pill, the other two 28px transparent links).

   min-width keeps them the same width regardless of label length, which is
   what stops "SIGN OUT" and "MENU" reading as different kinds of control.
   The blur matters because all three sit over page content, not over a
   panel. */
.hub-body :is(.hub-logout, .hub-navlink, .signin-btn) {
  font-size: var(--hub-btn-fs-sm);
  min-height: 32px;
  min-width: 88px;
  padding: 0.45rem 0.9rem;
  border-radius: var(--hub-radius-xs);
  --hub-btn-ls: 0.08em;
  text-transform: uppercase;
  background: var(--hub-surface-2);
  border: 1px solid var(--hub-border-strong);
  color: var(--hub-text-dim);
  backdrop-filter: blur(6px);
}
.hub-body :is(.hub-navlink, .signin-btn):hover {
  background: var(--hub-surface-3);
  border-color: var(--hub-border-hover);
  color: var(--hub-text);
}
/* Sign out keeps a red hover. It is the only destructive control of the
   three, and that cue is worth more than perfect uniformity on hover —
   everything else about it is identical. */
.hub-body .hub-logout:hover {
  background: var(--hub-danger-tint);
  border-color: var(--hub-danger-line);
  color: var(--hub-danger);
}
.hub-body :is(.hub-logout, .hub-navlink, .signin-btn) svg {
  width: 12px; height: 12px; flex-shrink: 0;
}

/* ── CHROME POSITION ─────────────────────────────────────────────────────
   Fixed to the same top-right corner on every page, so the controls stay put
   while the page scrolls and land in the same place whichever page you are
   on. Where a page has two of them (the dashboard's Menu + Sign out) the
   left one is offset by exactly one button width plus the gap, so they read
   as a pair rather than as two separately-placed controls. */
.hub-body :is(.hub-logout, .hub-navlink, .signin-btn) {
  position: fixed;
  top: var(--hub-chrome-top);
  right: var(--hub-chrome-right);
  z-index: var(--hub-z-chrome);
}
.hub-body .hub-navlink {
  right: calc(var(--hub-chrome-right) + var(--hub-chrome-w) + var(--hub-chrome-gap));
}

/* ── SIZE: large — the one primary call to action per page ───────────── */
.hub-body .btn {
  font-size: var(--hub-btn-fs-lg);
  min-height: var(--hub-btn-h-lg);
  padding: var(--hub-btn-pad-lg);
  width: 100%;
}

/* ── TONE: solid default ─────────────────────────────────────────────── */
.hub-body :is(.card-action-btn, .svc-restart-btn, .range-btn, .alert-count-btn,
               .wiki-new-btn, .wiki-save-btn, .incident-note-btn,
               .confirm-cancel, .todo-add-btn, .contact-btn,
               .social-link) {
  background: var(--hub-surface-2);
  border-color: var(--hub-border-strong);
  color: var(--hub-text-dim);
}
.hub-body :is(.card-action-btn, .svc-restart-btn, .range-btn, .alert-count-btn,
               .wiki-new-btn, .wiki-save-btn, .incident-note-btn,
               .confirm-cancel, .todo-add-btn, .contact-btn,
               .social-link):hover:not(:disabled) {
  background: var(--hub-surface-3);
  border-color: var(--hub-border-hover);
  color: var(--hub-text);
}

/* ── TONE: ghost — tertiary text actions that should not shout ───────── */
.hub-body :is(.wiki-del-btn, .dl-clear-all,
               .dl-job-act, .incident-add-note, .todo-del, .wiki-page-btn,
               .hub-seg button, .hub-tab) {
  background: none;
  border-color: transparent;
  color: var(--hub-text-faint);
}
.hub-body :is(.wiki-page-btn, .dl-clear-all, .dl-job-act,
               .incident-add-note, .hub-seg button, .hub-tab):hover {
  background: var(--hub-surface-2);
  color: var(--hub-text);
}
.hub-body :is(.wiki-del-btn, .todo-del):hover {
  background: var(--hub-danger-tint);
  color: var(--hub-danger);
}

/* Selected states sit at the top of the elevation ladder. */
.hub-body :is(.hub-tab.is-active, .hub-seg button.is-active,
               .range-btn.active, .alert-count-btn.active, .wiki-page-btn.active) {
  background: var(--hub-surface-3);
  border-color: var(--hub-border-hover);
  color: var(--hub-text);
}

/* ── TONE: semantic ──────────────────────────────────────────────────── */
.hub-body :is(.dl-btn, .btn, .card-action-btn.copied, .svc-restart-btn.done,
               .incident-note-btn.save, .confirm-ok.accent) {
  background: var(--hub-accent-tint);
  border-color: var(--hub-accent-line);
  color: var(--hub-accent);
}
.hub-body :is(.dl-btn, .btn):hover:not(:disabled) {
  background: var(--hub-accent);
  border-color: var(--hub-accent);
  color: var(--hub-bg-1);
}
.hub-body :is(.confirm-ok.danger, .svc-restart-btn.fail, .incident-note-btn.del) {
  background: var(--hub-danger-tint);
  border-color: var(--hub-danger-line);
  color: var(--hub-danger);
}
.hub-body :is(.confirm-ok.danger, .incident-note-btn.del):hover {
  background: var(--hub-danger);
  color: var(--hub-bg-1);
}
.hub-body :is(.confirm-ok.warn, .lunes-login-btn) {
  background: var(--hub-warn-tint);
  border-color: var(--hub-warn-line);
  color: var(--hub-warn-alt);
}
.hub-body :is(.confirm-ok.warn, .lunes-login-btn):hover {
  background: var(--hub-warn);
  color: var(--hub-bg-1);
}

/* Spotify keeps its brand pill — it is a third party's identity, not ours. */
.hub-body .spotify-connect-btn {
  background: var(--hub-spotify);
  border-color: var(--hub-spotify);
  color: #000;
  border-radius: var(--hub-radius-pill);
  font-weight: 600;
}
.hub-body .spotify-connect-btn:hover {
  background: var(--hub-spotify-hover);
  border-color: var(--hub-spotify-hover);
  color: #000;
}

/* Icon-only controls are square by design; the text scale does not apply. */
.hub-body :is(.hub-totop, .sp-btn, .search-wrap button) {
  padding: 0;
  min-height: 0;
  gap: 0;
  background: none;
  border-color: transparent;
}
.hub-body .search-wrap button { color: var(--hub-text-faint); }
.hub-body .search-wrap button svg { width: 17px; height: 17px; display: block; }
.hub-body .search-wrap button:hover { color: var(--hub-accent); }
.hub-body .sp-btn { border-radius: var(--hub-radius); border-color: transparent; }
.hub-body .sp-btn.play { border-radius: var(--hub-radius-pill); }
.hub-body .hub-totop {
  border-radius: var(--hub-radius);
  /* Keeps the shared curve, but re-adds the fade this control needs to show
     and hide itself — the generic button transition does not cover those. */
  transition: var(--hub-transition-interactive),
              opacity var(--hub-med) var(--hub-ease),
              visibility var(--hub-med) var(--hub-ease);
}

.hub-body button:disabled,
.hub-body .hub-btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* ── RESPONSIVE ─────────────────────────────────────────────────────── */

@media (max-width: 620px) {
  :root {
    --hub-chrome-top:   0.6rem;
    --hub-chrome-right: 0.6rem;
    --hub-chrome-w:     78px;
    --hub-chrome-gap:   0.35rem;
  }
  /* Slightly smaller chrome buttons so Menu + Sign out still fit side by
     side on a 360px screen without crowding the page title. */
  .hub-body :is(.hub-logout, .hub-navlink, .signin-btn) {
    min-width: var(--hub-chrome-w);
    padding: 0.4rem 0.6rem;
  }
  .hub-tabs { width: 100%; }
  .hub-tab  { flex: 1; padding: 0.6rem 0.75rem; }
  .hub-totop { left: 0.85rem; bottom: 0.85rem; }
}

/* Touch targets: the WCAG 2.2 floor is 24px, the practical floor is ~40px.
   Applied only on genuinely touch-primary devices so desktop density is
   preserved. */
@media (pointer: coarse) {
  button, [role="button"], a.hub-card, .hub-tab, .hub-btn {
    min-height: 40px;
  }
}

/* iOS Safari zooms the whole page in when a focused input's font-size is
   below 16px, and every field here is 0.7-0.95rem for density. Rather than
   disabling zoom in the viewport meta (which also blocks deliberate pinch
   zoom, an accessibility regression), the fields themselves go to 16px on
   the screens where iOS applies the rule. Desktop density is untouched. */
@media (max-width: 900px) {
  /* !important is deliberate and load-bearing here. Several fields are sized
     by ID-scoped rules (#view-services .dl-input and friends) which outrank
     any selector this layer can reasonably use, and iOS applies its zoom rule
     to the *computed* size. This is a device correction that has to win over
     every density preference, so it is the one place the hammer is right. */
  .hub-body :is(input, select, textarea),
  .hub-body :is(input, select, textarea):focus {
    font-size: 16px !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   POLISH LAYER — depth + motion

   The UI read as "flat and static": painted rectangles that hard-cut into
   place. This gives surfaces real depth (a resting shadow, a deeper lift on
   hover) and lets content settle in on load and on tab-switch instead of
   snapping. All motion is gated behind prefers-reduced-motion: no-preference.
   ═══════════════════════════════════════════════════════════════════════ */

/* Resting depth: cards read as objects sitting above the page, not drawn on it. */
.hub-body :is(.card, .widget, .server-card, .cert-card, .svc-status-card,
              .tz-card, .alert-panel, .choice, .project, .site,
              #view-services .svc, .dl-panel, .hub-card) {
  box-shadow: 0 1px 2px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.12);
}
/* Clickable cards lift into a deeper, softer shadow. box-shadow is already part
   of --hub-transition-interactive, so it eases rather than snaps. */
.hub-body :is(a.card, .choice, .project, .site, #view-services .svc,
              a.hub-card, .hub-card--link):hover {
  box-shadow: 0 16px 34px -14px rgba(0,0,0,0.55), 0 4px 12px -6px rgba(0,0,0,0.4);
}

@keyframes hub-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes hub-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* Skeleton placeholder — an alive shimmer instead of a static "Loading..." */
.hub-skeleton {
  background: linear-gradient(90deg,
    var(--hub-surface-2) 25%, var(--hub-surface-3) 42%, var(--hub-surface-2) 60%);
  background-size: 200% 100%;
  border-radius: var(--hub-radius-xs);
  color: transparent !important;
}

@media (prefers-reduced-motion: no-preference) {
  /* Content settles in on load, and again on tab-switch (the is-active class
     re-applies), rather than hard-cutting. Staggered so it cascades. */
  .hub-body .hero,
  .hub-body .hub-tabs,
  .hub-body .choices,
  .hub-view.is-active > section,
  .hub-view.is-active > .widgets,
  .hub-view.is-active > .servers-grid,
  .hub-body .wrap > header,
  .hub-body .wrap > section {
    animation: hub-rise 0.5s var(--hub-ease-out) both;
  }
  .hub-body .hub-tabs { animation-delay: 0.05s; }
  .hub-view.is-active > *:nth-child(1)   { animation-delay: 0.02s; }
  .hub-view.is-active > *:nth-child(2)   { animation-delay: 0.07s; }
  .hub-view.is-active > *:nth-child(3)   { animation-delay: 0.12s; }
  .hub-view.is-active > *:nth-child(4)   { animation-delay: 0.16s; }
  .hub-view.is-active > *:nth-child(5)   { animation-delay: 0.20s; }
  .hub-view.is-active > *:nth-child(n+6) { animation-delay: 0.23s; }
  .hub-body .wrap > section:nth-child(2) { animation-delay: 0.06s; }
  .hub-body .wrap > section:nth-child(3) { animation-delay: 0.11s; }
  .hub-body .wrap > section:nth-child(4) { animation-delay: 0.15s; }
  .hub-body .wrap > section:nth-child(n+5) { animation-delay: 0.19s; }

  .hub-skeleton { animation: hub-shimmer 1.5s ease-in-out infinite; }
}

@media (prefers-reduced-motion: reduce) {
  .hub-body::before { animation: none; }
  html { scroll-behavior: auto !important; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
