/* ============================================================
   Developer Portfolio, Viktor Bozzay
   Light / Dark themes · fully responsive · no frameworks
   ============================================================ */

:root {
  --font-display: 'Sora', system-ui, sans-serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;

  --accent: #6366f1;
  --accent-2: #a855f7;
  --accent-3: #ec4899;
  --gradient: linear-gradient(120deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);

  --radius: 18px;
  --radius-sm: 12px;
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 10px 30px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.18);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- Light theme ---------- */
[data-theme="light"] {
  --bg: #f6f7fb;
  --bg-soft: #eef0f8;
  --surface: #ffffff;
  --surface-2: #f1f2fa;
  --text: #181b2e;
  --text-muted: #5b6278;
  --border: rgba(24, 27, 46, 0.1);
  --nav-bg: rgba(255, 255, 255, 0.75);
  --card-bg: rgba(255, 255, 255, 0.85);
  --orb-opacity: 0.55;
  --terminal-bg: #0f1226;
  --terminal-text: #c9d1ff;
  --chip-bg: #eef0f8;
  --chip-text: #3c4257;
  --shadow-sm: 0 2px 10px rgba(24, 27, 46, 0.06);
  --shadow-md: 0 12px 32px rgba(24, 27, 46, 0.1);
  --shadow-lg: 0 24px 60px rgba(24, 27, 46, 0.16);
}

/* ---------- Dark theme ---------- */
[data-theme="dark"] {
  --bg: #0b0d1a;
  --bg-soft: #0f1226;
  --surface: #151930;
  --surface-2: #1b2040;
  --text: #eef0ff;
  --text-muted: #9aa1c0;
  --border: rgba(238, 240, 255, 0.1);
  --nav-bg: rgba(11, 13, 26, 0.75);
  --card-bg: rgba(21, 25, 48, 0.8);
  --orb-opacity: 0.5;
  --terminal-bg: #07081a;
  --terminal-text: #c9d1ff;
  --chip-bg: #1b2040;
  --chip-text: #c3c9e8;
}

/* ============================================================
   Base
   ============================================================ */
* { margin: 0; padding: 0; box-sizing: border-box; }

/* The hidden attribute must win over any author display rule (e.g. .modal { display: flex }) */
[hidden] { display: none !important; }

/* No grabbing images off the page: dragging a thumbnail out (to the desktop,
   into another tab) is off, and in the carousel a native image drag also
   fights the swipe handler. -webkit-user-drag covers Chrome and Safari;
   Firefox needs the dragstart guard in scripts/main.js, which is also what
   catches images rendered later. */
img {
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: 90px;
}

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  overflow-x: hidden;
  transition: background 0.4s var(--ease), color 0.4s var(--ease);
}

h1, h2, h3 { font-family: var(--font-display); line-height: 1.2; }

a { color: inherit; text-decoration: none; }

::selection { background: var(--accent); color: #fff; }

/* Scrollbar */
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: linear-gradient(var(--accent), var(--accent-3)); border-radius: 20px; }

/* ---------- Background orbs ---------- */
.bg-orbs { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
.orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  opacity: var(--orb-opacity);
  transition: opacity 0.5s;
}
.orb-1 { width: 480px; height: 480px; background: #6366f1; top: -140px; left: -120px; animation: float 16s ease-in-out infinite; }
.orb-2 { width: 420px; height: 420px; background: #a855f7; bottom: -160px; right: -100px; animation: float 20s ease-in-out infinite reverse; }
.orb-3 { width: 300px; height: 300px; background: #ec4899; top: 40%; left: 55%; opacity: calc(var(--orb-opacity) * 0.5); animation: float 24s ease-in-out infinite; }

@keyframes float {
  0%, 100% { transform: translate(0, 0) scale(1); }
  33% { transform: translate(50px, -40px) scale(1.26); }
  66% { transform: translate(-90px, 64px) scale(0.76); }
}

/* ============================================================
   Navbar
   ============================================================ */
.navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
  transition: background 0.4s var(--ease);
}

.nav-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 24px;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.logo {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1.15rem;
  background: var(--gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  letter-spacing: -0.5px;
}

.nav-links { display: flex; gap: 28px; }

.nav-links a {
  position: relative;
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--text-muted);
  transition: color 0.25s;
}
.nav-links a::after {
  content: '';
  position: absolute;
  left: 0; bottom: -6px;
  width: 100%; height: 2px;
  border-radius: 2px;
  background: var(--gradient);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s var(--ease);
}
.nav-links a:hover { color: var(--text); }
.nav-links a:hover::after { transform: scaleX(1); }

/* Current section, set by the scrollspy. The underline is the same one the
   hover state uses, just held open, so the two never fight over the element.
   Weight stays put on purpose: bolding the active link would re-measure the
   text and make the whole menu shift sideways as you scroll. */
.nav-links a.active { color: var(--text); }
.nav-links a.active::after { transform: scaleX(1); }

.nav-controls { display: flex; align-items: center; gap: 14px; }

/* Language switcher. Desktop: the six codes in a pill row. The dropdown
   trigger and the full language names belong to the mobile shape (see the
   768px block) and are inert here. */
.lang-switcher {
  position: relative;
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.lang-menu { display: flex; gap: 4px; }
.lang-current { display: none; }
.lang-name { display: none; }
/* Text labels (EN, DE, HU…) since the GB flag no longer stands in for
   English — language codes stay crisp at any size, no emoji font needed. */
.lang-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: none;
  background: transparent;
  color: var(--text-muted); /* buttons do not inherit color, without this, UA default (dark) text is invisible on dark theme */
  font-family: var(--font-mono);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  line-height: 1;
  padding: 6px 9px;
  border-radius: 999px;
  cursor: pointer;
  opacity: 0.65;
  transition: all 0.25s var(--ease);
}
.lang-btn:hover { opacity: 1; color: var(--text); transform: translateY(-1px); }
.lang-btn.active { opacity: 1; color: var(--accent); background: var(--surface); box-shadow: var(--shadow-sm); transform: translateY(-1px); }

/* CV button */
.nav-cv-link { display: none; }
.btn-cv {
  display: inline-flex;
  align-items: center;
  padding: 9px 20px;
  border-radius: 999px;
  background: var(--gradient);
  color: #fff;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.88rem;
  letter-spacing: 0.3px;
  box-shadow: 0 6px 18px rgba(99, 102, 241, 0.35);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.btn-cv:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(99, 102, 241, 0.5); }

/* Theme toggle */
.theme-toggle {
  position: relative;
  width: 42px; height: 42px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: all 0.3s var(--ease);
}
.theme-toggle:hover { transform: rotate(15deg) scale(1.05); box-shadow: var(--shadow-sm); }
.theme-toggle svg { width: 20px; height: 20px; grid-area: 1/1; transition: transform 0.4s var(--ease), opacity 0.3s; }
.theme-toggle .icon-sun { opacity: 0; transform: rotate(90deg) scale(0.4); }
.theme-toggle .icon-moon { opacity: 1; transform: rotate(0) scale(1); }
[data-theme="light"] .theme-toggle .icon-sun { opacity: 1; transform: rotate(0) scale(1); }
[data-theme="light"] .theme-toggle .icon-moon { opacity: 0; transform: rotate(-90deg) scale(0.4); }

/* Hamburger */
.hamburger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 42px; height: 42px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
  cursor: pointer;
  padding: 10px;
}
.hamburger span { display: block; height: 2px; border-radius: 2px; background: var(--text); transition: 0.3s var(--ease); }
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ============================================================
   Hero
   ============================================================ */
/* Same 1200px column as .section, so the hero lines up with every other
   part of the page; only the top padding is taller, for the fixed navbar. */
.hero {
  max-width: 1200px;
  margin: 0 auto;
  padding: 160px 24px 90px;
  min-height: 100vh;
  display: flex;
  align-items: center;
}

/* Top-aligned, not centred: the right column carries two windows and is far
   taller than the copy, so centring pushed the badge and the title well
   below the terminal's top edge. */
.hero-grid {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 60px;
  align-items: start;
  width: 100%;
}

.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  margin-bottom: 24px;
}
.pulse-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #22c55e;
  animation: pulse 2s infinite;
}
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5); }
  70% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}

