/* Sphere visualization tokens — one semantic name per drawn layer, read at
   runtime by `resolveCssColor()` in skill-galaxy.js. Light values live here;
   `html[data-theme="dark"]` below only overrides the ones a dark surface
   needs a different weight for. `--sg-active-link` and `--sg-label-active`
   alias `--accent` directly rather than re-deriving it: the accent itself
   already flips orange/blue per theme, so the alias is free theme safety.
   `--sg-shell-line` is a plain literal/token value, never a `color-mix()`:
   it feeds
   `THREE.Color.setStyle()`, and Chrome sometimes serializes a computed
   `color-mix()` result as a CSS Color 4 `color(srgb …)` string rather than
   `rgb()` — a form three.js r0.184's regex parser does not recognise. It
   fails silently (no throw, so `resolveCssColor()`'s try/catch never sees
   it) and leaves the colour at its default black. The dimmer "radial link"
   tier is therefore not its own CSS token: `applyTheme()` derives it in JS
   by lerping this already-safe colour toward `--bg`, the same arithmetic
   blending every other colour in this file already uses. `--sg-bloom-stops`
   paints straight into a CSS `radial-gradient()` and never passes through
   THREE.Color, so literal `rgba()` ramps there are unaffected — its geometry
   comes from `updateGlow()` instead, see the bloom rule below. */
body[data-page="home"] #about.skill-galaxy-section{--sg-panel-width:244px;--sg-radius:12px;--sg-line:color-mix(in srgb,var(--line) 74%,transparent);--sg-surface:color-mix(in srgb,var(--bg-elev) 86%,transparent);--sg-accent-soft:color-mix(in srgb,var(--accent) 13%,transparent);--sg-shell-line:#d6d5cf;--sg-active-link:var(--accent);--sg-label-default:var(--text-dim);--sg-label-active:var(--accent);padding:72px 0;overflow:clip;scroll-margin-top:var(--home-sticky-header-height)}
/* The bloom's colour ramp, one per theme. Measured off the frame's own
   "BG · sphere" asset composited over the band: warm paper highlight on
   light, the accent's own blue on dark. Percentages here resolve against
   whatever radius `--sg-glow-radius` carries, so the shape holds at any
   sphere size. */
body[data-page="home"] #about.skill-galaxy-section{
  --sg-bloom-stops:
    rgba(255,214,198,.6336) 0%,
    rgba(255,214,198,.6048) 10%,
    rgba(255,214,198,.5328) 21%,
    rgba(255,214,198,.4464) 31%,
    rgba(255,214,198,.3744) 41%,
    rgba(255,214,198,.2736) 51%,
    rgba(255,214,198,.1872) 62%,
    rgba(255,214,198,.1152) 72%,
    rgba(255,214,198,.0432) 82%,
    rgba(255,214,198,0) 95%}
html[data-theme="dark"] body[data-page="home"] #about.skill-galaxy-section{
  --sg-bloom-stops:
    rgba(6,92,222,.42) 0%,
    rgba(6,92,222,.42) 15%,
    rgba(6,92,222,.32) 31%,
    rgba(6,92,222,.19) 51%,
    rgba(6,92,222,.07) 72%,
    rgba(6,92,222,0) 93%}
body[data-page="home"] #about.skill-galaxy-section>.wrap{width:min(1080px,100%);max-width:1080px;padding:0}
.skill-galaxy-shell{position:relative;min-height:760px;overflow:hidden;border:1px solid var(--sg-line);border-radius:var(--sg-radius);color:var(--text);background:radial-gradient(color-mix(in srgb,var(--text) 16%,transparent) .65px,transparent .8px);background-color:color-mix(in srgb,var(--bg) 96%,var(--bg-elev));background-size:22px 22px;box-shadow:inset 0 1px 0 color-mix(in srgb,var(--text) 5%,transparent);isolation:isolate}
/* ── Sphere bloom ──────────────────────────────────────────────────────────
   One rule for every breakpoint, positioned and sized by the renderer rather
   than by CSS. `updateGlow()` in `skill-galaxy.js` projects the sphere's own
   centre and radius to stage pixels every frame the camera moves and writes
   them here as `--sg-glow-x/y/radius`.

   Percentages and fixed radii were what this replaced, and both were wrong
   for the same reason: the sphere is not stationary and not one size. It
   dollies sideways when the panel or sheet opens, scales with `state.zoom`,
   and projects to a different pixel radius at every stage height — so a
   `43.5%`-of-the-box position drifted off it and a literal `400px` circle
   stopped matching it the moment either changed.

   On `.skill-galaxy-stage`: the canvas fills exactly this box, so the
   renderer's own pixel coordinates need no translation, and the background
   still paints behind the transparent canvas. The fallbacks are only for the
   frames before the first `updateGlow()` — a centred circle, roughly the
   resting size.

   Stops live in `--sg-bloom-stops` so the geometry above is written once and
   only the colour ramp varies per theme. */
#about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-stage{
  background-image:radial-gradient(
    circle var(--sg-glow-radius,340px) at var(--sg-glow-x,50%) var(--sg-glow-y,50%),
    var(--sg-bloom-stops));
  background-repeat:no-repeat}
.skill-galaxy-shell::before{content:'';position:absolute;inset:0;z-index:0;pointer-events:none;background:linear-gradient(90deg,color-mix(in srgb,var(--bg) 73%,transparent),transparent 30%,transparent 72%,color-mix(in srgb,var(--bg) 68%,transparent))}
.skill-galaxy-tabs{position:absolute;z-index:7;top:24px;left:32px;display:flex;align-items:center;gap:32px;color:var(--text-dim);font:400 13px/1 var(--portfolio-utility);letter-spacing:.035em;text-transform:uppercase}
.skill-galaxy-tabs span{position:relative;display:flex;align-items:center;gap:14px;min-height:34px}.skill-galaxy-tabs i{font:400 19px/1 var(--portfolio-body);font-style:normal}.skill-galaxy-tabs b{font:inherit;font-weight:400}.skill-galaxy-tabs .is-active{color:var(--accent)}.skill-galaxy-tabs .is-active::after{content:'';position:absolute;right:0;bottom:-5px;left:28px;height:2px;background:var(--accent)}
.skill-galaxy-intro{display:none}.skill-galaxy-overline,.skill-galaxy-panel-kicker,.skill-galaxy-list-group h3,.skill-galaxy-view-label{color:var(--accent);font:600 10px/1.3 var(--portfolio-utility);letter-spacing:.065em;text-transform:uppercase}
.skill-galaxy-about{position:absolute;z-index:5;top:82px;left:32px;width:224px;padding:18px 18px 20px;border:1px solid var(--sg-line);border-radius:12px;background:color-mix(in srgb,var(--bg-elev) 69%,transparent);backdrop-filter:blur(10px)}
.skill-galaxy-about p{color:var(--text-dim);font:400 14px/1.55 var(--portfolio-body)}
.skill-galaxy-instruction{position:static;display:flex;width:auto;min-height:0;align-items:flex-start;gap:12px;margin-top:22px;padding:18px 0 0;border:0;border-top:1px solid var(--sg-line);border-radius:0;color:var(--text-dim);background:transparent;font:400 11px/1.45 var(--portfolio-body);white-space:pre-line}.skill-galaxy-instruction-icon{width:36px;height:42px;flex:0 0 36px}
.skill-galaxy-network{position:absolute;z-index:1;inset:55px 218px 58px 188px;min-width:0}.skill-galaxy-stage{position:absolute;inset:0;overflow:hidden;cursor:grab;touch-action:pan-y;user-select:none;outline:none}.skill-galaxy-stage:active{cursor:grabbing}.skill-galaxy-stage:focus-visible{outline:2px solid var(--accent);outline-offset:-4px;border-radius:14px}.skill-galaxy-stage canvas,.skill-galaxy-labels{position:absolute;inset:0;width:100%;height:100%}.skill-galaxy-stage canvas{display:block;opacity:0;transition:opacity .3s ease}.skill-galaxy-section.is-webgl-ready .skill-galaxy-stage canvas{opacity:1}.skill-galaxy-labels{pointer-events:none;overflow:hidden}
.skill-galaxy-node-label{position:absolute;left:0;top:0;display:block;max-width:124px;min-height:24px;padding:4px 5px 4px 0;border:0;color:var(--sg-label-default);background:transparent;font:500 10.5px/1.15 var(--portfolio-utility);text-align:left;white-space:normal;pointer-events:auto;cursor:inherit;opacity:.76;transform-origin:left center;transition:color .16s ease,opacity .16s ease,text-shadow .16s ease;will-change:transform}.skill-galaxy-node-label[hidden]{display:none!important}.skill-galaxy-node-label.is-connected{color:color-mix(in srgb,var(--sg-label-active) 78%,var(--text));opacity:1}.skill-galaxy-node-label[data-sg-label="ui-ux-design"].is-selected{color:var(--text);font-size:13px;font-weight:700;opacity:1;text-shadow:0 0 12px color-mix(in srgb,var(--text) 28%,transparent)}.skill-galaxy-node-label.is-selected{z-index:2;max-width:150px;color:var(--sg-label-active);font-size:12px;font-weight:700;opacity:1;text-shadow:0 0 12px color-mix(in srgb,var(--sg-label-active) 36%,transparent)}.skill-galaxy-node-label.is-muted{opacity:.38}
.skill-galaxy-controls{position:absolute;z-index:7;left:32px;right:auto;bottom:28px;display:flex;align-items:flex-end;pointer-events:none}.skill-galaxy-view-block,.skill-galaxy-zoom{pointer-events:auto}.skill-galaxy-view-label{display:block;margin-bottom:8px;color:var(--text-faint)}.skill-galaxy-view-label::after{content:'•';margin-left:8px;color:var(--accent);font-size:9px}
.skill-galaxy-view-switch{display:flex;width:104px;min-height:48px;overflow:hidden;border:1px solid var(--sg-line);border-radius:10px;background:var(--sg-surface)}.skill-galaxy-view-switch button,.skill-galaxy-zoom button{display:grid;width:52px;min-height:46px;place-items:center;border:0;color:var(--text-dim);background:transparent;cursor:pointer}.skill-galaxy-view-switch button+button,.skill-galaxy-zoom button+button{border-left:1px solid var(--sg-line)}.skill-galaxy-view-switch button.is-active{color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,var(--bg-elev));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent),inset 0 0 18px color-mix(in srgb,var(--accent) 10%,transparent)}
.skill-galaxy-icon{position:relative;display:block;width:28px;height:28px;flex:0 0 28px}.skill-galaxy-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.skill-galaxy-icon-light{display:none}html[data-theme="light"] .skill-galaxy-icon-dark{display:none}html[data-theme="light"] .skill-galaxy-icon-light{display:block}.skill-galaxy-view-switch .skill-galaxy-icon{width:30px;height:30px}.skill-galaxy-zoom{display:none!important}
.skill-galaxy-panel{--sg-line:color-mix(in srgb,var(--line) 72%,transparent);position:absolute;z-index:8;top:88px;right:24px;width:var(--sg-panel-width);max-height:612px;overflow:hidden;border:1px solid var(--sg-line);border-radius:14px;background:color-mix(in srgb,var(--bg) 88%,var(--bg-elev));box-shadow:0 18px 48px color-mix(in srgb,var(--bg) 58%,transparent),inset 0 1px 0 color-mix(in srgb,var(--text) 6%,transparent);backdrop-filter:blur(18px) saturate(130%);opacity:0;visibility:hidden;transform:translateX(10px);transition:opacity .24s ease,transform .24s ease,visibility .24s}.skill-galaxy-panel.is-open{opacity:1;visibility:visible;transform:none}.skill-galaxy-panel-inner,.skill-galaxy-related-inner{max-height:610px;padding:24px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}.skill-galaxy-panel-inner{display:flex;min-height:510px;height:auto;box-sizing:border-box;flex-direction:column}
.skill-galaxy-panel-close,.skill-galaxy-panel-expand{position:absolute;z-index:4;top:10px;width:36px;height:36px;border:0;color:var(--text-dim);background:transparent;font:300 21px/1 var(--portfolio-body);cursor:pointer}.skill-galaxy-panel-close{right:8px}.skill-galaxy-panel-expand{display:none;right:50px;font-size:15px}.skill-galaxy-sheet-handle{display:none}.skill-galaxy-panel-kicker{margin-bottom:14px;font-size:9px}.skill-galaxy-panel-heading{display:flex;align-items:center;gap:10px;padding-right:24px}.skill-galaxy-panel-heading h3{color:var(--text);font:500 18px/1.2 var(--portfolio-display);letter-spacing:-.01em}.skill-galaxy-panel-cube{width:30px;height:30px;flex-basis:30px;filter:drop-shadow(0 0 8px color-mix(in srgb,var(--accent) 28%,transparent))}.skill-galaxy-cube{width:25px;height:25px;flex-basis:25px;filter:drop-shadow(0 0 7px color-mix(in srgb,var(--accent) 24%,transparent))}
.skill-galaxy-panel-description{margin-top:18px;color:var(--text-dim);font:400 12px/1.55 var(--portfolio-body)}.skill-galaxy-panel-divider{height:1px;margin:18px 0;background:var(--sg-line)}.skill-galaxy-connected-list{display:grid;gap:4px}.skill-galaxy-connected-list button{display:grid;grid-template-columns:8px 1fr;align-items:start;gap:9px;min-height:41px;padding:5px 0;border:0;color:var(--text);background:transparent;font:400 12px/1.3 var(--portfolio-body);text-align:left;cursor:pointer}.skill-galaxy-connected-list button>span:first-child{width:6px;height:6px;margin-top:3px;border-radius:50%;background:var(--accent);box-shadow:0 0 7px color-mix(in srgb,var(--accent) 55%,transparent)}.skill-galaxy-connected-copy{display:grid;gap:2px}.skill-galaxy-connected-copy strong{font:400 12px/1.25 var(--portfolio-body)}.skill-galaxy-connected-copy small{color:var(--text-faint);font:400 10px/1.25 var(--portfolio-body)}
.skill-galaxy-related-cta{display:flex;width:100%;min-height:46px;align-items:center;justify-content:space-between;margin-top:auto;padding:12px 0 0;border:0;border-top:1px solid var(--sg-line);color:var(--accent);background:transparent;font:400 13px/1.3 var(--portfolio-body);text-align:left;cursor:pointer}.skill-galaxy-related-cta:disabled{color:var(--text-faint);cursor:default}.skill-galaxy-related-view{position:absolute;inset:0;background:color-mix(in srgb,var(--bg) 90%,var(--bg-elev));opacity:0;visibility:hidden;transform:translateX(100%);transition:opacity .22s ease,transform .22s ease,visibility .22s}.skill-galaxy-panel.is-related .skill-galaxy-related-view{opacity:1;visibility:visible;transform:none}.skill-galaxy-related-inner{display:flex;min-height:510px;box-sizing:border-box;flex-direction:column;padding:18px}.skill-galaxy-related-head{display:flex;gap:10px;align-items:flex-start;padding-right:12px}.skill-galaxy-related-head>button{width:38px;height:38px;flex:0 0 38px;border:1px solid var(--sg-line);border-radius:50%;color:var(--text);background:transparent;font-size:17px;cursor:pointer}.skill-galaxy-related-head h3{font:500 20px/1.15 var(--portfolio-display)}.skill-galaxy-related-head p{margin-top:4px;color:var(--text-dim);font:400 11px/1.3 var(--portfolio-body)}.skill-galaxy-related-head p span{color:var(--accent)}
.skill-galaxy-related-cards{display:grid;gap:8px;margin-top:16px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}.skill-galaxy-related-card{display:grid;grid-template-columns:minmax(0,1fr) 20px;gap:8px;align-items:center;width:100%;min-height:96px;padding:12px 10px 11px 12px;border:1px solid var(--sg-line);border-radius:10px;color:var(--text);background:color-mix(in srgb,var(--bg) 58%,transparent);text-align:left;cursor:pointer}.skill-galaxy-related-copy{display:grid;gap:6px;min-width:0}.skill-galaxy-related-copy strong{font:500 13px/1.25 var(--portfolio-display)}.skill-galaxy-related-tags{display:flex;gap:4px;flex-wrap:wrap}.skill-galaxy-related-tags i{padding:3px 6px;border:1px solid var(--sg-line);border-radius:8px;color:var(--text-dim);font:400 8.5px/1 var(--portfolio-body);font-style:normal}.skill-galaxy-related-description{display:-webkit-box;overflow:hidden;color:var(--text-dim);font:400 10px/1.35 var(--portfolio-body);-webkit-line-clamp:2;-webkit-box-orient:vertical}.skill-galaxy-related-arrow{color:var(--accent);font-size:18px}
.skill-galaxy-list{position:absolute;inset:26px 0 22px;display:none;overflow:auto;padding:0 8px 20px;scrollbar-width:thin}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-stage{display:none}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{display:block}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about{top:76px;right:286px;width:auto;min-height:74px;padding:16px 20px}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about p{max-width:720px}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-instruction{display:none}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{inset:170px 282px 70px 24px}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-panel{top:170px;right:24px}
.skill-galaxy-list-group{margin-bottom:9px}.skill-galaxy-list-group h3{margin:0 0 4px 9px;color:var(--text-faint);font-size:8.5px}.skill-galaxy-list-group>div{overflow:hidden;border:1px solid var(--sg-line);border-radius:10px;background:color-mix(in srgb,var(--bg-elev) 58%,transparent)}.skill-galaxy-list-row{display:grid;grid-template-columns:26px minmax(0,1fr) auto 20px;gap:8px;align-items:center;width:100%;min-height:40px;padding:4px 10px;border:0;border-bottom:1px solid var(--sg-line);color:var(--text);background:transparent;text-align:left;cursor:pointer}.skill-galaxy-list-row:last-child{border-bottom:0}.skill-galaxy-list-row.is-selected{color:var(--accent);background:var(--sg-accent-soft);box-shadow:inset 0 0 0 1px var(--accent),inset 0 0 22px color-mix(in srgb,var(--accent) 10%,transparent)}.skill-galaxy-list-row .skill-galaxy-cube{width:21px;height:21px;flex-basis:21px}.skill-galaxy-list-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:400 12.5px/1 var(--portfolio-body)}.skill-galaxy-list-count{color:var(--text-faint);font:400 10px/1 var(--portfolio-meta);white-space:nowrap}.skill-galaxy-list-chevron{font-size:15px;text-align:center}.skill-galaxy-section.is-webgl-fallback [data-sg-view="sphere"]{display:none}
.skill-galaxy-evidence-highlight{animation:skill-galaxy-evidence-pulse 1.6s ease both}@keyframes skill-galaxy-evidence-pulse{0%,100%{box-shadow:0 0 0 0 transparent}28%{box-shadow:0 0 0 3px var(--accent),0 0 24px color-mix(in srgb,var(--accent) 22%,transparent)}}html[data-theme="dark"] .skill-galaxy-shell{background-color:color-mix(in srgb,var(--bg) 98%,var(--bg-elev));background-image:radial-gradient(color-mix(in srgb,var(--text) 15%,transparent) .65px,transparent .8px),radial-gradient(color-mix(in srgb,var(--accent) 9%,transparent) .45px,transparent .75px);background-size:22px 22px,44px 44px;background-position:0 0,11px 7px}html[data-theme="light"] .skill-galaxy-shell{--sg-surface:color-mix(in srgb,var(--card) 80%,transparent);background-color:color-mix(in srgb,var(--bg) 92%,var(--card));background-image:radial-gradient(color-mix(in srgb,var(--text) 14%,transparent) .65px,transparent .8px),radial-gradient(color-mix(in srgb,var(--accent) 7%,transparent) .45px,transparent .75px);background-size:22px 22px,44px 44px;background-position:0 0,11px 7px}
/* skill-galaxy.css's own theme-qualified shell rules (same specificity,
   loaded earlier) embed their own glow at a fixed 54%/47% — the exact thing
   just moved onto `.skill-galaxy-network` above. Restating the dot patterns
   here, minus that glow layer, at the same specificity and later in the
   cascade is what actually drops it; declaring only `background-color`
   the way this rule used to left the image layers from that earlier one
   untouched, glow included. */

