/* vrc.dj  <tag-orbit> (_js/tag-orbit.js) - layout and states only.
   Chips are the site's .tag-chip (components.css); this file sizes them per
   placement and adds the orbit's own states (focus, dim, highlight, more,
   drop). Colours are tokens, so both themes follow core.css. */

tag-orbit {
  display: block;
  position: relative;
  width: 100%;
  max-width: 100%;
  min-height: 240px;
  container-type: inline-size;
  user-select: none;
  --to-fs: 12.5px;
  --to-pad: 5px 11px;
  --to-line: color-mix(in srgb, var(--text) 26%, transparent);
  --to-ring: var(--border);
  --to-bg: var(--bg-base);
}
tag-orbit:not([style*="height"]) { aspect-ratio: 4 / 3; }
tag-orbit[size="hero"] { --to-fs: 14px; --to-pad: 7px 14px; }
tag-orbit[size="mini"] { --to-fs: 11px; --to-pad: 3px 8px; min-height: 200px; }

/* No-JS fallback (the relation rows) until the element upgrades */
tag-orbit:not(.is-ready) { aspect-ratio: auto; min-height: 0; }

/* .to-stage is the crop frame: it never moves or scales, so a warp
   transform on .to-warp (below) can never paint past the component's own
   box - the whole point of the animation is a jump that stays "inside the
   card", not one that balloons over the surrounding page. */
.to-stage { position: absolute; inset: 0; overflow: hidden; }
.to-warp { position: absolute; inset: 0; }
/* Stacked layout: rows grow as far as the tag needs, so the stage scrolls
   rather than dropping the tail into a "+n more". */
tag-orbit.is-compact .to-stage { overflow-y: auto; overflow-x: hidden; }
/* A redraw for a new box size lands at once (tag-orbit.js _render, is-snap
   for the length of that one draw): the glides below are for moving between
   levels, and carried into a resize they floated the whole orbit - galaxies,
   rings, lines and the haze's glows - across to its new layout. */
tag-orbit.is-snap,
tag-orbit.is-snap * { transition: none !important; }
/* Fades in to the element's own opacity and then lets go of it - held at
   1 (a to-keyframe with fill: both) it pinned every line at full strength
   after a page load and beat the hover and search dimming. */
@keyframes to-in { from { opacity: 0; } }

.to-ring {
  position: absolute;
  border: 1px dashed var(--to-ring);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  /* Above the lines: the parent's line passes BEHIND the rings. Lifting the
     rings rather than pushing that line to a negative layer, which escapes
     tag-orbit and hides behind the host's own background. */
  z-index: 1;
  transition: all .45s cubic-bezier(.2, .8, .2, 1);
}
.to-ring--outer { opacity: .6; }
/* The one word left on a line: "secondary", on the first secondary parent's */
.to-tag {
  position: absolute;
  z-index: 3;
  transform: translate(-50%, -50%);
  padding: 1px 5px;
  background: var(--to-bg);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
  pointer-events: none;
  transition: all .45s cubic-bezier(.2, .8, .2, 1);
}

/* Lines run parent -> child and stop a few px short of each chip's edge
   (tag-orbit.js clips them to the measured chips); the dot marks where the
   child plugs in. */
.to-line {
  --lw: 2px;
  position: absolute;
  height: 0;
  border-top: var(--lw) solid var(--to-line);
  color: var(--to-line);
  transform-origin: 0 0;
  pointer-events: none;
  opacity: .9;
  /* A re-centre glides; a hover trace (below) answers at once. */
  transition: all .45s cubic-bezier(.2, .8, .2, 1), opacity .18s ease, box-shadow .18s ease, border-top-width .18s ease;
  animation: to-in .3s backwards;
}
.to-line::after {
  content: '';
  position: absolute;
  right: 0;
  top: calc(var(--lw) / -2 - 2.5px);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}
.to-line.sec { --lw: 1.5px; border-top-style: dashed; }
.to-line.faint { --lw: 1px; opacity: .35; border-top-style: dashed; }
.to-line.faint::after { display: none; }
/* A fan's ray - a sibling's back to the parent it shares with the centre, a
   child's back to the centre: thin and quiet, so thirty of them read as one
   fan rather than a thicket. Hovering one lifts its own (.is-lit, below). */
.to-line.ray { --lw: 1px; opacity: .2; }
.to-line.ray::after { display: none; }
.to-line.is-hidden { display: none; }

/* Hovering a chip traces its own lines (tag-orbit.js _hover): they come up,
   thicken and glow in their tag's colour, a pulse runs along each from
   parent to child, the chips at their far ends light up, and every other
   line steps back. A notch under full strength (user call): the centre chip
   lights every child's ray at once - EDM's 27 - and at full glow, full
   width and a near-white pulse the fan shouted. */
tag-orbit.is-tracing .to-line { opacity: .1; }
tag-orbit.is-tracing .to-line.is-lit {
  --lw: 2px;
  opacity: .9;
  z-index: 1;
  box-shadow:
    0 0 4px color-mix(in srgb, currentColor 55%, transparent),
    0 0 12px color-mix(in srgb, currentColor 25%, transparent);
}
tag-orbit.is-tracing .to-line.is-lit::after { display: block; box-shadow: 0 0 6px color-mix(in srgb, currentColor 70%, transparent); }
.to-line.is-lit::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--lw) * -1 - 1px);
  height: calc(var(--lw) + 2px);
  border-radius: 99px;
  background: linear-gradient(90deg, transparent 35%, color-mix(in srgb, currentColor 60%, white) 50%, transparent 65%) no-repeat;
  background-size: 300% 100%;
  animation: to-trace 1.3s linear infinite;
}
/* 100% -> 0% carries the band from just before the line's start (the parent
   end, tag-orbit.js draws every line a -> b parent first) past its end. */
@keyframes to-trace { from { background-position: 100% 0; } to { background-position: 0 0; } }

.to-node {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  text-decoration: none;
  z-index: 2;
  transition: left .45s cubic-bezier(.2, .8, .2, 1), top .45s cubic-bezier(.2, .8, .2, 1);
  animation: to-in .3s backwards;
}
.to-node:focus-visible { outline: none; }
.to-node:focus-visible .to-chip { outline: 2px solid var(--accent); outline-offset: 3px; }
.to-node small { font-size: 10px; line-height: 1; color: var(--text-muted); white-space: nowrap; }

/* The chip: a .tag-chip at the placement's size, its own tint
   (--tag-chip-bg, components.css) laid over a solid plate of the host's
   backdrop - a line that crosses a chip runs behind it, not through the
   label, and the chip looks the same as it would unplated. The stacked
   (phone) layout's centred chip is not a .to-node but is the same chip. */