.hero-title {
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  font-weight: 800;
  letter-spacing: -1.5px;
  margin-bottom: 10px;
}
.gradient-text {
  background: var(--gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-role {
  font-family: var(--font-mono);
  font-size: clamp(1.05rem, 2.4vw, 1.35rem);
  color: var(--accent);
  font-weight: 600;
  line-height: 1.5;
  min-height: 3em; /* reserve 2 lines so the typing effect never changes column height */
  display: flex;
  align-items: center;
  gap: 3px;
}
.type-cursor {
  width: 9px; height: 1.3em;
  background: var(--accent);
  border-radius: 2px;
  animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* No max-width on the hero copy: it fills its grid column, which is what
   carries the full-bleed hero out to the edges of the screen. */
.hero-desc {
  color: var(--text-muted);
  font-size: 1.06rem;
  margin: 18px 0 22px;
}

.hero-meta {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding: 9px 18px;
  border-radius: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 0.86rem;
  color: var(--text-muted);
  margin-bottom: 18px;
}

.hero-story {
  font-size: 0.92rem;
  line-height: 1.7;
  color: var(--text-muted);
  background: linear-gradient(90deg, rgba(99, 102, 241, 0.09), transparent 70%);
  border-left: 3px solid var(--accent);
  padding: 11px 18px;
  border-radius: 0 12px 12px 0;
  margin-bottom: 26px;
}

.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 28px;
  border-radius: 14px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.97rem;
  cursor: pointer;
  transition: all 0.3s var(--ease);
  border: none;
}
.btn-primary {
  background: var(--gradient);
  color: #fff;
  box-shadow: 0 10px 24px rgba(99, 102, 241, 0.35);
}
.btn-primary:hover { transform: translateY(-3px); box-shadow: 0 16px 34px rgba(99, 102, 241, 0.45); }
.btn-ghost {
  background: transparent;
  color: var(--text);
  border: 1.5px solid var(--border);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-3px); }

/* Stats (shown under the Projects section subtitle) */
.stats-row {
  display: flex;
  justify-content: center;
  gap: 36px;
  margin-top: 32px;
  flex-wrap: wrap;
}
.stat { display: flex; flex-direction: column; }
.stat-num {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 800;
  background: var(--gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  line-height: 1.1;
}
.stat-label { font-size: 0.85rem; color: var(--text-muted); margin-top: 4px; }

/* Terminal */
.hero-terminal { min-width: 0; animation: rise 0.9s var(--ease) 0.2s both; }
@keyframes rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: translateY(0); } }

.terminal {
  background: var(--terminal-bg);
  border-radius: 16px;
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--border);
  overflow: hidden;
  transform: perspective(1200px) rotateY(-4deg) rotateX(2deg);
  transition: transform 0.5s var(--ease);
}
.hero-terminal:hover .terminal { transform: perspective(1200px) rotateY(0deg) rotateX(0deg); }

