/*
 * SolutionsHub — styles.css
 *
 * Base rules below (through the "@media print" block) are ported verbatim
 * from the original design-canvas artifact's <style> block. Everything
 * after the "RESPONSIVE" divider is new: the source template was built as a
 * single fixed 1180x820 canvas frame (a preview artboard), so the page itself
 * had no responsive behavior at all. Almost all of its layout already uses
 * flexbox with flex-wrap and percentage/flex-basis sizing (verified while
 * porting the template), so it reflows reasonably well on its own — the
 * additions here handle the few remaining fixed-canvas assumptions:
 *   - the outer frame was a fixed 1180x820px box -> now fluid, fills the
 *     viewport (see .app-shell in index.html)
 *   - the six top-level view containers used a flat 56px left/right padding
 *     that comfortably fits a desktop canvas but eats most of a 375px phone
 *     screen -> reduced with !important overrides at narrower breakpoints
 *     (only way to win over the ported inline styles, which is where nearly
 *     all of this app's presentation intentionally still lives)
 */

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: var(--cbt-bg-page); }
a { color: var(--cbt-brand-blue); }
a:hover { color: var(--cbt-brand-blue-deep); }
button { font-family: 'Libre Franklin', sans-serif; }
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-thumb { background: var(--cbt-border-control); border-radius: 8px; }
::-webkit-scrollbar-track { background: transparent; }


@media print {
  body * { visibility: hidden; }
  .checkout-print-area, .checkout-print-area *,
  .summary-print-area, .summary-print-area * { visibility: visible; }
  .checkout-print-area, .summary-print-area { position: absolute; left: 0; top: 0; width: 100%; max-width: 100%; padding: 0 !important; }
  .checkout-print-area button, .summary-print-area button { display: none; }
  .summary-print-area { background: white !important; }
  .summary-print-area * { color: black !important; background: transparent !important; box-shadow: none !important; }
  .scope-detail-block { display: block !important; }

  /* The app shell and its scroll body are fixed to 100vh with
     overflow:hidden/auto so the on-screen app never scrolls the whole
     page — great for the UI, but during print that same overflow:hidden
     clips everything beyond one screen's worth of height, which is why a
     multi-page quote only ever printed its first page. Printing needs
     these ancestors to grow to their full content height instead of
     clipping to the viewport, so the print-area content underneath can
     paginate normally across as many printed pages as it needs. */
  .app-shell, .app-body {
    height: auto !important;
    min-height: 0 !important;
    overflow: visible !important;
  }
}

/* ============================= RESPONSIVE ============================= */

#app-root { height: 100%; }

.app-shell {
  /* fluid replacement for the original fixed 1180x820 canvas frame */
  max-width: 100%;
}

/* Never let the page itself scroll sideways, regardless of what a stray
   fixed-width element inside a view does. */
body { overflow-x: hidden; }

/* Header: on very narrow phones let the logo/breadcrumb/solution-button
   group wrap onto two lines instead of squeezing or overflowing. */
@media (max-width: 480px) {
  .app-header {
    flex-wrap: wrap;
    height: auto;
    min-height: 64px;
    padding: 10px 16px !important;
    gap: 8px;
    row-gap: 8px;
  }
}

/* Tablet: bring the desktop 56px side padding down a bit. */
@media (max-width: 900px) {
  .view-pad {
    padding-left: 32px !important;
    padding-right: 32px !important;
  }
}

/* Phone: bring it down further so a 322px overview card (the widest
   fixed-width element in the app) always has room next to the padding. */
@media (max-width: 480px) {
  .view-pad {
    padding-left: 18px !important;
    padding-right: 18px !important;
  }
}

/* The Managed IT device-count grid is a fixed 2-column grid; make it a
   single column on phones so labels and steppers aren't cramped. */
@media (max-width: 480px) {
  .mi-devices-grid {
    grid-template-columns: 1fr !important;
  }
}

