/*
 * Shimti Multimedia: Consolidated styles for background, UI elements, and animations
 * Provides styling for canvas (grid, particles), radial menu, welcome carousel, title panel,
 * connection lines, rings, squares, and circles. Includes responsive design and visual debugging cues.
 * @requires Orbitron variable font in assets/fonts/Orbitron-Variable.woff2
 * @requires DOM elements: #backgroundCanvas, #uiSvg, #radialMenu, #wheelMenu, #shimtiPanel, #shimtiPanelBottom, #welcomeText
 */

/* Font Definitions */
@font-face {
  font-family: 'Orbitron';

  /*
   * One variable WOFF2, covering the whole weight axis.
   *
   * This replaces a static Orbitron-Regular.ttf, and fixes two things at once.
   *
   * SIZE. WOFF2 is Brotli-compressed and is the universally supported web font format;
   * raw TrueType is not a web delivery format at all. The variable file is 16KB against
   * the single static weight's 24KB - smaller while carrying every weight rather than one.
   *
   * WEIGHT. Only weight 400 used to be declared, yet the stylesheet asks for 600 in
   * several places and so does the rotating wordmark. With no 600 to load, the browser
   * synthesised one by smearing the regular weight sideways - faux bold, visibly coarser
   * than the real thing, on the brand's own wordmark. The real weights were sitting
   * unused on disk the whole time. Declaring the axis range means 600 now resolves to
   * actual 600.
   */
  src: url('../fonts/Orbitron-Variable.woff2') format('woff2');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

/* Custom Properties */
:root {
  --panel-bg: rgba(180, 220, 255, 0.15);
  --text-color: #eaffff;
  --border-color: #fff;
  --brightness-filter: brightness(1.1);
  --focus-outline: #8cf;
  --error-bg: rgba(255, 100, 100, 0.3);
}

/* Global Styles */
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background-color: #000;
  overflow: hidden;
  font-family: 'Orbitron', sans-serif;
  user-select: none;
}

/* Canvas Styles */
canvas#backgroundCanvas {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 0;
  user-select: none;
  /* Decorative and aria-hidden, so it must never take a pointer event.
     It is positioned, and #uiSvg is not, so it paints ABOVE the menu regardless of the
     z-index here. Being almost entirely transparent, it then acted as an invisible lid
     over the navigation - the wheel looked fine and simply could not be clicked. */
  pointer-events: none;
}

/*
 * DEPTH MODEL
 *
 * The page is composed in three tiers. Anything added later belongs to one of them, and
 * its z-index should come from that tier's band rather than being picked to win an
 * argument with a neighbour.
 *
 *   BACKGROUND (0-9)
 *     0   #backgroundCanvas   the grid and the drifting dots beneath it
 *
 *   MIDDLE GROUND (10-19)
 *     10  #backgroundRings    concentric rings, segments, orbiting squares
 *     11  #ringTitle          curved SHIMTI MULTIMEDIA wordmark, over the rings it orbits
 *
 *   FOREGROUND (20+)
 *     20  #nodeWires          connectors, tucked beneath the menu they attach to
 *     30  #uiSvg              the radial menu
 *     40  #shimtiPanel, #shimtiPanelBottom   the draggable title and welcome nodes
 *     50  #sectionPanels      section previews, above everything they overlap
 *
 * The tiers are also what section previews are placed against: a preview must never
 * cover the foreground, and is free to pass over the middle ground and background.
 *
 * Two traps this layout has already fallen into, both worth not repeating:
 *
 *   - #uiSvg had no position. A static element paints beneath every positioned one
 *     whatever its z-index, so the menu sat behind the rings, the wires and the panels
 *     and read as background. It also meant the z-index ui-elements.js assigned to it
 *     inline had never done anything.
 *
 *   - Only #uiSvg and the node panels may accept pointer events. Every other layer here
 *     spans the whole viewport, so any one of them left interactive becomes an invisible
 *     lid over the navigation - which is exactly what #hitCanvas, #radialMenu and
 *     #backgroundCanvas each did in turn.
 */