.terminal-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 13px 16px;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.red { background: #ff5f57; }
.dot.yellow { background: #febc2e; }
.dot.green { background: #28c840; }
.terminal-title {
  margin-left: 10px;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: #6b729c;
}

/* ---- Showcase: the second window, project thumbnails on a loop ----
   Same .terminal chrome as the zsh window above it (dots, title bar, tilt,
   hover straighten — all inherited from .terminal) and the same width, so
   the two read as one stack. The stage is a 16:10 frame that scales with
   the column; the thumbnails are 630x330 and keep that ratio, so the frame
   is a touch squarer than they are and leaves a margin all round. */
.showcase {
  margin-top: 18px;
  width: 100%;
}
.showcase-stage {
  display: grid;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 45%, rgba(99, 102, 241, 0.16), transparent 62%),
    var(--terminal-bg);
}
/* All thumbnails share one grid cell, so they stack centred without
   absolute positioning — which keeps the keyframes free of centring
   offsets. Width is set and height follows, so each image keeps its own
   aspect ratio; --i (set on the element) is its slot in the deck.

   The delay is NEGATIVE: every card starts its loop already part-way
   through, so the deck is a full pile from the first frame instead of the
   whole pack sitting on top of each other until their turn comes. Both the
   cycle length and the card count come from showcase.js, which sets them on
   the stage. */
.showcase-img {
  grid-area: 1 / 1;
  place-self: center;
  cursor: zoom-in; /* same promise the project card thumbnails make */
  width: 84%;
  height: auto;
  max-height: 86%;
  object-fit: contain;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  will-change: transform;
  animation: showcase-deal var(--showcase-cycle, 16s) linear infinite;
  animation-delay: calc(var(--i) * var(--showcase-cycle, 16s) / calc(-1 * var(--showcase-count, 8)));
}
/* The @keyframes for the deck are NOT here: their percentages depend on how
   many projects are in the deck (one slot each), so they are generated in
   scripts/ui/showcase.js, which also renders the cards. Only the layout of
   the window and the card box lives in this file. */
/* The global reduced-motion rule collapses every animation to 0.01ms, which
   would freeze the whole pack on its last frame, stacked dead centre. So:
   no loop, and a static version of the same fan — the first six cards laid
   out one per depth, the rest tucked inside the deck as they would be
   mid-animation. The numbers mirror the depth() helper in showcase.js. */
@media (prefers-reduced-motion: reduce) {
  .showcase-img {
    animation: none;
    z-index: calc(30 - var(--i));
    transform:
      translateY(calc(var(--i) * 0.6%))
      scale(calc(1 - var(--i) * 0.018))
      rotate(calc(var(--i) * -2.6deg));
  }
  .showcase-img:nth-child(n + 7) {
    z-index: 22;
    transform: scale(0.88);
    box-shadow: none; /* inside the deck: no shadow to bleed out */
  }
}

.terminal-body {
  padding: 20px 22px 24px;
  font-family: var(--font-mono);
  font-size: 0.86rem;
  line-height: 1.9;
  color: var(--terminal-text);
}
.terminal-body p { white-space: normal; overflow-wrap: break-word; }
.t-prompt { color: #22d3ee; font-weight: 600; }
.t-cmd { color: #e5e7ff; }
.t-out { color: #9aa1c0; }
/* Folder names stay on one line, no break after the hyphen in e.g. "ba-team" */
.t-dir { white-space: nowrap; }
.t-out.success { color: #34d399; }
.t-cursor { color: #22d3ee; animation: blink 1s step-end infinite; }

/* ============================================================
   Sections
   ============================================================ */
.section {
  max-width: 1200px;
  margin: 0 auto;
  padding: 90px 24px;
}
/* Scroll-reveal offsets are transforms, and a transform still counts toward
   the page's scrollable area: the not-yet-revealed fade-left cards sit 40px
   to the right of where they will land, which made the whole page pannable
   sideways until the visitor scrolled past them (and the transform went to
   none). Clipping the sections horizontally removes that at the source, for
   every reveal direction, present and future. clip (not hidden) leaves the
   vertical axis alone, so nothing here becomes a scroll container, and the
   margin keeps the card hover shadows from being cut at the section edge. */
.section,
.hero {
  overflow-x: clip;
  overflow-clip-margin: 16px;
}
.section-head { text-align: center; margin-bottom: 52px; }
.section-kicker {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 12px;
}
.section-title {
  font-size: clamp(1.8rem, 3.6vw, 2.5rem);
  font-weight: 800;
  letter-spacing: -1px;
  margin-bottom: 14px;
}
.section-sub {
  color: var(--text-muted);
  max-width: 620px;
  margin: 0 auto;
  font-size: 1.02rem;
}
.section-cta { margin-top: 22px; }

/* ============================================================
   Filters + view toggle toolbar
   ============================================================ */
.projects-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 44px;
}
.filters {
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Carousel / grid segmented control. The active state is synced from
   state.view by scripts/render/projects.js. */
.view-toggle {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.view-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 18px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.88rem;
  cursor: pointer;
  transition: all 0.25s var(--ease);
}
.view-toggle-btn svg { width: 15px; height: 15px; }
.view-toggle-btn:hover { color: var(--text); }
.view-toggle-btn.active {
  background: var(--gradient);
  color: #fff;
  box-shadow: 0 8px 20px rgba(99, 102, 241, 0.35);
}
.filter-chip {
  padding: 9px 20px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.25s var(--ease);
}
.filter-chip:hover { color: var(--text); transform: translateY(-2px); border-color: var(--accent); }
.filter-chip.active {
  background: var(--gradient);
  color: #fff;
  border-color: transparent;
  box-shadow: 0 8px 20px rgba(99, 102, 241, 0.35);
}

/* ============================================================
   Projects grid
   ============================================================ */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 28px;
}

.project-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.35s;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* The hover lift is a grid-view interaction. In the carousel the slide's
   own transform IS its coverflow position, so a hover transform on the
   card would yank it out of the stage and jitter (hover → card moves away
   from the pointer → un-hover → snap back → …). Scoped to the grid with
   :not(.is-carousel). */
.projects-grid:not(.is-carousel) .project-card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-lg);
  border-color: rgba(99, 102, 241, 0.4);
}

.card-media {
  position: relative;
  aspect-ratio: 630 / 330;
  overflow: hidden;
  background: var(--bg-soft);
}
.card-media-btn {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  text-align: left;
}
.card-media-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease);
}
.projects-grid:not(.is-carousel) .project-card:hover .card-media img { transform: scale(1.07); }
.card-zoom {
  position: absolute;
  right: 14px;
  bottom: 14px;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(11, 13, 26, 0.65);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.18);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(6px) scale(0.9);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
  pointer-events: none;
}
.card-zoom svg { width: 17px; height: 17px; }
.projects-grid:not(.is-carousel) .project-card:hover .card-zoom { opacity: 1; transform: translateY(0) scale(1); }
/* In the carousel the zoom badge is only meaningful on the active slide
   (the lightbox opens from there); flanking slides are click-to-advance. */
.carousel-slide:not([data-state="active"]) .card-zoom { display: none; }

/* One badge per category the project declares, stacked in a row over the
   top-left of the image and wrapping if a project carries several. */
.card-categories {
  position: absolute;
  top: 14px; left: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-width: calc(100% - 28px);
  transition: opacity 0.3s var(--ease);
}
/* The badges step aside on hover: the pointer is on the card to look at the
   image (and the zoom badge is fading in at the same moment), so the labels
   have done their job. Opacity only — nothing that moves, so it is safe on
   the carousel slides too, whose transform is their position. */
.project-card:hover .card-categories { opacity: 0; }
.card-category {
  padding: 5px 14px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  background: rgba(11, 13, 26, 0.65);
  color: #fff;
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.18);
  transition: opacity 0.3s var(--ease);
}

.card-body { padding: 22px; display: flex; flex-direction: column; flex: 1; }

.card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.card-title { font-size: 1.22rem; font-weight: 700; letter-spacing: -0.3px; }
.card-sub { font-size: 0.8rem; color: var(--text-muted); font-family: var(--font-mono); }

/* Tabs */
.card-tabs {
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--surface-2);
  border-radius: 11px;
  margin-bottom: 14px;
}
.card-tab {
  flex: 1;
  padding: 8px 10px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  transition: all 0.25s var(--ease);
}
.card-tab:hover { color: var(--text); }
.card-tab.active {
  background: var(--surface);
  color: var(--accent);
  box-shadow: var(--shadow-sm);
}
/* Inset, so the ring stays inside the tablist pill instead of overlapping it. */
.card-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.card-desc {
  color: var(--text-muted);
  font-size: 0.93rem;
  height: 150px;
  flex-shrink: 0;
  overflow-y: auto;
  padding-right: 6px;
  scrollbar-width: thin;
  scrollbar-color: rgba(99, 102, 241, 0.45) transparent;
}
/* The panel is focusable (WAI Tabs: a panel with no focusable content takes
   tabindex="0"), which also gives the keyboard a way to scroll it. */
