/* =====================================================================================================
   Hub (SPEC3 §1, §2, §4): the hero's row of CRM tiles (app icons, or name plates for brands not cleared), and
   the port and dotted chute that lead into the mini Duit right below (its .mini-socket). Owner: hub. Markup:
   parts/hub.html (generated by tools/hub_build.py; the logos are the brands' own and published only once
   cleared, see tools/crm_logos/LOGOS.md). Motion: js/hub.js.

   PARTS    svg.hub-defs: <symbol id="logo-<id>"> app icons, for any part (viewBox 0 0 64 64); only brands cleared
                       in tools/crm_logos/manifest.py have one (none today), and with none the svg is left out
            .hub-row > .hub-track > ul.hub-list x2 (the second aria-hidden) > li > button.hub-tile
                       (.hub-icon > svg, .hub-name), or for a brand not cleared button.hub-tile.is-plain
                       (.hub-icon.hub-plate > .hub-name: the name as text on a neutral plate, no mark)
            .hub-link: .hub-hint (left) · .hub-wire > .hub-port + .hub-chute + .hub-glint (centre) ·
                       .hub-ctl > .hub-toggle (right)
   STATES   .hub              static: one row, centred, scrolls sideways when it does not fit (no JS, or
                              reduced motion), faded at the end(s) with more to scroll to.
            .hub.has-js       hub.js runs; it sets .is-more-start / .is-more-end on the static row.
            .hub.is-moving    the row is a clipped marquee (the track moved by a transform), one lap wide at most.
            .hub-tile.is-pop  the tile under the mouse (desktop), or just tapped (.is-tapped), or focused from the
                              keyboard (:focus-visible): it rises and grows, with a glow in its --glow colour
                              (--glow, set per tile), and its name brightens. Reduced motion: glow only.
            .hub-tile.is-port the tile passing over the port: a gentle glow.
            .hub-port.is-hot  data passing through the port.
   SIZES    --tile (icon), --pitch (one tile's slot: hub.js reads it from the layout), --stub (wire above the
            port), --port, --link (height of the port band), --reach (how far the chute runs below the hub to
            the mini Duit's socket: hub.js measures it; the default is the hero's row gap), --fade (edge fades).
   ===================================================================================================== */

.hub {
  --tile: 64px; --pitch: 120px; --stub: 8px; --port: 32px; --link: 60px; --fade: 8%;
  --reach: clamp(32px, 4vw, 48px);
  --pop: translateY(-7px) scale(1.15);
  position: relative; display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0;
  margin-top: -16px; /* the row's headroom for a popped tile's glow sits inside the hero's row gap */
}
@media (max-width: 599px) {
  .hub { --tile: 52px; --pitch: 100px; --stub: 8px; --port: 30px; --link: 56px; --fade: 11%; --pop: translateY(-6px) scale(1.14); }
}
.hub-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ---------- the row ---------- */
.hub-row {
  --fl: 0px; --fr: 0px; /* static edge fades, switched on per side below */
  width: 100%; min-width: 0; padding-block: 24px 4px; /* headroom: the moving row clips, and a popped tile rises and glows */
  overflow-x: auto; overscroll-behavior-x: contain; scroll-padding-inline: var(--fade);
  scrollbar-width: thin; scrollbar-color: var(--faint) transparent;
}
.hub-track { display: flex; width: max-content; min-width: 100%; }
.hub-list { display: flex; flex: none; margin-inline: auto; padding-inline: var(--gutter); }
.hub-list > li { flex: none; display: flex; justify-content: center; width: var(--pitch); }
.hub:not(.is-moving) .hub-list[aria-hidden="true"] { display: none; }

/* static row that does not fit: fade the side(s) with more to scroll to. Without JS it cannot know, so it fades
   the right end whenever the row is wider than the screen. */
.hub:not(.is-moving) .hub-row {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--fl), #000 calc(100% - var(--fr)), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 var(--fl), #000 calc(100% - var(--fr)), transparent);
}
.hub.has-js:not(.is-moving) .hub-row.is-more-start { --fl: var(--fade); }
.hub.has-js:not(.is-moving) .hub-row.is-more-end { --fr: var(--fade); }
@media (max-width: 1239px) { .hub:not(.has-js) .hub-row { --fr: var(--fade); } }

/* moving: one lap wide at most (9.5 slots), so a CRM and its copy are never on screen together */
.hub.is-moving .hub-row {
  max-width: calc(9.5 * var(--pitch)); margin-inline: auto;
  overflow: clip; /* clip, not hidden: focus must never scroll the row; hub.js brings a focused tile in */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
}
.hub.is-moving .hub-track { min-width: 0; will-change: transform; }
.hub.is-moving .hub-list { margin: 0; padding: 0; }

/* a tile: the brand's app icon, its name under it. The button (icon and name) carries the focus ring and the tap
   target; its top padding leaves room for the popped icon inside the ring. */