/* Background Ring Field */
/*
 * The stage holds every layer the homepage composes, so one transform moves all of them
 * together - see the comment on #stage in index.html for why that has to be one piece.
 *
 * inset: 0 makes this box exactly the viewport, so children asking for top:0/left:0 and
 * 100% resolve to what they did before it existed. It is the viewport in both
 * orientations, so nothing here needs to know which way the phone is held.
 *
 * translate3d rather than translate: it keeps the whole composition on its own
 * compositor layer, so dragging moves an already-painted texture instead of repainting a
 * particle canvas and a ring field every frame.
 */
#stage {
  position: fixed;
  inset: 0;
  transform: translate3d(0, 0, 0);
  /* The homepage is a fixed composition. Disable browser pan and pinch over it; menu
     taps remain ordinary pointer activation. */
  touch-action: none;
}

/*
 * The ring field is given the same size floor as the wordmark, and for the same reason.
 *
 * The radial menu is drawn in pixels - its icons sit 125px from centre on every screen -
 * while this layer filled the viewport, so its 200-unit viewBox was fitted to the smaller
 * side. On a desktop that puts the field's outer radius near 600px around a 125px wheel,
 * a ratio of about one to five, which is the composition: a small control panel set into
 * something much larger. On a 375px phone the wheel does not move but the field collapses
 * to a 187px radius, so the same wheel fills two thirds of it and the page reads as if it
 * were zoomed in on the middle.
 *
 * The phone floor is measured from the fullscreen desktop composition below. The outer
 * rings run off a phone screen by design: the phone is a crop, not a reflow.
 */
#backgroundRings {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 100vmin;
  height: 100vmin;
  /* The field intentionally extends to radius 191 around the 100-unit centre. Let that
     geometry continue beyond this 200-unit SVG viewport; html/body still clip at the
     browser edge, so landscape screens no longer expose a hard square cutoff. */
  overflow: visible;
  transform: translate(-50%, -50%);
  z-index: 10;
  pointer-events: none;
  background: transparent;
}

/*
 * The floor applies to touch screens only, and that scoping is the whole point.
 *
 * Applied everywhere, it also fires on any desktop window whose smaller side is under
 * the fullscreen reference - which is most non-maximised windows. There the
 * field becomes wider than the window and is cropped into a hard-edged band, with the
 * outer rings cut off at the frame. That is a regression, and it is invisible when the
 * only desktop you check is maximised on a 1397px-tall screen.
 *
 * A desktop window keeps fitting its viewport. A phone keeps the measured fullscreen
 * geometry and exposes only the part inside its portrait or landscape frame.
 */
@media (pointer: coarse) {
  /* mobile-animations.js applies the same transforms as SVG attributes. Unlike CSS
     transform animations, those do not allocate a compositor layer per ring. */
  .ring-spin-cw,
  .ring-spin-ccw,
  .ring-marker-spin,
  .rotating-squares,
  .rotating-square,
  .segmented-ring {
    animation: none !important;
  }

  #backgroundRings {
    width: max(100vmin, 1388px);
    height: max(100vmin, 1388px);
    /* Zooming out or panning reveals geometry outside the SVG's own viewport. */
    overflow: visible;
  }
}

/* The radial menu. Positioned so it sits in front of the ring field and the wires
   rather than behind them - see the stacking note above. */
#uiSvg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 30;
}

/* Arced wordmark. Background, like the ring field it orbits with - the menu and the
   two nodes are the only foreground. */
/*
 * The wordmark ring has to clear the menu icons, and the two are sized by different
 * rules - which is why they collide on a phone and nowhere else.
 *
 * #radialMenu is a fixed 400px square, so the icons sit at a constant 125px radius and
 * occupy 100-150px from centre on every screen. The wordmark does the opposite: this
 * element filled the viewport, and a 200-unit viewBox fitted to the smaller side puts
 * the arc at about 0.30 of that side. Above roughly 500px the arc clears the icons
 * comfortably; below it the arc shrinks into them. At 375px it lands at 113px, drawing
 * the text straight through the lightbulb and the envelope.
 *
 * A media query alone would only move the collision to whatever width the query did not
 * cover. The box is given a floor instead: never smaller than the size at which the arc
 * clears the icons. 640px puts the glyph baseline near 166px, comfortably outside their
 * 150px edge. At or above 640px this evaluates to exactly 100vmin, so the desktop layout
 * is untouched. If #radialMenu's 400px ever changes, this number follows it.
 *
 * The floor alone is not enough. Pushing the ring out also pushes the text outward from
 * its baseline, and at the default 8-unit size the far edge lands wider than a 375px
 * screen. The size is reduced below 600px to buy that room back - see .ring-title.
 */