.to-node .to-chip,
.to-flow-me .to-chip {
  font-size: var(--to-fs);
  padding: var(--to-pad);
  line-height: 1.2;
  font-weight: 500;
  white-space: nowrap;
  position: relative;
  background: linear-gradient(var(--tag-chip-bg), var(--tag-chip-bg)), var(--to-bg);
  box-shadow: var(--to-rim, 0 0 #0000);
}
.to-chip em { font-style: normal; font-size: .8em; opacity: .7; margin-left: 4px; }
.to-node.dim .to-chip { opacity: .55; }
/* A secondary parent reads as one at a glance: its chip dashed like its line */
.to-node.sec .to-chip { border-style: dashed; }
/* Clipped to its share of the parent row (tag-orbit.js sets max-width) */
.to-node .to-chip--clip { display: block; overflow: hidden; text-overflow: ellipsis; }
/* A dot's label stays parked: a trace lights the dot, it does not open it */
tag-orbit .to-node.is-linked:not(.blob) .to-chip { opacity: 1; }
tag-orbit .to-node.is-linked .to-blob {
  opacity: 1;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cat-color) 28%, transparent), var(--to-rim, 0 0 #0000);
}
.to-node:hover .to-chip,
.to-node.hl .to-chip {
  opacity: 1;
  box-shadow:
    0 0 0 3px color-mix(in srgb, var(--cat-color) 30%, transparent),
    0 0 16px color-mix(in srgb, var(--cat-color) 40%, transparent),
    var(--to-rim, 0 0 #0000);
}
.to-node.focus { z-index: 4; }
.to-node.focus .to-chip,
.to-flow-me .to-chip {
  font-weight: 600;
  font-size: calc(var(--to-fs) * 1.15);
  box-shadow:
    0 0 0 4px color-mix(in srgb, var(--cat-color) 25%, transparent),
    var(--shadow-lg),
    var(--to-rim, 0 0 #0000);
}
/* open-focus: in an interactive orbit every other chip re-centres, so the
   centred chip needs to read as the one exception rather than another node
   to zoom into - a small expand/contract glyph on the chip itself, not a separate
   control (it's a real link the whole chip already carries). */
.to-chip--open i { margin-left: 5px; font-size: .8em; opacity: .8; }
.to-flow-me { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.to-flow-me a { text-decoration: none; }
.to-alias {
  position: absolute;
  z-index: -1;
  padding: var(--to-pad);
  border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--cat-color) 50%, transparent);
  /* A tint, not a plate of the page colour: a plate vanishes where the orbit
     sits on that colour, but over the /tags haze or a tag's photo it was a
     solid slab stepping down-right from the chip - a heavy drop shadow. */
  background: color-mix(in srgb, var(--cat-color) 14%, transparent);
  /* Only the card edge peeks out: a label under the translucent chip would
     read as overlapping text. The card is the chip's own size, not its
     text's - a long alias under a short name (#electronicdancemusic under
     EDM) otherwise stuck out sideways as a slab. */
  width: 100%;
  height: 100%;
  overflow: hidden;
  color: transparent;
  font-size: var(--to-fs);
  line-height: 1.2;
  white-space: nowrap;
  pointer-events: none;
}
/* Blob: a sibling as a dot (sized by how many children it has) until hover,
   keyboard focus or a tap opens it. The label is the same .tag-chip sitting
   over the dot, so tag-chip-tooltip.js attaches to it exactly as anywhere
   else and screen readers still read the name. */
.to-blob {
  width: var(--d, 11px);
  height: var(--d, 11px);
  border-radius: 50%;
  background: color-mix(in srgb, var(--cat-color) 55%, var(--to-bg));
  border: 1px solid color-mix(in srgb, var(--cat-color) 85%, transparent);
  box-shadow: var(--to-rim, 0 0 #0000);
  transition: transform .14s var(--ease, ease), box-shadow .14s var(--ease, ease);
}
.to-node.blob { gap: 0; }
/* A dot is a small target: this invisible disc gives it a finger-sized one
   without pushing the dots themselves apart. */
.to-node.blob::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 30px;
  height: 30px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
}
.to-node.blob .to-chip {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(.92);
  opacity: 0;
  pointer-events: none;
  transition: opacity .14s var(--ease, ease), transform .14s var(--ease, ease);
}
.to-node.blob:hover,
.to-node.blob:focus-within,
.to-node.blob.is-open { z-index: 6; }
.to-node.blob:hover .to-chip,
.to-node.blob:focus-within .to-chip,
.to-node.blob.is-open .to-chip {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  pointer-events: auto;
}
.to-node.blob:hover .to-blob,
.to-node.blob:focus-within .to-blob,
.to-node.blob.is-open .to-blob {
  transform: scale(1.25);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cat-color) 22%, transparent), var(--to-rim, 0 0 #0000);
}
.to-node.dim .to-blob { opacity: .55; }
/* Ancestors stack above the parent, context rather than destinations - each
   level up shrinks and fades a bit more, so the stack itself reads as
   "further back" without needing a label. anc-N is distance from the
   parent (1 = nearest); only 1 and 2 exist (the stack caps at 2). */
.to-node.anc .to-chip { font-size: calc(var(--to-fs) * .85); opacity: .65; }
.to-node.anc-2 .to-chip { font-size: calc(var(--to-fs) * .74); opacity: .45; }
.to-node.anc:hover .to-chip { opacity: 1; }

.to-back {
  position: absolute;
  left: 10px;
  top: 10px;
  z-index: 5;
  display: inline-flex;
  gap: 6px;
  align-items: center;
  font: 500 12px inherit;
  font-family: inherit;
  color: var(--text-dim);
  background: color-mix(in srgb, var(--to-bg) 70%, transparent);
  border: 1px solid var(--to-ring);
  border-radius: 99px;
  padding: 5px 10px;
  cursor: pointer;
}
.to-back:hover { color: var(--text); border-color: var(--border-bright); }

.to-cat {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -.02em;
  pointer-events: none;
  z-index: 3;
  text-align: center;
  white-space: nowrap;
}
.to-cat small { display: block; font-size: 11px; font-weight: 500; color: var(--text-muted); margin-top: 3px; letter-spacing: 0; }

/* ── Universe level: one galaxy per category ─────────────────────────── */
/* (left, top) is the disc's centre, not the middle of disc-plus-labels: the
   haze behind, a ripple and the ring it sits on all centre on that point,
   and a gate's disc must sit exactly where a parent chip would. */