@media(max-width:960px){body[data-page="home"] #about.skill-galaxy-section{padding:0 0 48px;border-top:1px solid var(--line)}body[data-page="home"] #about.skill-galaxy-section>.wrap{padding:0}.skill-galaxy-shell{min-height:780px;border-inline:0;border-radius:0 20px 0 0}.skill-galaxy-tabs{position:relative;top:auto;left:auto;justify-content:center;gap:34px;padding:4px var(--mobile-gutter) 3px;border-bottom:1px solid var(--sg-line);font-size:13px}.skill-galaxy-tabs span{min-height:36px}.skill-galaxy-intro{position:relative;z-index:5;display:block;padding:20px clamp(20px,4vw,38px) 0}.skill-galaxy-overline{display:block}body[data-page="home"] .skill-galaxy-intro h2{display:block;max-width:560px;margin:10px 0 0;color:var(--text);font:400 clamp(23px,3.1vw,28px)/1.32 var(--portfolio-display);letter-spacing:-.02em}.skill-galaxy-about{display:none}.skill-galaxy-mobile-hints{display:grid;grid-template-columns:repeat(2,minmax(0,250px));gap:10px;max-width:540px;margin-top:22px}.skill-galaxy-mobile-hint{display:flex;min-height:70px;align-items:center;gap:11px;padding:12px 14px;border:1px solid var(--sg-line);border-radius:11px;color:var(--text-dim);background:color-mix(in srgb,var(--bg-elev) 62%,transparent);font:400 13px/1.35 var(--portfolio-body)}.skill-galaxy-hint-icon{width:31px;height:31px;flex-basis:31px}.skill-galaxy-network{position:relative;inset:auto;height:min(610px,67svh);margin-top:4px}.skill-galaxy-stage{inset:0 0 58px}.skill-galaxy-node-label{max-width:112px;font-size:9.5px}.skill-galaxy-node-label[data-sg-label="ui-ux-design"].is-selected{font-size:12px}.skill-galaxy-node-label.is-selected{max-width:132px;font-size:10.5px}.skill-galaxy-controls{left:var(--mobile-gutter);right:auto;bottom:10px}.skill-galaxy-view-label{font-size:8.5px}.skill-galaxy-view-switch{width:98px;min-height:46px}.skill-galaxy-view-switch button{width:49px;min-height:44px}.skill-galaxy-view-switch .skill-galaxy-icon{width:28px;height:28px}
.skill-galaxy-panel{position:fixed;z-index:95;top:auto;right:7px;bottom:7px;left:7px;width:auto;height:min(48svh,520px);max-height:min(48svh,520px);border-radius:28px 28px 8px 8px;background:color-mix(in srgb,var(--bg) 79%,var(--bg-elev));box-shadow:0 -16px 44px color-mix(in srgb,var(--bg) 68%,transparent),0 0 26px color-mix(in srgb,var(--accent) 7%,transparent),inset 0 1px 0 color-mix(in srgb,var(--text) 10%,transparent);backdrop-filter:blur(22px) saturate(140%);transform:translateY(calc(100% + 22px));display:flex;flex-direction:column}.skill-galaxy-panel.is-open{transform:none}.skill-galaxy-panel.is-expanded{top:calc(var(--home-sticky-header-height) + 8px);bottom:7px;height:auto;max-height:none}.skill-galaxy-panel-inner,.skill-galaxy-related-inner{max-height:none;min-height:0;padding:34px 40px 18px;overflow:auto}.skill-galaxy-panel-inner{height:auto;flex:1}.skill-galaxy-sheet-handle{display:block;position:absolute;z-index:5;top:7px;left:50%;width:54px;height:20px;border:0;background:transparent;transform:translateX(-50%);touch-action:none;cursor:ns-resize}.skill-galaxy-sheet-handle::after{content:'';display:block;width:42px;height:4px;margin:0 auto;border-radius:4px;background:var(--text-faint);opacity:.58}.skill-galaxy-panel-close,.skill-galaxy-panel-expand{top:18px;width:44px;height:44px;border:1px solid var(--sg-line);border-radius:50%}.skill-galaxy-panel-close{right:16px}.skill-galaxy-panel-expand{display:grid;right:68px;place-items:center}.skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand span{transform:rotate(180deg)}.skill-galaxy-panel-kicker{margin-bottom:10px;font-size:10px}.skill-galaxy-panel-heading{gap:12px;padding-right:100px}.skill-galaxy-panel-heading h3{font-size:26px}.skill-galaxy-panel-cube{width:38px;height:38px;flex-basis:38px}.skill-galaxy-panel-description{max-width:680px;margin-top:18px;font-size:15px;line-height:1.48}.skill-galaxy-panel-divider{margin:16px 0 24px}
/* `skill-galaxy.css` still carries an older, unrelated list-view sheet design
   (a fixed 340px panel, never updated for the unified 233px sheet this file
   composes everywhere else) whose `[data-sg-view="list"] .skill-galaxy-panel-divider`
   selector out-specifies the plain class above — so LIST view alone kept the
   old 4px margin and a 2px kicker gap regardless of what this rule said,
   while SPHERE view correctly picked up the spacing above. Matching that
   selector's specificity here, at the same load order, is what actually
   settles it — same fix `.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-panel-kicker`
   needs right after it, for the same reason. */
.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-panel-divider{margin:16px 0 24px}
.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-panel-kicker{margin-bottom:10px}
/* Same trap, same fix, one more selector: `skill-galaxy.css`'s legacy list-view
   sheet also carries `[data-sg-view="list"] .skill-galaxy-connected-list button`
   at 36px/`5px 12px` — a generic `button` selector, so it still catches the
   chip buttons this file redesigned to 28px/`6px 10px`, and out-specifies the
   plain `.skill-galaxy-connected-list button.skill-galaxy-chip` rule below.
   List view alone rendered taller chips than sphere view as a result (28px vs
   36px, no frame calling for the difference) — matching the legacy selector's
   specificity here settles it the same way. */
.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-connected-list button.skill-galaxy-chip{
  min-height:28px;padding:6px 10px}
.skill-galaxy-connected-list{display:flex;flex-wrap:wrap;gap:7px}.skill-galaxy-connected-list button{grid-template-columns:7px auto;min-height:28px;padding:6px 10px;border:1px solid var(--sg-line);border-radius:8px}.skill-galaxy-connected-copy{display:block}.skill-galaxy-connected-copy strong{font-size:13px}.skill-galaxy-connected-copy small{display:none}.skill-galaxy-related-cta{min-height:42px;padding-top:10px}.skill-galaxy-panel.is-related .skill-galaxy-panel-inner{display:none}.skill-galaxy-related-view{position:relative;display:none;min-height:0;flex:1;transform:none;opacity:1;visibility:visible;background:transparent}.skill-galaxy-panel.is-related .skill-galaxy-related-view{display:flex}.skill-galaxy-related-inner{display:flex;width:100%;min-height:0;padding:40px 28px 24px}.skill-galaxy-related-head{gap:14px;align-items:center}.skill-galaxy-related-head>button{width:48px;height:48px;flex-basis:48px}.skill-galaxy-related-head h3{font-size:26px}.skill-galaxy-related-head p{font-size:14px}.skill-galaxy-related-cards{flex:1;margin-top:18px}.skill-galaxy-related-card{min-height:91px;padding:12px 14px;border-radius:12px}.skill-galaxy-related-copy strong{font-size:15px}.skill-galaxy-related-description{font-size:12px}.skill-galaxy-related-tags i{font-size:10px}
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about{display:none}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{position:relative;inset:auto;height:auto;margin-top:18px}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{position:relative;inset:auto;display:block;margin:0 clamp(20px,4vw,32px) 76px;padding:0;overflow:visible}.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-shell{min-height:0}.skill-galaxy-list-row{grid-template-columns:27px minmax(0,1fr) auto 20px;min-height:44px;padding:4px 10px}.skill-galaxy-list-name{font-size:14px}.skill-galaxy-list-count{font-size:11px}.skill-galaxy-list-group h3{font-size:9px}}

@media(max-width:430px){.skill-galaxy-shell{min-height:780px}.skill-galaxy-intro{padding-top:18px}body[data-page="home"] .skill-galaxy-intro h2{font-size:22px}.skill-galaxy-mobile-hint{min-height:62px;padding:10px;font-size:10.5px}.skill-galaxy-hint-icon{width:27px;height:27px;flex-basis:27px}.skill-galaxy-network{height:500px;margin-top:0}.skill-galaxy-panel{height:min(49svh,430px);max-height:min(49svh,430px)}.skill-galaxy-panel-inner{padding:32px 18px 10px}.skill-galaxy-panel-heading{padding-right:84px}.skill-galaxy-panel-heading h3{font-size:22px}.skill-galaxy-panel-cube{width:34px;height:34px;flex-basis:34px}.skill-galaxy-panel-description{margin-top:12px;font-size:13px}.skill-galaxy-panel-divider{margin:11px 0 18px}.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-panel-divider{margin:11px 0 18px}.skill-galaxy-panel[data-sg-view="list"] .skill-galaxy-panel-kicker{margin-bottom:10px}.skill-galaxy-connected-list{gap:5px}.skill-galaxy-connected-list button{min-height:28px;padding:5px 9px}.skill-galaxy-connected-copy strong{font-size:12px}.skill-galaxy-panel-close,.skill-galaxy-panel-expand{top:14px;width:40px;height:40px}.skill-galaxy-panel-close{right:12px}.skill-galaxy-panel-expand{right:58px}.skill-galaxy-related-inner{padding:34px 16px 16px}.skill-galaxy-related-head h3{font-size:22px}.skill-galaxy-related-head p{font-size:12px}.skill-galaxy-related-card{min-height:86px;padding:10px 12px}.skill-galaxy-list-row{grid-template-columns:24px minmax(0,1fr) auto 18px;gap:7px;min-height:42px;padding-inline:8px}.skill-galaxy-list-row .skill-galaxy-cube{width:19px;height:19px}.skill-galaxy-list-name{font-size:12.5px}.skill-galaxy-list-count{font-size:9px}}
@media(max-width:360px){.skill-galaxy-mobile-hints{grid-template-columns:1fr}.skill-galaxy-network{height:520px}.skill-galaxy-list-count{display:none}.skill-galaxy-list-row{grid-template-columns:24px minmax(0,1fr) 18px}}

@media(prefers-reduced-motion:reduce){.skill-galaxy-stage canvas,.skill-galaxy-panel,.skill-galaxy-related-view,.skill-galaxy-node-label{transition:none}.skill-galaxy-evidence-highlight{animation-duration:1ms}}

@media(max-width:960px){
  .skill-galaxy-panel,
  .skill-galaxy-panel[data-sg-view="sphere"],
  .skill-galaxy-panel[data-sg-view="list"]{height:min(48svh,520px);max-height:min(48svh,520px)}
  .skill-galaxy-panel.is-expanded,
  .skill-galaxy-panel[data-sg-view="sphere"].is-expanded,
  .skill-galaxy-panel[data-sg-view="list"].is-expanded{top:calc(var(--home-sticky-header-height) + 8px);bottom:7px;height:auto;max-height:none}
}
@media(max-width:430px){
  .skill-galaxy-panel,
  .skill-galaxy-panel[data-sg-view="sphere"],
  .skill-galaxy-panel[data-sg-view="list"]{height:min(49svh,430px);max-height:min(49svh,430px)}
  .skill-galaxy-panel.is-expanded,
  .skill-galaxy-panel[data-sg-view="sphere"].is-expanded,
  .skill-galaxy-panel[data-sg-view="list"].is-expanded{height:auto;max-height:none}
}
.skill-galaxy-related-card{overflow:hidden}
.skill-galaxy-related-cards{max-height:420px}
@media(max-width:960px) and (orientation:landscape) and (max-height:560px){
  .skill-galaxy-panel,
  .skill-galaxy-panel[data-sg-view="sphere"],
  .skill-galaxy-panel[data-sg-view="list"],
  .skill-galaxy-panel.is-expanded{top:74px;right:8px;bottom:8px;left:auto;width:min(42vw,360px);height:auto;max-height:none}
}
@media(max-width:430px){
  .skill-galaxy-panel,
  .skill-galaxy-panel[data-sg-view="sphere"],
  .skill-galaxy-panel[data-sg-view="list"]{height:min(36svh,360px);max-height:min(36svh,360px);background:var(--bg);border-color:color-mix(in srgb,var(--accent) 18%,var(--line))}
  .skill-galaxy-panel.is-expanded,
  .skill-galaxy-panel[data-sg-view="sphere"].is-expanded,
  .skill-galaxy-panel[data-sg-view="list"].is-expanded{height:auto;max-height:none}
}
@media(max-width:960px) and (orientation:landscape) and (max-height:560px){
  .skill-galaxy-panel,
  .skill-galaxy-panel[data-sg-view="sphere"],
  .skill-galaxy-panel[data-sg-view="list"],
  .skill-galaxy-panel.is-expanded{top:74px;right:8px;bottom:8px;left:auto;width:min(42vw,360px);height:auto;max-height:none}
}

/* ── Figma parity pass · About + Skills ─────────────────────────────────────
   Matches the signed-off frames 360:5 (dark) and 384:2 (light).          */

/* Stage spans the whole 1080 wrap; the panel floats over its right edge
   rather than pushing the sphere into a narrow column. */
body[data-page="home"] #about.skill-galaxy-section{--sg-panel-width:344px}
.skill-galaxy-network{inset:55px 24px 58px 24px}