#ringTitle {
  position: fixed;
  top: 50%;
  left: 50%;
  width: max(100vmin, 640px);
  height: max(100vmin, 640px);
  transform: translate(-50%, -50%);
  z-index: 11;
  pointer-events: none;
  background: transparent;
}

/*
 * MIDDLE-GROUND RING FIELD
 *
 * Geometry is generated, not hand-placed. Radii are a geometric progression (ratio 1.15)
 * running from just outside the radial menu out past the screen corner, so spacing opens
 * up with radius the way a slide rule's does - equal gaps would read as a target rather
 * than an instrument. Stroke widths stay inside the local gap so nothing collides by
 * accident; the single overlap, the outer wash passing under the ring at r=181, is
 * deliberate and reads as depth.
 *
 * Direction alternates strictly outward, beginning with the wordmark. Period is
 * proportional to circumference, so every ring travels at the same linear rate and the
 * outer ones turn slowest. Each element carries its own animation-duration inline.
 *
 * Rotation is transform and nothing else. Of all animatable properties only transform
 * and opacity can be handled by the compositor; anything else - filter above all -
 * forces a main-thread paint on every frame.
 *
 * transform-box:view-box puts the origin in viewBox coordinates, so 100px 100px is the
 * field's centre. Without it the origin would be each element's own bounding box and the
 * rings would wobble instead of turning about a common axis.
 */
.ring-spin-cw,
.ring-spin-ccw {
  transform-box: view-box;
  transform-origin: 100px 100px;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.ring-spin-cw {
  animation-name: ringSpinCw;
}

.ring-spin-ccw {
  animation-name: ringSpinCcw;
}

@keyframes ringSpinCw {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes ringSpinCcw {
  from { transform: rotate(360deg); }
  to   { transform: rotate(0deg); }
}

/*
 * Markers turning on their own centres while the ring carries them around, as the ones in
 * the middle of the menu do.
 *
 * transform-box:fill-box here, not view-box: each marker turns about ITS OWN centre, so
 * the origin has to be its own box. The ring around them uses view-box, because that one
 * turns about the field's shared centre.
 *
 * Each carries a negative animation-delay, so they sit at different points in the cycle
 * and read as independent rather than as one stamped pattern. Costs nothing - same
 * animation, different start phase.
 *
 * These were 16 SMIL timelines before. As CSS transforms the compositor handles them, so
 * the effect comes back without the main-thread cost that made it worth removing.
 */
.ring-marker-spin {
  transform-box: fill-box;
  transform-origin: center;
  animation: ringSpinCw 6s linear infinite;
}

.ring-title {
  fill: var(--text-color);
  text-transform: uppercase;
  /* Static glow. This element also rotates, and animating a drop-shadow on moving text
     means re-running the filter over its bounding box every single frame - the most
     expensive thing that was on the page. The glow is constant now; the motion carries
     the effect. */
  filter: drop-shadow(0 0 6px rgba(180, 220, 255, 0.5));
}

/* ==========================================================================
   Node Wires
   Orthogonal connectors between the draggable panels and the radial menu, drawn by
   node-panels.js. Every segment is horizontal or vertical - no curves.
   ========================================================================== */

#nodeWires {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* Above the ring field, below the panels the wires connect to. */
  z-index: 20;
  /* Decoration: must never intercept a click meant for the wheel beneath it. */
  pointer-events: none;
}

.node-wire {
  fill: none;
  stroke: rgba(255, 255, 255, 0.3);
  stroke-width: 2;
  /* Square joins and caps keep the corners hard. Round joins would soften the right
     angles into something closer to the curve this deliberately is not. */
  stroke-linejoin: miter;
  stroke-linecap: square;
}

.node-port {
  fill: none;
  stroke: #ffffff;
  stroke-width: 1;
}

/* Draggable node panels */
.node-panel {
  cursor: grab;
  /* Suppresses the browser's own touch panning and long-press behaviour so a drag on a
     touch screen moves the node instead of scrolling or selecting it. */
  touch-action: none;
}

.node-panel:focus-visible {
  outline: 3px solid var(--focus-outline);
  outline-offset: 4px;
}

.node-panel.is-dragging {
  cursor: grabbing;
  border-color: var(--focus-outline);
}

/*
 * Branding Panel - default position: 12 o'clock.
 *
 * Centred on the same vertical axis as the radial menu, directly above it, with the
 * welcome panel mirroring it at 6 o'clock. node-panels.js converts this to explicit
 * coordinates on load and clears the centring transform, so it is only the starting
 * position - the node can be dragged anywhere from there.
 */
#shimtiPanel {
  position: fixed;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--panel-bg);
  padding: 8px 16px;
  border-radius: 10px;
  border: 2px solid var(--border-color);
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 40;
  user-select: none;
  /* Interactive: this is a draggable node, so it must receive pointer events. */
  pointer-events: auto;
}