.to-galaxy {
  position: absolute;
  transform: translate(-50%, calc(var(--to-r, 40px) * -1));
  transform-origin: 50% var(--to-r, 40px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  z-index: 2;
  cursor: pointer;
  transition: left .45s cubic-bezier(.2, .8, .2, 1), top .45s cubic-bezier(.2, .8, .2, 1), transform .2s var(--ease, ease);
  animation: to-in .3s backwards;
}
.to-gal-disc {
  position: relative;
  display: block;
  width: calc(var(--to-r, 40px) * 2);
  height: calc(var(--to-r, 40px) * 2);
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--cat-color) 55%, transparent);
  background:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--cat-color) 26%, transparent), transparent 70%),
    color-mix(in srgb, var(--cat-color) 8%, var(--to-bg));
  overflow: hidden;
  transition: border-color .2s var(--ease, ease), box-shadow .2s var(--ease, ease);
}
/* The specks inside a galaxy: one per tag, placed by the component */
.to-gal-disc i {
  position: absolute;
  border-radius: 50%;
  opacity: .75;
}
.to-gal-name {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--text);
  white-space: nowrap;
}
.to-galaxy small { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.to-galaxy:hover { transform: translate(-50%, calc(var(--to-r, 40px) * -1)) scale(1.04); }
.to-galaxy:hover .to-gal-disc,
.to-galaxy:focus-visible .to-gal-disc {
  border-color: color-mix(in srgb, var(--cat-color) 85%, transparent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cat-color) 18%, transparent), 0 0 30px color-mix(in srgb, var(--cat-color) 22%, transparent);
}
.to-galaxy:focus-visible { outline: none; }
/* Orbit gate: a root tag's stand-in "parent" is its category, not another
   tag - reusing the galaxy node (so clicking it already flies to catalog
   mode) but dashed and voided instead of filled, so it reads as a
   container you're leaving, never as a sibling chip. */
.to-galaxy--gate { gap: 3px; opacity: .8; }
.to-galaxy--gate .to-gal-disc {
  border-style: dashed;
  background: var(--to-bg);
}
.to-galaxy--gate .to-gal-name { font-size: 12px; font-weight: 600; }
.to-galaxy--gate small { font-size: 10px; }
.to-galaxy--gate:hover,
.to-galaxy--gate:focus-visible { opacity: 1; }
.to-gal-hits {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 99px;
  background: var(--cat-color);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  text-align: center;
}

/* Search: everything that does not match steps well back, so the hits are
   what there is to look at - but it stays legible as context (user call:
   the old .07 was too hard a cut). */
tag-orbit.is-searching .to-node.nohit,
tag-orbit.is-searching .to-galaxy.nohit { opacity: .18; filter: grayscale(.75); }
tag-orbit.is-searching .to-line,
tag-orbit.is-searching .to-ring,
tag-orbit.is-searching .to-tag { opacity: .12; }
tag-orbit.is-searching .to-node.hit { z-index: 7; }
tag-orbit.is-searching .to-node.hit .to-chip {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cat-color) 55%, transparent), 0 0 24px color-mix(in srgb, var(--cat-color) 45%, transparent), var(--to-rim, 0 0 #0000);
}
tag-orbit.is-searching .to-node.hit .to-blob {
  transform: scale(1.5);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cat-color) 40%, transparent), 0 0 20px color-mix(in srgb, var(--cat-color) 50%, transparent), var(--to-rim, 0 0 #0000);
}
tag-orbit.is-searching .to-galaxy.hit .to-gal-disc {
  border-color: color-mix(in srgb, var(--cat-color) 90%, transparent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cat-color) 22%, transparent), 0 0 36px color-mix(in srgb, var(--cat-color) 30%, transparent);
}

/* Breadcrumb: the way back out of a galaxy */
.to-crumb {
  position: absolute;
  left: 12px;
  top: 12px;
  z-index: 7;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 99px;
  border: 1px solid var(--to-ring);
  background: color-mix(in srgb, var(--to-bg) 78%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-size: 12px;
  color: var(--text-muted);
  max-width: calc(100% - 24px);
  overflow: hidden;
}
.to-crumb button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 4px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.to-crumb button:hover { color: var(--text); background: var(--link-hover-bg); }
/* Padded like the steps before it, so the pill ends as far from the last
   word as it starts from the first */
.to-crumb b { padding: 2px 4px; color: var(--text); font-weight: 600; white-space: nowrap; }

/* Travelling between levels: out, swap, in - so a jump reads as a move */
@keyframes to-warp-out { to { opacity: 0; transform: scale(1.35); } }
@keyframes to-warp-in { from { opacity: 0; transform: scale(.62); } }
@keyframes to-warp-back-out { to { opacity: 0; transform: scale(.7); } }
@keyframes to-warp-back-in { from { opacity: 0; transform: scale(1.3); } }
@keyframes to-fade-out { to { opacity: 0; } }
@keyframes to-fade-in { from { opacity: 0; } }
tag-orbit.is-warp-out .to-warp { animation: to-warp-out .19s ease-in both; }
tag-orbit.is-warp-in .to-warp { animation: to-warp-in .28s cubic-bezier(.2,.8,.2,1) both; }
tag-orbit.is-warp-back-out .to-warp { animation: to-warp-back-out .19s ease-in both; }
tag-orbit.is-warp-back-in .to-warp { animation: to-warp-back-in .28s cubic-bezier(.2,.8,.2,1) both; }
tag-orbit.is-fade-out .to-warp { animation: to-fade-out .11s both; }
tag-orbit.is-fade-in .to-warp { animation: to-fade-in .2s both; }

/* Narrow screens: galaxies as rows, each a full-width thumb target */
.to-flow--gal { gap: 10px; }
.to-galaxy--row {
  position: static;
  transform: none;
  flex-direction: row;
  gap: 12px;
  min-height: 56px;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--bg-card-faint);
  animation: none;
}
.to-galaxy--row:hover { transform: none; background: var(--link-hover-bg); }
.to-galaxy--row .to-gal-disc { width: 34px; height: 34px; flex: 0 0 auto; }
.to-galaxy--row small { margin-left: auto; }
/* The way up in a stacked layout (tag-orbit.js _flowGate): the rings' gate
   - dashed disc, name, count - laid in a row and centred where the rings
   have it, as a pill a thumb can hit. */
.to-flow-up { display: flex; justify-content: center; }
.to-galaxy.to-galaxy--flow {
  position: static;
  transform: none;
  flex-direction: row;
  gap: 8px;
  min-height: 44px;
  padding: 4px 16px 4px 6px;
  border: 1px dashed color-mix(in srgb, var(--cat-color) 45%, transparent);
  border-radius: 99px;
  animation: none;
}
.to-galaxy.to-galaxy--flow:hover { transform: none; background: var(--link-hover-bg); }
.to-galaxy--flow .to-gal-disc { flex: 0 0 auto; }
/* A sky is wider than its disc (tag-orbit.js _sky: up to 3.6x the radius);
   in a row the disc makes room for all of it, so it stays inside the pill
   and clear of the name */
.to-galaxy--flow.to-galaxy--sky .to-gal-disc { width: calc(var(--to-r, 16px) * 3.6); }

/* A node inside a flow row lays out in flow, not at absolute coordinates
   (the rings place nodes by left/top; these rows do not). */
.to-flow-row .to-node,
.to-flow--focus .to-node {
  position: static;
  transform: none;
  animation: none;
}

/* Focus on a phone: parents, the tag, then its children as a plain list of
   named chips (tap once to open). Siblings are not here - the page's own bar,
   swipe and dock move sideways (tag-nav.css). */
/* This one grows with its list instead of scrolling inside a fixed card: a
   phone page scrolls anyway, and a box that scrolls inside a scroll is worse
   than a longer page. */