/* Overview search results / dropdowns are positioned absolute at full
   width of their (now-narrower) parent — nothing extra needed there, but
   guard against any stray overflow while the viewport is very narrow. */
@media (max-width: 375px) {
  .app-shell, .app-body { overflow-x: hidden; }
}

/* ============================= COLOR TOKENS ============================= */
/* Added 2026-09-22 as the first step of the frontend-design pass (see the
   "solutionshub-frontend-design-recommendations" doc). This block is purely
   additive right now: nothing in index.html or app.js references these
   custom properties yet, so defining them changes nothing on screen. The
   dark-theme values below are exact copies of the literal oklch(...)/white
   values already used throughout index.html today (confirmed by a
   frequency/context pass over every color literal in the file) -- the goal
   is that swapping a literal for var(--token-name) later is a no-op visually.
   The light-mode overrides are a first design draft, not yet visually
   checked against the live app; they'll be verified with an isolated
   preview before the theme toggle is wired up.

   Two coexisting surface systems drive the naming below, discovered while
   cataloguing the literals: a pervasive "dark chrome" system (headers, page
   background, content panels, most text) and a "light card" system (white
   /near-white tiles -- the Overview pillar tiles, the search bar, and
   several product-selection cards embedded inside otherwise-dark views)
   that keeps dark text on a light surface in both systems' current usage.
   Tokens are grouped to match. Status/accent colors (badges, success,
   warning, danger, the two brand-purple uses) are shared across both
   surface systems and, for now, kept identical between the dark and light
   themes -- retuning them is part of the later visual-cleanup pass, not
   this step. */

:root {
  /* Dark chrome / canvas -- retoned 2026-09-25 to match the new
     staging.codebluetechnology.com site's navy/ink palette (fonts,
     colors, accents pulled from the live site's :root custom properties
     and computed styles). Same names/usage as before, new values. */
  --cbt-bg-page: #0c1220;
  --cbt-bg-header: #0a0f1c;
  --cbt-bg-panel: #141b2c;
  --cbt-bg-control: #1b2338;
  --cbt-bg-control-2: #242d45;
  --cbt-bg-control-3: #2c3550;
  --cbt-bg-control-4: #333d58;
  --cbt-bg-control-5: #3d4763;
  --cbt-border-header: rgba(255, 255, 255, 0.1);
  --cbt-border-control: #333d58;

  /* Light "paper" cards (Overview tiles, search bar, product cards) --
     unchanged; the new site is dark-only and gives no light-card
     reference to retone against. */
  --cbt-card-bg: oklch(0.98 0.006 255);
  --cbt-card-bg-alt: oklch(0.97 0.006 255);
  --cbt-card-border: oklch(0.9 0.006 255);
  --cbt-card-border-strong: oklch(0.88 0.006 255);

  /* Text on dark chrome/panels */
  --cbt-text-on-dark-primary: #eef0f5;
  --cbt-text-on-dark-secondary: #c1c6d4;
  --cbt-text-on-dark-tertiary: #a3a8ba;
  --cbt-text-on-dark-muted: #848aa0;
  --cbt-text-on-dark-faint: #6b7288;
  --cbt-text-on-dark-disabled: #565e77;

  /* Text on light cards -- unchanged, see note above */
  --cbt-text-on-light-primary: oklch(0.2 0.03 255);
  --cbt-text-on-light-secondary: oklch(0.45 0.02 255);
  --cbt-text-on-light-muted: oklch(0.58 0.02 255);

  /* Status / accent (shared across themes for now) -- unchanged; these
     are semantic (success/warning/danger/pillar-purple) rather than
     brand-decorative, so they're left alone rather than force-fit to
     the new site's palette. */
  --cbt-badge-amber-bg: oklch(0.78 0.14 85);
  --cbt-badge-amber-text: oklch(0.2 0.05 85);
  --cbt-warning-text: oklch(0.62 0.14 55);
  --cbt-success-text: oklch(0.55 0.14 150);
  --cbt-success-text-alt: oklch(0.5 0.14 155);
  --cbt-danger-text: oklch(0.7 0.18 30);
  --cbt-purple-inside-sales: oklch(0.5 0.16 300);
  --cbt-purple-peoplefirst: oklch(0.32 0.11 265);
  --cbt-note-bg: oklch(0.96 0.01 90);
  --cbt-note-label: oklch(0.5 0.05 80);
  --cbt-note-text: oklch(0.35 0.03 80);

  /* Brand accent tokens -- pulled directly from the new website's
     :root custom properties (--brand-blue/-green/-red and their
     "deep"/"tint" variants). Not wired into any component yet; added
     here so future work (buttons, links, highlights) has the real
     brand colors available as tokens instead of one-off literals. */
  --cbt-brand-blue: #4d7bd6;
  --cbt-brand-blue-deep: #6f95e0;
  --cbt-brand-blue-tint: rgba(77, 123, 214, 0.16);
  --cbt-brand-green: #9dc22c;
  --cbt-brand-green-deep: #b3d654;
  --cbt-brand-green-tint: rgba(157, 194, 44, 0.14);
  --cbt-brand-red: #d5636d;
  --cbt-brand-red-deep: #e58891;
  --cbt-brand-red-tint: rgba(213, 99, 109, 0.16);
}