#shimtiPanel img {
  height: 1.5em;
  aspect-ratio: 1;
  filter: var(--brightness-filter);
}

#shimtiPanel img[alt*="Failed to Load"] {
  background: var(--error-bg);
}

#shimtiPanel h1 {
  /* Heading for structure, not for size: the browser default margin and 2em type are
     cleared so the panel keeps the proportions it was designed with. */
  margin: 0;
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(0.9em, 2vw, 1.2em);
  color: var(--text-color);
  letter-spacing: 0.1em;
  font-weight: 600;
  /* Static rather than a pulsing drop-shadow: animating filter repaints on the main
     thread every frame, and this sits over a field that is already animating. */
  text-shadow: 0 0 8px rgba(180, 220, 255, 0.45);
}

/* Welcome Panel - default position: 6 o'clock, mirroring the branding node above. */
#shimtiPanelBottom {
  position: fixed;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--panel-bg);
  padding: 8px 16px;
  border-radius: 10px;
  border: 2px solid var(--border-color);
  /*
   * Sized to the greeting, not to a fixed 300px.
   *
   * The carousel cycles through 36 languages whose words differ enormously in width -
   * "Hi" against "Добро пожаловать" - so a fixed box left most of them swimming in empty
   * space and risked clipping the longest. width:auto with the panel's flex centring lets
   * it shrink-wrap the word.
   *
   * min-width only stops the very shortest words - "Hi", a two-glyph CJK greeting -
   * collapsing into something too small to read as a panel or to grab as a drag handle.
   * Kept low on purpose: at 140px it floored most of the 36 greetings, so "Hi",
   * "Welcome" and "Bienvenido" all rendered at exactly the same size and the panel never
   * appeared to respond to its contents at all.
   *
   * max-width keeps the longest greeting on screen. Deliberately NOT transitioned: the
   * wire attaches to this panel's measured edge, so an animating width would leave the
   * connector chasing a box that has not finished moving - the same lag that was removed
   * from dragging. The width changes while the text is faded out, so the change is not
   * visible anyway.
   */
  width: auto;
  min-width: 72px;
  max-width: min(90vw, 520px);
  min-height: 24px;
  z-index: 40;
  user-select: none;
  /* Interactive: this is a draggable node, so it must receive pointer events. */
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

#shimtiPanelBottom #welcomeText {
  font-family: 'Orbitron', sans-serif;
  font-size: 1em;
  color: var(--text-color);
  letter-spacing: 0.1em;
  font-weight: 600;
  text-align: center;
  white-space: nowrap;
  transition: opacity 0.5s ease;
  opacity: 1;
  /* No width: the panel now shrink-wraps this text, so a width here would either fight
     that or resolve circularly against a container it is itself sizing.
     No pointer-events either - the panel around it is already interactive, and this only
     ever needed to opt back in when the panel itself was pointer-events:none. */
}

#shimtiPanelBottom #welcomeText.fade-out {
  opacity: 0;
}

#shimtiPanelBottom #welcomeText.fade-in {
  opacity: 1;
}

/*
 * Radial Menu - sizing anchor only.
 *
 * This element is EMPTY. It exists solely so ui-elements.js can measure where and how
 * large to draw the wheel; the wheel itself is rendered into #uiSvg, a sibling.
 *
 * pointer-events MUST stay none. This is a 400x400 box at z-index 1000 positioned
 * directly over the wheel, so anything else makes it an invisible lid that swallows
 * every click and hover meant for the sector links underneath, leaving the navigation
 * completely unusable with a mouse.
 */