tag-orbit.is-compact:has(.to-flow--focus) {
  height: auto;
  aspect-ratio: auto;
  /* Keep the base min-height: the component skips rendering into a box with
     no height, and height:auto starts at zero before anything is drawn. */
}
tag-orbit.is-compact:has(.to-flow--focus) .to-stage,
tag-orbit.is-compact:has(.to-flow--focus) .to-warp {
  position: static;
  overflow: visible;
}
/* Two classes: the generic .to-flow (absolute, own scroll) is declared later
   in this file and would otherwise win on source order. */
.to-flow.to-flow--focus {
  position: static;
  overflow: visible;
  align-items: center;
  gap: 6px;
  padding: 6px 10px 14px;
}
.to-flow--focus .to-flow-row--plain {
  background: none;
  padding: 0;
  justify-content: center;
}
/* On /tags every level grows with the page the same way - a galaxy of 250
   tags scrolled inside a 4:3 box before - and a level change brings the
   orbit's top back into view (tag-orbit-view.js). Only there: the Tag
   Queue's orbit fills a fixed panel, where scrolling inside it is right. */
.tag-universe.is-compact {
  height: auto;
  aspect-ratio: auto;
  min-height: 120px; /* never 0: the component skips a box with no height */
  scroll-margin-top: calc(var(--nav-h, 72px) + 1rem);
}
.tag-universe.is-compact .to-stage,
.tag-universe.is-compact .to-warp,
.tag-universe.is-compact .to-flow {
  position: static;
  overflow: visible;
}
/* Stacked, the breadcrumb is the first row of the flow rather than a pill
   over its top-left corner, where it hid the centred chip. It wraps instead
   of cutting a long trail off, and every step is a thumb-sized target. */
tag-orbit.is-compact:has(.to-flow--focus) > .to-crumb,
.tag-universe.is-compact > .to-crumb {
  position: static;
  flex-wrap: wrap;
  row-gap: 0;
  width: fit-content;
  max-width: calc(100% - 12px);
  margin: 4px 6px 6px; /* in line with the flow's rows (.to-flow's padding) */
  padding: 2px 8px;
  border-radius: var(--radius-lg);
}
tag-orbit.is-compact:has(.to-flow--focus) > .to-crumb button,
.tag-universe.is-compact > .to-crumb button {
  min-height: 40px;
  padding: 0 8px;
}
tag-orbit.is-compact:has(.to-flow--focus) > .to-crumb b,
.tag-universe.is-compact > .to-crumb b { padding: 0 8px; }
/* Chips in a stacked list are tapped, not pointed at: a size up, with room
   round each one (.to-flow-row, below). */
tag-orbit.is-compact { --to-pad: 9px 14px; }
.to-flow-link {
  width: 2px;
  height: 14px;
  border-radius: 2px;
  background: var(--to-line);
}
.to-flow-kids { width: 100%; }

/* Catalog on small screens: grouped rows */
.to-flow { position: absolute; inset: 0; overflow: auto; padding: 8px 6px; display: flex; flex-direction: column; gap: 10px; }
.to-flow-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  padding: 10px;
  border-radius: var(--radius);
  background: var(--bg-card-faint);
}
.to-flow-row .to-node { flex-direction: row; }
.to-sep { width: 1px; height: 14px; background: var(--to-ring); margin: 0 3px; }

tag-orbit.is-error::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}

:root[data-motion="reduce"] .to-ring, :root[data-motion="reduce"] .to-tag, :root[data-motion="reduce"] .to-line, :root[data-motion="reduce"] .to-node, :root[data-motion="reduce"] .to-line::before { transition: none; animation: none; }

/* Morph between two pages' orbits (tag-orbit.js _morphIn). The chips start
   where they stood on the page just left - often outside this box - so
   nothing may clip them while they fly; a frame that moves lends its look to
   a ghost that grows (or shrinks) from the old box into this one, and gets it
   back when the ghost is removed (one that stays put keeps its own). Doubled selectors: these must beat the frames'
   own rules (.tag-neighbors-card, .tag-universe) wherever those load. */
tag-orbit.is-morphing .to-stage { overflow: visible; }
tag-orbit.is-orbit-frame-morph,
.tag-neighbors-card.is-orbit-frame-morph {
  position: relative;
  isolation: isolate;
  overflow: visible;
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.to-frame-ghost {
  position: absolute;
  z-index: -1;
  box-sizing: border-box;
  border: 1px solid transparent;
  pointer-events: none;
}
.to-morph-out { position: absolute; inset: 0; pointer-events: none; }

/* --- /tags: the control bar (tag-orbit-view.js) ------------------------ */
/* One row of sections: Find takes the room, then (for tag editors) the way
   into the Tag Queue, then the Tree | Orbit switch at the right end. Same
   glass as the activity banner (tag-activity.css), which it docks under. */
.tag-bar {
  position: relative;
  z-index: var(--z-dropdown); /* its list drops over the cards and the orbit */
  display: flex;
  align-items: stretch;
  min-height: 52px;
  margin: 0 0 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--bg-card) 62%, transparent);
  backdrop-filter: blur(18px) saturate(1.35);
  -webkit-backdrop-filter: blur(18px) saturate(1.35);
  /* No inset top highlight: docked, that edge is the seam, and a second line
     under it reads as a gap. The card's highlight is the banner's. */
  box-shadow: 0 14px 26px -16px rgba(0,0,0,.5);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.tag-bar:has(.tag-bar-in:focus) {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent), 0 14px 26px -16px rgba(0,0,0,.5);
}
/* Docked under the activity banner the two are one card: the banner is its
   head, the bar its foot, the banner's bottom border the seam. Both halves
   stay separate elements (the banner is one big button for the activity
   details; the bar is controls), so the join lives here, in one place. */