.hub-tile {
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  width: calc(var(--pitch) - 4px); margin: 0; padding: 12px 4px 5px; border: 0; border-radius: 12px; background: none;
  color: var(--soft); cursor: pointer; -webkit-tap-highlight-color: transparent;
  font-family: var(--sans); font-size: 13px; font-weight: 400; line-height: 1.3; letter-spacing: 0;
}
.hub-icon {
  --ring: color-mix(in oklch, var(--glow, var(--accent)) 70%, transparent);
  --halo: color-mix(in oklch, var(--glow, var(--accent)) 58%, transparent);
  position: relative; display: block; width: var(--tile); height: var(--tile); border-radius: 22%;
  transition: transform .22s cubic-bezier(.2, .9, .25, 1), box-shadow .22s ease-out;
}
.hub-icon svg { display: block; width: 100%; height: 100%; }
.hub-icon::after { /* the port glow, on its own layer so it can fade slowly without slowing the pop */
  content: ""; position: absolute; inset: 0; border-radius: inherit; opacity: 0; pointer-events: none;
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--glow, var(--accent)) 40%, transparent), 0 0 20px -5px var(--halo);
  transition: opacity .9s ease-out;
}
.hub-name { white-space: nowrap; transition: color .22s ease-out; }

/* a brand whose mark is not cleared for use (tools/crm_logos/manifest.py "cleared"; today, all ten): only its name,
   as text, on a neutral plate as tall as an icon and a little wider. No mark, no brand colour; the glow is --soft
   (set per tile by hub_build.py). The name sits on the plate, so there is none under it. */
.hub-plate {
  display: grid; place-items: center; width: calc(var(--pitch) - 24px); padding: 0 6px; /* popped (x1.15) it still clears the focus ring */
  border: 1px solid var(--line-strong); border-radius: calc(var(--tile) * .22); background: var(--surface);
}
.hub-plate .hub-name {
  max-width: 100%; white-space: normal; text-align: center; text-wrap: balance;
  font-size: 14px; font-weight: 600; line-height: 1.2; color: var(--soft);
}
@media (max-width: 599px) {
  .hub-plate { width: calc(var(--pitch) - 20px); padding: 0 4px; }
  .hub-plate .hub-name { font-size: 12px; }
}
/* .hub.is-names (hub_build.py: no brand cleared, so every tile is a plate): no names under the tiles, so the row
   is shorter and a popped plate's glow would meet the row's clip edge 9px below it. Moving, the row reaches 30px
   further down under the port band (which is transparent and sits above it), so the glow fades out instead of
   being cut, and the layout does not move. Static (reduced motion, no JS), the row scrolls sideways and its
   scrollbar must stay clear, so the tiles take the room themselves. */
.hub.is-names.is-moving .hub-row { padding-bottom: 34px; margin-bottom: -30px; }
.hub.is-names:not(.is-moving) .hub-tile { padding-bottom: 24px; }

/* the pop: hover (desktop, set by hub.js), a tap, keyboard focus */
.hub-tile.is-pop .hub-icon, .hub-tile.is-tapped .hub-icon, .hub-tile:focus-visible .hub-icon,
.hub:not(.has-js) .hub-tile:hover .hub-icon {
  transform: var(--pop);
  box-shadow: 0 0 0 1px var(--ring), 0 12px 30px -6px var(--halo), 0 0 26px 0 var(--halo);
}
.hub-tile.is-pop .hub-name, .hub-tile.is-tapped .hub-name, .hub-tile:focus-visible .hub-name,
.hub:not(.has-js) .hub-tile:hover .hub-name { color: var(--text); }
.hub-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* passing over the port: a gentle glow that comes up quickly and leaves slowly */
.hub-tile.is-port .hub-icon::after { opacity: 1; transition-duration: .3s; }
.hub-tile.is-port .hub-name { color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .hub-icon, .hub-icon::after, .hub-name { transition: none; }
  .hub-tile.is-pop .hub-icon, .hub-tile.is-tapped .hub-icon, .hub-tile:focus-visible .hub-icon,
  .hub:not(.has-js) .hub-tile:hover .hub-icon { transform: none; }
}

/* short laptop screens (1280x720, 1366x768): a tighter stack, so more of the mini Duit shows above the fold */
@media (min-width: 900px) and (max-height: 820px) {
  .hub { --tile: 56px; --pitch: 112px; --link: 52px; margin-top: -28px; }
  .hub-tile { gap: 7px; padding-top: 11px; }
  .hub-plate .hub-name { font-size: 13px; }
}