.card-desc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-desc::-webkit-scrollbar { width: 6px; }
.card-desc::-webkit-scrollbar-track { background: transparent; }
.card-desc::-webkit-scrollbar-thumb { background: rgba(99, 102, 241, 0.45); border-radius: 6px; }
.card-desc.fade-in { animation: fadeSlide 0.35s var(--ease); }
@keyframes fadeSlide {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Tech tags */
.card-tech { display: flex; flex-wrap: wrap; gap: 7px; margin: 16px 0 18px; }
.tech-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 12px;
  border-radius: 8px;
  background: var(--chip-bg);
  color: var(--chip-text);
  font-size: 0.76rem;
  font-weight: 500;
  font-family: var(--font-mono);
}
.tech-tag img { width: 15px; height: 15px; flex-shrink: 0; }

/* Card links. The row never wraps: with three links (repo / demo / npm) a
   wrapped third button broke the card rhythm and, in the carousel, made the
   slides unequal. The buttons share the row equally (flex: 1 1 0 + min-width: 0)
   and the label ellipsises on the narrowest cards instead of pushing a button
   onto a second line. */
.card-links { display: flex; gap: 8px; flex-wrap: nowrap; margin-top: auto; }
.card-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 0;
  min-width: 0;
  gap: 6px;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 0.84rem;
  font-weight: 600;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  transition: all 0.25s var(--ease);
}
.card-link-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card-link:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-2px); }
.card-link.primary {
  background: var(--gradient);
  color: #fff;
  border-color: transparent;
}
.card-link.primary:hover { box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4); }
.card-link svg { width: 15px; height: 15px; flex-shrink: 0; }

/* The card entrance is a data-reveal now, see the scroll reveal block. The
   keyframe version it replaces ran with animation-fill-mode: forwards, and a
   filled animation outranks normal declarations in the cascade, so its final
   transform: translateY(0) was permanently beating .project-card:hover and
   the cards never lifted on hover. */

/* ============================================================
   Projects carousel (coverflow, ported from the CV repo)
   ============================================================ */
/* In carousel mode the grid container is a plain block; the stage positions
   the slides absolutely. The card hover lift is scoped to grid mode only
   (see .project-card:hover above), so it never fights the coverflow
   transforms. */
.projects-grid.is-carousel { display: block; }

/* Flanking slides are clickable (click = advance), so hovering them raises
   their dimmed opacity as a cue. The coverflow transform is their
   position, so it must never change on hover — that would tear the card
   out of the stage and, because the card moves away from under the
   pointer, trigger an endless hover/un-hover oscillation. */
.carousel-slide[data-state="prev"]:hover,
.carousel-slide[data-state="next"]:hover {
  opacity: 0.65;
}
/* The zoom-in cursor belongs to the lightbox (grid cards + the active
   slide). On flanking slides a click advances the carousel, so the media
   button must not promise a zoom — the card's pointer cursor applies. */
.carousel-slide:not([data-state="active"]) .card-media-btn { cursor: pointer; }

/* The coverflow paints far outside the stage: the flanking slides sit at
   ±0.68 slide widths and the hidden ones (opacity: 0, but still part of the
   scrollable overflow) at ±1.15, which on anything narrower than ~1300px
   reaches past the viewport and gives the whole page a horizontal scroll.
   overflow-x: clip cuts that at the section's content box — unlike
   overflow: hidden it does NOT force the other axis to auto, so the stage
   keeps spilling its shadows vertically and nothing becomes a scroll
   container. The clip margin leaves room for the active slide's shadow and
   stays inside the section's 24px / 18px padding, so it can never reach the
   viewport edge. */
.projects-carousel {
  position: relative;
  overflow-x: clip;
  overflow-clip-margin: 12px;
}

.carousel-stage {
  --slide-w: 420px;
  position: relative;
  height: 620px;
  perspective: 1100px;
  perspective-origin: 50% 32%;
  overflow: visible;
  outline: none;
  touch-action: pan-y;
}
.carousel-stage:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
  border-radius: var(--radius);
}

.carousel-slide {
  position: absolute;
  width: var(--slide-w);
  height: 100%; /* fill the fixed-height stage → every slide is the same height */
  left: 50%;
  top: 0;
  margin-left: calc(var(--slide-w) / -2);
  background: var(--surface);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition:
    transform 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    opacity 0.45s ease,
    box-shadow 0.45s ease;
  will-change: transform, opacity;
  user-select: none;
  -webkit-user-select: none;
  /* The stage allows vertical pans (page + the desc panel scroll), so a
     horizontal gesture is handed to the drag handlers in JS. */
  touch-action: pan-y;
}
/* While the pointer drags the coverflow, the per-state transition would
   lag behind the finger; disabled for the duration, re-enabled on release
   so the settle (page or snap-back) animates. */
.carousel-stage.is-dragging .carousel-slide { transition: none; }

/* A swipe has to start anywhere on a slide, not just over the title. The
   scrollable regions inside one — the project description, the skills chip
   block — default to touch-action: auto, so the browser claimed the
   horizontal gesture for a pan of its own and fired pointercancel at the
   drag handler, which then bailed out. pan-y on the whole subtree keeps
   their vertical scrolling (and the page's) while handing every horizontal
   gesture to the JS, the same deal the stage and the slides already have.
   Nothing inside a slide scrolls sideways, so this costs nothing and any
   element added later is covered by default. */
.carousel-slide * { touch-action: pan-y; }
/* Equal-height cards: the media keeps its aspect ratio, the body fills the
   rest, and the description absorbs whatever is left and scrolls inside it.
   Grid cards keep their fixed 150px description (see .card-desc above). */
.carousel-slide .card-media { flex-shrink: 0; }
.carousel-slide .card-body { flex: 1; min-height: 0; }
.carousel-slide .card-desc {
  height: auto;
  flex: 1 1 0;
  min-height: 0;
}
/* Icon-only tech tags: compact square chips, no label padding. */
.carousel-slide .tech-tag {
  padding: 4px;
  gap: 0;
}
/* Slides are narrower than grid cards (down to 78vw on phones), so the
   three-button row gets a tighter box to keep "Forrás / Demo / npm" on one
   line without the labels truncating. */
.carousel-slide .card-links { gap: 6px; }
.carousel-slide .card-link {
  padding: 8px 9px;
  gap: 5px;
  font-size: 0.8rem;
}

/* Coverflow positions, scaled from the CV repo's index page. The offsets
   are fractions of the slide width so the layout survives any slide size. */