.listing-results-column:has(> .te-activity-banner):has(> #listingContent > .tag-bar) > #listingContent {
  margin-top: 0 !important; /* Bootstrap's .mt-3 is !important too */
}
.listing-results-column:has(> #listingContent > .tag-bar) > .te-activity-banner {
  margin-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06);
}
/* Lifting or re-colouring the head alone would tear the card at the seam */
.listing-results-column:has(> #listingContent > .tag-bar) > .te-activity-banner:hover,
.listing-results-column:has(> #listingContent > .tag-bar) > .te-activity-banner:focus-visible {
  transform: none;
  border-color: var(--border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
.listing-results-column:has(> .te-activity-banner) > #listingContent > .tag-bar {
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
/* Orbit view: the orbit is the page (tag-orbit[bleed]) and this card floats
   over it - denser glass, so the glows under it only tint it (the light
   theme's near-clear --bg-card would leave dark text on a photo), and a
   deeper shadow to lift it off. The haze is behind the page (--z-bg); the
   banner still says "on top" outright instead of leaning on paint order.
   That z-index goes through :where() to stay below the window picker's own
   lift (tag-activity.css); the glass needs the full selector, as
   tag-activity.css loads after this file. */
.listing-results-column:has(> #listingContent.is-orbit-view) > .te-activity-banner,
.listing-results-column > #listingContent.is-orbit-view > .tag-bar {
  background: color-mix(in srgb, var(--bg-elevated) 72%, transparent);
  -webkit-backdrop-filter: blur(24px) saturate(1.4);
  backdrop-filter: blur(24px) saturate(1.4);
}
:where(.listing-results-column:has(> #listingContent.is-orbit-view)) > .te-activity-banner { z-index: 1; }
.listing-results-column > #listingContent.is-orbit-view > .tag-bar { box-shadow: 0 22px 44px -18px rgba(0,0,0,.6); }
.tag-bar-find {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: .6rem;
  min-width: 0;
  margin: 0;
  padding: 0 .75rem 0 1.15rem; /* the banner's inset: the search icon sits under the tag icon */
  color: var(--text-muted);
  cursor: text;
}
.tag-bar-in {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: .95rem;
  outline: none;
}
.tag-bar-in::placeholder { color: var(--text-muted); }
.tag-bar-count { flex: none; font-size: .75rem; color: var(--text-muted); white-space: nowrap; }
.tag-bar-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--link-hover-bg);
  color: var(--text-dim);
  font-size: .68rem;
  cursor: pointer;
}
.tag-bar-clear:hover { color: var(--text); }
.tag-bar-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 .3rem;
  box-sizing: border-box;
  border: 1px solid var(--border-bright);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  font-size: .7rem;
  line-height: 1;
  color: var(--text-secondary);
}
.tag-bar-views {
  position: relative;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 .35rem;
  border-left: 1px solid var(--border);
}
/* The plate under the chosen view: one element that travels between the two
   buttons instead of a background blinking from one to the other, so a drag
   can leave it half way across and the switch never says something the page
   is not doing (tag-orbit-view.js pill()). Its transition is written by the
   same call - one slide, one duration - and the buttons keep only their own
   colour. */
.tag-bar-ind {
  position: absolute;
  top: 50%;
  left: 0;
  width: 0;
  height: 40px;
  transform: translate3d(0, -50%, 0);
  pointer-events: none;
}
/* The plate's look, once. Until script has placed the moving plate
   (.is-measured, tag-orbit-view.js pill()) the chosen button wears it
   itself: the bar is the server's, and the switch has to read right in its
   first frame too. */
.tag-bar-ind,
.tag-bar-views:not(.is-measured) .tag-bar-view.is-on {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-sm);
}
.tag-bar-views:not(.is-measured) .tag-bar-ind { visibility: hidden; }
:root[data-motion="reduce"] .tag-bar-ind { transition: none !important; }
.tag-bar-view {
  position: relative;
  z-index: 1; /* over the plate */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  height: 40px;
  padding: 0 .85rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color .15s var(--ease), background .15s var(--ease);
}
.tag-bar-view:hover { color: var(--text); background: var(--link-hover-bg); }
.tag-bar-view.is-on { color: var(--text); }
.tag-bar-view:focus-visible,
.tag-bar-queue:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The Tag Queue leaves the page, so it is accent, not a third view - but the
   same inset button shape as the views, in a section of its own. */
.tag-bar-go {
  display: flex;
  align-items: center;
  padding: 0 .35rem;
  border-left: 1px solid var(--border);
}
.tag-bar-queue {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  height: 40px;
  padding: 0 .85rem;
  box-sizing: border-box;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent-bright);
  font-size: .8rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s var(--ease), border-color .15s var(--ease);
}
.tag-bar-queue:hover {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  border-color: var(--accent);
  color: var(--accent-bright);
}

/* The list: the tags this page shows, best first; picking one goes there */
.tag-bar-list {
  position: absolute;
  left: 0;
  top: calc(100% + 6px);
  width: min(560px, 100%);
  max-height: 360px;
  overflow-y: auto;
  padding: 5px;
  box-sizing: border-box;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-lg);
}
.tag-bar-hit {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .45rem .6rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: .85rem;
  text-decoration: none;
}
.tag-bar-hit:hover,
.tag-bar-hit.is-active { background: var(--link-hover-bg); color: var(--text); }
.tag-bar-hit.is-active { box-shadow: inset 2px 0 0 var(--accent); }
.tag-bar-dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--cat-color, var(--tag-color-fallback)) 70%, transparent);
  border: 1px solid color-mix(in srgb, var(--cat-color, var(--tag-color-fallback)) 90%, transparent);
}
.tag-bar-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-bar-where { flex: none; font-size: .7rem; color: var(--text-muted); }
.tag-bar-empty { margin: 0; padding: .7rem; font-size: .8rem; color: var(--text-muted); text-align: center; }
/* Pinned to the list's bottom edge: with ten hits the list scrolls, and a hint
   that scrolled away with them would never be read. The negative insets cover
   the list's own 5px padding so nothing peeks out underneath. */
.tag-bar-hint {
  position: sticky;
  bottom: -5px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 4px -5px -5px;
  padding: .5rem 1.05rem .55rem;
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
  font-size: .7rem;
  color: var(--text-muted);
}
/* Tree view with nothing left after the filter */
.tag-find-none {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  margin: 2rem 0;
  font-size: .9rem;
  color: var(--text-muted);
}
/* "No hits" belongs to the tree (Orbit marks its own), and it is inside the
   tree view now - parked with it, so there is nothing to hide here. */
/* No keyboard, no key hints */
@media (hover: none) {
  .tag-bar-key,
  .tag-bar-hint { display: none; }
}
@media (max-width: 575.98px) {
  .tag-bar-find { gap: .5rem; padding: 0 .5rem 0 .9rem; }
  .tag-bar-count { display: none; }
  /* Under 16px iOS zooms the page into the field on focus */
  .tag-bar-in { font-size: 16px; }
  .tag-bar-go,
  .tag-bar-views { padding: 0 .25rem; }
  .tag-bar-view,
  .tag-bar-queue { width: 44px; padding: 0; }
  /* Icons only on a phone; the words stay for screen readers */
  .tag-bar-view span,
  .tag-bar-queue span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .tag-bar-list { width: 100%; }
}

/* On a tag's backdrop (tag-backdrop.js sets .has-backdrop while the /tags
   page shows the centred tag's photos behind it): nothing here may be
   see-through.
   Dimming goes by strength instead of letting the picture through, and every
   chip and dot wears --to-rim - a ring in the page colour, which their own
   shadows above already include (it is empty everywhere else) - so it lifts
   off whatever the image does behind it. --bg-base / --text make both
   themes their own. */
tag-orbit.has-backdrop {
  --to-line: color-mix(in srgb, var(--text) 58%, transparent);
  --to-rim:
    0 0 0 2px color-mix(in srgb, var(--bg-base) 82%, transparent),
    0 4px 14px color-mix(in srgb, var(--bg-base) 55%, transparent);
}
/* :not(.blob) - a dot's label is parked at opacity 0 until it opens, and
   this rule is more specific than the one parking it: on a tag with a
   backdrop every dimmed dot showed its name at once, all of them piled on
   each other round the centre. */