#radialMenu {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 400px;
  height: 400px;
  user-select: none;
  pointer-events: none;
  /* No z-index: the element is empty and non-interactive, so it paints nothing and
     receives nothing. It previously claimed 1000, which sat outside the depth model and
     implied an importance it does not have. */
}

/* z-index is deliberately absent: this is an SVG group, and SVG children paint in
   document order rather than by z-index, so the 1000 !important that used to be here
   was inert. Its position in the stack comes from #uiSvg, which contains it. */
#wheelMenu {
  width: 100%;
  height: 100%;
  user-select: none;
  pointer-events: all !important;
}

#wheelMenu g {
  transition: all 0.3s ease;
  pointer-events: all !important;
}

#wheelMenu g:hover path {
  fill: rgba(180, 220, 255, 0.4) !important;
  stroke: #8cf !important;
  stroke-width: 3 !important;
}

#wheelMenu g:hover image {
  filter: brightness(1.8) drop-shadow(0 0 10px rgba(180, 220, 255, 0.8)) !important;
}

#wheelMenu g:focus {
  outline: 3px solid #8cf !important;
  outline-offset: 2px;
}

#wheelMenu g.mouse-active:focus {
  outline: none !important;
}

/* Six icons, each previously animating its own drop-shadow filter - six main-thread
   repaints per frame for a glow nobody reads as pulsing. Static now; the hover state
   still brightens them. */
#wheelMenu g image {
  filter: drop-shadow(0 0 4px rgba(180, 220, 255, 0.45));
}

/*
 * The core: a ball of light breathing slowly at the centre of the menu.
 *
 * It used to run two animations at once on different periods - a 3s glow against a 2s
 * opacity fade - which never lined up, so instead of breathing it shimmered at a beat
 * frequency neither animation intended. One slow cycle drives everything now: scale,
 * opacity and glow rise and fall together, which is what reads as a light source rather
 * than a flickering element.
 *
 * transform-box:fill-box is required for scaling an SVG shape: without it the transform
 * origin is the SVG viewport's origin, not the circle's own centre, so the core would
 * swing around the middle of the screen instead of swelling in place.
 */
/* transform-box is required here, not optional. coreHalo scales this circle, and without
   fill-box the origin is the SVG viewport's corner rather than the circle's own centre -
   so it swings across the screen instead of swelling in place. */
#wheelMenu .inner-filled-circle {
  transform-box: fill-box;
  transform-origin: center;
  animation: coreHalo 9s ease-in-out infinite;
}

#wheelMenu .holo-core {
  transform-box: fill-box;
  transform-origin: center;
  filter: drop-shadow(0 0 12px rgba(180, 220, 255, 0.65));
  animation: corePulse 6s ease-in-out infinite;
}

/* The halo rings breathe on the same slow cycle, each offset by a negative delay so the
   pulse reads as travelling outward from the core rather than the whole stack throbbing
   as one. */
#wheelMenu .holo-ring {
  transform-box: fill-box;
  transform-origin: center;
  animation: coreHalo 6s ease-in-out infinite;
}

#wheelMenu .holo-ring.ring-0 { animation-delay: -0.4s; }
#wheelMenu .holo-ring.ring-1 { animation-delay: -0.8s; }
#wheelMenu .holo-ring.ring-2 { animation-delay: -1.2s; }

/* ==========================================================================
   Section Preview Panels
   Shown when a radial sector is hovered or keyboard-focused.
   ========================================================================== */

#sectionPanels {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
}

/*
 * Preview panels are positioned by node-panels.js, which anchors each one beside the
 * branding node and keeps it inside the viewport.
 *
 * The stylesheet deliberately sets no left/top here: the two would fight, and whichever
 * ran last would win. The data-hemisphere attribute is retained in the markup because it
 * still describes which side of the wheel a section sits on, but it no longer drives
 * placement.
 */