/* Every position starts with --drag-x, the live finger offset set by the
   drag handlers (scripts/ui/carousel.js). All states move together so the
   coverflow feels like one rigid row you push sideways; 0 by default, so
   the static layout is unchanged. */
.carousel-slide[data-state="active"] {
  transform: translateX(var(--drag-x, 0px)) translateZ(0) rotateY(0deg);
  opacity: 1;
  z-index: 10;
  box-shadow: var(--shadow-lg);
}
.carousel-slide[data-state="prev"],
.carousel-slide[data-state="next"] {
  opacity: 0.45;
  z-index: 5;
  cursor: pointer;
}
.carousel-slide[data-state="prev"] {
  transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * -0.68)) translateZ(-70px) rotateY(42deg) scale(0.82);
}
.carousel-slide[data-state="next"] {
  transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * 0.68)) translateZ(-70px) rotateY(-42deg) scale(0.82);
}
.carousel-slide[data-state="hidden-prev"],
.carousel-slide[data-state="hidden-next"] {
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}
.carousel-slide[data-state="hidden-prev"] {
  transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * -1.15)) translateZ(-130px) rotateY(55deg) scale(0.6);
}
.carousel-slide[data-state="hidden-next"] {
  transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * 1.15)) translateZ(-130px) rotateY(-55deg) scale(0.6);
}

.carousel-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 18px 0 6px;
}
.carousel-arrow {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  display: grid;
  place-items: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: all 0.25s var(--ease);
}
.carousel-arrow:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-2px); }
.carousel-arrow svg { width: 18px; height: 18px; }

.carousel-dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 7px;
  max-width: 440px;
}
.carousel-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  background: var(--chip-bg);
  padding: 0;
  cursor: pointer;
  transition: all 0.2s var(--ease);
}
.carousel-dot:hover { transform: scale(1.2); border-color: var(--accent); }
.carousel-dot.active {
  background: var(--accent);
  transform: scale(1.4);
  box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
}

/* ============================================================
   Skills
   ============================================================ */
.skills-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
}
/* Carousel view: like the projects grid, the container becomes a plain
   block and the stage positions the slides. */
.skills-groups.is-carousel { display: block; }
/* The toolbar here holds only the view toggle, so it sits on the right the
   way it does next to the project filters. */
.skills-toolbar { justify-content: flex-end; }

/* A skill group is a title plus a handful of chips — far shorter than a
   project card, so the stage is shorter too, and the slide is sized for a
   chip block rather than a 16:9 image. */
.skills-carousel .carousel-stage {
  --slide-w: 380px;
  height: 340px;
}
/* Hovering a flanking slide only brightens it (the click-to-advance cue
   from .carousel-slide[data-state="prev"]:hover); the border tint is the
   whole hover feedback here, nothing that moves. */
.skills-groups.is-carousel .skill-group:hover { border-color: rgba(99, 102, 241, 0.35); }
.skill-group.carousel-slide {
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow: hidden;
}
/* Many chips (10 in the longest group) must not spill out of the fixed
   stage height; the block scrolls inside the slide instead. */
.skill-group.carousel-slide .skill-chips {
  align-content: flex-start;
  overflow-y: auto;
  min-height: 0;
  flex: 1;
  padding-right: 4px;
  scrollbar-width: thin;
  scrollbar-color: rgba(99, 102, 241, 0.45) transparent;
}
.skill-group {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
  backdrop-filter: blur(8px);
  transition: transform 0.3s var(--ease), border-color 0.3s;
}
/* Grid-only hover lift. In the carousel the slide's own transform IS its
   coverflow position, so any transform on hover yanks the card out of the
   stage — and because the card then moves out from under the pointer, it
   un-hovers, snaps back, re-hovers… a visible jitter. Same reason the
   project cards scope their lift to :not(.is-carousel). */
.skills-groups:not(.is-carousel) .skill-group:hover {
  transform: translateY(-4px);
  border-color: rgba(99, 102, 241, 0.35);
}
.skill-group-title {
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 16px;
  font-family: var(--font-mono);
}
.skill-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 11px;
  border-radius: 8px;
  background: var(--chip-bg);
  color: var(--chip-text);
  font-size: 0.75rem;
  font-weight: 500;
  border: 1px solid var(--border);
  transition: all 0.25s var(--ease);
  cursor: default;
}
.chip img {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  transition: transform 0.25s var(--ease);
}
.chip:hover {
  transform: translateY(-3px) scale(1.03);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: var(--shadow-sm);
}
.chip:hover img { transform: scale(1.2) rotate(-4deg); }

/* ============================================================
   Contact
   ============================================================ */
/* Fluid like .projects-grid: the track count follows the available width,
   so the cards wrap instead of overflowing to the right on narrow screens. */
.contact-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}
/* The horizontal padding is load-bearing: with the 12px gap it leaves ~197px
   of content width, and the longest value (the 23-char email in 0.85rem
   JetBrains Mono) needs ~188px to stay on one line. Widening either the gap
   or this padding breaks the email onto a second line. */
.contact-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  padding: 26px 12px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  backdrop-filter: blur(8px);
  transition: all 0.3s var(--ease);
}
.contact-card:hover {
  transform: translateY(-6px);
  border-color: rgba(99, 102, 241, 0.5);
  box-shadow: var(--shadow-lg);
}
.contact-icon {
  font-size: 4.8rem;
  margin-bottom: 8px;
  transition: transform 0.35s var(--ease);
}
.contact-icon svg {
  width: clamp(3.6rem, 12.8vw, 4.8rem);
  height: clamp(3.6rem, 12.8vw, 4.8rem);
  display: block;
}
.contact-icon .contact-img {
  width: clamp(4rem, 15.2vw, 5.76rem);
  height: clamp(4rem, 15.2vw, 5.76rem);
  max-width: 100%;
  object-fit: cover;
  border-radius: 16px;
  display: block;
}
.contact-card:hover .contact-icon { transform: scale(1.2) rotate(-6deg); }
.contact-name { font-weight: 700; font-size: 1.05rem; }
/* Long mono values (e.g. the email address) must wrap, not widen the track. */
.contact-value {
  font-size: 0.85rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* ============================================================
   Timeline
   ============================================================ */
.timeline-list {
  position: relative;
  max-width: 900px;
  margin: 0 auto;
  padding: 10px 0;
}
.timeline-list::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 3px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, var(--accent), var(--accent-3));
  border-radius: 3px;
  opacity: 0.45;
}
.timeline-item {
  position: relative;
  width: 50%;
  padding: 0 44px 40px 0;
}
.timeline-item:nth-child(even) {
  margin-left: 50%;
  padding: 0 0 40px 44px;
}
.timeline-item:last-child { padding-bottom: 0; }
.timeline-dot {
  position: absolute;
  top: 6px;
  right: -8px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--gradient);
  box-shadow: 0 0 0 4px var(--bg), 0 0 14px rgba(99, 102, 241, 0.55);
  z-index: 1;
}
.timeline-item:nth-child(even) .timeline-dot { right: auto; left: -8px; }
.timeline-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px;
  backdrop-filter: blur(8px);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease), border-color 0.3s;
}
.timeline-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: rgba(99, 102, 241, 0.4);
}
.timeline-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.timeline-period {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--accent);
  letter-spacing: 0.3px;
}
.timeline-badge {
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  background: linear-gradient(120deg, #22c55e, #16a34a);
  color: #fff;
}
.timeline-role { font-size: 1.1rem; font-weight: 700; margin-bottom: 2px; }
.timeline-company {
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--text);
  opacity: 0.85;
  margin-bottom: 8px;
}
.timeline-desc { font-size: 0.88rem; color: var(--text-muted); }
/* Compact view: period, role and company only. Toggled by the toolbar
   (scripts/render/timeline.js), compact being the phone default. */