/* ---------- the port band: hint left, port and chute centred, Pause right; as wide as the mini Duit ---------- */
.hub-link {
  position: relative; z-index: 1; /* the chute runs on over the hero's row gap, above the window's glow */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  grid-template-rows: calc(2 * var(--stub) + var(--port)) minmax(0, 1fr); column-gap: 16px;
  width: min(1100px, 100% - 2 * var(--gutter)); height: var(--link); margin-inline: auto;
}
.hub-hint {
  grid-area: 1 / 1; align-self: center; justify-self: start; max-width: 26ch;
  font-family: var(--mono); font-size: 12px; line-height: 1.45; color: var(--soft);
}
.hub-ctl { grid-area: 1 / 3; align-self: center; justify-self: end; }

/* the wire runs from the row down through the port and on to the socket (--reach below the hub) */
.hub-wire { grid-area: 1 / 2 / 3 / 3; position: relative; width: 44px; margin-bottom: calc(-1 * var(--reach)); }
.hub-wire::before { /* the dotted connector: square dots */
  content: ""; position: absolute; top: 0; bottom: 0; left: calc(50% - 1px); width: 2px;
  background: linear-gradient(var(--line-strong) 2px, transparent 2px) 0 2px / 2px 6px repeat-y;
}
.hub-port {
  position: absolute; z-index: 2; top: var(--stub); left: calc(50% - var(--port) / 2);
  display: grid; place-items: center; width: var(--port); height: var(--port);
  border: 1px solid var(--line-strong); border-radius: 50%; background: var(--bg); color: var(--text);
  transition: border-color .3s ease-out;
}
.hub-port svg { width: 15px; height: 15px; }
.hub-port-dot { transition: fill .3s ease-out; }
.hub-port.is-hot { border-color: color-mix(in oklch, var(--hot, var(--accent)) 80%, transparent); }
.hub-port.is-hot .hub-port-dot { fill: var(--hot, var(--accent)); }
/* pulses run inside the chute: from the port's centre down to the socket, clipped at both ends, so a pulse
   leaves from inside the port and slips into the socket */
.hub-chute { position: absolute; z-index: 1; top: calc(var(--stub) + var(--port) / 2); bottom: 0; left: 0; right: 0; overflow: hidden; }
/* a short glint where the chute meets the window, as a picked CRM's pulse arrives */
.hub-glint {
  position: absolute; z-index: 2; bottom: 0; left: calc(50% - 36px); width: 72px; height: 1px; opacity: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
@media (max-width: 599px) {
  .hub-link { column-gap: 10px; }
  .hub-port svg { width: 14px; height: 14px; }
  .hub-hint { max-width: 13ch; }
}

/* the pulse: a head with a dotted wake (hub.js moves it with the Web Animations API). A picked CRM sends a gold
   one; a tile passing the port sends a smaller, quieter one in the tile's --glow (.is-quiet, --hot). */
.hub-pulse { position: absolute; top: 0; left: 50%; width: 0; height: 0; pointer-events: none; }
.hub-pulse b {
  position: absolute; left: -4.5px; top: -4.5px; width: 9px; height: 9px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent) 20%, transparent), 0 0 14px 3px color-mix(in oklch, var(--accent) 45%, transparent);
}
.hub-pulse i {
  position: absolute; left: -1px; bottom: 8px; width: 2px; height: 36px;
  background: linear-gradient(var(--accent) 2px, transparent 2px) 0 0 / 2px 6px repeat-y;
  -webkit-mask-image: linear-gradient(transparent, #000); mask-image: linear-gradient(transparent, #000);
}
.hub-pulse.is-quiet { opacity: .75; }
.hub-pulse.is-quiet b {
  left: -3px; top: -3px; width: 6px; height: 6px; background: var(--hot);
  box-shadow: 0 0 10px 1px color-mix(in oklch, var(--hot) 55%, transparent);
}
.hub-pulse.is-quiet i {
  bottom: 5px; height: 24px; background-image: linear-gradient(var(--hot) 2px, transparent 2px);
}

/* the toggle: small, mono, with a 44px tap target */
.hub-toggle {
  position: relative; display: inline-flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px 0 10px;
  border: 1px solid var(--faint); border-radius: 999px; background: transparent; color: var(--soft);
  font-family: var(--mono); font-size: 12px; line-height: 1; letter-spacing: .02em;
}
.hub-toggle::after { content: ""; position: absolute; inset: -7px -2px; }
.hub-toggle:hover { border-color: var(--line-strong); color: var(--text); }
.hub-toggle-ico { position: relative; width: 8px; height: 9px; }
.hub-toggle-ico::before, .hub-toggle-ico::after { content: ""; position: absolute; top: 0; width: 3px; height: 9px; background: currentColor; }
.hub-toggle-ico::before { left: 0; }
.hub-toggle-ico::after { right: 0; }
.hub-toggle.is-off .hub-toggle-ico::before { width: 0; height: 0; background: none; border-style: solid; border-width: 4.5px 0 4.5px 8px; border-color: transparent transparent transparent currentColor; }
.hub-toggle.is-off .hub-toggle-ico::after { display: none; }