/* Light theme -- draft values, not yet wired to a toggle (no code sets
   data-theme="light" anywhere yet). Kept here so the token names are
   final and the design intent is recorded while the analysis is fresh;
   will be visually sanity-checked with an isolated preview before the
   toggle ships. */
:root[data-theme="light"] {
  --cbt-bg-page: oklch(0.97 0.006 255);
  --cbt-bg-header: oklch(0.99 0.003 255);
  --cbt-bg-panel: white;
  --cbt-bg-control: oklch(0.94 0.006 255);
  --cbt-bg-control-2: oklch(0.92 0.006 255);
  --cbt-bg-control-3: oklch(0.89 0.006 255);
  --cbt-bg-control-4: oklch(0.87 0.006 255);
  --cbt-bg-control-5: oklch(0.85 0.006 255);
  --cbt-border-header: oklch(0.88 0.006 255);
  --cbt-border-control: oklch(0.8 0.006 255);

  /* Light cards need a real border now that the page behind them is also
     light -- today's card-border values would nearly vanish. */
  --cbt-card-bg: white;
  --cbt-card-bg-alt: oklch(0.98 0.006 255);
  --cbt-card-border: oklch(0.82 0.01 255);
  --cbt-card-border-strong: oklch(0.78 0.01 255);

  --cbt-text-on-dark-primary: oklch(0.2 0.03 255);
  --cbt-text-on-dark-secondary: oklch(0.4 0.02 255);
  --cbt-text-on-dark-tertiary: oklch(0.48 0.02 255);
  --cbt-text-on-dark-muted: oklch(0.55 0.02 255);
  --cbt-text-on-dark-faint: oklch(0.62 0.02 255);
  --cbt-text-on-dark-disabled: oklch(0.75 0.02 255);

  --cbt-text-on-light-primary: oklch(0.2 0.03 255);
  --cbt-text-on-light-secondary: oklch(0.45 0.02 255);
  --cbt-text-on-light-muted: oklch(0.58 0.02 255);
}

/* ============================= MOTION ============================= */
/* Added per the animation/design-eng audit (2026-09-15). Tokens, press
   feedback, view/modal entrance, and a compositor-only cross-sell glow
   replacing the old animated-box-shadow version. */

:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --dur-fast: 160ms;
  --dur-view: 200ms;
}

/* Visible keyboard focus everywhere. Previously only the search input set
   outline:none, with nothing anywhere replacing it. */
:focus-visible {
  outline: 2px solid oklch(0.78 0.14 85);
  outline-offset: 2px;
}

/* Press feedback for every clickable control, targeted via the inline
   cursor:pointer style already present on every button/tile (91 spots
   across index.html + app.js) instead of adding a class to each one. */
