/* CODE VOID portfolio page.
   The page is dark regardless of the site-wide light/dark setting, so the palette is
   pinned on html[data-page="portfolio"]. app.js never writes data-page, and the extra
   [data-theme] keeps this more specific than :root[data-theme="dark"] in styles.css. */
html[data-page="portfolio"][data-theme] {
  color-scheme: dark;
  --bg: #05050f;
  --surface: rgba(14, 17, 38, 0.82);
  --text: #e8ecff;
  --muted: #6b7a99;
  --border: rgba(170, 204, 255, 0.2);
  --accent: #4de8ff;
  --accent-strong: #b388ff;
  --accent-soft: rgba(77, 232, 255, 0.12);
  --focus: #4de8ff;
  --shadow: none;
  /* Token colours from the design spec, reused by the HTML overlay tags. */
  --tok-kw: #b388ff;
  --tok-fn: #4de8ff;
  --tok-str: #ffd257;
  --tok-num: #9dffb0;
}

/* No overflow-x:hidden here on purpose: setting it on <body> can promote body to a
   scroll container, which would break window.scrollY / documentElement.scrollHeight
   and therefore the whole scroll-driven timeline. Overflow is contained per element. */
.portfolio-page { background: #05050f; }

.portfolio-page .site-header,
.portfolio-page .site-footer {
  background: rgba(5, 5, 15, 0.72);
  border-color: rgba(170, 204, 255, 0.14);
}

.portfolio-page .segmented { background: rgba(5, 5, 15, 0.5); }
/* CODE VOID is a dark space either way, so the theme control here only carries the
   choice to the rest of the site. Say so rather than leaving a button that looks broken. */
.theme-note {
  flex-basis: 100%;
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
}
.portfolio-page .segmented button[aria-pressed="true"] { color: #05050f; background: var(--accent); }
.portfolio-page .button-link { background: rgba(14, 17, 38, 0.7); }
.portfolio-page .button-link.primary { color: #05050f; background: var(--accent); border-color: var(--accent); }

#void-canvas {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  /* portfolio.js sizes the drawing buffer from window.innerHeight, which on iOS is
     the visible height. A fixed inset:0 box is the taller layout viewport, so the
     scene stretches unless the css height tracks the same thing. */
  height: 100dvh;
}

/* ---------- boot overlay (CSS only, removed by portfolio.js) ---------- */

.void-boot {
  position: fixed;
  inset: 0;
  z-index: 9;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  background: #05050f;
  color: var(--tok-fn);
  font-family: Consolas, "Courier New", monospace;
  font-size: 1rem;
  letter-spacing: 0.08em;
  transition: opacity 0.5s ease;
}
html.has-js body.no-js .void-boot { display: flex; }
.void-boot.is-done { opacity: 0; pointer-events: none; }
.void-boot-line::after { content: "…"; }
.void-boot-cursor {
  width: 0.55rem;
  height: 1.05rem;
  background: var(--tok-fn);
  animation: void-blink 1s steps(1) infinite;
}
@keyframes void-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* ---------- static / fallback presentation (also the no-JS view) ---------- */

.void-scroll { display: none; }

.void-static-note { display: none; }
.no-js .void-static-note,
.void-fallback .void-static-note { display: block; }

.portfolio-page h1 {
  font-family: Consolas, "Courier New", monospace;
  letter-spacing: 0.06em;
}
.portfolio-page .eyebrow { color: var(--accent); }

.void-section-title { margin-top: 2.5rem; }

.void-works {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.void-section-title { grid-column: 1 / -1; }

.work-card {
  padding: 1.15rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.work-name {
  margin: 0 0 0.5rem;
  color: var(--tok-fn);
  font-family: Consolas, "Courier New", monospace;
  font-size: 1.1rem;
}
.work-lead { margin: 0 0 0.5rem; font-weight: 700; }
.work-desc { margin: 0 0 0.9rem; color: var(--muted); }

.work-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 0.9rem;
  padding: 0;
  list-style: none;
}
.work-tags .tag {
  padding: 0.2rem 0.55rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-family: Consolas, "Courier New", monospace;
  font-size: 0.82rem;
  line-height: 1.6;
}
.tag-lang { color: var(--tok-kw); }
.tag-lib { color: var(--tok-fn); }
.tag-feat { color: var(--tok-str); }

.work-status { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0; }
.work-archive {
  margin: 0;
  color: var(--muted);
  font-family: Consolas, "Courier New", monospace;
  font-size: 0.85rem;
}

.void-profile { margin-top: 2.5rem; }
.profile-points {
  margin: 0 0 1.1rem;
  padding-left: 1.1rem;
  max-width: 44rem;
  color: var(--muted);
}
.profile-points li { margin: 0.3rem 0; }
.profile-handle {
  margin: 0 0 0.5rem;
  font-family: Consolas, "Courier New", monospace;
  font-size: 1.25rem;
  font-weight: 700;
}
.profile-word { margin: 0 0 1.1rem; color: var(--muted); max-width: 42rem; }
.profile-links { display: flex; flex-wrap: wrap; gap: 0.65rem; margin: 0; }
.profile-exit {
  margin: 1.6rem 0 0;
  color: var(--muted);
  font-family: Consolas, "Courier New", monospace;
  font-size: 0.95rem;
}

/* Chapter rail, scroll hint and modal chrome only exist in the 3D view. */
body:not(.void-3d) .void-chapters,
body:not(.void-3d) .void-hint,
body:not(.void-3d) .void-backdrop,
body:not(.void-3d) .void-works-rail,
body:not(.void-3d) .work-close { display: none; }

/* The 3D cards are canvas-only, so this rail is the keyboard route to the same
   detail panels. It appears with the cards. */
.void-works-rail {
  position: fixed;
  top: 50%;
  left: clamp(0.6rem, 2vw, 1.5rem);
  z-index: 4;
  transform: translateY(-50%);
}
.void-works-rail[hidden] { display: none; }
.void-works-rail-title {
  margin: 0 0 0.4rem;
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
}
.void-works-rail ul {
  display: grid;
  gap: 0.3rem;
  margin: 0;
  padding: 0;
  max-height: 68vh;
  overflow-y: auto;
  list-style: none;
}
.void-works-rail button {
  width: 100%;
  min-height: 2.2rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  background: rgba(10, 13, 32, 0.7);
  font-family: Consolas, "Courier New", monospace;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}
.void-works-rail button:hover,
.void-works-rail button:focus-visible { border-color: var(--accent); color: var(--accent); }

/* ---------- 3D view ---------- */

.void-3d #void-canvas { display: block; }
.void-3d .void-scroll { display: block; height: 600vh; }

.void-3d main {
  width: 100%;
  margin: 0;
  padding: 0;
}

.void-3d .site-header {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 5;
  backdrop-filter: blur(6px);
}
.void-3d .site-footer {
  position: relative;
  z-index: 1;
  backdrop-filter: blur(6px);
}

/* The document text stays in the DOM and becomes the overlay layer, so nothing is
   duplicated between the 3D view and the fallback view. */
.void-3d .void-doc {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.void-3d .void-intro { display: none; }
/* Only the works heading is dropped in 3D; the profile panel keeps its own heading.
   Scoped this narrowly on purpose: the headings carry data-lang, and styles.css
   reveals the matching one at (0,3,2), so a rule that hides them must outrank that
   without also suppressing the profile heading. */
html[data-lang] body.void-3d .void-doc #works > .void-section-title { display: none; }
.void-3d .void-works { display: block; }
.void-3d .work-card { display: none; }

.void-3d .work-card.is-open {
  display: block;
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: min(34rem, calc(100% - 2rem));
  max-height: min(78vh, 40rem);
  overflow-y: auto;
  transform: translate(-50%, -50%);
  background: rgba(10, 13, 32, 0.94);
  border-color: rgba(77, 232, 255, 0.4);
  pointer-events: auto;
}

.void-3d .void-profile {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  margin: 0;
  padding: 1.4rem clamp(1rem, 5vw, 4rem) 5.5rem;
  /* Stop short of the fixed header on a short window rather than sliding under it. */
  max-height: min(72vh, calc(100vh - var(--void-header-h, 12rem) - 1rem));
  overflow-y: auto;
  background: linear-gradient(to top, rgba(5, 5, 15, 0.95) 55%, rgba(5, 5, 15, 0));
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s ease;
}
.void-3d .void-profile.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
/* The chapter rail sits above the profile panel on the right, so the text has to
   stop short of it. Below 768px the rail moves to the bottom and padding-bottom
   in the narrow media query takes over instead. */
@media (min-width: 768px) { .void-3d .void-profile { padding-right: 5rem; } }
@media (min-width: 1024px) { .void-3d .void-profile { padding-right: 10rem; } }
.void-3d .void-profile .void-section-title { margin-top: 0; }

.void-backdrop {
  position: fixed;
  inset: 0;
  z-index: 2;
  background: rgba(3, 3, 12, 0.74);
}
.void-modal .void-backdrop { z-index: 6; }
.void-modal .void-doc { z-index: 7; }

.work-close {
  min-height: 2.6rem;
  margin-top: 1rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  background: rgba(5, 5, 15, 0.6);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.work-close:hover, .work-close:focus-visible { border-color: var(--accent); }

/* ---------- chapter rail ---------- */

/* Six items clear the header at normal window heights, but the same centring would
   slide under it on a short one, so it uses the same band as the works rail. */
.void-chapters {
  position: fixed;
  top: calc(var(--void-header-h, 12rem) + 0.75rem);
  bottom: 1.5rem;
  right: clamp(0.6rem, 2vw, 1.5rem);
  z-index: 4;
  display: flex;
  flex-direction: column;
  justify-content: center;
  transform: none;
}
.void-chapters ol {
  display: grid;
  gap: 0.55rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.void-chapters button {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  min-height: 2.2rem;
  padding: 0.2rem 0.35rem;
  border: 0;
  border-radius: 6px;
  color: var(--muted);
  background: transparent;
  font: inherit;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}
.void-chapters .chapter-dot {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border: 1px solid currentColor;
  border-radius: 50%;
}
.void-chapters button[aria-current="true"] { color: var(--accent); }
.void-chapters button[aria-current="true"] .chapter-dot { background: currentColor; }
.void-chapters button:hover { color: var(--text); }

.void-hint {
  position: fixed;
  bottom: clamp(1rem, 4vh, 2.5rem);
  left: 50%;
  z-index: 4;
  margin: 0;
  /* It only fades out, so without this it would keep swallowing taps aimed at the
     works rail, which sits at the same height on narrow screens. */
  pointer-events: none;
  transform: translateX(-50%);
  color: var(--muted);
  font-size: 0.85rem;
  letter-spacing: 0.14em;
  transition: opacity 0.4s ease;
}
.void-hint.is-hidden { opacity: 0; }

/* ---------- responsive ---------- */

/* Below this width the labels no longer fit, which left five unlabelled dots
   sitting over the 3D scene with nothing to say what they did. Scrolling reaches
   the same chapters, so the rail is dropped here rather than shown without the
   labels that explain it. */
@media (max-width: 1023px) {
  .void-chapters { display: none; }
}

@media (max-width: 767px) {
  .void-works { grid-template-columns: 1fr; }
  .void-hint { bottom: 3.2rem; }
  .void-3d .void-profile { padding-bottom: 9rem; }

  .void-3d .void-works-rail {
    top: calc(var(--void-header-h, 12rem) + 0.5rem);
    right: auto;
    bottom: auto;
    left: 0.5rem;
    width: min(11rem, calc(50vw - 0.75rem));
    transform: none;
    padding: 0;
    overflow: visible;
  }
  .void-works-rail ul {
    max-height: calc(100vh - var(--void-header-h, 12rem) - 5rem);
    overflow-x: hidden;
    overflow-y: auto;
  }
  .void-works-rail button {
    width: 100%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* iOS Safari lays fixed elements out against the large viewport, so anything
     pinned to the bottom sits behind the toolbar while it is on screen.
     100vh - 100dvh is exactly the toolbar's current height, and zero once it is
     hidden, so these lift by the right amount and no more.
     This block stays last: @supports adds no specificity, so an ordinary rule
     written after it would simply win and drop the lift again. */
  @supports (height: 100dvh) {
    .void-works-rail ul { max-height: calc(100dvh - var(--void-header-h, 12rem) - 5rem - env(safe-area-inset-bottom, 0px)); }
    .void-hint { bottom: calc(100vh - 100dvh + 3.2rem + env(safe-area-inset-bottom, 0px)); }
    .void-3d .void-profile { padding-bottom: calc(9rem + 100vh - 100dvh + env(safe-area-inset-bottom, 0px)); }
  }
}

@media (prefers-reduced-motion: reduce) {
  .void-boot-cursor { animation: none; }
  .void-boot, .void-hint, .void-3d .void-profile { transition: none; }
}