/* Connected skills read as chips, not as a stacked list. One spec everywhere. */
.skill-galaxy-connected-list{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
/* Desktop's kicker carries no margin of its own, so this 14px is the entire
   gap to it. Mobile's kicker already carries its own 10px `margin-bottom` —
   and flex items never collapse margins with each other, so the two simply
   added to 24px instead of the 10px the sheet is actually speced for, eating
   room the collapsed 233px sheet has none to spare and clipping the chip
   row's own scrollbar off the bottom edge. Zeroing this row's share leaves
   the kicker's 10px as the only source of that gap. */
@media(max-width:960px){
  .skill-galaxy-connected-list{margin-top:0}
}
.skill-galaxy-chip{display:inline-flex;align-items:center;gap:7px;min-height:28px;
  padding:6px 10px;border:1px solid var(--sg-line);border-radius:8px;
  background:color-mix(in srgb,var(--bg-card) 55%,transparent);
  color:var(--text-dim);font:400 11.5px/1.25 var(--portfolio-body);cursor:pointer;
  transition:border-color .18s ease,color .18s ease,background .18s ease}
.skill-galaxy-chip:hover,.skill-galaxy-chip:focus-visible{border-color:var(--accent);color:var(--text)}
.skill-galaxy-chip-dot{width:6px;height:6px;flex:0 0 6px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 5px color-mix(in srgb,var(--accent) 60%,transparent)}
.skill-galaxy-chip-label{white-space:nowrap}

/* Related work: compact scrollable list, no imagery. */
.skill-galaxy-related-kicker{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.skill-galaxy-related-kicker small{color:var(--text-faint);font:inherit;letter-spacing:inherit}
.skill-galaxy-related-list{display:block;max-height:var(--sg-related-max,260px);margin-top:6px;
  overflow-y:auto;overscroll-behavior:contain}
.skill-galaxy-related-item{position:relative;display:grid;gap:6px;width:100%;
  padding:14px 26px 14px 0;border:0;border-top:1px solid var(--sg-line);
  background:transparent;text-align:left;cursor:pointer}
.skill-galaxy-related-item:first-child{padding-top:4px;border-top:0}
.skill-galaxy-related-type{color:var(--text-faint);font:600 9px/1.3 var(--portfolio-utility);
  letter-spacing:.08em;text-transform:uppercase}
.skill-galaxy-related-title{color:var(--text);font:600 14px/1.35 var(--portfolio-body)}
.skill-galaxy-related-item .skill-galaxy-related-tags{display:flex;flex-wrap:wrap;gap:6px}
.skill-galaxy-related-item .skill-galaxy-related-tags i{padding:3px 8px;border:1px solid var(--sg-line);
  border-radius:6px;background:color-mix(in srgb,var(--bg-card) 50%,transparent);
  color:var(--text-faint);font:400 10px/1.2 var(--portfolio-body);font-style:normal}
.skill-galaxy-related-item .skill-galaxy-related-description{display:-webkit-box;overflow:hidden;
  -webkit-box-orient:vertical;-webkit-line-clamp:2;
  color:var(--text-faint);font:400 12px/1.42 var(--portfolio-body)}
.skill-galaxy-related-item .skill-galaxy-related-arrow{position:absolute;top:14px;right:0;
  color:var(--accent);font-size:14px;line-height:1}
.skill-galaxy-related-item:first-child .skill-galaxy-related-arrow{top:4px}
.skill-galaxy-related-item:hover .skill-galaxy-related-title,
.skill-galaxy-related-item:focus-visible .skill-galaxy-related-title{color:var(--accent)}
.skill-galaxy-related-empty{margin-top:10px;color:var(--text-faint);font:400 12px/1.5 var(--portfolio-body)}
/* In-progress Related Work: informational, not a disabled control. It is a
   plain `<div>` (see `relatedListMarkup`/`renderRelatedWork` in
   skill-galaxy.js), so it can never receive focus and the rules above never
   apply — but a mouse can still hover it, and `:hover` matches by class,
   tag notwithstanding, so the two mouse-only effects below need their own
   cancellation: the title accent-on-hover just above, and the corner-marks
   fade `.more-case:hover .case-corner-marks` draws on Playrix's own homepage
   card carries the identical trap, fixed the identical way there. */
.skill-galaxy-related-item.is-in-progress,
.skill-galaxy-related-card.is-in-progress{cursor:default}
.skill-galaxy-related-item.is-in-progress .skill-galaxy-related-title,
.skill-galaxy-related-card.is-in-progress .skill-galaxy-related-copy strong{color:var(--text-faint)}
.skill-galaxy-related-item.is-in-progress:hover .skill-galaxy-related-title{color:var(--text-faint)}

/* One hint line, one icon — the old block wrapped into a broken two-column shape. */
.skill-galaxy-instruction{align-items:center;gap:10px}
.skill-galaxy-instruction span:last-child{white-space:normal}

/* View label sits beside the switch, as in the frames. */
.skill-galaxy-view-block{display:flex;align-items:center;gap:14px}
.skill-galaxy-view-label{order:2;margin:0}

/* List view: never let the floating toggle sit on top of the last row. */
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{padding-bottom:104px}
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{right:calc(var(--sg-panel-width) + 40px)}
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-controls{
  background:linear-gradient(to top,var(--bg) 58%,transparent);padding:18px 0 0}

/* Light theme: the wireframe, the faint nodes and the links all need more ink
   on paper than they do on near-black. This rule's own specificity — a type
   selector plus an attribute plus a class, (0,2,1) — accidentally outranks
   the plain two-class `.is-connected`/`.is-selected` rules above (0,2,0):
   equal on id and class count, so the tie breaks on this rule's extra `html`
   type selector. Desktop dodges it by accident, not by design — its own
   `.is-connected`/`.is-selected` overrides carry `#about.skill-galaxy-section`
   ahead of them, an ID that outranks everything here regardless. Nothing
   below 961px has an ID-qualified equivalent, so in light theme every
   connected or selected node label on mobile silently lost its accent tint
   to this rule and read as a plain default label. Excluding both states by
   name is cheaper than re-fighting specificity with a new override, and it
   leaves plain labels' own text-dim tint untouched. */
html[data-theme="light"] .skill-galaxy-node-label:not(.is-connected):not(.is-selected){color:var(--text-dim)}
html[data-theme="light"] .skill-galaxy-chip{background:color-mix(in srgb,#fff 78%,transparent)}
html[data-theme="light"] .skill-galaxy-related-item .skill-galaxy-related-tags i{
  background:color-mix(in srgb,#fff 70%,transparent)}

@media(max-width:960px){
  body[data-page="home"] #about.skill-galaxy-section{--sg-panel-width:auto}
  .skill-galaxy-network{inset:auto}
  .skill-galaxy-related-list{--sg-related-max:none;max-height:none}
  .skill-galaxy-panel .skill-galaxy-related-list{--sg-related-max:none}
  .skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{right:0;padding-bottom:96px}
}

/* Specificity corrections: the earlier connected-list rules zero out the chip
   frame, and the related list must absorb the panel's leftover height so it
   scrolls inside the panel instead of overflowing it. */
.skill-galaxy-connected-list button.skill-galaxy-chip{
  display:inline-flex;grid-template-columns:none;align-items:center;gap:7px;
  min-height:28px;padding:6px 10px;border:1px solid var(--sg-line);border-radius:8px;
  background:color-mix(in srgb,var(--bg-card) 55%,transparent);
  color:var(--text-dim);font:400 11.5px/1.25 var(--portfolio-body)}
.skill-galaxy-connected-list button.skill-galaxy-chip:hover,
.skill-galaxy-connected-list button.skill-galaxy-chip:focus-visible{
  border-color:var(--accent);color:var(--text)}
.skill-galaxy-connected-list button.skill-galaxy-chip>.skill-galaxy-chip-dot{
  width:6px;height:6px;margin:0;flex:0 0 6px;border-radius:50%;background:var(--accent);
  box-shadow:0 0 5px color-mix(in srgb,var(--accent) 60%,transparent)}

.skill-galaxy-panel-inner{min-height:0}
.skill-galaxy-panel .skill-galaxy-related-list{flex:1 1 auto;min-height:120px;max-height:none}
.skill-galaxy-panel-inner>.skill-galaxy-related-kicker{flex:0 0 auto}

/* List view: the content column ends just before the detail panel instead of
   leaving a dead gap between them. */
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{
  inset:170px calc(var(--sg-panel-width) + 44px) 70px 24px}
.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{right:0}

/* ══════════════════════════════════════════════════════════════════════════
   Figma parity · desktop — frames 384:7 / 384:263 (light) and 360:10 / 360:11
   (dark). Geometry is expressed in the frame's own coordinates: a 1080 content
   column inside a full-bleed 900px band, so every offset below is the Figma
   value minus the 180px frame margin.
   ══════════════════════════════════════════════════════════════════════════ */
@media(min-width:961px){
  body[data-page="home"] #about.skill-galaxy-section{
    --sg-panel-width:344px;
    --sg-card:color-mix(in srgb,var(--card) 62%,transparent);
    --sg-card-border:color-mix(in srgb,var(--line) 56%,transparent);
    --sg-card-shadow:0 4px 12.3px rgba(0,0,0,.23);
    --sg-line:var(--line);
    /* One gutter for every inner frame. The block is 1080 wide like More Case
       Studies; 32px in from it is the 1016 column Recent Positions, Education
       and the artwork showcase all sit on, so the section lines up with the
       blocks above and below it rather than with its own edges. */
    --sg-gutter:32px;
    padding:0;
    /* The block carries its own outline now, so the page-wide hairlines and the
       page-wide dot field would read as a second, competing frame. Both move
       inside the block. */
    border-top:0;
    border-bottom:0;
    background:none;
    background-color:var(--bg);
  }
  html[data-theme="dark"] body[data-page="home"] #about.skill-galaxy-section{
    --sg-card-shadow:0 4px 18px rgba(0,0,0,.5);
  }
  body[data-page="home"] #about.skill-galaxy-section>.wrap{
    width:min(1080px,100% - 64px);max-width:1080px;padding:0}

  /* The band itself: no card, no vignette — the dot field runs edge to edge and
     only the sphere bloom sits inside the column.

     The bloom itself is no longer painted here. It was a fixed
     `circle 400px at 43.5% 49.5%` of this box, which only ever lined up with
     the sphere at one zoom level and one panel state; it now lives on
     `.skill-galaxy-stage`, sized and positioned from the renderer's own
     camera — see the bloom rule at the top of this file. What stays is the
     dot field and the band's own ground colour. */
  #about.skill-galaxy-section .skill-galaxy-shell{
    height:900px;min-height:0;
    border:0;border-radius:0;box-shadow:none;
    /* The frame clips its contents, as the mockup does. The stage stays wider
       than the block on purpose — see the stage rule — so without this the
       sphere would paint straight over the outline. */
    overflow:hidden;
    background:radial-gradient(color-mix(in srgb,var(--text) 14%,transparent) .65px,transparent .8px);
    background-size:22px 22px;
    background-color:var(--bg);
  }
  /* The outline is painted, not laid out. A real border on the shell would move
     its padding box in by 1px and every absolutely-positioned frame inside with
     it, putting the 1016 column at 213 instead of 212 and breaking the very
     alignment this change is for. An overlay sits exactly on the 1080 box and
     costs the children nothing. */
  #about.skill-galaxy-section .skill-galaxy-shell::before{
    display:block;z-index:7;
    border:1px solid var(--theme-border-default);
    background:none}

  /* Header — the site's standard section label: ink "+", accent words, rule. */
  #about.skill-galaxy-section .skill-galaxy-tabs{
    top:64px;left:32px;right:32px;
    gap:11px;
    color:var(--portfolio-orange-text);
    font:600 19.44px/23.33px var(--portfolio-utility);
    letter-spacing:0;
  }
  #about.skill-galaxy-section .skill-galaxy-tabs span{gap:10px;min-height:0;color:inherit}
  #about.skill-galaxy-section .skill-galaxy-tabs i{font:inherit;color:inherit}
  #about.skill-galaxy-section .skill-galaxy-tabs span:first-child i{color:var(--text)}
  #about.skill-galaxy-section .skill-galaxy-tabs .is-active::after{display:none}
  #about.skill-galaxy-section .skill-galaxy-tabs::after{
    content:'';flex:1;height:1px;margin-left:27px;background:var(--line)}

  /* Sphere stage · centred on the frame's sphere centre (470/446 of the column). */
  #about.skill-galaxy-section .skill-galaxy-network{inset:0 var(--sg-gutter)}
  #about.skill-galaxy-section .skill-galaxy-stage{
    left:0;top:21px;right:auto;bottom:auto;width:min(940px,100%);height:850px}

  #about.skill-galaxy-section .skill-galaxy-node-label{
    max-width:150px;min-height:16px;padding:2px 4px 2px 0;
    color:color-mix(in srgb,var(--text-dim) 65%,transparent);
    font:400 11.5px/16px var(--portfolio-body);opacity:1}
  #about.skill-galaxy-section .skill-galaxy-node-label.is-connected{
    color:var(--accent);font:400 12px/16px var(--portfolio-body);text-shadow:none}
  #about.skill-galaxy-section .skill-galaxy-node-label.is-selected{
    color:var(--portfolio-orange-text);font:600 13px/18px var(--portfolio-body);text-shadow:none}
  #about.skill-galaxy-section .skill-galaxy-node-label[data-sg-label="ui-ux-design"].is-selected{
    color:var(--text);font:600 15px/20px var(--portfolio-body);text-shadow:none}
  #about.skill-galaxy-section .skill-galaxy-node-label.is-muted{opacity:.72}

  /* About plaque · sphere view (300×234 at 0/125). Frame 432:3690 gives it
     0 4px 6.15px — half the panel's blur, since it is the smaller card and
     reads as sitting closer to the surface. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-about{
    box-shadow:0 4px 6.15px rgba(0,0,0,.23)}
  html[data-theme="dark"] #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-about{
    box-shadow:0 4px 9px rgba(0,0,0,.5)}
  #about.skill-galaxy-section .skill-galaxy-about{
    left:var(--sg-gutter);top:125px;right:auto;width:300px;
    padding:19px;border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:var(--sg-card-shadow);
    backdrop-filter:blur(1.3px)}
  #about.skill-galaxy-section .skill-galaxy-about p{
    max-width:260px;color:var(--text-dim);font:400 14px/21px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-instruction{
    align-items:flex-start;gap:12px;
    margin-top:18px;padding:14px 0 0;border-top:1px solid var(--line);
    color:var(--text-faint);font:400 12px/17px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-instruction-icon{width:28px;height:28px;flex:0 0 28px}
  #about.skill-galaxy-section .skill-galaxy-about-meta{display:none}

  /* View toggle · the EN/RU segmented control, 102×46 at 0/822. */
  #about.skill-galaxy-section .skill-galaxy-controls{
    left:var(--sg-gutter);right:auto;top:822px;bottom:auto;align-items:center}
  /* Frame 459:5486 puts the toggle itself at y=822, 46 tall, bottom flush with
     the panel at 868 — 32 from the block edge, the same gutter as the sides.
     The shared list-view rule adds `padding:18px 0 0` and a scrim so a
     page-length list cannot run under the toggle; on desktop the list is a
     bounded card, so the scrim has nothing to fade and the padding was pushing
     the toggle 18px past its mark. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-controls{
    padding:0;background:none}
  #about.skill-galaxy-section .skill-galaxy-view-block{gap:11px}
  #about.skill-galaxy-section .skill-galaxy-view-label{
    order:2;margin:0;color:var(--text-dim);
    font:400 11px/15px var(--portfolio-utility);letter-spacing:.05em}
  #about.skill-galaxy-section .skill-galaxy-view-label::after{display:none}
  #about.skill-galaxy-section .skill-galaxy-view-switch .skill-galaxy-icon{width:44px;height:44px}

  /* Detail panel · 344×743 at 737/125. */
  #about.skill-galaxy-section .skill-galaxy-panel{
    top:125px;right:var(--sg-gutter);left:auto;bottom:auto;
    width:344px;height:743px;max-height:none;
    border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:var(--sg-card-shadow);
    backdrop-filter:blur(1.3px)}
  #about.skill-galaxy-section .skill-galaxy-panel::before{
    content:'';position:absolute;z-index:3;top:0;left:16px;
    width:312px;height:1px;
    background:color-mix(in srgb,var(--portfolio-orange-text) 35%,transparent)}
  #about.skill-galaxy-section .skill-galaxy-panel-inner{
    display:flex;flex-direction:column;height:100%;min-height:0;max-height:none;
    padding:19px;overflow:hidden}
  #about.skill-galaxy-section .skill-galaxy-panel-close{
    top:15px;right:16px;width:32px;height:32px;
    display:grid;place-items:center;
    border:1px solid var(--line);border-radius:8px;
    background:color-mix(in srgb,var(--card) 60%,transparent);
    color:var(--text);font:300 17px/1 var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-panel-kicker{
    margin:0;color:var(--accent);
    font:600 10px/13px var(--portfolio-utility);letter-spacing:.55px}
  #about.skill-galaxy-section .skill-galaxy-panel-heading{
    gap:5px;margin:12px 0 0 -3px;padding-right:44px}
  #about.skill-galaxy-section .skill-galaxy-panel-cube{
    width:34px;height:34px;flex-basis:34px;filter:none}
  #about.skill-galaxy-section .skill-galaxy-panel-heading h3{
    color:var(--text);font:700 20px/26px var(--portfolio-display);letter-spacing:-.2px}
  #about.skill-galaxy-section .skill-galaxy-panel-description{
    margin-top:17px;color:var(--text-dim);font:400 13px/20.5px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-panel-divider{
    margin:20px 0 16px;background:var(--line)}
  #about.skill-galaxy-section .skill-galaxy-connected-list{gap:8px;margin-top:9px}
  #about.skill-galaxy-section .skill-galaxy-connected-list button.skill-galaxy-chip{
    min-height:28px;padding:6px 10px;gap:7px;
    border:1px solid var(--line);border-radius:8px;
    background:var(--card);
    color:var(--text-dim);font:400 11.5px/14px var(--portfolio-body)}
  /* Filled solid, not the 55% tint above — dark theme's own `--card` here is
     already the opaque navy card colour, but light theme's resolves to the
     warm paper tint (`--color-paper-light`), not true white, so it needs its
     own override to land on literal white. */
  html[data-theme="light"] #about.skill-galaxy-section .skill-galaxy-connected-list button.skill-galaxy-chip{
    background:#fff}
  #about.skill-galaxy-section .skill-galaxy-panel-inner>.skill-galaxy-panel-divider:nth-of-type(2){
    margin-top:18px}
  #about.skill-galaxy-section .skill-galaxy-related-kicker small{
    color:var(--text-faint);font:400 10px/13px var(--portfolio-utility);
    letter-spacing:.55px;text-transform:none}
  #about.skill-galaxy-section .skill-galaxy-related-list{
    margin-top:7px;padding-right:13px;
    scrollbar-color:var(--accent) color-mix(in srgb,var(--text) 7%,transparent)}
  #about.skill-galaxy-section .skill-galaxy-related-item{
    gap:6px;padding:14px 24px 14px 0;
    border-top:1px solid color-mix(in srgb,var(--line) 75%,transparent)}
  #about.skill-galaxy-section .skill-galaxy-related-item:first-child{padding-top:0;border-top:0}
  #about.skill-galaxy-section .skill-galaxy-related-type{
    color:var(--text-faint);font:600 9px/12px var(--portfolio-utility);letter-spacing:.5px}
  #about.skill-galaxy-section .skill-galaxy-related-title{
    color:var(--text);font:600 14px/19px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-tags{gap:6px}
  #about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-tags i{
    padding:3px 8px;border:1px solid color-mix(in srgb,var(--line) 80%,transparent);
    border-radius:6px;background:color-mix(in srgb,var(--card) 50%,transparent);
    color:var(--text-faint);font:400 10px/12px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-description{
    color:var(--text-faint);font:400 12px/17px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-arrow{
    top:0;right:0;color:var(--accent);font-size:16px}
  #about.skill-galaxy-section .skill-galaxy-related-item:first-child .skill-galaxy-related-arrow{top:0}

  /* ── List view ─────────────────────────────────────────────────────────── */
  /* About plaque widens to the full column and grows the counts cell. */
  /* Frame 459:5685 is 1016x97 at 32/116, which leaves exactly 20px above the
     two cards at y=233. That 97 is not reachable with this copy: the frame's
     text node is 700px wide inside its own 645px column — it overflows in
     Figma and so reads as two lines, while the real column wraps it to three
     (63px) in EN and RU alike. The counts cell measures 63 too, so neither
     column is the odd one out. Vertical padding is 17 rather than the frame's
     21, which lands the strip on 97 and restores the 20px gap; every frame
     position in the block still matches the frame exactly. Shortening the
     sentence to two lines would let the padding go back to 21 — that is a copy
     decision, not a layout one. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about{
    left:var(--sg-gutter);top:116px;right:var(--sg-gutter);width:auto;height:97px;
    display:flex;align-items:flex-start;gap:32px;padding:16px 23px}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about p{
    max-width:700px;flex:1 1 auto}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about-meta{
    display:block;align-self:stretch;padding-left:32px;
    border-left:1px solid var(--line)}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about-meta p{
    width:260px;max-width:260px;margin-top:6px;
    color:var(--text-faint);font:400 12px/17px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-about-count{
    display:block;color:var(--text-faint);
    font:600 11px/14.3px var(--portfolio-utility);letter-spacing:.605px;text-transform:uppercase}

  /* Skills list card · 707×568 at 0/233. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{inset:0 var(--sg-gutter)}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    left:0;top:233px;right:auto;bottom:auto;width:min(652px,calc(100% - 364px));height:568px;
    padding:22px;overflow:auto;
    border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:var(--sg-card-shadow);
    backdrop-filter:blur(1.3px);
    /* Frame 442:1602 draws the indicator at x=639 y=14 h=542 inside a 651x569
       card: 9px in from the right edge and inset 14px top and bottom, so it
       clears the 16px corners instead of running edge to edge. */
    --sg-list-bar-inset:14px;
    scrollbar-width:none;-ms-overflow-style:none}
  /* The card takes the sheet's drawn indicator, not a native bar. The platform
     scrollbar on these targets is an overlay: it reserves no width and paints
     nothing at rest, so styling it still left the card looking unscrollable
     until the pointer moved. `.skill-galaxy-list-scrollbar` is the same element
     the related-work list uses, placed and sized from the list's own scroll
     metrics by syncListScrollbar. Hide the native one so the two never both
     appear. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list::-webkit-scrollbar{
    display:none;width:0;height:0}
  #about.skill-galaxy-section .skill-galaxy-list-group{margin-bottom:18px}
  #about.skill-galaxy-section .skill-galaxy-list-group:last-child{margin-bottom:0}
  #about.skill-galaxy-section .skill-galaxy-list-group h3{
    margin:0 0 10px;color:var(--text-faint);
    font:600 10px/13px var(--portfolio-utility);letter-spacing:.55px}
  #about.skill-galaxy-section .skill-galaxy-list-group>div{
    border:0;border-radius:0;background:transparent;overflow:visible}
  #about.skill-galaxy-section .skill-galaxy-list-row{
    grid-template-columns:24px minmax(0,1fr) auto 16px;gap:12px;
    min-height:50px;padding:13px 14px;
    border:1px solid transparent;border-bottom-color:color-mix(in srgb,var(--line) 60%,transparent);
    border-radius:8px;background:transparent;box-shadow:none;color:var(--text)}
  #about.skill-galaxy-section .skill-galaxy-list-row:last-child{
    border-bottom-color:color-mix(in srgb,var(--line) 60%,transparent)}
  #about.skill-galaxy-section .skill-galaxy-list-row .skill-galaxy-cube{
    width:24px;height:24px;flex-basis:24px;filter:none}
  #about.skill-galaxy-section .skill-galaxy-list-name{font:400 14px/20px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-list-count{
    color:var(--text-faint);font:400 11px/15px var(--portfolio-utility)}
  #about.skill-galaxy-section .skill-galaxy-list-chevron{
    width:16px;color:var(--text-faint);font-size:14px;line-height:1}
  #about.skill-galaxy-section .skill-galaxy-list-row.is-selected{
    border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);
    background:color-mix(in srgb,var(--accent) 9%,transparent);
    box-shadow:none;color:var(--portfolio-orange-text)}
  #about.skill-galaxy-section .skill-galaxy-list-row.is-selected .skill-galaxy-list-name{
    font-weight:600;color:var(--portfolio-orange-text)}

  /* Panel sits under the plaque in list view. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-panel{
    top:233px;right:var(--sg-gutter);height:635px}
}

/* The cube marks are exported icons now, not the old CSS-drawn cube: drop the
   leftover border, skew and glow that the legacy .skill-galaxy-cube rules
   still paint underneath the image. */