.timeline-list.is-compact .timeline-desc { display: none; }
/* Only the toggle sits in this toolbar, so it lines up on the right, the
   same edge the skills toggle sits on. */
.timeline-toolbar { justify-content: flex-end; margin-top: -18px; }

/* ============================================================
   Footer
   ============================================================ */
.footer {
  border-top: 1px solid var(--border);
  padding: 34px 24px;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.9rem;
}
.footer-built { font-size: 0.8rem; margin-top: 6px; opacity: 0.8; }

/* Back to top */
.back-to-top {
  position: fixed;
  bottom: 26px; right: 26px;
  width: 48px; height: 48px;
  border-radius: 50%;
  border: none;
  background: var(--gradient);
  color: #fff;
  cursor: pointer;
  display: grid;
  place-items: center;
  opacity: 0;
  transform: translateY(16px);
  pointer-events: none;
  transition: all 0.35s var(--ease);
  box-shadow: 0 10px 24px rgba(99, 102, 241, 0.4);
  z-index: 90;
}
.back-to-top.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
.back-to-top:hover { transform: translateY(-4px); }
.back-to-top svg { width: 20px; height: 20px; }


/* ============================================================
   Image loading placeholders
   ============================================================ */
/* While an <img> is still fetching, its slot shows a loader so the page
   reads as "working" on a slow connection (DevTools 3G) instead of
   staring back with empty holes. Big images (project cards, lightbox)
   get the robot loader, small icons a compact ring. State: the frame
   shows the loader until .is-loaded lands on it (set by
   scripts/ui/image-loader.js). */

/* The wrapper centers the loader without fighting its own animated
   transform (panex / eggRst drive transform on the .loader itself). */
.img-loader {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.img-frame { position: relative; }
.img-frame .img-loader { display: none; }
.img-frame:not(.is-loaded) .img-loader { display: grid; }

/* Big image: project card thumbnails. The hover zoom keeps its own
   transform transition, the opacity fade is added next to it. */
.card-media-btn img {
  opacity: 0;
  transition: opacity 0.4s var(--ease), transform 0.6s var(--ease);
}
.card-media-btn.is-loaded img { opacity: 1; }

/* Small icons: tech tags, skill chips, contact cards. */
.tech-tag img { opacity: 0; transition: opacity 0.4s var(--ease); }
.tech-tag.is-loaded img { opacity: 1; }
.chip img { opacity: 0; transition: opacity 0.4s var(--ease), transform 0.25s var(--ease); }
.chip.is-loaded img { opacity: 1; }
.contact-icon .contact-img { opacity: 0; transition: opacity 0.4s var(--ease); }
.contact-icon.is-loaded .contact-img { opacity: 1; }

/* Anchor the ring over the icon slot (the first flex item) instead of
   the middle of the whole chip, which also holds the label text. */
.tech-tag .img-loader,
.chip .img-loader {
  inset: 0 auto 0 0;
  width: 15px;
}
.chip .img-loader { width: 13px; }

/* Compact ring spinner for the small icons. */
.mini-loader {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: imgSpin 0.8s linear infinite;
}
@keyframes imgSpin { to { transform: rotate(360deg); } }

/* Pan & egg loader, kept as the .loader-pan variant (not used in the
   markup, preserved on request). --size scales the whole drawing, the
   default 1px renders it about 120 wide with the egg above the pan. */
.loader-pan {
  --color-1: #3e494d;
  --color-2: #5d6063;
  --color-3: #6c4924;
  --color-4: #4b2d21;
  --color-5: #4d5457;
  --color-6: #9f9e9e;
  --color-7: #fff;
  --color-8: #fff6;
  --color-9: #ffc400;
  --color-10: #ffae00;
  --color-11: #0002;
  --color-12: #0003;
  --size: 1px;

  position: relative;
  width: calc(120 * var(--size));
  height: calc(14 * var(--size));
  border-radius: 0 0 calc(15 * var(--size)) calc(15 * var(--size));
  background-color: var(--color-1);
  box-shadow: 0 calc(-1 * var(--size)) calc(4 * var(--size)) var(--color-2) inset;
  animation: panex 0.5s linear alternate infinite;
  transform-origin: calc(170 * var(--size)) 0;
  z-index: 10;
  perspective: calc(300 * var(--size));
}
.loader-pan::before {
  content: '';
  position: absolute;
  left: calc(100% - calc(2 * var(--size)));
  top: 0;
  z-index: -2;
  height: calc(10 * var(--size));
  width: calc(70 * var(--size));
  border-radius: 0 calc(4 * var(--size)) calc(4 * var(--size)) 0;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--color-3), var(--color-4)),
    linear-gradient(var(--color-5) calc(24 * var(--size)), transparent 0),
    linear-gradient(var(--color-6) calc(24 * var(--size)), transparent 0);
  background-size:
    calc(50 * var(--size)) calc(10 * var(--size)),
    calc(4 * var(--size)) calc(8 * var(--size)),
    calc(24 * var(--size)) calc(4 * var(--size));
  background-position:
    right center,
    calc(17 * var(--size)) center,
    0 center;
}
.loader-pan::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  z-index: -2;
  transform: translate(-50%, calc(-20 * var(--size))) rotate3d(75, -2, 3, 78deg);
  width: calc(55 * var(--size));
  height: calc(53 * var(--size));
  background: var(--color-7);
  background-image:
    radial-gradient(circle calc(3 * var(--size)), var(--color-8) 90%, transparent 10%),
    radial-gradient(circle calc(12 * var(--size)), var(--color-9) 90%, transparent 10%),
    radial-gradient(circle calc(12 * var(--size)), var(--color-10) 100%, transparent 0);
  background-repeat: no-repeat;
  background-position:
    calc(-4 * var(--size)) calc(-6 * var(--size)),
    calc(-2 * var(--size)) calc(-2 * var(--size)),
    calc(-1 * var(--size)) calc(-1 * var(--size));
  box-shadow:
    calc(-2 * var(--size)) calc(-3 * var(--size)) var(--color-11) inset,
    0 0 calc(4 * var(--size)) var(--color-12) inset;
  border-radius: 47% 36% 50% 50% / 49% 45% 42% 44%;
  animation: eggRst 1s ease-out infinite;
}