.section-panel {
  position: fixed;
  left: 0;
  top: 0;
  width: min(390px, 26vw);
  margin: 0;

  /* Hidden with opacity, never display:none or visibility:hidden.
     Each sector link references this panel's paragraph via aria-describedby, and both
     of those would drop the text out of the accessibility tree and break it.
     It also has to keep a real size while closed, since it is measured before it is
     shown in order to work out where it will fit. */
  opacity: 0;
  pointer-events: none;

  /* Only opacity transitions. Animating position would make the panel visibly drift into
     place after the wire had already arrived, reintroducing exactly the lag that was
     just removed from the node wires. */
  transition: opacity 0.28s ease;
}

.section-panel.is-open {
  opacity: 1;
}

.section-panel-surface {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.55);
  border: 2px solid var(--border-color);
  border-radius: 10px;
  backdrop-filter: blur(6px);
  box-shadow: 0 0 24px rgba(180, 220, 255, 0.15);
}

.section-panel-image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  /* Reserved by width/height plus aspect-ratio, so swapping the src in on first
     reveal cannot shift the panel's layout. */
}

.section-panel-title {
  margin: 0;
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(1rem, 1.6vw, 1.35rem);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-color);
}

.section-panel-copy {
  margin: 0;
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(0.72rem, 0.95vw, 0.85rem);
  line-height: 1.65;
  letter-spacing: 0.02em;
  color: var(--text-color);
}

/*
 * Narrow screens: the side panels become a bottom sheet.
 *
 * At 26vw a panel is roughly 100px wide on a phone, which is unreadable, and the wheel
 * itself already fills most of the width. Anchoring to the bottom edge keeps the copy
 * legible and clear of the wheel, and matches where a thumb already is.
 *
 * 900px rather than the 768px used elsewhere: the squeeze here is caused by the wheel
 * and two side panels competing for width, which starts well before tablet width.
 */
@media (max-width: 900px) {
  /* !important because node-panels.js may have written an inline left/top before the
     viewport was narrowed. Anchoring is disabled below this width, but a panel anchored
     while the window was wide must still fall back cleanly to the bottom sheet. */
  .section-panel {
    top: auto !important;
    bottom: 64px;
    left: 50% !important;
    right: auto;
    width: min(440px, 92vw);
    max-height: 46vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(-50%);
  }

  /* The image is the first thing to sacrifice when height is tight - the copy is what
     the visitor needs in order to choose a section. */
  .section-panel-image {
    aspect-ratio: 21 / 9;
  }
}

/* The wordmark ring is pushed outward on small screens so it clears the menu icons
   (see #ringTitle). Text on a path grows outward from its baseline, so at the default
   8-unit size the ring's far edge would then sit wider than the screen. Smaller type
   keeps the whole ring on a phone, and it is set here rather than on the element because
   the font-size attribute in the markup is what this has to beat. */
@media (max-width: 600px) {
  .ring-title {
    font-size: 5.5px;
  }
}

@media (max-width: 480px) {
  .section-panel-image {
    display: none;
  }

  .section-panel {
    bottom: 56px;
    max-height: 40vh;
  }
}

/* Focus ring for the radial sectors, which are SVG links. */
#wheelMenu a:focus-visible {
  outline: 3px solid var(--focus-outline);
  outline-offset: 3px;
}

#wheelMenu a:focus:not(:focus-visible) {
  outline: none;
}

/* Navigation shown only when JavaScript is unavailable. The wheel is drawn in JS, so
   without this there would be no navigation at all. */
.nav-fallback {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: center;
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
}

.nav-fallback a {
  display: block;
  padding: 0.6rem 1.1rem;
  background: var(--panel-bg);
  border: 2px solid var(--border-color);
  border-radius: 10px;
  color: var(--text-color);
  font-family: 'Orbitron', sans-serif;
  letter-spacing: 0.1em;
  text-decoration: none;
}

/* Connection Lines and Points */
.connection-line {
  stroke: rgba(255, 255, 255, 0.3);
  stroke-width: 2;
}

.connection-point {
  stroke: #ffffff;
  stroke-width: 1;
  fill: none;
}

/* Stationary Ring and Rotating Squares */
.stationary-ring {
  stroke: #ffffff;
  stroke-width: 2;
  fill: rgba(180, 220, 255, 0.08);
}

.rotating-squares {
  animation: rotateClockwise 60s linear infinite;
  transform-origin: center;
}