.skill-galaxy-icon.skill-galaxy-cube,
.skill-galaxy-icon.skill-galaxy-panel-cube{
  border:0;background:transparent;transform:none;box-shadow:none;filter:none}
.skill-galaxy-icon.skill-galaxy-cube::before,
.skill-galaxy-icon.skill-galaxy-cube::after{display:none}

/* ── Shared across viewports ───────────────────────────────────────────────
   Card surfaces and the view toggle are one spec in every frame (A, B and C). */
body[data-page="home"] #about.skill-galaxy-section{
  --sg-card:color-mix(in srgb,var(--card) 62%,transparent);
  --sg-card-border:color-mix(in srgb,var(--line) 56%,transparent);
  --sg-card-shadow:0 4px 12.3px rgba(0,0,0,.23)}
html[data-theme="dark"] body[data-page="home"] #about.skill-galaxy-section{
  --sg-card-shadow:0 4px 18px rgba(0,0,0,.5);
  /* Near-black needs less ink than paper for the same read: the cage and the
     cage drops weight here, matching the light/dark asymmetry the rest of
     this file already applies to every other line on the band. `--text-faint`
     is the muted primitive the original per-theme JS branch already reached
     for on dark — the token just carries that same choice now instead of an
     `if`. A plain alias, not a `color-mix()`: see the light block's comment
     above for why. */
  --sg-shell-line:var(--text-faint)}

/* Desktop card surface, read off frames 459:5693 (Skills), 459:5534 (detail
   panel) and 459:5685 (about strip). All three carry one spec:
   rgba(248,246,241,.8) over a 1.3px backdrop blur, r16, and
   0 4px 12.3px rgba(0,0,0,.23). `--card` is #F8F6F1 on light, so 80% of it is
   the frame's fill exactly; on dark the same 80% lands on the band's own navy
   primitive, so the pair stays in step. Mobile keeps 62% until its own frames
   are worked through.

   This has to sit after the shared block above, not inside the desktop block
   further up the file: both carry the same specificity, so source order decides
   and the shared value would win. */
@media(min-width:961px){
  body[data-page="home"] #about.skill-galaxy-section{
    --sg-card:color-mix(in srgb,var(--card) 80%,transparent)}
}

/* View toggle · the same component as the EN/RU segmented control, only the
   colours change. Same grid, same 102×46 track, same 50×44 panes, and the
   active pane is square-cornered so the track's own radius clips it on one
   side instead of floating as a pill inside the frame. */
#about.skill-galaxy-section .skill-galaxy-view-switch{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  width:102px;height:46px;min-height:46px;overflow:hidden;
  border:1px solid var(--line);border-radius:10px;background:var(--color-bg-control)}
/* Each pane keeps the track's radius on its own outside corners and stays
   square where the two meet, so the active pane reads as a filled half of the
   control rather than a pill floating inside it. */
#about.skill-galaxy-section .skill-galaxy-view-switch button{
  width:100%;min-width:0;height:44px;min-height:44px;
  border:0;background:transparent;box-shadow:none}
#about.skill-galaxy-section .skill-galaxy-view-switch button[data-sg-view="sphere"]{
  border-radius:9px 0 0 9px}
#about.skill-galaxy-section .skill-galaxy-view-switch button[data-sg-view="list"]{
  border-radius:0 9px 9px 0}
#about.skill-galaxy-section .skill-galaxy-view-switch button + button{border-left:0}
#about.skill-galaxy-section .skill-galaxy-view-switch button.is-active{background:#d8d6cf;box-shadow:none}
#about.skill-galaxy-section .skill-galaxy-view-switch button:not(.is-active) .skill-galaxy-icon{opacity:.55}
html[data-theme="dark"] #about.skill-galaxy-section .skill-galaxy-view-switch{background:#11110f}
html[data-theme="dark"] #about.skill-galaxy-section .skill-galaxy-view-switch button.is-active{
  background:#101d53}
#about.skill-galaxy-section .skill-galaxy-view-switch .skill-galaxy-icon{
  width:44px;height:44px;transition:opacity .16s ease}
#about.skill-galaxy-section .skill-galaxy-view-label{
  order:2;margin:0;color:var(--text-dim);
  font:400 11px/15px var(--portfolio-utility);letter-spacing:.05em;text-transform:uppercase}
#about.skill-galaxy-section .skill-galaxy-view-label::after{display:none}

@media(max-width:960px){
  #about.skill-galaxy-section .skill-galaxy-controls{align-items:center}

  #about.skill-galaxy-section .skill-galaxy-view-block{display:flex;align-items:center;gap:11px}

  /* Mobile list view · the frame leads with the about plaque, not the heading,
     and the rows are plain hairline rows inside one scroll column. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-intro{display:none}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about{
    display:block;position:relative;z-index:5;inset:auto;width:auto;
    margin:16px clamp(20px,4vw,32px) 2px;padding:16px;
    border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:var(--sg-card-shadow);backdrop-filter:blur(1.3px)}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about p{
    max-width:none;color:var(--text-dim);font:400 14px/21px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-about-meta{display:none}

  #about.skill-galaxy-section .skill-galaxy-list-group{margin-bottom:16px}
  #about.skill-galaxy-section .skill-galaxy-list-group h3{
    margin:0 0 8px 4px;color:var(--text-faint);
    font:600 10px/13px var(--portfolio-utility);letter-spacing:.55px}
  #about.skill-galaxy-section .skill-galaxy-list-group>div{
    border:0;border-radius:0;background:transparent;overflow:visible}
  #about.skill-galaxy-section .skill-galaxy-list-row{
    grid-template-columns:24px minmax(0,1fr) auto 16px;gap:12px;
    min-height:50px;padding:13px 10px;
    border:1px solid transparent;border-bottom-color:color-mix(in srgb,var(--line) 60%,transparent);
    border-radius:8px;background:transparent;box-shadow:none}
  #about.skill-galaxy-section .skill-galaxy-list-row .skill-galaxy-cube{
    width:24px;height:24px;flex-basis:24px}
  #about.skill-galaxy-section .skill-galaxy-list-name{font:400 14px/20px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-list-count{
    color:var(--text-faint);font:400 11px/15px var(--portfolio-utility)}
  #about.skill-galaxy-section .skill-galaxy-list-chevron{color:var(--text-faint);font-size:14px}
  #about.skill-galaxy-section .skill-galaxy-list-row.is-selected{
    border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);
    background:color-mix(in srgb,var(--accent) 9%,transparent);
    box-shadow:none;color:var(--portfolio-orange-text)}
  #about.skill-galaxy-section .skill-galaxy-list-row.is-selected .skill-galaxy-list-name{
    font-weight:600;color:var(--portfolio-orange-text)}
}

/* ══════════════════════════════════════════════════════════════════════════
   Interaction states and mobile-sheet parity.
   The detail panel is portaled to <body> on mobile, so every shared rule below
   is written for both homes: inside the section and on the loose panel.
   ══════════════════════════════════════════════════════════════════════════ */

/* Selection belongs to the palette, not to the browser. */
#about.skill-galaxy-section ::selection,
.skill-galaxy-panel ::selection{
  background:color-mix(in srgb,var(--accent) 26%,transparent);color:var(--text)}

/* Skill rows are buttons: they have to answer the pointer. */
#about.skill-galaxy-section .skill-galaxy-list-row{
  transition:background-color .16s ease,border-color .16s ease,color .16s ease}
#about.skill-galaxy-section .skill-galaxy-list-row:hover:not(.is-selected){
  background:color-mix(in srgb,var(--accent) 5%,transparent);
  border-color:color-mix(in srgb,var(--accent) 24%,transparent)}
#about.skill-galaxy-section .skill-galaxy-list-row:hover:not(.is-selected) .skill-galaxy-list-chevron,
#about.skill-galaxy-section .skill-galaxy-list-row:hover:not(.is-selected) .skill-galaxy-list-count{
  color:var(--accent)}

/* Node labels are the sphere's real hit targets. */
#about.skill-galaxy-section .skill-galaxy-node-label{cursor:pointer}
#about.skill-galaxy-section .skill-galaxy-node-label:hover{
  color:var(--portfolio-orange-text);opacity:1}

/* Segmented control: the ring must not be clipped, the idle pane must respond. */
#about.skill-galaxy-section .skill-galaxy-view-switch,
.skill-galaxy-panel .skill-galaxy-view-switch{overflow:visible}
#about.skill-galaxy-section .skill-galaxy-view-switch button,
.skill-galaxy-panel .skill-galaxy-view-switch button{transition:background-color .16s ease}
#about.skill-galaxy-section .skill-galaxy-view-switch button:hover:not(.is-active),
.skill-galaxy-panel .skill-galaxy-view-switch button:hover:not(.is-active){
  background:color-mix(in srgb,var(--text) 6%,transparent)}

/* Close and expand: the only controls that leave or resize the panel. */
#about.skill-galaxy-section .skill-galaxy-panel-close,
.skill-galaxy-panel .skill-galaxy-panel-close,
#about.skill-galaxy-section .skill-galaxy-panel-expand,
.skill-galaxy-panel .skill-galaxy-panel-expand{
  transition:background-color .16s ease,border-color .16s ease,color .16s ease}
#about.skill-galaxy-section .skill-galaxy-panel-close:hover,
.skill-galaxy-panel .skill-galaxy-panel-close:hover,
#about.skill-galaxy-section .skill-galaxy-panel-expand:hover,
.skill-galaxy-panel .skill-galaxy-panel-expand:hover{
  border-color:color-mix(in srgb,var(--accent) 55%,transparent);
  background:color-mix(in srgb,var(--accent) 8%,transparent);
  color:var(--portfolio-orange-text)}

/* Related work: the open marker sits on its item's first line, not on the rule
   above it. */
#about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item .skill-galaxy-related-arrow{top:14px}
#about.skill-galaxy-section .skill-galaxy-related-item:first-child .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item:first-child .skill-galaxy-related-arrow{top:0}
#about.skill-galaxy-section .skill-galaxy-related-item:hover .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item:hover .skill-galaxy-related-arrow{
  transform:translate(1px,-1px)}