@keyframes eggRst {
  0%,
  100% {
    transform: translate(-50%, calc(-20 * var(--size))) rotate3d(90, 0, 0, 90deg);
    opacity: 0;
  }
  10%,
  90% {
    transform: translate(-50%, calc(-30 * var(--size))) rotate3d(90, 0, 0, 90deg);
    opacity: 1;
  }
  25% {
    transform: translate(-50%, calc(-40 * var(--size))) rotate3d(85, 17, 2, 70deg);
  }
  75% {
    transform: translate(-50%, calc(-40 * var(--size))) rotate3d(75, -3, 2, 70deg);
  }
  50% {
    transform: translate(-55%, calc(-50 * var(--size))) rotate3d(75, -8, 3, 50deg);
  }
}
@keyframes panex {
  0% {
    transform: rotate(-5deg);
  }
  100% {
    transform: rotate(10deg);
  }
}

/* Robot loader, the active loading animation for big images: a white
   robot head whose face slides side to side and ears wiggle. --size
   scales the whole drawing, the default 1px renders it 160×185. */
.loader {
  --color-1: #fff;
  --color-2: #000;
  --color-3: #cfecf9;
  --size: 1px;

  width: calc(160 * var(--size));
  height: calc(185 * var(--size));
  position: relative;
  background: var(--color-1);
  border-radius: calc(100 * var(--size)) calc(100 * var(--size)) 0 0;
}
.loader::after {
  content: '';
  position: absolute;
  width: calc(100 * var(--size));
  height: calc(125 * var(--size));
  left: 50%;
  top: calc(25 * var(--size));
  transform: translateX(-50%);
  background-image:
    radial-gradient(circle, var(--color-2) 48%, transparent 55%),
    radial-gradient(circle, var(--color-2) 48%, transparent 55%),
    radial-gradient(circle, var(--color-1) 30%, transparent 45%),
    radial-gradient(circle, var(--color-2) 48%, transparent 51%),
    linear-gradient(var(--color-2) calc(20 * var(--size)), transparent 0),
    linear-gradient(var(--color-3) calc(60 * var(--size)), transparent 0),
    radial-gradient(circle, var(--color-3) 50%, transparent 51%),
    radial-gradient(circle, var(--color-3) 50%, transparent 51%);
  background-repeat: no-repeat;
  background-size:
    calc(16 * var(--size)) calc(16 * var(--size)),
    calc(16 * var(--size)) calc(16 * var(--size)),
    calc(10 * var(--size)) calc(10 * var(--size)),
    calc(42 * var(--size)) calc(42 * var(--size)),
    calc(12 * var(--size)) calc(3 * var(--size)),
    calc(50 * var(--size)) calc(25 * var(--size)),
    calc(70 * var(--size)) calc(70 * var(--size)),
    calc(70 * var(--size)) calc(70 * var(--size));
  background-position:
    calc(25 * var(--size)) calc(10 * var(--size)),
    calc(55 * var(--size)) calc(10 * var(--size)),
    calc(36 * var(--size)) calc(44 * var(--size)),
    50% calc(30 * var(--size)),
    50% calc(85 * var(--size)),
    50% calc(50 * var(--size)),
    50% calc(22 * var(--size)),
    50% calc(45 * var(--size));
  animation: faceLift 3s linear infinite alternate;
}
.loader::before {
  content: '';
  position: absolute;
  width: 140%;
  height: calc(125 * var(--size));
  left: -20%;
  top: 0;
  background-image:
    radial-gradient(circle, var(--color-1) 48%, transparent 50%),
    radial-gradient(circle, var(--color-1) 48%, transparent 50%);
  background-repeat: no-repeat;
  background-size: calc(65 * var(--size)) calc(65 * var(--size));
  background-position:
    0 calc(12 * var(--size)),
    calc(145 * var(--size)) calc(12 * var(--size));
  animation: earLift 3s linear infinite alternate;
}

@keyframes faceLift {
  0% {
    transform: translateX(-60%);
  }
  100% {
    transform: translateX(-30%);
  }
}
@keyframes earLift {
  0% {
    transform: translateX(calc(10 * var(--size)));
  }
  100% {
    transform: translateX(0);
  }
}

/* ============================================================
   Reveal on scroll
   ============================================================ */
/* ---------- Scroll reveal ----------
   scripts/ui/reveal.js decides when, these rules decide what. An element
   opts in with data-reveal="<direction>" and may tune the timing with
   data-reveal-delay / data-reveal-duration, which arrive as the two custom
   properties below. Both the attribute and the class are removed once the
   element has played, so nothing here outlives the animation. */
[data-reveal] {
  opacity: 0;
  transition:
    opacity var(--reveal-duration, 0.6s) var(--ease) var(--reveal-delay, 0ms),
    transform var(--reveal-duration, 0.6s) var(--ease) var(--reveal-delay, 0ms);
}
[data-reveal="fade-up"] { transform: translateY(30px); }
[data-reveal="fade-down"] { transform: translateY(-30px); }
/* Named after the direction of travel, as in "fades to the right",
   so fade-right starts off to the left. */
[data-reveal="fade-right"] { transform: translateX(-40px); }
[data-reveal="fade-left"] { transform: translateX(40px); }
[data-reveal="zoom-in"] { transform: scale(0.94); }
[data-reveal="zoom-in-up"] { transform: translateY(24px) scale(0.95); }

[data-reveal].revealed { opacity: 1; transform: none; }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 44px; }
  .hero { padding-top: 130px; }
  /* Stacked under the copy now, so the two windows take the whole width
     that is left rather than sitting in a 520px box. */
  .hero-terminal { max-width: 100%; }
  .projects-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
  .carousel-stage { --slide-w: 360px; height: 560px; }
  /* The skills stage needs its own step: the selector above is one class
     less specific than .skills-carousel .carousel-stage, so it would never
     reach it. */
  .skills-carousel .carousel-stage { --slide-w: 340px; height: 320px; }
}