tag-orbit.has-backdrop .to-node.dim:not(.blob) .to-chip,
tag-orbit.has-backdrop .to-node.dim .to-blob { opacity: .9; }
tag-orbit.has-backdrop .to-node.anc .to-chip { opacity: .92; }
tag-orbit.has-backdrop .to-node.anc-2 .to-chip { opacity: .8; }
tag-orbit.has-backdrop .to-blob {
  background: color-mix(in srgb, var(--cat-color) 78%, var(--to-bg));
  border-color: var(--cat-color);
}
tag-orbit.has-backdrop .to-ring { opacity: .6; }
tag-orbit.has-backdrop .to-node small {
  color: var(--text);
  text-shadow: 0 0 3px var(--bg-base), 0 0 8px var(--bg-base);
}

/* Orbit view: the cards step aside for one full-width universe. It has no
   frame: its haze (or the centred tag's backdrop) is the whole page's
   background ([bleed], below), with the banner and the bar floating over it.
   The box only says where the orbit lays itself out - where the card was,
   so nothing inside moved when the frame went. */
/* --- The strip: Tree and Orbit side by side (tag-orbit-view.js) --- */
/* The switch is a move, not a state, so the two views are two places rather
   than two ways of dressing one box: the page slides between them, under
   the pill or under a finger. At rest the one showing is in the flow and
   the page is exactly as tall as it is; the other is parked beside it - out
   of the flow, out of the tab order and out of a reader's way, but still
   rendered, which is what lets the orbit hold the level it was left on and
   be measured before it moves. Hiding the cards by class (this used to be
   .is-orbit-view .listing-tag-catalog-category) said the same thing a
   second time, from the other end, and only about the cards. */
.tag-views {
  position: relative;
  min-width: 0;
  /* The orbit's box, named once: the orbit itself and the empty view that
     holds its place until it is drawn (below) */
  --tag-universe-h: min(78vh, 820px);
  --tag-universe-min-h: 460px;
}
/* Sideways over the orbit is the strip's, down the page is the page's -
   told to the browser outright, so it never has to wait on us to find out.
   On the orbit's side only: touch-action intersects down the tree, so the
   same line on the track would reach the forests inside the cards, which are
   wider than the card and are scrolled sideways to be read (.tt-scroll,
   listing.css) - which is how a phone reads one at all. There the strip
   waits its turn instead (tag-orbit-view.js scrollerBlocks). */
.tag-view--orbit { touch-action: pan-y pinch-zoom; }
.tag-view { min-width: 0; }
/* The gap #listingContent gave the cards before they moved in here */
.tag-view--tree {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
/* Parked: no box and nothing spilling out of it, so the page is only ever as
   tall as the view that is showing. Out of the flow alone was not enough -
   an absolutely positioned box still counts towards the page's scrollable
   overflow, which left the whole Tree scrolling on under the footer while
   the Orbit was up (and, under a Tree filtered down to one card, the orbit's
   78vh doing the same). It stays rendered either way: the orbit keeps the
   level it was left on, and stripBegin() measures the incoming view the
   moment it becomes the resting one, before anything moves.
   Which view rests is one attribute on the track, data-view, and the server
   renders it (listing_index.php, from listing_tags_view()): the page is on
   the right view from its first frame, script or no script. */
.tag-views:not(.is-sliding)[data-view="orbit"] > .tag-view--tree,
.tag-views:not(.is-sliding)[data-view="tree"] > .tag-view--orbit {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  overflow: clip;
  visibility: hidden;
  pointer-events: none;
}
/* ...and asleep: hidden is not stopped, and a parked orbit's haze drifts,
   breathes and ripples on infinite animations, its galaxies' stars with it,
   every frame, for nobody. Held where they are, they pick up from the same
   spot when it comes back into view. Its drawing sleeps in tag-orbit-view.js
   (park()). The !important because the component's own rules set these
   animations with the shorthand, which would put play-state back to
   running. */
.tag-views:not(.is-sliding)[data-view="orbit"] > .tag-view--tree,
.tag-views:not(.is-sliding)[data-view="orbit"] > .tag-view--tree *,
.tag-views:not(.is-sliding)[data-view="tree"] > .tag-view--orbit,
.tag-views:not(.is-sliding)[data-view="tree"] > .tag-view--orbit *,
.tag-views:not(.is-sliding)[data-view="tree"] > .tag-view--orbit *::before,
.tag-views:not(.is-sliding)[data-view="tree"] > .tag-view--orbit *::after { animation-play-state: paused !important; }
/* The Orbit view is served empty - the orbit is drawn by script - and holds
   the orbit's box meanwhile, so the page does not grow under it when it
   arrives. */
.tag-views[data-view="orbit"] > .tag-view--orbit:empty {
  height: var(--tag-universe-h);
  min-height: var(--tag-universe-min-h);
}
/* Mid-slide both are out of the flow, and the track holds the height while
   it tweens from the one it had to the one it is going to. Each is its own
   layer for the ride, so a frame of the slide is a move and never a repaint
   (the Tree is thousands of nodes). */
.tag-view--float {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  visibility: visible;
  will-change: transform;
}
/* Nothing here is clipped while it moves: the strip runs the screen's width
   (tag-orbit-view.js stripBegin) and the body's overflow-x: hidden is the
   only edge it meets. A clip anywhere round the views would cut the orbit's
   haze too - it is the page's background, reaching to the page's edges from
   inside the orbit - which is exactly the box the Tree used to leave behind,
   haze inside it and none outside, until it was let go in one frame at the
   end.
   The transform carrying the orbit makes it a stacking context, and its
   haze - normally at the page's background (--z-bg) - would be shut in it,
   painted over the Tree it passes, the bar and the footer. So the moving
   orbit as a whole goes down to that layer instead: under all of the page,
   over the tag backdrop's stage (--z-bg-deep), haze under its own chips -
   the order it has at rest. */
.tag-views.is-sliding > .tag-view--orbit { z-index: var(--z-bg); }
/* And while it moves the haze may spill past its own edge. Its ground stays
   put for the slide, covering the page, while its glows travel with their
   galaxies (tag-orbit-view.js hazeAt()) - so on the way in and out the
   glows are off the ground's edge, and at rest the ones on the page's edges
   are cut in half by it (so it never stretches the page). Carried across
   the screen, that half was a straight line through a glow. Unclipped they
   stay whole and fade as whole discs; whatever spills sideways is past the
   body's overflow-x: hidden, and below, the strip's own height covers it. */
.tag-views.is-sliding .to-amb { overflow: visible; }
/* The column clips sideways to hold the cards' glows in (listing.css); the
   haze has to reach past it to the page's edges - in Orbit, and in either
   direction while the strip moves, since the class has already changed by
   the time an orbit that is leaving slides out. */
.listing-results-column:has(> #listingContent.is-orbit-view),
.listing-results-column:has(> #listingContent > .tag-views.is-sliding) { overflow-x: visible; }
.tag-universe {
  display: block;
  height: var(--tag-universe-h);
  min-height: var(--tag-universe-min-h);
  aspect-ratio: auto;
  margin-top: .25rem;
  /* The big view is read from further back (user call): chips a size up -
     tag-orbit.js sizes its layouts by --to-fs, so they make room - and the
     words around them with it. */
  --to-fs: 14px;
  --to-pad: 6px 13px;
}
.tag-universe .to-cat { font-size: 20px; }
.tag-universe .to-cat small,
.tag-universe .to-galaxy small { font-size: 12px; }
.tag-universe .to-gal-name { font-size: 16.5px; }
.tag-universe .to-galaxy.to-galaxy--gate .to-gal-name { font-size: 13px; }
.tag-universe .to-galaxy.to-galaxy--loose .to-gal-name { font-size: 14px; }
.tag-universe .to-tag { font-size: 10.5px; }
.tag-universe .to-crumb { font-size: 13px; }
/* /tags chips (both views): a little more contrast, a little less fill
   (user call). The label and the rim step up, the tint and - in the orbit -
   the plate under it step back, so a chip reads clearly without sitting on
   the haze as a solid block. The light theme keeps its own AA pivot. */
.listing-tag-catalog-category .tag-chip,
.tag-universe .to-chip {
  --tag-chip-bg: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 11%, transparent);
  border-color: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 46%, transparent);
  color: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 48%, #fff);
}
[data-theme="light"] .listing-tag-catalog-category .tag-chip,
[data-theme="light"] .tag-universe .to-chip {
  --tag-chip-bg: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 11%, rgba(255,255,255,.72));
  border-color: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 52%, transparent);
  color: color-mix(in srgb, var(--cat-color, rgb(124,58,237)) 38%, #0f172a);
}
.tag-universe .to-node .to-chip,
.tag-universe .to-flow-me .to-chip {
  background: linear-gradient(var(--tag-chip-bg), var(--tag-chip-bg)), color-mix(in srgb, var(--to-bg) 88%, transparent);
}
/* Over a tag's photo nothing may be see-through: the plate goes solid again */
tag-orbit.has-backdrop .to-node .to-chip,
tag-orbit.has-backdrop .to-flow-me .to-chip {
  background: linear-gradient(var(--tag-chip-bg), var(--tag-chip-bg)), var(--to-bg);
}
/* Under 768px the rings keep the big view's height (a 4:3 box squeezed
   them into half the screen and left the rest empty); a phone's stacked
   list sizes itself (.tag-universe.is-compact). */