#about.skill-galaxy-section .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-arrow{transition:transform .16s ease}

/* Coarse pointers get a 44px target without changing a single visible edge. */
@media(pointer:coarse){
  #about.skill-galaxy-section .skill-galaxy-chip,
  .skill-galaxy-panel .skill-galaxy-chip{position:relative}
  #about.skill-galaxy-section .skill-galaxy-chip::after,
  .skill-galaxy-panel .skill-galaxy-chip::after{
    content:'';position:absolute;inset:-8px;border-radius:12px}
}

/* Mobile sheet · the sheet never repeats the section's view toggle; the only
   view control on the page is the one under the sphere. */
@media(max-width:960px){
  /* The sheet leads with the skill, not with a label above it. The grabber owns
     the top strip, so the heading starts below it instead of under it. */
  .skill-galaxy-panel .skill-galaxy-panel-kicker:first-child{display:none}
  .skill-galaxy-panel .skill-galaxy-panel-heading{margin-top:0}

  /* Resize control on the left, dismiss on the right, as drawn. */
  .skill-galaxy-panel .skill-galaxy-panel-expand{
    left:16px;right:auto;display:grid;place-items:center;
    width:40px;height:40px;border-radius:50%;font-size:18px}
  .skill-galaxy-panel .skill-galaxy-panel-close{width:40px;height:40px;border-radius:50%}
  .skill-galaxy-panel .skill-galaxy-panel-expand span{
    display:block;transition:transform .2s ease}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand span{transform:rotate(180deg)}
  .skill-galaxy-panel .skill-galaxy-panel-heading{padding-right:56px;padding-left:0}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-heading{padding-left:56px}
}

/* One authored transition: swapping views moves a whole surface, so it fades
   in rather than snapping. Everything else on this section already moves. */
@media(prefers-reduced-motion:no-preference){
  #about.skill-galaxy-section .skill-galaxy-list,
  #about.skill-galaxy-section .skill-galaxy-stage{
    animation:skill-galaxy-view-in .22s cubic-bezier(.22,.8,.3,1) both}
}
@keyframes skill-galaxy-view-in{
  from{opacity:0;transform:translateY(4px)}
  to{opacity:1;transform:none}
}

/* The grabber is the sheet's resize affordance, as drawn; the round chevron
   appears only in the expanded state, where the frame places it. */
@media(max-width:960px){
  .skill-galaxy-panel .skill-galaxy-panel-expand{display:none}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand{display:grid}
  .skill-galaxy-panel .skill-galaxy-sheet-handle{
    width:64px;height:26px;cursor:pointer;border-radius:8px}
  .skill-galaxy-panel .skill-galaxy-sheet-handle:hover::after{opacity:.8}

  /* A sheet, not a floating card. It slides up out of the bottom edge, so it
     spans the whole screen width and finishes flush against it: no side gaps,
     no lower corners, and no border on the three edges that are the screen.
     The 7px inset it used to keep drew an outline all the way round and left a
     strip of the page visible below its lower edge, which reads as a card
     lying on the page rather than a surface coming out of it. The top hairline
     stays — that edge is real. */
  .skill-galaxy-panel{
    right:0;bottom:0;left:0;
    border-inline:0;border-bottom:0;
    border-radius:28px 28px 0 0}
  .skill-galaxy-panel.is-expanded{bottom:0}
}

/* The portaled sheet keeps the panel's own typography and scrollbar. */
.skill-galaxy-panel .skill-galaxy-related-kicker small{
  color:var(--text-faint);font:400 10px/13px var(--portfolio-utility);
  letter-spacing:.55px;text-transform:none}

/* Node labels are the section's content, not chrome: the frame's 65% ink drops
   idle labels to ~3.1:1 on paper, so the base tint carries 4.5:1 and the depth
   ramp in the renderer still fades what sits behind the sphere.
   Excluding `.is-connected`/`.is-selected` matters everywhere below 961px:
   this rule's own `#about.skill-galaxy-section` ID outranks the plain
   two-class `.is-connected`/`.is-selected` rules above regardless of theme,
   and desktop is the only breakpoint that ever re-wins the fight back with
   its own ID-qualified override (`@media(min-width:961px)`, further down).
   Without the exclusion here, every connected or selected label on mobile —
   in *either* theme — silently lost its accent tint to this idle-contrast
   rule and read as a plain default label. */
#about.skill-galaxy-section .skill-galaxy-node-label:not(.is-connected):not(.is-selected){
  color:color-mix(in srgb,var(--text-dim) 82%,transparent)}
/* Light-only optical balance: idle names are fully opaque and move 20% from
   the muted text primitive toward primary text. Both inputs remain semantic
   theme tokens. Connected and selected states keep their semantic accent
   colours; the dark theme keeps the shared 82% rule. */
html[data-theme="light"] body[data-page="home"] #about.skill-galaxy-section
  .skill-galaxy-node-label:not(.is-connected):not(.is-selected){
  color:color-mix(in srgb,var(--text-dim) 80%,var(--text) 20%);
  opacity:1!important}

/* The sphere is the section's subject: its stage runs the full width of the
   viewport so a zoomed-in sphere is never cut off at the content column, while
   the plaque, panel and toggle stay on the 1080 grid. */
@media(min-width:961px){
  /* Positioned with a margin, not a transform: the view-switch keyframe owns
     this element's transform and would reset it. */
  #about.skill-galaxy-section .skill-galaxy-stage{
    left:50%;right:auto;top:21px;bottom:auto;
    width:100vw;margin-left:-50vw;height:850px}
}

/* ══════════════════════════════════════════════════════════════════════════
   Round 3 · frames 367:4 (mobile) and 369:38 (node hierarchy).
   ══════════════════════════════════════════════════════════════════════════ */

/* Transition markers are the site's arrow: same glyph, stroke and nudge as the
   Positions and Education rows. */
#about.skill-galaxy-section .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-arrow{
  display:grid;place-items:center;width:24px;height:24px;
  color:var(--color-action-primary);font-size:0;
  transition:transform 210ms cubic-bezier(.2,.75,.2,1),color 210ms ease}
#about.skill-galaxy-section .skill-galaxy-related-arrow svg,
.skill-galaxy-panel .skill-galaxy-related-arrow svg{
  width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
#about.skill-galaxy-section .skill-galaxy-related-item:hover .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item:hover .skill-galaxy-related-arrow,
#about.skill-galaxy-section .skill-galaxy-related-item:focus-visible .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item:focus-visible .skill-galaxy-related-arrow{
  transform:translate3d(3px,-3px,0)}
#about.skill-galaxy-section .skill-galaxy-related-item,
.skill-galaxy-panel .skill-galaxy-related-item{padding-right:38px}
#about.skill-galaxy-section .skill-galaxy-related-item .skill-galaxy-related-arrow,
.skill-galaxy-panel .skill-galaxy-related-item .skill-galaxy-related-arrow{right:2px}

/* Row chevrons come from the same drawn family. */
#about.skill-galaxy-section .skill-galaxy-list-chevron{
  display:grid;place-items:center;width:16px;height:16px;font-size:0}
#about.skill-galaxy-section .skill-galaxy-list-chevron svg{
  width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}

/* Close and collapse speak the site's control language: rounded square, border,
   accent on hover. Inside this band they carry no fill of their own — the
   frame draws them as an outline on the panel surface, and the site's control
   grey would read as a third surface against the navy glass. */
#about.skill-galaxy-section .skill-galaxy-panel-close,
.skill-galaxy-panel .skill-galaxy-panel-close,
#about.skill-galaxy-section .skill-galaxy-panel-expand,
.skill-galaxy-panel .skill-galaxy-panel-expand{
  display:grid;place-items:center;border-radius:10px;
  border:1px solid var(--line);background:transparent;
  color:var(--text-dim);font:400 16px/1 var(--portfolio-body)}
#about.skill-galaxy-section .skill-galaxy-panel-close:hover,
.skill-galaxy-panel .skill-galaxy-panel-close:hover,
#about.skill-galaxy-section .skill-galaxy-panel-expand:hover,
.skill-galaxy-panel .skill-galaxy-panel-expand:hover{
  border-color:var(--accent);background:var(--accent);color:var(--color-white)}
.skill-galaxy-panel .skill-galaxy-panel-expand svg{
  width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round;display:block}
.skill-galaxy-panel .skill-galaxy-panel-expand span{display:grid;place-items:center}

/* Desktop keeps the frame's 32px control; mobile matches the 46px modal close. */
@media(min-width:961px){
  #about.skill-galaxy-section .skill-galaxy-panel-close{
    width:32px;height:32px;border-radius:8px;font-size:14px}
  /* Sphere reads from divider to divider of the band. */
  #about.skill-galaxy-section .skill-galaxy-stage{top:0;height:900px}
}
@media(max-width:960px){
  .skill-galaxy-panel .skill-galaxy-panel-close,
  .skill-galaxy-panel .skill-galaxy-panel-expand{width:46px;height:46px;font-size:18px}
  .skill-galaxy-panel .skill-galaxy-panel-close{right:14px;top:14px}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand{left:14px;top:14px}
}

/* ── Mobile · frame 367:4 ──────────────────────────────────────────────────
   Section label like every other block, the about copy instead of an overline
   and headline, then the hints and a full-width sphere. */
@media(max-width:960px){
  /* Card surfaces · frames 442:190 and 442:355 draw every card in this band at
     full strength — #F8F6F1 on #F2F0EA with a #D2D1CB hairline, which is
     `--line` at 72%. The shared 62% is a desktop-era value and washes out on a
     phone, where the band's dot field reads straight through the card.

     This has to sit after the shared block near line 486: same specificity, so
     source order decides, and the shared value would otherwise win. */
  body[data-page="home"] #about.skill-galaxy-section{
    --sg-card:var(--card);
    --sg-card-border:color-mix(in srgb,var(--line) 72%,transparent)}

  #about.skill-galaxy-section .skill-galaxy-shell{
    display:flex;flex-direction:column;padding-bottom:8px}

  /* Heading · frame 442:696. ABOUT is the live control, so it carries the
     accent, its underline and the disclosure chevron; SKILLS names the view you
     are already in and stays ink. Both `+` glyphs are ink. 14px between the two
     blocks. */
  #about.skill-galaxy-section .skill-galaxy-tabs{
    order:1;position:relative;top:auto;left:auto;
    justify-content:flex-start;gap:14px;
    margin:0 clamp(20px,4vw,32px);padding:22px 0 0;border-bottom:0;
    color:var(--text);
    font:600 16px/1.2 var(--portfolio-utility);letter-spacing:0;text-transform:uppercase}
  #about.skill-galaxy-section .skill-galaxy-tabs span{gap:8px;min-height:0;color:inherit}
  #about.skill-galaxy-section .skill-galaxy-tabs i{font:inherit;color:var(--text)}
  #about.skill-galaxy-section .skill-galaxy-tabs .is-active::after{display:none}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about{cursor:pointer}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about b{
    color:var(--accent);
    text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-skills b{color:var(--text)}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about:focus-visible{
    outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-arrow{
    display:grid;width:12px;height:12px;place-items:center;color:var(--accent)}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-arrow svg{
    display:block;width:12px;height:12px;
    fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
  #about.skill-galaxy-section .skill-galaxy-tabs::after{
    content:'';flex:1;height:1px;margin-left:14px;background:var(--line)}

  /* About is a disclosure, not a fixture · 442:190 and 442:355 draw the band
     without it, 433:8042 and 433:9420 with it. The ABOUT half of the heading
     opens it and its own close button dismisses it; the state survives a view
     switch, because both open frames exist in sphere and in list. */
  #about.skill-galaxy-section .skill-galaxy-about,
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about{display:none}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about,
  #about.skill-galaxy-section[data-sg-about="open"][data-sg-view="list"] .skill-galaxy-about{
    order:2;display:block;position:relative;z-index:5;inset:auto;width:auto;
    margin:16px clamp(20px,4vw,32px) 0;
    /* The close button owns the top right corner, so the copy stops clear of it
       rather than running underneath. */
    padding:16px 54px 16px 16px;
    border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:var(--sg-card-shadow);backdrop-filter:blur(1.3px)}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about-close{
    display:grid;position:absolute;z-index:2;top:11px;right:11px;
    width:35px;height:36px;place-items:center;
    border:1px solid var(--line);border-radius:10px;
    color:var(--text-dim);background:color-mix(in srgb,var(--bg-elev) 60%,transparent);
    font:400 12px/1 var(--portfolio-body);cursor:pointer}
  #about.skill-galaxy-section .skill-galaxy-about p{
    max-width:none;color:var(--text-dim);font:400 14px/21px var(--portfolio-body)}
  #about.skill-galaxy-section .skill-galaxy-about-full{display:none}
  #about.skill-galaxy-section .skill-galaxy-about-short{display:block}
  #about.skill-galaxy-section .skill-galaxy-instruction{display:none}

  /* The headline pair is gone; the hint cards keep their slot. */
  #about.skill-galaxy-section .skill-galaxy-intro{
    order:3;padding:0 clamp(20px,4vw,32px)}
  #about.skill-galaxy-section .skill-galaxy-overline,
  body[data-page="home"] #about.skill-galaxy-section .skill-galaxy-intro h2{display:none}
  #about.skill-galaxy-section .skill-galaxy-mobile-hints{margin-top:12px}
  /* The hint cards are the same surface as every other card in the band, and
     the frame fills them solid: at 62% the dot field showed through and the two
     cards read as holes rather than as objects. */
  #about.skill-galaxy-section .skill-galaxy-mobile-hint{
    border-color:var(--sg-card-border);background:var(--bg-elev)}
  #about.skill-galaxy-section .skill-galaxy-network{order:4}
  #about.skill-galaxy-section .skill-galaxy-controls{order:5;position:relative;
    inset:auto;margin:10px clamp(20px,4vw,32px) 6px}

  #about.skill-galaxy-section .skill-galaxy-network{
    position:relative;inset:auto;height:min(66svh,520px);margin-top:14px}
  #about.skill-galaxy-section .skill-galaxy-stage{
    position:absolute;inset:0;left:0;width:100%;height:100%;margin:0}

  /* The sphere gets a drawn region for the same reason the list does. On a
     phone the stage takes the drag in every direction, so page scroll has to
     happen outside it — and a boundary the user cannot see is a boundary they
     cannot aim for. Outline only, no fill: the band's bloom is measured
     against the frame and a surface between it and the canvas would flatten
     the sphere back into a wireframe cage. Desktop keeps the full-bleed stage
     and its horizontal-only drag. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-network{
    margin:14px clamp(20px,4vw,32px) 0;overflow:hidden;
    border:1px solid var(--sg-card-border);border-radius:16px}
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-stage{
    border-radius:inherit;
    /* The stage itself keeps the page's vertical swipe. Only the marked region
       below takes it away — see `.skill-galaxy-interaction-area`. */
    touch-action:pan-y}

  /* Interaction Area · frames 442:190 and 433:8042 draw it explicitly, and they
     are right to. The stage rotates on a drag in every direction, so page
     scroll has to survive somewhere inside the band: the frame reserves the
     right 61px of the card for it. A boundary the reader cannot see is one they
     can only find by failing, so it is drawn — 1px dashed on a 2/2 rhythm,
     rounded on the left where it follows the card and square on the right where
     it is a divider, with the label lying along that edge on a chip of band
     colour that breaks the dashes cleanly around it.

     The frame draws all of this in annotation red. On the page it takes the
     band's own hairline instead: dashed at 2/2 it carries about half the ink of
     the card's solid edge, so it reads as a subdivision of the card rather than
     as a second frame competing with it.

     `touch-action` is resolved from the element under the finger, so scoping it
     to this box — not to the stage — is what makes the drawn boundary true. It
     sits above the canvas and below the labels, so it never intercepts a tap on
     a node, and a drag on it bubbles to the stage's own pointer handlers. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area{
    display:block;position:absolute;inset:0 61px 0 0;
    border:1px dashed var(--line);border-radius:15px 0 0 15px;
    touch-action:none}
  /* Nothing here is lifted: the box and its label both stay at `z-index:auto`,
     so canvas, boundary, label and node labels paint in DOM order and a skill
     name always wins the overlap. Raising the label above them did read better
     where the two met — until a node landed on the boundary and this chip's
     opaque backing cropped the skill's name in half. The marker is chrome and
     the skill names are the content; the content takes the top. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    position:absolute;top:50%;left:calc(100% - 10px);
    padding:0 3px;
    /* No backing box — the dashed boundary runs behind the label exactly as
       it runs everywhere else along its own edge, rather than being masked
       out under a rectangle of page colour. Stepped 10px in from the edge,
       matching the sheet-open and landscape variants below, so the dashes
       run beside the label instead of straight through its letters — flush
       on `left:100%` the line bisects the text and neither reads. */
    color:var(--text-faint);
    /* The frame sets this in Inter, not in the utility mono. Mono runs the
       label 60% longer, and every extra millimetre is a longer hole punched
       through the sphere's cage by the chip behind it, so it takes the body
       face at the frame's size instead. */
    font:500 7px/1 var(--portfolio-body);letter-spacing:.04em;white-space:nowrap;
    /* Decorative, and it sits over the labels now, so it must never take a tap
       meant for a node. */
    pointer-events:none;
    transform:translate(-50%,-50%) rotate(-90deg)}

  /* List view is a bounded scroll area, never an endless page column. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{
    height:auto;margin-top:14px}
  /* The bounded region is drawn, not implied. Nested scrolling is only
     legible when the inner viewport has an edge of its own, so the list takes
     the same card as the about plaque above it and keeps a 16px right gutter
     clear for its own indicator. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    position:relative;inset:auto;display:block;
    height:min(58svh,520px);margin:0 clamp(20px,4vw,32px);padding:14px 20px 14px 14px;
    overflow:auto;overscroll-behavior:contain;
    border:1px solid var(--sg-card-border);border-radius:16px;
    /* Frame 442:382 gives this card an edge and no shadow: it is set into the
       band, not lifted off it, and the only lifted surface here is the about
       plaque above. */
    background:var(--sg-card);box-shadow:none;
    /* Frames 442:355 and 433:9420 draw the indicator 3px wide, 13px in from the
       card's right edge, running from 13px below its top to 17px above its
       bottom. `syncListScrollbar` adds the card's own 1px border to each of
       these, so every token carries the frame value minus that border. */
    --sg-list-bar-inset:12px;
    --sg-list-bar-inset-end:16px;
    --sg-list-bar-gutter:12px}
  /* One bar, always the drawn one: the platform scrollbar here is an overlay
     that paints nothing at rest, and leaving it enabled would put a second,
     differently placed bar beside the indicator whenever it did appear. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    scrollbar-width:none;-ms-overflow-style:none}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list::-webkit-scrollbar{
    display:none;width:0;height:0}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-shell{min-height:0}

  /* Connected-skill chips · frame 433:8546 fills them at 55% behind a
     full-strength hairline. Drawn transparent they were a ghost on the sheet's
     warm surface, which is the one place in the band where a chip has to hold
     its own shape against a tint. Mobile only: the desktop chip is still an
     open question and the desktop frame disagrees with this one. */
  #about.skill-galaxy-section .skill-galaxy-connected-list button.skill-galaxy-chip,
  .skill-galaxy-panel .skill-galaxy-connected-list button.skill-galaxy-chip{
    border-color:var(--line);
    background:color-mix(in srgb,var(--bg-elev) 55%,transparent)}
}