.rotating-square {
  fill: rgba(180, 220, 255, 0.08);
  animation: selfRotate 2s linear infinite;
}

/* Segmented Rings */
.segmented-ring.ring-0 {
  animation: rotateClockwise 100s linear infinite;
  transform-origin: center;
}

.segmented-ring.ring-1 {
  animation: rotateCounterClockwise 80s linear infinite;
  transform-origin: center;
}

.segmented-ring.ring-2 {
  animation: rotateCounterClockwise 60s linear infinite;
  transform-origin: center;
}

/* Dotted Circle */
.dotted-circle {
  stroke: rgba(255, 255, 255, 0.3);
  stroke-width: 2;
  stroke-dasharray: 4, 4;
  fill: none;
}

/* Thick Circle */
.thick-circle {
  fill: rgba(180, 220, 255, 0.08);
  stroke: none;
}

/* Animations */
@keyframes glowPulse {
  0% { filter: drop-shadow(0 0 2px rgba(180, 220, 255, 0.3)); }
  50% { filter: drop-shadow(0 0 8px rgba(180, 220, 255, 0.6)); }
  100% { filter: drop-shadow(0 0 2px rgba(180, 220, 255, 0.3)); }
}

/* One cycle: the core swells, brightens and glows harder, then settles. */
/* Scale and opacity only. The glow is a static filter on .holo-core, so the breathing
   costs the compositor a transform and nothing else. */
@keyframes corePulse {
  0%, 100% {
    opacity: 0.7;
    transform: scale(0.92);
  }
  50% {
    opacity: 1;
    transform: scale(1.08);
  }
}

/* The surrounding halo, on the same rhythm but wider and fainter. */
@keyframes coreHalo {
  0%, 100% {
    opacity: 0.45;
    transform: scale(0.96);
  }
  50% {
    opacity: 0.9;
    transform: scale(1.05);
  }
}

@keyframes circleGlowPulse {
  0% { filter: drop-shadow(0 0 3px rgba(180, 220, 255, 0.4)); }
  50% { filter: drop-shadow(0 0 8px rgba(180, 220, 255, 0.7)); }
  100% { filter: drop-shadow(0 0 3px rgba(180, 220, 255, 0.4)); }
}

@keyframes opacityPulse {
  0%, 100% { opacity: 0.7; }
  50% { opacity: 1; }
}

@keyframes rotateClockwise {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

@keyframes rotateCounterClockwise {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(-360deg); }
}

@keyframes selfRotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(-360deg); }
}

/* Responsive Design */
@media (max-width: 768px) {
  #shimtiPanel {
    top: 10px;
    padding: 6px 12px;
  }

  #shimtiPanel img {
    height: 1.5em;
  }

  #shimtiPanel span {
    font-size: clamp(0.7em, 2vw, 1em);
  }

  #shimtiPanelBottom {
    bottom: 12px;
    padding: 6px 12px;
    max-width: 88vw;
  }

  #shimtiPanelBottom #welcomeText {
    font-size: 0.9em;
  }

  #radialMenu {
    width: 300px;
    height: 300px;
  }
}

@media (max-width: 480px) {
  #shimtiPanel {
    top: 10px;
    padding: 4px 8px;
  }

  #shimtiPanel img {
    height: 1.2em;
  }

  #shimtiPanel span {
    font-size: clamp(0.6em, 2vw, 0.9em);
  }

  #shimtiPanelBottom {
    bottom: 12px;
    padding: 4px 8px;
    max-width: 92vw;
  }

  #shimtiPanelBottom #welcomeText {
    font-size: 0.8em;
  }

  #radialMenu {
    width: 250px;
    height: 250px;
  }
}

/* A coarse viewport is a movable crop of the desktop composition, not a separately
   reflowed composition. Keep the three concentric systems on the measured 1388px/400px
   geometry; the narrow phone viewport simply hides what lies beyond its edges. This
   follows the max-width rules so pointer type, rather than width, wins on a phone. */