@media (max-width: 768px) {
  .nav-links {
    position: fixed;
    top: 72px; left: 0; right: 0;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 20px;
    background: var(--nav-bg);
    backdrop-filter: blur(16px);
    border-bottom: 1px solid var(--border);
    transform: translateY(-130%);
    transition: transform 0.4s var(--ease);
    z-index: 99;
  }
  .nav-links.open { transform: translateY(0); }
  .nav-links a { padding: 10px 0; font-size: 1.05rem; }
  .nav-cv-link { display: block; }
  .hamburger { display: flex; }
  .btn-cv { display: none; } /* below 768px the mobile menu is the only CV source */
  .stats-row { gap: 26px; }

  /* Hero CTAs: the primary action takes a full row, the two secondary ones
     split the next one. flex-basis 50% minus half the gap lines them up with
     the primary button's edges, and min-width: 0 lets a long label wrap
     inside its button instead of forcing a third row. */
  .hero-actions { gap: 12px; }
  .hero-actions .btn { flex: 1 1 calc(50% - 6px); min-width: 0; }
  .hero-actions .btn-primary { flex-basis: 100%; }

  /* Contact cards on phones: the desktop tile (big icon over centred text)
     eats most of the screen, so the card turns into a strip — a small icon
     on the left, spanning both text rows, so it ends up as tall as the two
     lines (name + value) beside it. Desktop keeps the tile. The markup is
     three flat children (icon / name / value), hence grid placement rather
     than a wrapper div. */
  .contact-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 14px;
    row-gap: 2px;
    text-align: left;
    padding: 14px 16px;
  }
  .contact-icon {
    grid-row: 1 / span 2;
    display: grid;
    place-items: center;
    font-size: 2.4rem;
    line-height: 1;
    margin-bottom: 0;
  }
  .contact-icon svg { width: 46px; height: 46px; }
  .contact-icon .contact-img { width: 50px; height: 50px; border-radius: 12px; }
  /* No rotation once the icon sits inline next to the text — a tilted square
     would collide with the name. */
  .contact-card:hover .contact-icon { transform: scale(1.08); }
  .contact-name { grid-column: 2; }
  .contact-value { grid-column: 2; }

  /* The paragraph is no longer hidden by the breakpoint: the compact view
     does it (see .timeline-list.is-compact), and phones default to compact,
     so the result is the same — except a phone visitor can now switch the
     descriptions back on. */
  /* One column here, so the alternating left/right entrance has nothing to
     alternate between; they come up from below like everything else. */
  [data-reveal="fade-left"],
  [data-reveal="fade-right"] { transform: translateY(30px); }

  .timeline-list::before { left: 8px; transform: none; }
  .timeline-item,
  .timeline-item:nth-child(even) {
    width: 100%;
    margin-left: 0;
    padding: 0 0 32px 30px;
  }
  .timeline-item:nth-child(even) .timeline-dot,
  .timeline-dot { left: 0; right: auto; }
}

/* The six-code language row is ~200px wide; from ~1024px down the nav runs
   out of room for it (at 900px it pushed the theme toggle past the viewport
   edge, below 768px the hamburger). It collapses into a dropdown here: one
   trigger showing the current language, and the same .lang-btn list as a
   popup panel under it. */
@media (max-width: 1024px) {
  .lang-switcher {
    padding: 0;
    background: none;
    border: none;
    border-radius: 0;
  }
  .lang-current {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 11px;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.74rem;
    font-weight: 600;
    letter-spacing: 0.5px;
    line-height: 1;
    cursor: pointer;
    transition: all 0.25s var(--ease);
  }
  .lang-current:hover { border-color: var(--accent); color: var(--accent); }
  .lang-caret { width: 12px; height: 12px; transition: transform 0.25s var(--ease); }
  .lang-switcher.open .lang-caret { transform: rotate(180deg); }

  /* Hidden with visibility (not display) so the open/close transition runs
     and the panel keeps its measured width. */
  .lang-menu {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    z-index: 120;
    flex-direction: column;
    gap: 2px;
    min-width: 156px;
    padding: 6px;
    /* Opaque, not the navbar's 0.75 alpha: the panel sits over cards and
       photos, and the text has to stay readable against all of them. */
    background: var(--surface);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--border);
    border-radius: 14px;
    box-shadow: var(--shadow-md);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity 0.22s var(--ease), transform 0.22s var(--ease), visibility 0.22s;
  }
  .lang-switcher.open .lang-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  /* Full opacity here: the 0.65 that dims the desktop pill row is fine
     against a solid navbar, but over the blurred panel it made the labels
     hard to read. The inactive/active distinction is carried by colour
     instead. */
  .lang-menu .lang-btn {
    width: 100%;
    justify-content: flex-start;
    padding: 9px 11px;
    border-radius: 10px;
    font-size: 0.74rem;
    opacity: 1;
    color: var(--text);
  }
  /* No hover lift inside the panel — it would slide the row out from under
     the finger mid-tap. */
  .lang-menu .lang-btn:hover,
  .lang-menu .lang-btn.active { transform: none; }
  .lang-menu .lang-btn:hover { background: var(--surface-2); }
  .lang-menu .lang-btn.active { color: var(--accent); background: var(--surface-2); }
  .lang-name {
    display: inline;
    font-family: var(--font-body);
    font-size: 0.86rem;
    font-weight: 500;
    letter-spacing: 0;
  }
}

@media (max-width: 480px) {
  .section { padding: 70px 18px; }
  .hero { padding: 120px 18px 70px; }
  .projects-grid { grid-template-columns: 1fr; }
  /* The coverflow needs the flanking slides to peek, so on phones the slide
     gets narrower and the side offsets shrink with it (they are fractions
     of --slide-w, but translateZ/scale stay put). */
  .carousel-stage { --slide-w: 78vw; height: 540px; }
  .skills-carousel .carousel-stage { --slide-w: 78vw; height: 300px; }
  .carousel-slide[data-state="prev"] {
    transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * -0.62)) translateZ(-60px) rotateY(40deg) scale(0.82);
  }
  .carousel-slide[data-state="next"] {
    transform: translateX(calc(var(--drag-x, 0px) + var(--slide-w) * 0.62)) translateZ(-60px) rotateY(-40deg) scale(0.82);
  }
  .projects-toolbar { justify-content: center; }
  /* …except the timeline's, which stays on the right edge at every width. */
  .timeline-toolbar { justify-content: flex-end; }
  /* Phone cards leave ~240px for the link row; shrink the buttons rather
     than let a third one wrap to its own line. */
  .card-links { gap: 6px; }
  .card-link { padding: 8px 8px; gap: 4px; font-size: 0.78rem; }
  .card-link svg { width: 14px; height: 14px; }
  .carousel-slide .card-link { padding: 7px 6px; font-size: 0.76rem; }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  html { scroll-behavior: auto; }
}