[style*="cursor:pointer"] {
  transition: transform var(--dur-fast) var(--ease-out);
}
[style*="cursor:pointer"]:active {
  transform: scale(0.97);
}

/* Top-level view entrance (Overview / Pillar / Service drill-down) -- the
   most-repeated transition in the whole app. Exit stays an instant cut:
   sc-if removes the subtree synchronously in runtime.js, so an exit
   transition needs a runtime.js change, not just CSS. */
.view-enter {
  animation: viewEnter var(--dur-view) var(--ease-out) both;
}
@keyframes viewEnter {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Image lightbox entrance. transform-origin stays centered (correct for
   a modal, unlike a trigger-anchored popover). */
.modal-backdrop-enter {
  animation: modalBackdropEnter var(--dur-view) ease both;
}
@keyframes modalBackdropEnter {
  from { opacity: 0; }
  to { opacity: 1; }
}
.modal-panel-enter {
  animation: modalPanelEnter var(--dur-view) var(--ease-out) both;
}
@keyframes modalPanelEnter {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

/* Cross-sell attention ring -- compositor-only (transform/opacity) via a
   pseudo-element, replacing the old animated box-shadow (a paint
   property looping forever on the iPads this app targets). */
.cross-sell-glow {
  position: relative;
}
.cross-sell-glow::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  box-shadow: 0 0 0 3px oklch(0.78 0.14 85 / 0.55), 0 0 22px 4px oklch(0.78 0.14 85 / 0.35);
  opacity: 0;
  transform: scale(0.96);
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .cross-sell-glow::after {
    animation: crossSellPulse 2.2s var(--ease-out) infinite;
  }
}
@media (prefers-reduced-motion: reduce) {
  .cross-sell-glow::after {
    opacity: 0.7;
    transform: scale(1);
  }
}
@keyframes crossSellPulse {
  0%, 100% { opacity: 0; transform: scale(0.96); }
  50% { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .view-enter, .modal-backdrop-enter, .modal-panel-enter {
    animation: none;
  }
}

/* ============================================================
   PeopleFirst Support pills (Managed IT configurator)
   Added 2026-09-23, per Michael -- a read-only visual readout of what the
   device/add-on picker above has selected. See app.js's pfActive /
   peopleFirstPills for the active-state mapping.

   Restyled 2026-09-23, also per Michael, after the first pass: the
   original two-across layout for the last 6 pills was too narrow for
   their labels ("Data Backup for Computer", "Quarterly Risk Evaluation"),
   and the active state used a solid gold fill -- he wanted white pills
   with a gold glow instead, "like reminders throughout the rest of the
   Solutions Hub." That's literally .cross-sell-glow (see above) plus the
   near-white/gold-border card treatment CROSS_SELL_GLOW uses in app.js
   for the cross-sell suggestion cards -- reused here rather than
   reinvented, applied to .pf-pill--active below. Switched to one pill
   per row (no more pairing) so every label fits on one line.

   Originally paired with a "PeopleFirst Support" campaign photo (assets/
   peoplefirst-illustration.png) next to the pills, and the whole panel
   was position:fixed, centered on the viewport regardless of scroll.
   Both removed 2026-09-23, per Michael:
     - the photo "scales badly" -- dropped entirely, including its markup,
       assets reference, and the .pf-figure/.pf-figure-panel rules that
       used to live here (see git history if it's ever wanted back).
     - the panel itself is no longer position:fixed/viewport-centered --
       Michael wants the pills "fixed to the size of the network block of
       input fields" specifically, i.e. scrolling together with that one
       section rather than hovering independent of scroll position. So
       .pf-panel now lives in normal document flow, nested directly
       inside the "Size the network" sc-if block in index.html (right
       after the device-count rows), wrapped in a plain
       `<div style="position:relative;">` that sc-if adds around that
       block's own content -- .pf-panel is position:absolute relative to
       THAT wrapper, not the viewport, so it scrolls exactly with the
       block Michael named instead of drifting through unrelated parts
       of the form as the page scrolls.

   Desktop-only by design, not just by convenience: the configurator
   column is a max-width:740px block starting 50px in from its own
   padding, so this panel only has room to sit beside it -- without
   overlapping -- once the viewport is wide enough. Michael confirmed
   hiding it below that (rather than stacking it under the form) is
   fine, so it never shows on tablets or phones, only on wide
   desktop/laptop screens.

   Reordering (added 2026-09-23, per Michael): selected pills should
   physically glide to the top of the list, unselected ones to the
   bottom, as selections change -- not just re-sort instantly. The
   template runtime (runtime.js) patches an sc-for's rows by INDEX, not
   by identity, so reordering the underlying array would just swap
   label/class text between the same fixed DOM slots with no visible
   motion. Instead the array order never changes; app.js ranks each pill
   (active pills first, each group keeping PEOPLEFIRST_PILLS's own
   order) and writes that rank as an inline translateY() -- see
   pillStyle in app.js's peopleFirstPills. .pf-pill is absolutely
   positioned within .pf-pills so its own transform is what moves it,
   and the transition below is what makes that move animate instead of
   jump. PF_ROW_STEP in app.js (47px) must stay equal to this rule's
   height (39px) plus the vertical gap between rows (8px) -- there's no
   single shared source for that number since one lives in CSS px and
   the other in a JS constant, so a future height/spacing change here
   needs the matching PF_ROW_STEP update in app.js.
   ============================================================ */
.pf-panel {
  display: none;
  position: absolute;
  top: 0;
  left: calc(100% + 40px);
  width: 260px;
}
.pf-pills {
  position: relative;
  width: 260px;
  height: 603px; /* 13 pills * 47px row step - 8px (no trailing gap) */
}
.pf-pill {
  box-sizing: border-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 39px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  padding: 0 18px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.3;
  white-space: nowrap;
  text-align: center;
  color: var(--cbt-text-on-dark-muted);
  background: var(--cbt-bg-control-3);
  border: 2px solid transparent;
  transition: transform .5s cubic-bezier(.4,0,.2,1), background-color .45s ease, color .45s ease, border-color .45s ease;
}
.pf-pill--active {
  color: var(--cbt-text-on-light-primary);
  background: oklch(0.98 0.006 255);
  border-color: oklch(0.78 0.14 85);
}
@media (prefers-reduced-motion: reduce) {
  .pf-pill { transition: none; }
}
@media (min-width: 1440px) {
  .pf-panel { display: block; }
}

/* Project Mode tray (2026-09-23) -- see claude/solutions-creator-project-mode.md.
   Fixed bottom-right, sibling of .app-body in the markup (same reasoning
   as .pf-panel above) so it stays genuinely pinned to the viewport on
   every screen while the rep browses across pillars. */
.pm-tray {
  position: fixed;
  right: 28px;
  bottom: 28px;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}
.pm-tray-fab {
  width: 56px;
  height: 56px;
  border-radius: 999px;
  border: none;
  background: oklch(0.55 0.14 265);
  box-shadow: 0 10px 30px rgba(0,0,0,.4);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
  padding: 0;
}
.pm-tray-fab-count {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 22px;
  height: 22px;
  border-radius: 999px;
  background: oklch(0.78 0.14 85);
  color: oklch(0.2 0.05 85);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
}
.pm-tray-panel {
  width: 320px;
  max-height: 70vh;
  background: var(--cbt-card-bg);
  border-radius: 18px;
  padding: 16px;
  box-shadow: 0 16px 40px rgba(0,0,0,0.45);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.pm-tray-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.pm-tray-title {
  font-family: 'Jost', sans-serif;
  font-size: 16px;
  font-weight: 700;
  color: var(--cbt-text-on-light-primary);
}
.pm-tray-count {
  font-size: 12px;
  font-weight: 600;
  color: var(--cbt-text-on-light-muted);
}
.pm-tray-list {
  overflow-y: auto;
  max-height: 46vh;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.pm-tray-group-name {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--cbt-text-on-light-muted);
  margin-bottom: 6px;
}
.pm-tray-item {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--cbt-card-border);
}
.pm-tray-group .pm-tray-item:first-of-type {
  border-top: none;
}
.pm-tray-item-text {
  flex: 1;
  cursor: pointer;
  min-width: 0;
}
.pm-tray-item-service {
  font-size: 13px;
  font-weight: 700;
  color: var(--cbt-text-on-light-primary);
}
.pm-tray-item-detail {
  margin-top: 2px;
  font-size: 12px;
  color: var(--cbt-text-on-light-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pm-tray-item-remove {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: none;
  background: var(--cbt-card-border);
  color: var(--cbt-text-on-dark-disabled);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  line-height: 1;
}
.pm-tray-review {
  border: none;
  border-radius: 999px;
  padding: 11px 18px;
  background: oklch(0.78 0.14 85);
  color: oklch(0.2 0.05 85);
  font-size: 13.5px;
  font-weight: 700;
  cursor: pointer;
}
@media (max-width: 480px) {
  .pm-tray { right: 16px; bottom: 16px; }
  .pm-tray-panel { width: calc(100vw - 32px); }
}

/* Project Mode star badge (2026-09-23) -- see
   claude/solutions-creator-project-mode.md. One shared button style used
   on pillar/service/category tiles, always placed as a normal flex-row
   sibling next to whatever else sits at that tile's top-right (an
   existing "N selected" badge, a checkmark, a chevron) rather than
   absolutely positioned, so it can never overlap them. .pm-star--on is
   the "starred" state (tapped -> added to the project cart as a
   placeholder). Uses currentColor so the SVG star inside just inherits
   `color` rather than needing separate fill/stroke fields computed in JS. */
.pm-star-btn {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  background: rgba(255,255,255,0.78);
  color: oklch(0.55 0.02 255);
}
.pm-star-btn.pm-star--on {
  background: oklch(0.78 0.14 85);
  color: oklch(0.4 0.1 85);
}
.pm-star-btn svg {
  fill: none;
  stroke: currentColor;
}
.pm-star-btn.pm-star--on svg {
  fill: currentColor;
  stroke: currentColor;
}
/* Tray placeholder rows (starred but not yet configured) -- see the
   projectTrayGroups computation in app.js. */
.pm-tray-item--placeholder .pm-tray-item-service {
  color: var(--cbt-text-on-light-secondary);
}
.pm-tray-item--placeholder .pm-tray-item-detail {
  color: oklch(0.55 0.1 85);
  font-weight: 600;
}

/* ---- Home screen tiles (switchboard style, 2026-10-07): dark panels, lit edge, status light, heartbeat on hover ---- */
.hub-tiles { margin-top: 36px; display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 22px; max-width: 1380px; }
.hub-tile { --c: #4d7bd6; position: relative; display: flex; flex-direction: column; min-height: 252px; padding: 26px; border-radius: 22px; overflow: hidden; text-decoration: none; color: #eef0f5; cursor: pointer;
  background: linear-gradient(160deg, #1d2a4d, #0f1830); border: 1px solid rgba(255,255,255,.08); box-shadow: 0 1px 2px rgba(15,24,48,.15), 0 14px 30px -16px rgba(15,24,48,.55); transition: transform .25s, border-color .25s; }
.hub-tile::before { content: ''; position: absolute; inset: 0; background-image: radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px); background-size: 16px 16px; -webkit-mask-image: linear-gradient(180deg, transparent 20%, #000); mask-image: linear-gradient(180deg, transparent 20%, #000); pointer-events: none; }
.hub-tile::after { content: ''; position: absolute; left: 26px; right: 26px; top: 0; height: 3px; border-radius: 0 0 3px 3px; background: var(--c); box-shadow: 0 0 18px 1px var(--c); opacity: .8; transition: opacity .25s, left .3s, right .3s; }
.hub-tile-top { display: flex; justify-content: space-between; align-items: flex-start; position: relative; }
.hub-tile-chip { flex: none; width: 56px; height: 56px; border-radius: 16px; border: 1.5px solid var(--c); color: var(--c); display: grid; place-items: center; background: rgba(255,255,255,.03); }
.hub-tile-chip svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.hub-tile-led { width: 11px; height: 11px; border-radius: 50%; margin-top: 6px; background: #9dc22c; box-shadow: 0 0 10px 2px rgba(157,194,44,.7); }
.hub-tile-badge { background: var(--c); color: #fff; font-size: 12.5px; font-weight: 700; padding: 4px 12px; border-radius: 999px; }
.hub-tile-soon { font-size: 13px; font-weight: 600; padding: 4px 12px; border-radius: 999px; background: rgba(227,176,75,.15); color: #f0c870; }
.hub-tile-txt { margin-top: auto; padding-top: 34px; position: relative; }
.hub-tile-title { font-family: 'Jost', sans-serif; font-size: 26px; font-weight: 700; line-height: 1.1; letter-spacing: -.01em; color: #fff; }
.hub-tile-desc { margin-top: 8px; font-size: 16.5px; line-height: 1.45; color: #c4cadb; }
.hub-beat { display: block; width: 100%; height: 30px; margin-top: 16px; position: relative; overflow: visible; }
.hub-beat path { fill: none; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.hub-beat .base { stroke: rgba(255,255,255,.16); stroke-width: 1.5; transition: stroke .25s; }
.hub-beat .pulse { stroke: var(--c); stroke-width: 2.4; stroke-dasharray: 16 84; stroke-dashoffset: 16; opacity: 0; filter: drop-shadow(0 0 3px var(--c)); animation: hub-sweep 1.35s linear infinite paused; transition: opacity .2s; }
.hub-tile:hover, .hub-tile:focus-visible { transform: translateY(-4px); border-color: color-mix(in srgb, var(--c) 60%, transparent); }
.hub-tile:hover::after, .hub-tile:focus-visible::after { opacity: 1; left: 14px; right: 14px; }
.hub-tile:hover .hub-beat .pulse, .hub-tile:focus-visible .hub-beat .pulse { opacity: 1; stroke-width: 2.8; filter: drop-shadow(0 0 6px var(--c)); animation-play-state: running; }
.hub-tile:hover .hub-beat .base, .hub-tile:focus-visible .hub-beat .base { stroke: color-mix(in srgb, var(--c) 40%, transparent); }
.hub-tile:focus-visible { outline: 3px solid #4d7bd6; outline-offset: 3px; }
.hub-tile--soon { --c: #e3b04b; cursor: default; }
.hub-tile--soon .hub-tile-led { background: #e3b04b; box-shadow: 0 0 10px 2px rgba(227,176,75,.6); animation: hub-blink 2.4s ease-in-out infinite; }
.hub-tile--soon:hover { transform: none; border-color: rgba(255,255,255,.08); }
.hub-tile--soon:hover::after { left: 26px; right: 26px; opacity: .8; }
.hub-tile--soon .hub-beat .pulse, .hub-tile--soon:hover .hub-beat .pulse { opacity: 0; animation: none; }
.hub-tile--soon:hover .hub-beat .base { stroke: rgba(255,255,255,.16); }
:root[data-theme="dark"] .hub-tile { background: linear-gradient(160deg, #222f55, #1a2447); border-color: rgba(255,255,255,.12); }
@keyframes hub-sweep { from { stroke-dashoffset: 16; } to { stroke-dashoffset: -84; } }
@keyframes hub-blink { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .hub-tile, .hub-tile::after { transition: none; } .hub-tile--soon .hub-tile-led { animation: none; } .hub-beat .pulse { animation: none; stroke-dasharray: none; } }
button.hub-tile { font: inherit; text-align: left; width: 100%; }