/* The collapsed sheet shows only the dismiss control; the grabber resizes it. */
@media(max-width:960px){
  .skill-galaxy-panel .skill-galaxy-panel-expand{display:none}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand{display:grid}
}

/* The short about line is the mobile variant only; desktop keeps the full copy
   and shows no resize control on the docked panel. */
.skill-galaxy-about-short{display:none}
@media(min-width:961px){
  #about.skill-galaxy-section .skill-galaxy-panel-expand{display:none}
}

/* ── Mobile · expanded sheet · frame 410:7098 ──────────────────────────────
   The full-screen sheet is a sheet, not a floating card: it runs to the left,
   right and bottom screen edges and stops 60px below the sticky header so the
   section label and its rule stay readable above it. Inside, the title is
   centred alone between the two controls and the skill cube drops down to pair
   with the description, which is what separates this frame from the docked
   desktop panel where cube and title share a line. */
@media(max-width:960px){
  /* The surface is the same navy glass in both sheet states; only the expanded
     state loses its corners and its inset. */
  .skill-galaxy-panel[data-sg-view],
  .skill-galaxy-panel{background:color-mix(in srgb,var(--card) 85%,transparent)}
  .skill-galaxy-panel[data-sg-view].is-expanded,
  .skill-galaxy-panel.is-expanded{
    top:calc(var(--home-sticky-header-height) + 60px);
    right:0;bottom:0;left:0;height:auto;max-height:none;
    border-radius:16px 16px 0 0}

  /* Luminous edge · dropped. It was the docked desktop panel's device carried
     onto the sheet: a 1px bar pinned to `top:0`, inset 24px from each side,
     at `--text` 25%. Inset from the corners the way it was, it never read as
     part of the sheet's own 1px border — it read as a stray divider laid
     across the top of the panel, the more so once the sheet became fully
     opaque and stopped needing a highlight to lift it off the sphere behind
     it. The border alone now runs the whole perimeter at one weight. Desktop
     keeps its own copy (`min-width:961px`, further up) where the panel is a
     floating card against the stage rather than a docked surface. */
  .skill-galaxy-panel::before{display:none}

  /* Grabber · 56×4, clear of the title row below it. */
  .skill-galaxy-panel .skill-galaxy-sheet-handle{top:8px;height:20px}
  .skill-galaxy-panel .skill-galaxy-sheet-handle::after{width:56px;height:4px;margin-top:2px}

  /* Controls · 40×40 as drawn, with a 44px touch target underneath. */
  .skill-galaxy-panel .skill-galaxy-panel-close,
  .skill-galaxy-panel .skill-galaxy-panel-expand{width:40px;height:40px;font-size:17px}
  .skill-galaxy-panel .skill-galaxy-panel-close::after,
  .skill-galaxy-panel .skill-galaxy-panel-expand::after{
    content:'';position:absolute;inset:-2px;border-radius:50%}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-close{top:22px;right:16px}
  .skill-galaxy-panel.is-expanded .skill-galaxy-panel-expand{top:22px;left:20px}

  /* Header · title centred on its own row, cube beside the description. */
  .skill-galaxy-panel[data-sg-view] .skill-galaxy-panel-inner,
  .skill-galaxy-panel .skill-galaxy-panel-inner{
    display:grid;grid-template-columns:36px minmax(0,1fr);
    /* `auto` (the default) sizes each implicit row from its item's
       *automatic* minimum contribution, which — same rule as flexbox — drops
       to 0 for any item whose `overflow` isn't `visible`. An explicit
       `min-height` on an item (the connected-chips row, the scrollbar row)
       stops the ITEM from collapsing below it, but doesn't feed back into
       the row TRACK's own size, so the item just overflows its undersized
       row into the next one — the scrollbar rendered flush against, and
       overlapping, the tail of the chip row it's meant to sit 10px under.
       `min-content` sizes every row from its item's genuine natural height
       instead, which does respect an explicit min-height, closing the gap. */
    grid-auto-rows:min-content;
    column-gap:14px;row-gap:0;align-content:start;
    padding:32px 20px 0}
  .skill-galaxy-panel .skill-galaxy-panel-heading{display:contents}
  .skill-galaxy-panel .skill-galaxy-panel-inner>*{grid-column:1/-1}
  /* The clearance for the two 40px controls lives here, on the title itself.
     The heading around it is `display:contents` so the cube can drop out of the
     title line and pair with the description, which means the heading generates
     no box at all and padding set on it is discarded. Symmetrical, so the title
     stays centred on the sheet axis rather than drifting left of it, and long
     names wrap between the controls instead of running under them.
     `overflow-wrap:break-word` covers the narrow end: below ~340px a single long
     Russian word no longer fits the padded column, and without it the word
     overflows the box and runs under the close button. Not `anywhere` — that
     also drops the min-content contribution, which collapses the landscape
     side panel's flex heading to a one-character column. */
  .skill-galaxy-panel .skill-galaxy-panel-heading h3{
    grid-column:1/-1;grid-row:1;
    margin:0 0 16px;padding:0 56px;overflow-wrap:break-word;
    font:700 22px/28px var(--portfolio-display);text-align:center}
  .skill-galaxy-panel .skill-galaxy-panel-cube{
    grid-column:1;grid-row:2;width:36px;height:36px;flex-basis:36px;margin:0}
  .skill-galaxy-panel .skill-galaxy-panel-description{
    grid-column:2;grid-row:2;max-width:none;margin:0;
    font:400 13px/20px var(--portfolio-body)}

  /* Related work · one-line titles and a scroll indicator that is always drawn,
     3px wide, sitting just inside the sheet edge. */

}

/* Scroll indicator · the frames draw this explicitly, and they are right to:
   platform scrollbars are overlay on touch and on this project's desktop
   targets, so they never paint at rest and the list looks like it ends rather
   than continues. The panel owns a 3px indicator instead, 9px in from its own
   right edge, and the native bar is hidden so the two never both appear. */
#about.skill-galaxy-section .skill-galaxy-related-list,
.skill-galaxy-panel .skill-galaxy-related-list,
.skill-galaxy-related-list{scrollbar-width:none;-ms-overflow-style:none}
#about.skill-galaxy-section .skill-galaxy-related-list::-webkit-scrollbar,
.skill-galaxy-panel .skill-galaxy-related-list::-webkit-scrollbar,
.skill-galaxy-related-list::-webkit-scrollbar{display:none;width:0;height:0}
.skill-galaxy-related-scrollbar{
  position:absolute;z-index:6;right:9px;width:3px;
  border-radius:999px;background:color-mix(in srgb,var(--text) 7%,transparent);
  pointer-events:none}
.skill-galaxy-related-scrollbar[hidden]{display:none}
.skill-galaxy-related-scrollbar i{
  display:block;width:3px;border-radius:999px;background:var(--accent)}
@media(prefers-reduced-motion:no-preference){
  .skill-galaxy-related-scrollbar i{transition:transform .12s linear}
}

/* The drag boundary belongs to the portrait sphere card alone. Desktop has a
   full-bleed stage with a horizontal-only drag and needs no escape, and short
   landscape runs the sphere edge to edge with no card to divide. Off by
   default; the portrait block turns it on. */
.skill-galaxy-interaction-area{display:none}
/* Landscape-only decoration; see the rule that turns it on for why. */
.skill-galaxy-sphere-top-fade{display:none}

/* The compact list card carries the same indicator: drawn at rest, so the
   bordered region reads as its own scroll viewport rather than as the end of
   the list. Position comes from the script, which measures the list against
   its holder. */
.skill-galaxy-list-scrollbar{
  position:absolute;z-index:6;width:3px;
  border-radius:999px;background:color-mix(in srgb,var(--text) 7%,transparent);
  pointer-events:none}
.skill-galaxy-list-scrollbar[hidden]{display:none}
.skill-galaxy-list-scrollbar i{
  display:block;width:3px;border-radius:999px;background:var(--accent)}
@media(prefers-reduced-motion:no-preference){
  .skill-galaxy-list-scrollbar i{transition:transform .12s linear}
}

/* Connected-skill chips scroll sideways, and on every mobile sheet — docked
   beside the band in landscape, stacked below it in portrait — the
   platform's own thin bar is overlay-only on touch and never paints at rest,
   the same problem the two indicators above solve for their own lists.
   In-flow rather than absolutely positioned: `syncChipScrollbar` sizes the
   thumb in percent of the track's own width, so it needs no viewport math to
   stay under the row it tracks. Shown on every breakpoint, including desktop
   — `[hidden]` (set by the script whenever the row doesn't actually overflow)
   is what keeps it off screens where the chips all fit on one line. */
.skill-galaxy-chip-scrollbar{
  display:block;position:relative;overflow:hidden;height:3px;margin:10px 0 20px;
  flex-shrink:0;min-height:3px;
  border-radius:999px;background:color-mix(in srgb,var(--text) 7%,transparent);
  pointer-events:none}
.skill-galaxy-chip-scrollbar[hidden]{display:none}
.skill-galaxy-chip-scrollbar i{
  position:absolute;top:0;left:0;display:block;height:3px;
  border-radius:999px;background:var(--accent)}
@media(prefers-reduced-motion:no-preference){
  .skill-galaxy-chip-scrollbar i{transition:left .12s linear}
}

/* ── Card surface scope ────────────────────────────────────────────────────
   The navy card belongs to this band alone. `--dark-card` is the shared
   primitive behind Recent Positions captions, Education modals and the case
   surfaces, so the About block rebinds `--card` locally instead of moving the
   primitive. The mobile sheet is portalled to <body>, so it needs the same
   binding on itself rather than inheriting it from the section. */
html[data-theme="dark"] body[data-page="home"] #about.skill-galaxy-section,
html[data-theme="dark"] .skill-galaxy-panel{
  --card:var(--dark-card-about);
  --color-bg-card:var(--dark-card-about);
  --line:var(--dark-border-about);
  --color-border-default:var(--dark-border-about);
}

/* The docked panel and the sheet share one scroll model: a one-line title so
   rows keep an even rhythm, and a 16px gutter so no copy runs under the
   indicator. */
#about.skill-galaxy-section .skill-galaxy-related-list,
.skill-galaxy-panel .skill-galaxy-related-list{padding-right:16px}
#about.skill-galaxy-section .skill-galaxy-related-title,
.skill-galaxy-panel .skill-galaxy-related-title{
  display:-webkit-box;overflow:hidden;
  -webkit-box-orient:vertical;-webkit-line-clamp:1}
/* Publication hierarchy: draft evidence stays readable, but its title uses
   the same quiet role as its status and description. This rule intentionally
   follows the desktop title override above so both the docked panel and the
   portalled sheet/full Related Work view keep the in-progress distinction. */
#about.skill-galaxy-section .skill-galaxy-related-item.is-in-progress .skill-galaxy-related-title,
.skill-galaxy-panel .skill-galaxy-related-item.is-in-progress .skill-galaxy-related-title,
.skill-galaxy-panel .skill-galaxy-related-card.is-in-progress .skill-galaxy-related-copy strong,
#about.skill-galaxy-section .skill-galaxy-related-item.is-in-progress .skill-galaxy-related-description,
.skill-galaxy-panel .is-in-progress .skill-galaxy-related-description{
  color:var(--text-faint)}

/* ── Short landscape phones ────────────────────────────────────────────────
   The band is only a few hundred pixels tall here, so the sphere takes the
   whole block divider to divider, the sheet docks full-height beside it
   instead of a small floating card, and the toggle sits down on the lower
   divider instead of floating in the middle of the stage. */