@media (max-width: 767.98px) {
  .tag-universe { margin-top: 0; }
  /* #listingContent's own gap is room enough under the bar on a phone */
  #listingContent.is-orbit-view > .tag-bar { margin-bottom: 0; }
}

/* --- The ambient haze (tag-orbit.js _ambient; el[ambient], the /tags orbit) --- */
/* Under the levels with no picture of their own - the universe and a
   galaxy's level: soft glows in the galaxies' colours that drift, turn and
   breathe like an old console's menu, ripples pulsing out of the ones that
   matter and a slow sheen passing over. JS only says where each glow goes;
   the motion is all here, on transform and opacity. Screen-blended in the
   dark theme, so two colours that meet light each other; a plain pastel
   wash in the light one. A tag's backdrop covers it (.has-backdrop, the
   haze fading as the photo fades in), and the stacked phone layout has no
   room for it. .to-amb is the ground (the floor gradient and the sheen);
   .to-amb-in holds the glows, in the box's own coordinates. */
.to-amb {
  --amb-blend: screen;
  --amb-core: 62%;
  /* currentColor resolves on each ring, where it is its glow's colour */
  --amb-ring: color-mix(in srgb, currentColor 80%, white);
  --amb-sheen: rgba(255,255,255,.05);
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  isolation: isolate;
  pointer-events: none;
  background: radial-gradient(120% 95% at 50% 42%, color-mix(in srgb, var(--accent) 14%, var(--bg-base)), var(--bg-base) 72%);
  transition: opacity 1s ease;
}
[data-theme="light"] .to-amb {
  --amb-blend: normal;
  --amb-core: 44%;
  --amb-ring: color-mix(in srgb, currentColor 85%, black);
  --amb-sheen: rgba(255,255,255,.35);
}
.to-amb-in {
  position: absolute;
  inset: 0;
  transition: transform .6s cubic-bezier(.2, .8, .2, 1);
}
/* [bleed] (the /tags orbit): the haze is the page's background. The ground
   reaches from the box to the document's edges (tag-orbit.js _bleed() sets
   --bleed-*) and scrolls with the page, while .to-amb-in is pulled back in
   to the box, so every glow stays on its galaxy. It sits behind the page -
   the banner, the bar, the footer and the nav's glass all float over it -
   and above the tag backdrop's fixed stage (--z-bg-deep), which it fades
   out over. It escapes the orbit to get there: tag-orbit is not a stacking
   context. Coming in, the whole page's ground fades rather than pops. */
/* Nothing queries it as a container, and containment could make it a
   stacking context in some engine - which would trap the haze over the
   banner. */
tag-orbit[bleed] { container-type: normal; }
tag-orbit[bleed] .to-amb {
  z-index: var(--z-bg);
  inset: calc(var(--bleed-t, 0px) * -1) calc(var(--bleed-r, 0px) * -1) calc(var(--bleed-b, 0px) * -1) calc(var(--bleed-l, 0px) * -1);
  border-radius: 0;
  animation: to-in .8s ease;
}
tag-orbit[bleed] .to-amb-in {
  inset: var(--bleed-t, 0px) var(--bleed-r, 0px) var(--bleed-b, 0px) var(--bleed-l, 0px);
}
tag-orbit.has-backdrop .to-amb { opacity: 0; }
tag-orbit.is-compact .to-amb { display: none; }
/* Diving into a galaxy the glows lean in with the chips; backing out, away.
   Only the glows: the ground may reach the page's edges, and scaled past
   them it would stretch the page for the length of the warp. */
tag-orbit.is-warp-out .to-amb-in { transform: scale(1.08); transition-duration: .19s; }
tag-orbit.is-warp-back-out .to-amb-in { transform: scale(.95); transition-duration: .19s; }
.to-amb::after {
  content: '';
  position: absolute;
  inset: -30% -60%;
  background: linear-gradient(105deg, transparent 42%, var(--amb-sheen) 50%, transparent 58%);
  animation: to-amb-sheen 17s ease-in-out infinite;
}
@keyframes to-amb-sheen {
  0%, 100% { transform: translateX(-28%); }
  50% { transform: translateX(28%); }
}

/* A glow: 100px across, set in place and sized by translate/scale inline -
   so a level change glides - its colour in `color`. Its inner disc fades
   out on a bell curve rather than in steps, so where glows overlap they
   melt into one field instead of reading as discs with an edge, and it
   only turns and squashes about its own centre (--d, --dl): it never
   wanders off the galaxy it belongs to. */