@media (pointer: coarse) {
  .node-panel {
    -webkit-tap-highlight-color: transparent;
    transition: border-color 90ms ease, background-color 90ms ease,
      box-shadow 90ms ease, outline-color 90ms ease;
  }

  .node-panel.is-dragging {
    border-color: #dff8ff;
    background-color: rgba(16, 45, 60, 0.96);
    box-shadow:
      0 0 0 3px rgba(111, 224, 255, 0.34),
      0 0 20px 7px rgba(83, 204, 255, 0.38);
    outline: 2px solid rgba(223, 248, 255, 0.9);
    outline-offset: 3px;
  }

  a[data-section].is-holding path {
    filter: drop-shadow(0 0 8px rgba(180, 235, 255, 0.95));
  }

  .section-panel {
    bottom: 64px;
    width: min(320px, 82vw);
    max-height: 32vh;
  }

  .section-panel-surface {
    gap: 0.45rem;
    padding: 0.65rem 0.75rem;
    backdrop-filter: blur(3px);
  }

  .section-panel-image {
    display: none;
  }

  .section-panel-title {
    font-size: 0.85rem;
  }

  .section-panel-copy {
    font-size: 0.68rem;
    line-height: 1.45;
  }

  #ringTitle {
    width: max(100vmin, 1388px);
    height: max(100vmin, 1388px);
  }

  .ring-title {
    font-size: 8px;
  }

  #radialMenu {
    width: 400px;
    height: 400px;
  }

  #shimtiPanel {
    top: 20px;
    padding: 8px 16px;
  }

  #shimtiPanel img {
    height: 1.5em;
  }

  #shimtiPanel h1 {
    font-size: 1.2em;
  }

  #shimtiPanelBottom {
    bottom: 12px;
    padding: 8px 16px;
    max-width: min(90vw, 520px);
  }

  #shimtiPanelBottom #welcomeText {
    font-size: 1em;
  }
}

/*
 * Reduced Motion
 * Honours the OS-level "reduce motion" setting (WCAG 2.1 SC 2.3.3). This site animates
 * continuously - spinning rings, orbiting squares, pulsing glows - which can provoke
 * nausea and dizziness in people with vestibular disorders.
 *
 * This block covers CSS animations and transitions only. SMIL <animateTransform> inside
 * the SVGs ignores CSS entirely and is paused separately via pauseAnimations() in the
 * inline script in index.html. Both halves are required for full coverage.
 */
/*
 * Reduced motion.
 *
 * This setting is about MOTION - spinning, scaling, drifting, parallax - because that is
 * what provokes nausea and dizziness in people with vestibular disorders. It is not a
 * request for a still image. Changes in opacity, colour and glow are explicitly fine, and
 * the separate hazard of flashing is governed by WCAG 2.3.1, which is about three flashes
 * a second across a large area - nowhere near a 2.5px lamp blinking about once a second.
 *
 * The previous rule here set animation-duration to 0.01ms on every element, which killed
 * every animation on the page indiscriminately: the core stopped breathing and the
 * console lamps stopped blinking, even though neither involves any movement. What was
 * left was a dead interface rather than a calmer one.
 *
 * So: rotation and scaling stop, glow and opacity continue.
 */
@media (prefers-reduced-motion: reduce) {
  /* Everything that actually moves. The ring field rotates, so it stops here - this is
     also what replaced the SMIL pause script, since none of this is SMIL any more. */
  .ring-spin-cw,
  .ring-spin-ccw,
  .ring-marker-spin,
  .rotating-squares,
  .rotating-square,
  .segmented-ring.ring-0,
  .segmented-ring.ring-1,
  .segmented-ring.ring-2 {
    animation: none !important;
  }

  /* The core keeps breathing, but by brightness alone - the scaling is dropped. */
  #wheelMenu .holo-core {
    animation-name: corePulseStill !important;
  }

  #wheelMenu .holo-ring,
  #wheelMenu .inner-filled-circle {
    animation-name: coreHaloStill !important;
  }

  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Keep the carousel readable: it fades text in and out, so with transitions
     effectively disabled the text must remain fully opaque rather than stuck faded. */
  #shimtiPanelBottom #welcomeText,
  #shimtiPanelBottom #welcomeText.fade-out,
  #shimtiPanelBottom #welcomeText.fade-in {
    opacity: 1;
  }
}

/* Reduced-motion counterparts: identical rhythm, no transform. */
@keyframes corePulseStill {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}

@keyframes coreHaloStill {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 0.9; }
}