@media(max-width:960px) and (orientation:landscape) and (max-height:560px){
  body[data-page="home"] #about.skill-galaxy-section{padding:0}
  body[data-page="home"] #about.skill-galaxy-section>.wrap{width:100%;max-width:none;padding:0}
  /* One screen, no more: shell height is the viewport minus the sticky
     header, so scrolling the section flush under the header (see
     `revealSectionHead`, called whenever the sheet opens) leaves nothing
     above or below the fold. */
  #about.skill-galaxy-section .skill-galaxy-shell{
    display:block;position:relative;
    height:calc(100vh - var(--home-sticky-header-height));
    height:calc(100dvh - var(--home-sticky-header-height));min-height:0;
    padding:0;border-radius:0;border-inline:0;overflow:hidden;
    /* The base card's `inset 0 1px 0` highlight reads as a bevel on a bordered
       desktop card; docked full-bleed under the sticky header here it is just
       a stray hairline sitting under the header with nothing to explain it. */
    box-shadow:none}
  #about.skill-galaxy-section .skill-galaxy-tabs{
    display:flex;position:absolute;z-index:6;top:14px;left:20px;right:auto;
    margin:0;padding:0;border-bottom:0}
  #about.skill-galaxy-section .skill-galaxy-tabs::after{display:none}
  #about.skill-galaxy-section .skill-galaxy-intro,
  #about.skill-galaxy-section .skill-galaxy-mobile-hints,
  #about.skill-galaxy-section .skill-galaxy-about{display:none}

  /* Sphere fills the band edge to edge. A general mobile rule gives the
     sphere card a 20-32px side margin and its own solid border — portrait's
     card treatment. Landscape draws its own boundary (Interaction Area,
     below) and needs the card's real edges to line up with the sheet's and
     with that boundary's own math, both of which measure against this
     element's box — so the margin and the second border are cancelled here,
     at the same specificity the general rule carries, for the same reason it
     already keeps `margin:0` above it. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-network{
    margin:0;border:0;border-radius:0}
  #about.skill-galaxy-section .skill-galaxy-network{
    position:absolute;inset:0;width:auto;height:auto;margin:0}
  #about.skill-galaxy-section .skill-galaxy-stage{
    position:absolute;inset:0;width:100%;height:100%;
    /* The stage itself keeps the page's vertical swipe — only the marked
       Interaction Area below takes it away, same contract as portrait. */
    touch-action:pan-y}

  /* A 100px top-to-bottom fade, sitting between the canvas and the Interaction
     Area in the markup on purpose: painting after the canvas puts it over the
     sphere and its labels near the top edge, and painting before the
     boundary keeps the boundary's own dashed frame crisp on top of it rather
     than faded itself. No z-index — same reason nothing else in this stage
     carries one: DOM order is the whole stacking contract here, and a raised
     layer would start winning fights with the node labels it isn't supposed
     to touch. 80px still let a label under ABOUT show through; 100px clears it. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-sphere-top-fade{
    display:block;position:absolute;top:0;left:0;right:0;height:100px;
    background:linear-gradient(to bottom,var(--bg) 0%,color-mix(in srgb,var(--bg) 0%,transparent) 100%);
    pointer-events:none}

  /* Interaction Area is the real drag boundary here, not an annotation:
     every touch inside it drives the sphere, nothing outside it does — that
     is page scroll instead. Even with the card full-bleed and no sheet open,
     it keeps a 50px strip on the right clear — a phone with only the sphere
     to grab needs somewhere on the band itself to swipe the page, the same
     reasoning portrait's own 61px gutter is built on. With the sheet open it
     gives up exactly the sheet's own width instead (below), which is wider
     than this strip and so replaces it rather than adding to it. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area{
    display:block;position:absolute;inset:0 50px 0 0;
    border:1px dashed var(--line);border-radius:16px;touch-action:none}
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    position:absolute;top:50%;left:calc(100% - 10px);padding:0 3px;
    /* No backing box here either — see the portrait rule's own note above. */
    color:var(--text-faint);
    font:500 7px/1 var(--portfolio-body);letter-spacing:.04em;white-space:nowrap;
    pointer-events:none;transform:translate(-50%,-50%) rotate(-90deg)}

  /* The sheet: full height from the sticky header down, kept the same 50px
     off the band's right edge the closed Interaction Area already reserves —
     so the strip that lets the page scroll is in the same place whether the
     sheet is open or not, never flush to the true viewport edge. Rounded on
     all four corners (frame 434:14577) since it now floats clear of the
     viewport edge on every side, not just the left. Colour and blur come
     from the shared `--sg-sheet` recipe on `.skill-galaxy-panel[data-sg-view]`
     a few hundred lines down (the same tokens portrait's bottom sheet uses);
     its shadow is dropped — a docked sheet reads as part of the band, not as
     a surface lifted off it — but the frame's own hairline border stays. */
  #about.skill-galaxy-section .skill-galaxy-panel,
  #about.skill-galaxy-section .skill-galaxy-panel.is-expanded{
    position:absolute;z-index:8;top:0;right:50px;bottom:0;left:auto;
    width:min(42vw,340px);height:auto;max-height:none;
    /* Full border, all four sides — dropping just the top (an earlier pass
       here, meant to avoid doubling a line the header draws under itself)
       left border-radius with nothing to round the top corners into: the
       left/right strokes ran straight up to a hard square edge instead of
       curving, reading as a broken outline with a stray flat top rather than
       one continuous rounded rectangle. The actual doubled-line problem was
       the shell's own inset box-shadow (fixed separately, see the shell rule
       in the short-landscape query below); the border itself was never the
       cause and belongs on every side, same as the frame draws it. */
    border:1px solid var(--sg-card-border);border-radius:16px;
    box-shadow:none;transform:none}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-sheet-handle,
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-expand{display:none}
  /* Only the skill's name is pinned; the description, connected chips and
     related work all scroll together underneath it. `renderPanel` wraps
     everything after the heading in `.skill-galaxy-panel-scroll` just for
     this breakpoint, so there is one scrollport to give that flex row to. */
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-inner{
    display:flex;flex-direction:column;height:100%;padding:0;overflow:hidden}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-heading{
    display:flex;flex:0 0 auto;gap:10px;align-items:center;
    padding:20px 44px 14px 18px}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-heading h3{
    margin:0;font:700 17px/22px var(--portfolio-display);text-align:left}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-cube{
    width:26px;height:26px;flex-basis:26px}
  /* The drawn indicator (`syncRelatedScrollbar`) is the only scrollbar meant
     to show here — the platform's own overlay one, left enabled, drew right
     alongside it and the two never quite agreed with each other. Hidden the
     same way the list already hides its native bar. The extra right padding
     is the content's own clearance off the drawn bar, wider than before so
     the two read as separated rather than touching. */
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-scroll{
    flex:1;min-height:0;overflow:auto;padding:0 26px 14px;
    scrollbar-width:none;-ms-overflow-style:none;
    /* `syncRelatedScrollbar` reads these for its top/bottom clearance, same
       token pattern `syncListScrollbar` already uses for the skill list a
       few hundred lines up — matched to that list's own 9px so both drawn
       bars this breakpoint can show at once stop the same distance off their
       card's corners instead of one running flush to it and the other short. */
    --sg-related-bar-inset:9px}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-scroll::-webkit-scrollbar{
    display:none;width:0;height:0}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-description{
    margin-top:0;font:400 12px/17px var(--portfolio-body)}
  /* Narrow-mobile rules elsewhere shrink this to 8px, tuned for the shortest
     portrait sheets. With the description, chips and related work all in one
     scroll column here, a divider that close to the next block reads as
     crowded rather than as a section break. */
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-divider{
    margin:18px 0}
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-close{
    top:12px;right:12px;width:32px;height:32px;border-radius:8px}

  /* Toggle pinned 10px off the card's own left and bottom edge, both segments
     always present — closing the sheet is a tap on its own close button, not
     on the view it covers. The *track's* own fill is still gone — that was
     the flat card-coloured backing sitting behind both icons before either
     was pressed, and dropping it is what "no backing" meant. Each pane gets
     a faint fill of its own back, though: bare on the dot field, an inactive
     icon had nothing at all to sit on and read as loose rather than part of
     a control. The active pane's own (stronger) fill is unchanged. */
  #about.skill-galaxy-section .skill-galaxy-controls{
    position:absolute;z-index:7;top:auto;bottom:10px;left:10px;right:auto;margin:0}
  #about.skill-galaxy-section .skill-galaxy-view-switch{background:none}
  #about.skill-galaxy-section .skill-galaxy-view-switch button{
    background:color-mix(in srgb,var(--card) 55%,transparent)}
}

/* ── Rotated phone · About + Skills band ─────────────────────────────
   Landscape phones only (874×402 and its neighbours). Portrait and desktop keep
   every rule they had. The sphere stays centred on its own card at every
   state, the list owns the band, the Sphere/List pair always shows both
   segments, ABOUT becomes the control that opens a compact About panel, and
   SKILLS goes plain. The two decorative defaults below sit outside the query
   so the arrow and the close button never appear in any other layout. */
.skill-galaxy-tabs .skill-galaxy-tab-arrow{display:none}
.skill-galaxy-about .skill-galaxy-about-close{display:none}

@media(max-width:960px) and (orientation:landscape) and (max-height:560px){
  /* The sheet's title keeps the portrait `padding:0 56px`, which is clearance
     for two 40px controls centred over a full-width sheet. Here the heading is
     a left-aligned flex row with one 32px close button, and the heading itself
     already reserves 44px for it — so those 56px only squeezed the title into a
     third of the panel, and in the expanded state (reachable through the QA
     hook, not the UI, since expand is hidden here) pushed long Russian titles
     across the close button. */
  #about.skill-galaxy-section .skill-galaxy-panel .skill-galaxy-panel-heading h3{padding:0}

  /* ABOUT reads as a control: accent, underlined, with the same chevron the
     list rows carry. SKILLS is a plain label beside it. */
  #about.skill-galaxy-section .skill-galaxy-tabs{gap:20px}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about{
    gap:6px;cursor:pointer;-webkit-tap-highlight-color:transparent}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about b{
    color:var(--accent);text-decoration:underline;text-underline-offset:3px}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about i{color:var(--accent)}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-about:focus-visible{
    outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-arrow{
    display:inline-flex;width:12px;height:12px;color:var(--accent)}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-arrow svg{
    width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2.4;
    stroke-linecap:round;stroke-linejoin:round}
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-skills b,
  #about.skill-galaxy-section .skill-galaxy-tabs .skill-galaxy-tab-skills i{color:var(--text)}

  /* The list owns the band: 20px under the heading, down to the block's lower
     divider. It keeps the sphere card's own 50px right gutter (open or not —
     see the Interaction Area note above), so there is always somewhere on the
     band itself to swipe the page. It stops flush at the sheet with no gap of
     its own when one is open — the sheet's own 50px gutter (below) already
     covers that job, so reserving a second one here would only double it. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-network{
    position:absolute;inset:0;width:auto;height:auto;margin:0}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    position:absolute;top:53px;right:50px;bottom:0;left:20px;
    width:auto;height:auto;margin:0;
    /* Right padding clears the drawn indicator's own reserved band (9px
       gutter + 3px bar = 12px off this edge) plus a 10px gap of its own past
       that — row text stopping flush at the bar's left edge read as content
       running into it rather than as a scroll indicator sitting clear of the
       list. 22px = 12 (the bar's own band) + 10 (this gap). */
    padding:11px 22px 72px 10px;overflow:auto}
  #about.skill-galaxy-section[data-sg-view="list"]:has(.skill-galaxy-panel.is-open) .skill-galaxy-list{
    right:calc(min(42vw,340px) + 50px)}

  /* Figma's own row rhythm (frame 434:14216): 50px rows, 9px of that as
     padding, an 8px radius on the hover/press state, 23px cubes with a 12px
     gap to the label, groups 16px apart, the heading 4px in from the card
     edge. Scoped to landscape only — portrait's own numbers are signed off
     and stay exactly as they are. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list-row{
    min-height:50px;gap:12px;padding:9px 0;border-radius:8px}
  /* The cube sits flush with the card's own padding in the frame, but on the
     page that reads as glued to the edge — a few px of its own breathing room
     reads better than relying on the card's padding alone. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list-row .skill-galaxy-cube{
    width:23px;height:23px;flex-basis:23px;margin-left:6px}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list-group{margin-bottom:16px}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list-group h3{
    margin:0 0 8px;padding-left:4px}
  /* The drawn indicator's three clearances — off the list's right edge, off
     its top, off its bottom — share one value so the bar reads as evenly set
     into its own track rather than pinned to one side of it. 9px matches the
     related-work indicator's own gutter (`.skill-galaxy-related-scrollbar`,
     a few hundred lines up), so the two bars this breakpoint can show at
     once — this one and the docked sheet's, in list view with the sheet
     open — sit the same distance off their respective edges.
     `--sg-list-bar-inset`/`-end` otherwise fall through to the general
     mobile card's 12px/16px (a portrait-card value, tuned to clear that
     card's own corner radius) — this list has no radius of its own to clear,
     and inheriting it left the bar stopping well short of the bottom
     corner. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    --sg-list-bar-gutter:9px;--sg-list-bar-inset:9px;--sg-list-bar-inset-end:9px}

  /* Both segments of the Sphere/List toggle stay on screen in every state —
     closing the open sheet is a tap on its own close button, not a forced
     switch back to Sphere. List view no longer re-centres the pair; it keeps
     the same 10px/10px corner the sphere view parks it in. The mode name
     beside it is redundant here — the pressed icon already states which view
     is active — and dropped in both, not only list. */
  #about.skill-galaxy-section .skill-galaxy-view-label{display:none}

  /* About is a fixed-size card, not a plaque that stretches to fill the band —
     frame 434:14575 draws it the same size and in the same spot whether the
     sheet is open or closed, so it no longer reacts to `data-sg-sheet` at all.
     Dressed like the band's other mobile cards (`--sg-card`, the same token
     portrait's own list/hint cards use at full strength). It is anchored
     inside the shell, so page scroll carries it with the block. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-about,
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-about{display:none}
  /* Matched in both variants on purpose: a general mobile rule (elsewhere in
     this file) styles the disclosure with the identical pair — a plain
     selector and a `[data-sg-view="list"]`-qualified twin, the second there
     only so list view carries the same specificity sphere view gets for
     free. Writing only the plain selector here would lose to that twin
     whenever the list is the active view — same trap, same fix, as the
     Interaction Area and shell-height overrides above. */
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about,
  #about.skill-galaxy-section[data-sg-about="open"][data-sg-view="list"] .skill-galaxy-about{
    display:block;position:absolute;z-index:9;
    top:54px;left:20px;right:auto;bottom:auto;width:300px;
    margin:0;padding:16px 52px 16px 18px;
    border:1px solid var(--sg-card-border);border-radius:16px;
    background:var(--sg-card);box-shadow:0 4px 12.3px rgba(0,0,0,.23);
    backdrop-filter:none}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about p{
    max-width:none;margin:0;color:var(--text-dim);font:400 13px/19px var(--portfolio-body)}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about-meta,
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-instruction{display:none}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-about-close{
    display:grid;position:absolute;z-index:2;top:10px;right:10px;
    width:35px;height:36px;place-items:center;
    border:1px solid var(--sg-card-border);border-radius:10px;
    color:var(--text-dim);background:color-mix(in srgb,var(--card) 60%,transparent);
    font:300 16px/1 var(--portfolio-body);cursor:pointer}

  /* Section dividers: sphere drops both (the Interaction Area's own drawn
     edge already frames the band, so the shell's plain hairline above and
     below it just doubled that line); list keeps the top one — it is the
     only thing separating the band from "More Case Studies" above once the
     list fills the rest of the screen — and drops only the bottom. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-shell{
    border-top:0;border-bottom:0}
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-shell{
    border-bottom:0}
  /* The shell's own hairline is only half the story — a general mobile rule
     also draws one on the *section* itself, the actual line between More
     Case Studies and this band (see its own comment a few hundred lines
     down). Sphere's full-bleed card and its own drawn boundary already read
     as the section's edge, so that line doubles it here too. */
  body[data-page="home"] #about.skill-galaxy-section[data-sg-view="sphere"]{
    border-top:0}
}

/* ══════════════════════════════════════════════════════════════════════════
   Round 5 · mobile portrait, second pass.
   Frames 351:2 (half-open sheet, now 233 tall — it was 282) and 433:7574
   (full screen). Appended on purpose: every mobile block above carries the
   same specificity, so source order is what makes these land.
   ══════════════════════════════════════════════════════════════════════════ */
@media(max-width:960px){
  /* The drag boundary is a control surface, not a hairline, so it takes the
     palette's hover accent: #a83408 on paper, #8dcbff on dark. Dashed at 2/2 it
     still spends about half the ink of a solid line at that weight. */
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area{
    border-color:var(--theme-accent-hover)}
  #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    color:var(--theme-accent-hover)}

  /* The chevron states the next action, so it turns when the plaque is open. */
  #about.skill-galaxy-section .skill-galaxy-tab-arrow svg{
    transition:transform .2s ease}
  #about.skill-galaxy-section[data-sg-about="open"] .skill-galaxy-tab-arrow svg{
    transform:rotate(180deg)}

  /* One rule above this band, none below Recent Positions: the divider between
     More Case Studies and About stays (it was drawn twice — section and shell —
     so the shell's copy goes and the section keeps the single line), and no
     clipped corner on the shell that used to sit under it. */
  body[data-page="home"] #about.skill-galaxy-section{
    border-top:1px solid var(--line);padding-bottom:0}
  /* The top divider separates page sections, so it takes the page's line, not
     the band's. On dark the section rebinds `--line` (and its source token) to
     the navy `--dark-border-about`, which left this one divider blue-grey while
     every other section drew `--dark-border` — so the primitive is named
     directly, past the rebinding. */
  html[data-theme="dark"] body[data-page="home"] #about.skill-galaxy-section{
    border-top-color:var(--dark-border)}
  /* Same trap, same fix: the heading's trailing rule drew `var(--line)` and
     went navy with it. It reads as page chrome next to the divider above, so
     it takes the page primitive too. */
  html[data-theme="dark"] #about.skill-galaxy-section .skill-galaxy-tabs::after{
    background:var(--dark-border)}
  #about.skill-galaxy-section .skill-galaxy-shell{
    min-height:0;border-top:0;border-radius:0;padding-bottom:0}
  /* The band used to run on for most of a screen past its last control. The
     switch closes the block; 20px below it is the block's end. */
  #about.skill-galaxy-section .skill-galaxy-controls{margin-bottom:20px}
}

/* ── The sheet ─────────────────────────────────────────────────────────────
   Portalled to <body> on mobile, so none of this can be nested under the
   section. Frame 351:2 parks the collapsed sheet 233px tall and frame 433:7574
   opens it 54px below the nav, which is what keeps the section's own heading
   on screen behind it. */