.to-amb-glow {
  position: absolute;
  left: -50px;
  top: -50px;
  width: 100px;
  height: 100px;
  mix-blend-mode: var(--amb-blend);
  will-change: transform, opacity;
  transition: translate 1.8s cubic-bezier(.45, 0, .2, 1), scale 1.8s cubic-bezier(.45, 0, .2, 1), color 1.8s ease, opacity 1.2s ease;
}
.to-amb-glow > b {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, currentColor var(--amb-core), transparent) 0%,
    color-mix(in srgb, currentColor calc(var(--amb-core) * .82), transparent) 18%,
    color-mix(in srgb, currentColor calc(var(--amb-core) * .52), transparent) 40%,
    color-mix(in srgb, currentColor calc(var(--amb-core) * .24), transparent) 62%,
    color-mix(in srgb, currentColor calc(var(--amb-core) * .07), transparent) 82%,
    transparent 100%);
  animation: to-amb-drift var(--d, 20s) ease-in-out var(--dl, 0s) infinite alternate;
  transition: scale .8s cubic-bezier(.2, .8, .2, 1), filter .8s ease;
}
/* The galaxy under the pointer swells its own */
.to-amb-glow.is-lit > b { scale: 1.18; filter: brightness(1.3) saturate(1.15); }
@keyframes to-amb-drift {
  0%   { transform: rotate(0deg) scale(1, 1); }
  30%  { transform: rotate(50deg) scale(1.12, .9); }
  65%  { transform: rotate(-30deg) scale(.92, 1.1); }
  100% { transform: rotate(80deg) scale(1.06, .96); }
}

/* The spotlight (a galaxy's level): a soft pool of the page colour under
   the rings, so tags in the galaxy's own colour sit on calm ground while
   the haze stays vivid round the edges (user pick, with a touch quieter
   haze, over rims or a colour swap). 200px across, set in place and sized
   by translate/scale inline so it glides with the level; --bg-base makes it
   a dark pool on the dark theme and a pale one on the light. */
.to-amb-spot {
  position: absolute;
  left: -100px;
  top: -100px;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--bg-base) 80%, transparent),
    color-mix(in srgb, var(--bg-base) 62%, transparent) 50%,
    color-mix(in srgb, var(--bg-base) 26%, transparent) 80%,
    transparent);
  opacity: 0;
  transition: translate 1.8s cubic-bezier(.45, 0, .2, 1), scale 1.8s cubic-bezier(.45, 0, .2, 1), opacity 1.2s ease;
}

/* A ripple: two rings, half a beat apart, breathing out of a glow's heart
   to about its edge and fading as they go - the galaxies pulsing. It
   follows its glow (translate and colour glide the same way) but is sized
   in px (--rr, the glow's radius, set by the JS) rather than scaled with
   it, so the ring stays a hairline. Only one the JS turns on (.is-on). */
.to-amb-rip {
  display: none;
  position: absolute;
  left: 0;
  top: 0;
  mix-blend-mode: var(--amb-blend);
  transition: translate 1.8s cubic-bezier(.45, 0, .2, 1), color 1.8s ease;
}
.to-amb-rip.is-on { display: block; }
.to-amb-ring {
  position: absolute;
  left: calc(var(--rr, 100px) * -1);
  top: calc(var(--rr, 100px) * -1);
  width: calc(var(--rr, 100px) * 2);
  height: calc(var(--rr, 100px) * 2);
  border-radius: 50%;
  border: 1px solid var(--amb-ring);
  box-shadow: 0 0 6px color-mix(in srgb, currentColor 45%, transparent);
  opacity: 0;
  will-change: transform, opacity;
  animation: to-amb-ripple var(--amb-ripple) cubic-bezier(.2, .5, .4, 1) var(--rd, 0s) infinite;
}
/* Quiet on purpose: surfaces faintly, eases out, gone before the glow's edge */
@keyframes to-amb-ripple {
  0%   { transform: scale(.16); opacity: 0; }
  18%  { opacity: .3; }
  100% { transform: scale(.9); opacity: 0; }
}

/* A galaxy of stars (tag-orbit.js paintSky), one per tag: its disc steps
   back to a plain holder and the sky - painted face-on - tilts into an
   oval and turns, slowly (the JS sets size, tilt, pace and where in the
   turn it is). Hovering one brightens it, and so does a Find hit. */
.to-galaxy--sky .to-gal-disc,
.to-galaxy--sky:hover .to-gal-disc,
.to-galaxy--sky:focus-visible .to-gal-disc {
  background: none;
  border-color: transparent;
  box-shadow: none;
  overflow: visible;
}
.to-gal-sky {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--sky, 120px);
  height: var(--sky, 120px);
  pointer-events: none;
  transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scaleY(.58);
  animation: to-gal-spin 180s linear infinite;
  transition: filter .25s var(--ease, ease);
}
@keyframes to-gal-spin {
  from { transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scaleY(.58) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scaleY(.58) rotate(360deg); }
}
.to-galaxy--sky:hover .to-gal-sky,
.to-galaxy--sky:focus-visible .to-gal-sky { filter: brightness(1.5); }
tag-orbit.is-searching .to-galaxy--sky.hit .to-gal-sky { filter: brightness(1.7); }
/* A gate with a sky is drawn as what it leads to: "All tags" as a little
   universe of every galaxy's colour (tag-orbit.js paintUniverse), a root
   tag's category as that very galaxy, as the universe shows it (user call).
   No disc under either - the sky is the icon. Specific enough to beat the
   gate's own glass fill further down. */
tag-orbit .to-galaxy.to-galaxy--gate.to-galaxy--sky .to-gal-disc { background: none; border-color: transparent; }
/* Unsorted is the lack of a category: set apart (bottom right), quieter */
.to-galaxy--loose { opacity: .7; }
.to-galaxy--loose:hover,
.to-galaxy--loose:focus-visible { opacity: 1; }
.to-galaxy--loose .to-gal-name { font-size: 13px; font-weight: 600; }
/* A galaxy's words keep a halo of the page colour, readable over the
   brightest haze (--bg-base: dark or light by theme). */
/* A gate stays the dashed "way out" it is everywhere, only see-through */
tag-orbit[ambient] .to-galaxy--gate .to-gal-disc {
  background: color-mix(in srgb, var(--cat-color) 16%, transparent);
}
tag-orbit[ambient] .to-gal-name,
tag-orbit[ambient] .to-galaxy small,
tag-orbit[ambient] .to-cat {
  text-shadow:
    0 1px 2px color-mix(in srgb, var(--bg-base) 85%, transparent),
    0 0 14px color-mix(in srgb, var(--bg-base) 75%, transparent);
}
tag-orbit[ambient] .to-galaxy small { color: var(--text-dim); }

:root[data-motion="reduce"] .to-amb-glow > b,
:root[data-motion="reduce"] .to-amb::after,
:root[data-motion="reduce"] .to-gal-sky { animation: none; }
:root[data-motion="reduce"] .to-amb-rip.is-on { display: none; }
:root[data-motion="reduce"] .to-amb-glow { transition-duration: .01s, .01s, .01s, .3s; }
:root[data-motion="reduce"] .to-amb-spot { transition-duration: .01s, .01s, .3s; }
:root[data-motion="reduce"] tag-orbit.is-warp-out .to-amb-in,
:root[data-motion="reduce"] tag-orbit.is-warp-back-out .to-amb-in { transform: none; }