@media(max-width:960px){
  /* The panel carries `data-sg-view`, and every rule that sizes or colours it
     across this file and `skill-galaxy.css` is written as
     `.skill-galaxy-panel[data-sg-view]` — including one in a `max-width:430px`
     block, which is what actually decided the height at 390px. Matching that
     specificity is the only way source order gets to settle it. */
  .skill-galaxy-panel[data-sg-view]{
    /* Solid, matching the page exactly: the frame's own #fff4f1-at-87%-with-blur
       read as translucent chrome over the sphere, and the sheet is meant to
       read as an opaque surface of the page, not a glassy overlay. `--bg`
       already carries the right value per theme, so one token covers both. */
    --sg-sheet:var(--bg);
    background:var(--sg-sheet);
    /* The base mobile `.skill-galaxy-panel` rule (this one's own ancestor
       recipe, still in effect since this only touches `background`) carries
       `backdrop-filter:blur(22px) saturate(140%)` from the translucent-glass
       version of the sheet this replaced. A blur behind a fully opaque
       background paints nothing, but WebKit still composites it as its own
       filtered layer — right at this element's own clipped, rounded edge,
       which is exactly where a stray seam from that layer boundary would
       show, worst along the top where the sheet sits flush under the sticky
       header. An opaque surface has no backdrop to blur; killing the filter
       here removes both the pointless compositing cost and that seam. */
    backdrop-filter:none;
    -webkit-backdrop-filter:none;
    border-radius:16px 16px 0 0;
    box-shadow:0 18px 52px color-mix(in srgb,#000 55%,transparent),
      inset 0 1px 0 color-mix(in srgb,var(--accent) 43%,transparent);
    height:min(233px,34svh);max-height:min(233px,34svh);
    overscroll-behavior:none}
  /* Frame 433:7574 opens the sheet 54px below the nav — far enough to leave the
     band's own heading readable above it, which is the whole reason the page is
     parked under the sticky header first. */
  .skill-galaxy-panel[data-sg-view].is-expanded{
    top:calc(var(--home-sticky-header-height) + 54px);bottom:0;
    height:auto;max-height:none}

  /* Nothing in the sheet scrolls but the related-work rows. The sheet is a
     fixed composition — title, description, chips, then the one list that can
     outgrow it — and letting the whole inner surface scroll meant the RELATED
     WORK heading and its count slid away from the rows they label. */
  .skill-galaxy-panel .skill-galaxy-panel-inner{
    display:flex;min-height:0;flex-direction:column;overflow:hidden}
  /* The higher-specificity mobile composition above intentionally keeps the
     title/cube/description as a two-column grid. In an expanded portrait
     sheet its last implicit track used to resolve to the Related Work list's
     full max-content height, so the list extended below the clipped sheet and
     had no scroll range of its own. Keep those approved first eight tracks,
     but make the ninth (Related Work) own exactly the remaining height. */
  .skill-galaxy-panel[data-sg-view].is-expanded:not(.is-related) .skill-galaxy-panel-inner{
    display:grid;
    grid-template-rows:repeat(8,min-content) minmax(0,1fr)}
  .skill-galaxy-panel .skill-galaxy-related-list{
    min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;
    touch-action:pan-y;-webkit-overflow-scrolling:touch}
  .skill-galaxy-panel .skill-galaxy-related-kicker{flex:0 0 auto}
}

/* ── Sheet open · the band gives up its slack ──────────────────────────────
   With the sheet parked over the lower screen and the page frozen behind it,
   the band has exactly the space between the sticky header and the sheet's top
   edge. The shell is already a column, so it is given that height and the
   sphere takes whatever the fixed rows leave — no magic numbers to drift.
   `data-sg-sheet` is set by the script whenever the sheet is open on mobile. */
@media(max-width:960px){
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-shell{
    /* The trailing 10px is the gap between the sphere card and the sheet's top
       edge — flush, the card looked docked to the sheet. */
    height:calc(100svh - var(--home-sticky-header-height) - var(--sg-sheet-height,min(233px,34svh)) - 10px);
    min-height:0;
    /* The shell's own hairline (drawn under the sphere card at every other
       width) reads as a second, redundant divider once the sheet's opaque
       surface starts right where that line would sit — the sheet's own top
       edge is the boundary now. */
    border-bottom:0}
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-network{
    height:auto;min-height:0;flex:1;overflow:hidden}

  /* List and sheet are two adjacent scroll surfaces, never overlapping ones.
     The base list card has a fixed 58svh height for the closed state; once the
     sheet opens it instead consumes the network's remaining flex height. */
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="list"] .skill-galaxy-list{
    box-sizing:border-box;height:100%;max-height:100%;min-height:0}

  /* The view switch answers "which view do I want" — a question the open sheet
     has already answered. Hiding it hands its row to the sphere, and it comes
     back the moment the sheet goes. */
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-controls{display:none}

  /* The hints are a first-run aid, so they stay in their compact form always —
     the full-height cards spent two lines of screen on advice most visits have
     already taken. Sized here rather than in the base block so the solid fill
     above still applies. */
  #about.skill-galaxy-section .skill-galaxy-mobile-hint{
    min-height:0;padding:7px 10px;gap:8px;font-size:10px}
  #about.skill-galaxy-section .skill-galaxy-hint-icon{
    width:20px;height:20px;flex-basis:20px}
  /* The heading needs air before the hint row — the old 12px read as one
     crowded block under ABOUT / SKILLS. */
  #about.skill-galaxy-section .skill-galaxy-mobile-hints{margin-top:24px}
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-mobile-hints{
    margin-top:12px}

  /* With the sheet up the whole card is the drag surface, and the boundary
     says so: the dashed edge and its label move out to the card itself. The
     page is frozen behind the sheet, so `touch-action:none` over the full box
     costs nothing, and the drawn edge keeps the contract visible — every touch
     inside is the sphere's. The label steps 10px inward so the card edge does
     not cut it in half. */
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="sphere"] .skill-galaxy-interaction-area{
    inset:0;border-radius:16px}
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    left:calc(100% - 10px)}
}

/* Portrait sheet may only exist while About owns the viewport. The shell is
   intentionally shorter (it ends above the sheet), but the section itself
   continues behind its own opaque sheet to the viewport bottom so the next
   page section can never appear through the breathing gap between them. */
@media(max-width:960px) and (orientation:portrait){
  body[data-page="home"] #about.skill-galaxy-section[data-sg-sheet="open"]{
    min-height:calc(100svh - var(--home-sticky-header-height))}
}

/* Half-open portrait sheet: CONNECTED SKILLS is the whole story here —
   RELATED WORK only fits once the sheet is fully expanded. The panel's own
   fixed column (heading → description → divider → chips) stops scrolling at
   `overflow:hidden` a few hundred lines up, so anything past the chip row
   that doesn't fit inside 233px used to just clip — the second divider and
   the RELATED WORK label peeking in with none of its own rows visible,
   which read as broken rather than as "not shown yet." `.divider ~ .divider`
   is the second one specifically (the empty-state markup shares a plain
   `.skill-galaxy-panel-kicker` with "SELECTED SKILL" above the heading, so
   matching on structure rather than a class name is what tells them apart),
   and every later sibling goes with it. Scoped to exclude the short-landscape
   sheet, which has no half-open state of its own and always shows both.
   No `#about` ancestor: the panel is portalled to `<body>` on mobile portrait
   (see `syncPanelPortal`), so it is never actually nested under the section
   by the time this matters — the same reason every other rule that reaches
   into the portrait sheet targets `.skill-galaxy-panel` directly. */
@media(max-width:960px) and (not ((orientation:landscape) and (max-height:560px))){
  .skill-galaxy-panel[data-sg-view]:not(.is-expanded) .skill-galaxy-panel-divider ~ .skill-galaxy-panel-divider,
  .skill-galaxy-panel[data-sg-view]:not(.is-expanded) .skill-galaxy-panel-divider ~ .skill-galaxy-panel-divider ~ *{
    display:none}
}

/* A selection without a row of its own marks its group instead: 22 of the 67
   skills are listed, and the tinted heading is how the list still answers when
   Animation or Theming is picked on the sphere. The class comes and goes with
   `revealSelectedListRow`. Every width — desktop's card and the phone's. */
#about.skill-galaxy-section .skill-galaxy-list-group.is-selected-group h3{
  color:var(--portfolio-orange-text)}

/* The drag boundary on dark takes the info border — the blue accent-hover it
   used was a reading tint, not a control colour, and it sank into the cage. */
@media(max-width:960px){
  html[data-theme="dark"] #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area{
    border-color:var(--color-status-info-border)}
  html[data-theme="dark"] #about.skill-galaxy-section[data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    color:var(--color-status-info-border)}
}

/* ── Connected skills · one scrollable row ─────────────────────────────────
   Every connection is a chip now (the top-6 cap is gone), and the row scrolls
   sideways instead of wrapping: a hub like UI/UX Design carries 18+ chips, and
   wrapped they pushed RELATED WORK below the fold of the half-open sheet.
   Both homes — the docked desktop panel and the portalled mobile sheet. */
#about.skill-galaxy-section .skill-galaxy-connected-list,
.skill-galaxy-panel .skill-galaxy-connected-list{
  display:flex;flex-wrap:nowrap;align-items:center;flex-shrink:0;min-height:max-content;
  /* A non-`visible` `overflow` disables an item's automatic min-size floor —
     `min-height:auto` normally protects it down to its own content height,
     but with `overflow-y:hidden` that floor becomes 0 — in *both* layout
     modes this row lives in: `flex-shrink:0` alone fixed it on desktop
     (`.skill-galaxy-panel-inner` is a flex column there), but did nothing on
     mobile, where `.skill-galaxy-panel-inner` is `display:grid` — the same
     rule applies to `auto`-sized grid tracks and `flex-shrink` isn't a grid
     property at all, so the row still collapsed there (down to ~9px for a
     2-line RU title + long description, `product-architecture`). An explicit
     `min-height` (not `auto`) is never subject to that override in either
     mode, so `max-content` is the one declaration that floors it everywhere. */
  overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;
  /* Thin but present: on touch the swipe is the affordance, on desktop the
     bar is the only sign the row continues. */
  scrollbar-width:thin;
  scrollbar-color:color-mix(in srgb,var(--text) 22%,transparent) transparent;
  /* Split 6/6 instead of 0/6: the row's own height used to match its chips'
     content height with nothing to spare, so a sub-pixel rounding difference
     between the row's box and its centred child clipped the chip's top
     border under `overflow-y:hidden` — worse once the half-open sheet has no
     free height left to grow into (its bottom already sits flush with the
     scrollbar below). Taking 3px off the scrollbar's own `margin-top` funds
     the 3px added here, so total height — and the sheet's 233px frame — is
     unchanged. */
  padding:3px 0 3px}
#about.skill-galaxy-section .skill-galaxy-connected-list button.skill-galaxy-chip,
.skill-galaxy-panel .skill-galaxy-connected-list button.skill-galaxy-chip{
  flex:0 0 auto}

/* ── Landscape sheet · winning the last word ───────────────────────────────
   Two general mobile rules above (both keyed off `[data-sg-sheet="open"]`,
   written for portrait's stacked layout) land after the landscape query and
   at the same specificity, so source order would let them win: the shell
   shrinking as if a bottom sheet sat under it, and the toggle hiding as if
   portrait's full-screen sheet covered it. Landscape's sheet runs beside the
   band instead, so it needs neither — put back here, last, on purpose.
   `.skill-galaxy-related-list` also gets a general `flex:1;overflow:auto]`
   above, meant for when it sits directly in the sheet's flex column; here it
   sits inside `.skill-galaxy-panel-scroll` instead (see `renderPanel`), which
   already owns the one scrollbar, so the general rule is cancelled to avoid
   a second, empty one nested inside it. */
@media(max-width:960px) and (orientation:landscape) and (max-height:560px){
  /* No floor: a general mobile rule elsewhere zeroes the base shell's own
     360px minimum for exactly this reason — on the shortest landscape phones
     even the closed section is under 360px tall, and a floor here would be
     the one thing standing between "fits the screen" and running 20-odd
     pixels past the bottom edge. */
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-shell{
    height:calc(100vh - var(--home-sticky-header-height));
    height:calc(100dvh - var(--home-sticky-header-height));min-height:0}
  #about.skill-galaxy-section[data-sg-sheet="open"] .skill-galaxy-controls{display:flex}
  /* The landscape list is a sibling surface to the docked sheet, so its full
     rounded frame must remain visible inside the same clipped one-screen band.
     The shell can land 1–2 fractional CSS pixels below the visual viewport
     after the section's top hairline (notably in mobile WebKit); a 4px lower
     clearance keeps the complete stroke visible without changing the card's
     useful scroll area or the approved right strip. The general
     mobile sheet-open rule also assigns `height:100%` later than the original
     landscape list recipe; with an absolute `top`, that pushes the list 53px
     beyond the band. Restore inset-driven sizing here for both sheet states. */
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    bottom:4px}
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="list"] .skill-galaxy-list{
    height:auto;max-height:none;min-height:0}
  /* A general mobile rule (also after this query, same reason as the two
     above) gives the toggle a 20px margin-bottom on top of whatever `bottom`
     it already carries — meant for portrait, where nothing else marks the
     block's own end. Landscape's `bottom:10px` is already the full 10px off
     the card the toggle is meant to sit at. */
  #about.skill-galaxy-section .skill-galaxy-controls{margin-bottom:0}
  /* Dark theme carries its own, theme-qualified track fill (`#11110f`) — more
     specific than the plain `background:none` above because of the
     `html[data-theme="dark"]` ancestor, so it needs its own matching
     override. The active pane's own fill (`#101d53`) is the selection state,
     not the backing, and stays. */
  html[data-theme="dark"] #about.skill-galaxy-section .skill-galaxy-view-switch{background:none}
  .skill-galaxy-panel .skill-galaxy-related-list{flex:none;overflow:visible}
  /* Unlike portrait's bottom sheet, landscape's sheet sits beside the sphere
     card, not below it — so "the whole card is the drag surface" (the general
     rule above) is exactly wrong here: it would let a drag starting under the
     visible sheet still spin the sphere. The boundary gives up the sheet's own
     width plus its 50px right gutter instead, so the two surfaces never
     contest the same touch and the boundary's edge still meets the sheet's. */
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="sphere"] .skill-galaxy-interaction-area{
    inset:0 calc(min(42vw,340px) + 50px) 0 0;border-radius:16px}
  #about.skill-galaxy-section[data-sg-sheet="open"][data-sg-view="sphere"] .skill-galaxy-interaction-area span{
    left:calc(100% - 10px)}
  /* The open sheet used to need a hand-computed `background-position` shift
     here: `.skill-galaxy-network` never resizes in landscape (`inset:0` in
     every state — only the Interaction Area's box and the camera framing
     inside it give up the sheet's width), so a percentage glow position had
     nothing to track and had to be nudged left by half the reserved width by
     hand. `updateGlow()` reads the camera that actually did the dollying, so
     the glow follows the sphere without being told how far it moved. */
  /* `.skill-galaxy-chip-scrollbar`'s own drawn bar is the only one meant to
     show here — a general mobile rule above (same specificity, same reason
     as the rest of this section) sets the row's native bar to `thin` as the
     touch affordance for every other breakpoint it appears in; landscape has
     its own indicator instead, so left enabled the native one would paint a
     second, differently-placed bar right under the drawn one. */
  #about.skill-galaxy-section .skill-galaxy-connected-list,
  .skill-galaxy-panel .skill-galaxy-connected-list{
    scrollbar-width:none;-ms-overflow-style:none}
  #about.skill-galaxy-section .skill-galaxy-connected-list::-webkit-scrollbar,
  .skill-galaxy-panel .skill-galaxy-connected-list::-webkit-scrollbar{
    display:none;width:0;height:0}
}

/* The shared view-enter tween translates its surface 4px down. That motion is
   harmless on the unbounded desktop/portrait cards, but it temporarily pushes
   landscape List's bottom border through the clipped one-screen band exactly
   while the user changes views or selects a skill. Keep the same 220ms fade
   here without moving the frame itself. */
@media(max-width:960px) and (orientation:landscape) and (max-height:560px) and (prefers-reduced-motion:no-preference){
  #about.skill-galaxy-section[data-sg-view="list"] .skill-galaxy-list{
    animation-name:skill-galaxy-landscape-list-in}
}
@keyframes skill-galaxy-landscape-list-in{
  from{opacity:0}
  to{opacity:1}
}

/* Same reasoning, portrait's own stacked sheet: `.skill-galaxy-chip-scrollbar`
   now shows there too (the general rule a few hundred lines up turned it on
   for every mobile width, not just landscape), so the native bar has to go
   there as well — same selector, same specificity as the `scrollbar-width:thin`
   rule it overrides, placed after it purely for source-order priority. */
@media(max-width:960px){
  #about.skill-galaxy-section .skill-galaxy-connected-list,
  .skill-galaxy-panel .skill-galaxy-connected-list{
    scrollbar-width:none;-ms-overflow-style:none}
  #about.skill-galaxy-section .skill-galaxy-connected-list::-webkit-scrollbar,
  .skill-galaxy-panel .skill-galaxy-connected-list::-webkit-scrollbar{
    display:none;width:0;height:0}
}

/* Same reasoning again, desktop's own docked panel: `.skill-galaxy-chip-scrollbar`
   now shows there too, so the native `scrollbar-width:thin` set on the row
   itself has to go, or it doubles the drawn bar directly under it. */
@media(min-width:961px){
  #about.skill-galaxy-section .skill-galaxy-connected-list,
  .skill-galaxy-panel .skill-galaxy-connected-list{
    scrollbar-width:none;-ms-overflow-style:none}
  #about.skill-galaxy-section .skill-galaxy-connected-list::-webkit-scrollbar,
  .skill-galaxy-panel .skill-galaxy-connected-list::-webkit-scrollbar{
    display:none;width:0;height:0}
}

/* ── Sheet close / collapse · header-matched chrome ───────────────────────
   Both buttons sit on the sphere or the sheet's own surface rather than on
   the header's blurred nav bar, so `.theme-toggle`'s own translucent
   `--color-bg-control` reads inconsistently there — solid instead, the exact
   flat colour the header's own controls are built from (`--color-white`,
   `--dark-card`), not the header's blur-composited appearance. Mobile only:
   desktop's panel keeps its transparent, border-only close button. */
@media(max-width:960px){
  .skill-galaxy-panel-close,
  .skill-galaxy-panel-expand{background:var(--color-white)}
  html[data-theme="dark"] .skill-galaxy-panel-close,
  html[data-theme="dark"] .skill-galaxy-panel-expand{background:var(--dark-card)}
}
