/* Marvin Advergames — retro site styles */

/* Self-hosted (not loaded from Google's CDN) so visitor IPs never reach
   fonts.gstatic.com — only the latin subset, which covers EN/IT copy. */
@font-face {
  font-family: "Press Start 2P";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/press-start-2p-latin.woff2") format("woff2");
}
@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/vt323-latin.woff2") format("woff2");
}

:root {
  --bg: #f7f7f5;
  --bg-alt: #ececea;
  --ink: #18181b;
  --muted: #6b6b70;
  --card-bg: #ffffff;
  --yellow: #ffc72c;
  --blue: #2563eb;
  /* Was a blue-violet (#7c3aed) that read as near-identical to --blue on
     laptop screens with narrower gamuts. First shift (to #c026d3) fixed
     that but landed too close to --coral — same lightness/saturation as
     coral, differing only by hue, which is its own confusable pair. This
     darker plum-fuchsia clears both: further from blue in hue, and from
     coral in lightness (much darker) as well as hue. */
  --purple: #86198f;
  --mint: #34d399;
  /* --mint itself (used as border/background/accent everywhere) is 1.8:1
     against --bg — fine for a border, fails WCAG AA badly as text. This
     darker variant is for the two places mint is actually used as text
     color on a light background (link hover, typewriter cursor): 5.5:1+
     against both --bg and white. */
  --mint-text: #1e7155;
  --coral: #e11d48;
  /* Was only 29.6° from --blue on the hue wheel — the tightest gap in the
     whole palette, tighter even than the original blue/purple problem.
     Deepened toward petrol/teal so it separates from blue by lightness
     (23pt gap now, was 17) as well as hue, without drifting closer to
     --mint on the other side. */
  --cyan: #155e75;
  --gb-screen: #9bbc0f;
  --gb-pixel: #306230;
  --gb-pixel-dark: #0f380f;
  /* Always this exact dark value, in both themes — a handful of elements
     (game-embed placeholders, dialog-box title bars, the cartridge label,
     Marvin's own speech-bubble chrome) use "ink" as a fixed dark screen/
     chrome fill paired with hardcoded white text, not as the page's text
     color. --ink itself flips with the theme below; this doesn't, so
     those stay looking like dark hardware regardless of site theme. */
  --ink-fixed: #18181b;
  /* Same "fixed" idea as --ink-fixed, but for the accent hues: --blue/
     --purple/--coral/--cyan brighten in dark mode so they stay legible as
     TEXT against a dark page — but a few spots (comment-chip badges,
     dialog-box-title accent bars, .btn hover) use those same tokens as a
     BACKGROUND FILL paired with hardcoded white text. Brightened fills
     would drop white-text contrast to ~2:1. These four keep the original,
     already-vetted-for-white-text values in both themes. */
  --blue-fill: #2563eb;
  --purple-fill: #86198f;
  --coral-fill: #e11d48;
  --cyan-fill: #155e75;
  --shadow: 4px 4px 0 rgba(0, 0, 0, 0.35);
  --radius: 2px;
  --font-head: "Press Start 2P", monospace;
  --font-body: "VT323", monospace;
  --maxw: 1120px;
}

/* Dark theme — same token system, so every component that already reads
   from these variables (borders, text, card backgrounds, accent colors)
   picks it up automatically. Not a naive value-for-value invert: the
   accent hues are brightened just enough to stay legible as text against
   a dark page, and --ink-fixed above intentionally does NOT flip, so the
   game-screen/dialog-title chrome stays dark hardware in both themes.
   Two selectors, same values: the media query covers visitors who never
   touch the toggle (no-JS or first paint before the toggle script runs)
   by following their OS preference; the attribute is what the toggle
   button in main.js actually switches, and — being a plain attribute
   selector on :root — outranks the bare :root light defaults regardless
   of which one is written first in the file. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #17161b;
    --bg-alt: #1f1e24;
    --ink: #f0f0ee;
    --muted: #a8a5ae;
    --card-bg: #232228;
    --blue: #6b9bff;
    --purple: #d17fd6;
    --mint-text: #5eeab3;
    --coral: #ff6f8f;
    --cyan: #4fd1e8;
    --shadow: 4px 4px 0 rgba(0, 0, 0, 0.7);
  }
}

:root[data-theme="dark"] {
  --bg: #17161b;
  --bg-alt: #1f1e24;
  --ink: #f0f0ee;
  --muted: #a8a5ae;
  --card-bg: #232228;
  --blue: #6b9bff;
  --purple: #d17fd6;
  --mint-text: #5eeab3;
  --coral: #ff6f8f;
  --cyan: #4fd1e8;
  --shadow: 4px 4px 0 rgba(0, 0, 0, 0.7);
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 20px;
  line-height: 1.5;
}

img { max-width: 100%; display: block; }

a { color: var(--blue); }
a:hover { color: var(--mint-text); }

/* Keyboard-focus indicator — site had none, meaning keyboard/screen-reader
   users had no visible way to tell what's focused. :focus-visible (not
   :focus) so it only shows for keyboard/programmatic focus, not mouse
   clicks. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
iframe:focus-visible {
  outline: 3px solid var(--yellow);
  outline-offset: 2px;
}

h1, h2, h3, .pixel {
  font-family: var(--font-head);
  line-height: 1.4;
  letter-spacing: 0.5px;
}

h1 { font-size: clamp(1.4rem, 4vw, 2.4rem); }
h2 { font-size: clamp(1.1rem, 2.6vw, 1.6rem); }
h3 { font-size: 1rem; }

/* Titles cycle through the 4 palette colors dark/saturated enough to stay
   legible as running text (yellow/mint are reserved for fills & borders,
   not text) — sequential by section position so nearby sections never
   land on the same color, unlike a simple odd/even split. */
main > section:nth-of-type(4n+1) h2 { color: var(--blue); }
main > section:nth-of-type(4n+2) h2 { color: var(--purple); }
main > section:nth-of-type(4n+3) h2 { color: var(--coral); }
main > section:nth-of-type(4n+4) h2 { color: var(--cyan); }

.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* Header / nav */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg);
  border-bottom: 3px solid var(--ink);
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 0.9rem;
  padding-bottom: 0.9rem;
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-head);
  font-size: 0.85rem;
  text-decoration: none;
  color: var(--ink);
}

.brand img { height: 44px; width: auto; }

.main-nav {
  display: flex;
  gap: 1.6rem;
  flex-wrap: wrap;
  font-size: 1.25rem;
}

.main-nav a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  padding-bottom: 4px;
}

/* Each nav item gets its own underline color from the palette — text stays
   ink for legibility, only the accent line changes, so every section reads
   as distinct without ever recoloring body/nav text into yellow or mint.
   5 items, 5 different colors — no repeats. */
.main-nav a:nth-child(1) { --nav-color: var(--yellow); }
.main-nav a:nth-child(2) { --nav-color: var(--blue); }
.main-nav a:nth-child(3) { --nav-color: var(--purple); }
.main-nav a:nth-child(4) { --nav-color: var(--mint); }
.main-nav a:nth-child(5) { --nav-color: var(--coral); }

.main-nav a:hover,
.main-nav a[aria-current="page"] {
  border-bottom-color: var(--nav-color);
  color: var(--ink);
}

/* "Soluzioni" nav dropdown — appended after the 5 colored links above
   rather than inserted among them, so it doesn't shift their nth-child
   color mapping. Styled as plain nav text (no border/background) so it
   reads as one more nav item, not a transplanted header-controls button. */
.nav-dropdown { position: relative; }

.nav-dropdown-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font: inherit;
  color: var(--ink);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 0 0 4px;
  cursor: pointer;
}

.nav-dropdown-toggle:hover,
.nav-dropdown-toggle[aria-expanded="true"] {
  border-bottom-color: var(--blue);
}

.nav-dropdown-caret { font-size: 0.65rem; }

/* Anchored to the toggle's right edge, not left — "Soluzioni" is the
   rightmost nav item, so a left-anchored menu would grow off the edge of
   the viewport. Opening leftward keeps it inside the nav's own space. */
.nav-dropdown-menu {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  border: 2px solid var(--ink);
  width: max-content;
  max-width: min(320px, calc(100vw - 2 * 1.5rem));
  z-index: 60;
  font-size: 1.1rem;
}

.nav-dropdown-menu[hidden] { display: none; }

.nav-dropdown-menu a {
  text-decoration: none;
  color: var(--muted);
  padding: 8px 12px;
  white-space: nowrap;
  border-bottom: none;
}

.nav-dropdown-menu a:hover,
.nav-dropdown-menu a[aria-current="page"] {
  color: var(--ink);
  background: var(--bg-alt);
}

@media (max-width: 760px) {
  .nav-dropdown-menu {
    position: static;
    border: none;
    background: none;
    width: auto;
    margin-top: 0.5rem;
    padding-left: 1rem;
    gap: 0.6rem;
  }

  .nav-dropdown-menu a { padding: 0; }
}

/* Groups the language switcher and theme toggle as one flex item instead
   of two separate ones — .site-header .container uses space-between,
   which would otherwise redistribute leftover space (and therefore the gap
   between these two specifically) every time the nav links' translated
   text changes width from one language to the next. With a fixed gap
   inside this wrapper, the two buttons stay glued to each other; only the
   wrapper as a whole shifts with the nav width, not the buttons relative
   to one another. */
.header-controls {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Was 4 always-visible buttons (EN/IT/FR/ES) — replaced with a single
   current-language trigger + dropdown so the header doesn't eat so much
   horizontal space now that it also carries the theme toggle. */
.lang-switch {
  position: relative;
  font-family: var(--font-head);
  font-size: 0.8rem;
}

.lang-switch-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  height: 34px;
  font-family: var(--font-head);
  font-size: 0.8rem;
  color: var(--ink);
  background: var(--card-bg);
  border: 2px solid var(--ink);
  padding: 0 8px;
  cursor: pointer;
}

.lang-switch-caret { font-size: 0.65rem; }

/* 15x10 source pixel art, upscaled — pixelated keeps it crisp instead of
   blurring into the anti-aliased mush a browser's default smoothing would
   give a 15px-wide source image at this scale. */
.lang-flag {
  width: 18px;
  height: 12px;
  image-rendering: pixelated;
  border: 1px solid var(--ink);
  flex-shrink: 0;
}

.lang-switch-menu {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  border: 2px solid var(--ink);
  min-width: 100%;
  z-index: 60;
}

.lang-switch-menu[hidden] { display: none; }

.lang-switch-menu a {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--muted);
  padding: 6px 10px;
  white-space: nowrap;
}

.lang-switch-menu a:hover {
  color: var(--ink);
  background: var(--bg-alt);
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 2px solid var(--ink);
  background: var(--card-bg);
  color: var(--ink);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0;
  flex-shrink: 0;
}

.theme-toggle:hover { border-color: var(--yellow); }

.theme-toggle-icon.icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle-icon.icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle-icon.icon-moon { display: inline; }

/* Logo also has a light/dark-specific PNG (the wordmark is solid dark
   pixels with no room to recolor via CSS) — same show/hide swap as the
   theme-toggle sun/moon icons above. */
.theme-logo.logo-dark { display: none; }
:root[data-theme="dark"] .theme-logo.logo-light { display: none; }
:root[data-theme="dark"] .theme-logo.logo-dark { display: inline; }

/* Hamburger nav toggle — hidden on desktop, where nav + lang-switch show
   inline as before. On mobile it collapses both into a dropdown, since a
   5-item nav plus EN/IT was wrapping onto 2-3 stacked rows and eating a
   huge chunk of the viewport before any real content appeared. */
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 42px;
  height: 38px;
  border: 3px solid var(--ink);
  background: var(--card-bg);
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}

.nav-toggle span {
  display: block;
  width: 20px;
  height: 3px;
  background: var(--ink);
}

/* On desktop this wrapper is invisible to layout — .main-nav and
   .lang-switch behave as if they were direct flex children of
   .site-header .container again, exactly like before the wrapper existed.
   The mobile media query below overrides this back to a real box so it can
   be shown/hidden as a dropdown. */
.site-nav { display: contents; }

@media (max-width: 760px) {
  .nav-toggle { display: flex; }

  .site-nav {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
    padding-top: 1rem;
  }

  .site-nav.is-open { display: flex; }

  .main-nav { flex-direction: column; align-items: flex-start; gap: 0.9rem; font-size: 1.1rem; }
}

/* Buttons */
.btn {
  display: inline-block;
  font-family: var(--font-head);
  font-size: 0.85rem;
  text-decoration: none;
  color: var(--ink-fixed);
  background: var(--yellow);
  border: 3px solid var(--ink);
  padding: 0.9rem 1.2rem;
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: transform 0.15s ease, background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

.btn:hover {
  transform: scale(1.08);
  background: var(--blue-fill);
  color: #fff;
  border-color: var(--ink);
}

.btn:active {
  transform: scale(0.97) translate(2px, 2px);
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.35);
}

.btn.btn-alt { background: var(--card-bg); color: var(--ink); }
.btn.btn-green { background: var(--mint); color: var(--ink-fixed); }

/* Explicit variant + :hover pairing needed: .btn.btn-alt/.btn.btn-green have
   the same specificity as .btn:hover and come later in the file, so without
   this they'd win over the plain hover rule above and the hover color
   change would silently not show on those two button variants. */
.btn.btn-alt:hover,
.btn.btn-green:hover {
  background: var(--blue-fill);
  color: #fff;
  border-color: var(--ink);
}

/* Hero */
.hero {
  padding: 3.5rem 0 2.5rem;
}

.hero .container {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: 2.5rem;
  align-items: center;
}

@media (max-width: 860px) {
  .hero .container { grid-template-columns: 1fr; }
}

.hero-solo .container {
  grid-template-columns: 1fr;
  max-width: 760px;
}

.hero-embed-section {
  padding: 1.5rem 0 2rem;
}

.embed-fullscreen {
  aspect-ratio: 1280 / 720;
  height: auto;
  margin: 0 auto;
}

/* On mobile the game itself scales responsively (Construct's own "scale
   outer" mode) rather than being locked to 16:9, so it can run portrait
   instead of letterboxing a small landscape rectangle. It still sits in
   the normal page frame (same container padding as everywhere else) and
   stops short of the full viewport height on purpose, so a sliver of the
   next section peeks through as a "there's more, scroll down" cue. */
@media (max-width: 760px) {
  .embed-fullscreen { aspect-ratio: 3 / 4; margin: 0 auto; }
}

.hero-tag {
  font-family: var(--font-head);
  color: var(--purple);
  font-size: 0.9rem;
  margin-bottom: 0.8rem;
  display: block;
}

.hero p.lead { font-size: 1.3rem; max-width: 46ch; margin-left: auto; margin-right: auto; }

.cta-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }

/* Sections */
section { padding: 3rem 0; }
section h2 { margin-bottom: 1.2rem; }
.section-alt { background: var(--bg-alt); }

.grid {
  display: grid;
  gap: 1.5rem;
}

.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }

@media (max-width: 860px) {
  .grid-3 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .grid-3, .grid-2 { grid-template-columns: 1fr; }
}

.card {
  position: relative;
  background: var(--card-bg);
  border: 3px solid var(--ink);
  border-top-width: 6px;
  border-top-color: var(--card-accent, var(--mint));
  box-shadow: var(--shadow);
  padding: 1.2rem;
  display: flex;
  flex-direction: column;
}

/* Portfolio cards: the grid already stretches every card in a row to the
   tallest one's height, but the "Play now" line used to just trail
   whatever the description's own length left it at — different word count,
   different row, no two cards lining their button up. Growing the
   description to fill the leftover space pushes the button to the same
   spot at the bottom every time, keyed off the card frame instead of the
   text. Cards with no button (team, homepage "Why Marvin") are untouched —
   :has() only matches the specific paragraph sitting right before one. */
.card > p:has(+ p > a.btn) {
  flex: 1 1 auto;
}

/* A lone card outside a grid (e.g. a closing CTA) isn't competing with
   siblings for differentiation — keep it to a single accent (matching its
   own button) instead of border/heading/button each pulling a different
   color. (.card.center h2 needs to out-specificity the section h2 cycle
   above, which is section+nth-of-type+h2 — two classes beats that.) */
.card.center { border-top-color: var(--mint); }
.card.center h2 { color: var(--ink); }

/* Each card in a grid gets its own accent from the palette (cycling through
   all 6) instead of a uniform edge — cards read as distinct/dynamic while
   staying inside the same small color system. A 6-item grid (Team,
   Portfolio) gets 6 unique colors with zero repeats. */
.grid .card:nth-child(6n+1) { --card-accent: var(--yellow); }
.grid .card:nth-child(6n+2) { --card-accent: var(--blue); }
.grid .card:nth-child(6n+3) { --card-accent: var(--purple); }
.grid .card:nth-child(6n+4) { --card-accent: var(--mint); }
.grid .card:nth-child(6n+5) { --card-accent: var(--coral); }
.grid .card:nth-child(6n+6) { --card-accent: var(--cyan); }

/* Team */
.avatar {
  width: 100%;
  aspect-ratio: 1;
  border: 3px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-head);
  font-size: 1.6rem;
  color: #fff;
  margin-bottom: 0.9rem;
  image-rendering: pixelated;
}

.avatar-real {
  width: 100%;
  aspect-ratio: 1;
  border: 3px solid var(--ink);
  margin-bottom: 0.9rem;
  overflow: hidden;
  background: #cdf3e1;
}

.avatar-real img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: pixelated;
}

.team-card h3 { margin: 0 0 0.2rem; }

/* Role caption uses its own 4-color rotation (only the tones dark enough
   to read as text) independent of the card's border accent above it —
   the border can land on yellow/mint, which would be illegible as text. */
.team-card:nth-child(4n+1) .role { color: var(--blue); }
.team-card:nth-child(4n+2) .role { color: var(--purple); }
.team-card:nth-child(4n+3) .role { color: var(--coral); }
.team-card:nth-child(4n+4) .role { color: var(--cyan); }
.team-card .role { font-size: 0.95rem; margin-bottom: 0.5rem; display: block; }
.team-card .skills { font-size: 0.95rem; color: var(--muted); }

/* Portfolio */
.cartridge {
  border: 3px solid var(--ink);
  background: var(--ink-fixed);
  color: #fff;
  padding: 0.6rem 0.8rem;
  font-family: var(--font-head);
  font-size: 0.8rem;
  margin-top: 0;
  margin-bottom: 0.9rem;
}

.game-embed {
  border: 3px solid var(--ink);
  background: var(--ink-fixed);
  background-size: cover;
  background-position: center;
  overflow: hidden;
  margin-bottom: 0.9rem;
  position: relative;
}

/* Silent looping attract-mode clip sitting behind the PLAY overlay — same
   spot a static cover image would go, just animated. Always muted, so this
   never reintroduces the "audio before you clicked anything" problem the
   old GIF-as-background never had either; it's purely decorative. */
.game-embed-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Retro "boot sequence" screen behind the homepage embed's PLAY control —
   same spot a cover image/preview video sits on the per-game pages, just
   built entirely from text since the classic-site embed has no cover
   asset of its own. The art/log lines are decorative (aria-hidden
   individually, see markup) but the screen itself is NOT — the real PLAY
   button lives inside it, as the terminal's own final line, so it must
   stay out of an aria-hidden ancestor. Animation gets stopped by JS once
   the visitor actually clicks Play. */
.boot-screen {
  position: absolute;
  inset: 0;
  background: var(--gb-pixel-dark);
  color: var(--gb-screen);
  padding: 1.1rem 1.4rem;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transform-origin: 50% 50%;
  animation: boot-power-on 0.55s cubic-bezier(0.2, 0.9, 0.3, 1) both, boot-glow 4s ease-in-out 0.55s infinite;
}

/* Game Boy–style intro logo: the wordmark slides down from off-screen and
   settles center — same beat as the hardware's own boot logo — then flashes
   once and clears before the POST lines start typing. Source PNG is solid
   black on transparent, which is exactly what a CSS mask wants: recolor to
   the phosphor green via mask-image rather than fighting a raster filter to
   get the right hue. Boot-screen.js adds .is-centered / .is-leaving and
   skips this whole element under prefers-reduced-motion. */
.boot-logo {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 56%;
  max-width: 300px;
  aspect-ratio: 836 / 231;
  background-color: var(--gb-screen);
  -webkit-mask: url("../img/marvin-boot-logo.png") no-repeat center / contain;
  mask: url("../img/marvin-boot-logo.png") no-repeat center / contain;
  transform: translate(-50%, -480%);
  z-index: 5;
  transition: transform 0.85s cubic-bezier(0.32, 0.1, 0.4, 1);
}

.boot-logo.is-centered { transform: translate(-50%, -50%); }

.boot-logo.is-leaving {
  transition: opacity 0.3s ease-out;
  opacity: 0;
}

/* One brief bright flash across the whole screen as the logo clears — the
   same "blip" beat the hardware has — not a repeating strobe, so it stays
   well clear of any flash/photosensitivity threshold. */
.boot-screen.is-logo-flash::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--gb-screen);
  z-index: 6;
  animation: boot-logo-flash 0.22s ease-out;
  pointer-events: none;
}

@keyframes boot-logo-flash {
  0% { opacity: 0; }
  35% { opacity: 0.85; }
  100% { opacity: 0; }
}

/* Press Start 2P, not the body's VT323 — the art's proportions were drawn
   for this font's near-square cell at line-height 1.6 (matching how the
   original renders it) — a tighter line-height flattens the whole figure
   vertically, same distortion as the wrong font did sideways.
   Starts invisible — boot-screen.js reveals it (.is-visible) only after
   the logo intro clears, so the sequence reads as logo, THEN the rest of
   the machine, instead of everything sitting there at once behind it. */
.boot-ascii {
  font-family: var(--font-head);
  font-size: 0.24rem;
  line-height: 1.6;
  white-space: pre;
  margin: 0 0 0.5rem;
  opacity: 0;
  transition: opacity 0.6s ease-out;
}

.boot-ascii.is-visible { opacity: 1; }

.boot-title {
  font-family: var(--font-head);
  font-size: 0.75rem;
  display: block;
  margin-bottom: 0.6rem;
  opacity: 0;
  transition: opacity 0.6s ease-out;
}

.boot-title.is-visible { opacity: 1; }

.boot-lines p {
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.35;
  margin: 0;
  min-height: 1.35em;
}

/* POST-style status lines (CPU/MEM/CART/LANG) — the [ OK ] tag snaps in
   right after its line finishes typing rather than being typed itself, so
   it reads as a check completing, not more scrolling text. */
.boot-line-ok {
  display: inline-block;
  margin-left: 0.6em;
  color: var(--gb-screen);
  font-weight: bold;
  animation: boot-ok-pop 0.24s ease-out;
}

@keyframes boot-ok-pop {
  0% { transform: scale(1.5); opacity: 0; }
  55% { transform: scale(0.92); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .boot-line-ok { animation: none; }
}

/* Fills whatever room is left below the button — a live data-noise panel
   instead of more static text, so the empty space on a wide/short desktop
   frame becomes the main event instead of dead air. flex:1 makes it
   adapt automatically to any frame size, desktop or mobile, without
   needing separate fixed-height tuning per breakpoint. */
.boot-glitch {
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0.7rem;
  position: relative;
}

.boot-glitch canvas {
  width: 100%;
  height: 100%;
  display: block;
}

/* Same [hidden]-losing-to-an-unqualified-rule issue as the boot-prompt
   fix above — an author `display: block` beats the UA `[hidden]` default,
   so the reduced-motion fallback needs this said explicitly or the empty
   canvas keeps occupying the space meant for the static fact list. */
.boot-glitch canvas[hidden] {
  display: none;
}

/* prefers-reduced-motion fallback: the same facts the animated version
   would decrypt, framed as their own quiet panel instead of a bare list —
   a static screen shouldn't read as the leftover version. */
.boot-glitch-static {
  font-family: var(--font-body);
  font-size: 1.05rem;
  line-height: 1.7;
  border: 1px solid rgba(155, 188, 15, 0.3);
  background: rgba(155, 188, 15, 0.05);
  padding: 0.9rem 1.1rem;
}
.boot-glitch-static-title {
  display: block;
  font-family: var(--font-head);
  font-size: 0.7rem;
  opacity: 0.75;
  margin-bottom: 0.6rem;
}
.boot-glitch-static p { margin: 0; }
.boot-glitch-static p::before { content: "> "; opacity: 0.55; }

/* The PLAY control, reimagined as the terminal's own next typed line
   instead of a separate arcade-style button floating over a dark scrim —
   in normal flow (not the full-frame absolute overlay the per-game pages
   use). Given real visual weight (inverse video, pulsing) since it's the
   one thing on this screen a visitor should actually notice and act on;
   everything else here is atmosphere. */
.classic-embed .game-embed-play {
  position: static;
  display: block;
  width: 100%;
  height: auto;
  margin: 0.5rem 0;
  padding: 0.2rem 0;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.classic-embed .game-embed-play .boot-prompt {
  display: inline-block;
  background: var(--gb-screen);
  color: var(--gb-pixel-dark);
  border: 0;
  padding: 0.5em 0.8em;
  box-shadow: none;
  font-family: var(--font-head);
  font-size: 0.95rem;
  line-height: 1.4;
  animation: boot-prompt-pulse 1.3s ease-in-out infinite;
}

.classic-embed .game-embed-play:hover .boot-prompt {
  animation-play-state: paused;
  filter: brightness(1.15);
}

/* The unqualified rule above's `display: inline-block` otherwise beats the
   UA stylesheet's `[hidden] { display: none }` at equal specificity (same
   three classes either way, but this adds the attribute selector), which
   would make the boot sequence's own hidden-until-typed convention a no-op
   for this one element. */
.classic-embed .game-embed-play .boot-prompt[hidden] {
  display: none;
}

/* Blinking, not strobing — a slow opacity fade, well clear of flash/
   photosensitivity thresholds, same reasoning as boot-glow below. */
@keyframes boot-prompt-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* CRT shader over the actual classic-site game once it's loaded and
   running (data-game-active, set by embeds.js on click) — keeps that
   "old hardware" character continuous with the boot screen that came
   before it. Pure overlay, not a filter on the iframe itself: the real
   game keeps its own actual colors, hue and saturation untouched — the
   CRT feel comes entirely from the scanline/vignette glass sitting on
   top, the same technique real CRT shaders use. Scoped to only once the
   game is active so it doesn't stack a second scanline layer on top of
   the boot screen's own while that's still showing. */
.classic-embed[data-game-active="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.35) 100%),
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.12) 1px, transparent 1px, transparent 3px);
}

/* CRT glass: scanlines + vignette, darkening toward black now that the
   screen itself is the dark side of the GB palette. */
.boot-screen::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.45) 100%),
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.25) 0, rgba(0, 0, 0, 0.25) 1px, transparent 1px, transparent 3px);
  background-position: center, 0 0;
  animation: boot-scanline-drift 11s linear infinite;
  pointer-events: none;
}

/* Scanlines drift very slowly downward, like a tube with a slightly
   imperfect vertical sync, instead of sitting perfectly static — the
   vignette layer's position never changes so only the lines themselves
   move. Slow and low-amplitude enough to leave running unconditionally,
   same call already made for boot-glow below. */
@keyframes boot-scanline-drift {
  0% { background-position: center, 0 0; }
  100% { background-position: center, 0 132px; }
}

.boot-screen .type-cursor { color: var(--gb-screen); }

/* A slow, low-amplitude glow — the bright phosphor-green bloom a real dark
   CRT would have, not any strobing "turn-on" flicker (real hard
   brightness-cycling is an actual photosensitivity risk, not worth it for
   a decorative backdrop). */
@keyframes boot-glow {
  0%, 100% { box-shadow: inset 0 0 30px rgba(155, 188, 15, 0.2); }
  50% { box-shadow: inset 0 0 55px rgba(155, 188, 15, 0.4); }
}

/* Classic CRT tube power-on: the picture starts collapsed to a thin
   horizontal line (the way an old tube's electron beam actually warms up),
   snaps open with a brightness overshoot, wobbles once, then settles —
   instead of a plain fade-in. One-shot on load, not looped. */
@keyframes boot-power-on {
  0% { transform: scaleY(0.015); filter: brightness(3.2); opacity: 0.4; }
  35% { transform: scaleY(1.06); filter: brightness(2.4); opacity: 1; }
  50% { transform: scaleY(0.98); filter: brightness(0.75); }
  68% { transform: scaleY(1.015); filter: brightness(1.35); }
  100% { transform: scaleY(1); filter: brightness(1); opacity: 1; }
}

/* Must come after the base .boot-* rules above — same selector, same
   specificity, so source order is what decides the winner once the media
   query matches. */
@media (max-width: 760px) {
  .boot-screen { padding: 0.6rem 1rem; }
  .boot-ascii { font-size: 0.16rem; line-height: 1.6; margin-bottom: 0.3rem; }
  .boot-title { font-size: 0.6rem; margin-bottom: 0.3rem; }
  .boot-lines p { font-size: 0.75rem; line-height: 1.2; min-height: 1.2em; }
  .classic-embed .game-embed-play { margin: 0.35rem 0; }
  .classic-embed .game-embed-play .boot-prompt { font-size: 0.58rem; }
  .boot-glitch { margin-top: 0.4rem; }
  .boot-glitch-static { font-size: 0.75rem; line-height: 1.35; padding: 0.6rem 0.8rem; }
  .boot-glitch-static-title { font-size: 0.58rem; }
}

/* Semi-transparent so the loaded, animating game shows through behind it —
   a "press start" overlay rather than a solid loading box. */
.game-embed-play {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: rgba(24, 24, 27, 0.55);
  color: var(--mint);
  font-family: var(--font-head);
  font-size: 0.8rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1.6;
  transition: background-color 0.2s ease;
}

.game-embed-play:hover { background: rgba(24, 24, 27, 0.7); }

.game-embed-play[data-state="loading"] { cursor: default; }
.game-embed-play[data-state="loading"] span { opacity: 0.7; }

.game-embed-play[hidden] { display: none; }

.game-embed-play span {
  display: inline-block;
  background: var(--yellow);
  color: var(--ink-fixed);
  border: 3px solid var(--ink);
  padding: 1.1rem 2rem;
  font-size: 1.1rem;
  box-shadow: var(--shadow);
  transition: transform 0.15s ease;
}

.game-embed-play:not([data-state="loading"]):hover span { transform: scale(1.06); }

.game-embed-fullscreen {
  position: absolute;
  right: 0.6rem;
  bottom: 0.6rem;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  background: rgba(24, 24, 27, 0.6);
  color: var(--mint);
  border: 2px solid var(--ink);
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.15s ease;
  z-index: 2;
}

.game-embed-fullscreen:hover { background: rgba(24, 24, 27, 0.85); transform: scale(1.08); }
.game-embed-fullscreen[hidden] { display: none; }

.cover-frame {
  display: block;
  aspect-ratio: 3 / 2;
  border: 3px solid var(--ink);
  overflow: hidden;
  margin-bottom: 0.9rem;
  background: var(--ink-fixed);
}

.cover-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.15s ease;
}

.cover-frame:hover img { transform: scale(1.03); }

.tag {
  display: inline-block;
  font-family: var(--font-head);
  font-size: 0.75rem;
  background: var(--yellow);
  color: var(--ink-fixed);
  border: 2px solid var(--ink);
  padding: 3px 6px;
  margin-bottom: 0.6rem;
}

.client-tag { color: var(--muted); font-size: 0.95rem; margin-bottom: 0.5rem; }

/* Little cartridge mockups tucked onto matching portfolio cards, like a
   physical box-art peeking out of the corner. */
.cart-badge {
  position: absolute;
  top: -16px;
  right: -14px;
  width: 84px;
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  transform: rotate(7deg);
  transition: transform 0.2s ease;
  z-index: 2;
}

.card:nth-child(even) .cart-badge { transform: rotate(-6deg); }
.card:hover .cart-badge { transform: rotate(0deg) scale(1.08); }

@media (max-width: 600px) {
  .cart-badge { width: 60px; top: -10px; right: -8px; }
}

/* Decorative scatter strip of other Marvin cartridges/titles, purely for
   flavour — not tied to a specific case study. */
.cart-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-end;
  gap: 1.8rem;
  margin: 2.4rem 0;
}

.cart-strip a {
  display: block;
}

.cart-strip img {
  width: 88px;
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  transition: transform 0.2s ease;
}

.cart-strip a:nth-child(odd) img { transform: rotate(-6deg); }
.cart-strip a:nth-child(even) img { transform: rotate(5deg); }
.cart-strip a:hover img { transform: rotate(0deg) scale(1.1); }

/* Footer */
.site-footer {
  border-top: 3px solid var(--ink);
  padding: 2.5rem 0;
  background: var(--bg-alt);
}

.site-footer .container {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.social-list {
  list-style: none;
  padding: 0;
  margin: 0.6rem 0 0;
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

.social-list a { text-decoration: underline; }

.small-print { color: var(--muted); font-size: 0.9rem; margin-top: 1.5rem; }

/* Footer eco note — small badge, same treatment as the portfolio genre
   tags (mint fill, ink border/text — mint is too light to use as text
   color directly, so it stays fill-only here too). */
.eco-badge {
  display: inline-block;
  font-family: var(--font-head);
  font-size: 0.75rem;
  background: var(--mint);
  color: var(--ink);
  border: 2px solid var(--ink);
  padding: 4px 8px;
  margin-top: 0.8rem;
}

/* Video reel */
.video-frame {
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  background: #000;
  max-width: 760px;
  margin: 0 auto;
  overflow: hidden;
}

.video-frame video { width: 100%; display: block; }

/* Retro dialog box (matches the in-game "CIAO!" popup chrome) */
.dialog-box {
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  background: var(--card-bg);
  margin-bottom: 1.2rem;
}

.dialog-box-title {
  background: var(--ink-fixed);
  color: #fff;
  font-family: var(--font-head);
  font-size: 0.8rem;
  padding: 0.5rem 0.7rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Coral is reserved for a single accent per section instead of coloring
   every popup title bar the same — see index.html for which ones. */
.dialog-box-title.accent-purple { background: var(--purple-fill); }
.dialog-box-title.accent-blue { background: var(--blue-fill); }
.dialog-box-title.accent-coral { background: var(--coral-fill); }
.dialog-box-title.accent-cyan { background: var(--cyan-fill); }
/* Yellow and mint are too light for white text to pass contrast, so this
   one flips to dark text/close-button instead — same move already used
   for the widget's own GB-screen title bar. */
.dialog-box-title.accent-yellow { background: var(--yellow); color: var(--ink-fixed); }
.dialog-box-title.accent-yellow .dialog-box-close { border-color: var(--ink-fixed); color: var(--ink-fixed); }

.dialog-box-title .dialog-box-close {
  border: 1px solid #fff;
  width: 14px;
  height: 14px;
  line-height: 12px;
  text-align: center;
  font-size: 0.6rem;
}

.dialog-box-body { padding: 1rem 1.1rem; font-size: 1.15rem; }
.dialog-box-body .who { display: block; margin-top: 0.4rem; color: var(--muted); font-size: 0.95rem; }

/* Scrolling ticker of short, real reactions pulled from social comments —
   reads like a retro arcade high-score marquee. Pure CSS loop: the comment
   list is duplicated once in the HTML so scrolling exactly -50% loops
   seamlessly. Pauses on hover so it's actually readable if you want to. */
.comment-ticker {
  overflow: hidden;
  border: 3px solid var(--ink);
  background: var(--card-bg);
  box-shadow: var(--shadow);
  padding: 1rem 0;
  margin: 1.5rem 0;
}

.comment-ticker-track {
  display: flex;
  width: max-content;
  gap: 1rem;
  animation: ticker-scroll 45s linear infinite;
}

.comment-ticker:hover .comment-ticker-track { animation-play-state: paused; }

.comment-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  white-space: nowrap;
  font-size: 1.1rem;
  padding: 0.5rem 1rem;
  border: 2px solid var(--ink);
  background: var(--yellow);
  color: var(--ink-fixed);
  text-decoration: none;
  cursor: pointer;
}

.comment-chip:hover { text-decoration: underline; }

.comment-chip .who { font-size: 0.85rem; opacity: 0.75; }

.comment-chip:nth-child(6n+2) { background: var(--mint); }
.comment-chip:nth-child(6n+3) { background: var(--blue-fill); color: #fff; }
.comment-chip:nth-child(6n+4) { background: var(--coral-fill); color: #fff; }
.comment-chip:nth-child(6n+5) { background: var(--purple-fill); color: #fff; }
.comment-chip:nth-child(6n+6) { background: var(--cyan-fill); color: #fff; }

@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Subtle CRT scanline overlay for cover art / portraits */
.cover-frame, .avatar-real { position: relative; }
.cover-frame::after, .avatar-real::after {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.08),
    rgba(0, 0, 0, 0.08) 1px,
    transparent 1px,
    transparent 3px
  );
  pointer-events: none;
}

/* Utility */
.center { text-align: center; }
.mt-0 { margin-top: 0; }
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

/* Animated Marvin mascot (9-frame idle sprite — stands and blinks rather
   than walking in place, which looked odd on a decorative element that
   never actually moves). */
.mascot-sprite {
  width: 112px;
  aspect-ratio: 112 / 124;
  display: inline-block;
  background-image: url("../img/marvin-idle-sprite.png");
  background-repeat: no-repeat;
  background-size: 900% 100%;
  image-rendering: pixelated;
  animation: marvin-mascot-idle 1.5s steps(9, jump-none) infinite;
}

.mascot-sprite.sm { width: 62px; }
.mascot-sprite.lg { width: 154px; }

@keyframes marvin-mascot-idle {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

/* Little "dance" sequence for the Home CTA mascot only (assets/js/
   marvin-mascot.js): idle, then a jump, then a few steps right/left back
   to center, on a loop. .mascot-dance carries the position transition;
   .dance-walk / .dance-jump swap the sprite sheet for those phases. */
.mascot-dance {
  position: absolute;
  left: 0;
  bottom: 0;
  /* Only position slides smoothly — the left/right facing flip (via the
     separate `scale` property, not `transform`) snaps instantly, like a
     classic sprite flip, instead of visibly rotating through itself. */
  transition-property: translate;
}

/* Sized to match .mascot-dance exactly so the bubble's `left: 100%`
   anchors to Marvin's own right edge, not the much wider .card around
   him — .mascot-dance can't carry role="img" AND contain the bubble
   itself, since AT ignores the text content of anything inside a
   role="img" subtree, so this sibling wrapper holds both instead.
   Height is pinned to the taller jump/land frame (128px) rather than
   left to shrink-to-fit: .dance-jump's own aspect-ratio is 4px taller
   than idle's, and letting the wrap's height follow that made the
   whole card visibly grow and shrink by those 4px every time the jump
   phase played. .mascot-dance is bottom-anchored inside so his feet
   stay on the ground line either way. */
.mascot-dance-wrap {
  position: relative;
  width: 112px;
  height: 128px;
  margin: 0 auto 1rem;
}

/* Minimal ground line so his feet read as standing on something. */
.mascot-dance::after {
  content: "";
  position: absolute;
  left: 18px;
  right: 18px;
  bottom: 4px;
  height: 2px;
  background: var(--ink);
  opacity: 0.15;
}

/* "Which one's the real Marvin" bubble — a scripted, one-time exchange
   with the corner widget (assets/js/marvin-mascot.js + marvin-widget.js).
   Deliberately plain (white/black) rather than the widget's GB-screen
   toast look, so the two read as visually distinct characters. */
.mascot-dance-bubble {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 14px;
  width: 170px;
  padding: 0.6rem 0.75rem;
  background: var(--card-bg);
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.3;
  text-align: left;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

.mascot-dance-bubble.is-visible { opacity: 1; }

@media (max-width: 600px) {
  .mascot-dance-wrap {
    width: auto;
  }
  .mascot-dance-bubble {
    position: static;
    transform: none;
    margin: 0.6rem auto 0;
    left: auto;
    top: auto;
  }
}

/* Claudio breaks the fourth wall to end the clone-fight — a single,
   center-screen line styled distinctly from both Marvins' own speech
   (neither GB-screen nor plain-white) so it reads as "the real world"
   stepping in. Click anywhere to dismiss early, otherwise auto-closes. */
.marvin-interrupt {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(24, 24, 27, 0.7);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
  cursor: pointer;
}

.marvin-interrupt.is-visible {
  opacity: 1;
  pointer-events: auto;
}

.marvin-interrupt-box {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  max-width: 420px;
  background: var(--ink-fixed);
  border: 3px solid var(--yellow);
  box-shadow: var(--shadow);
  padding: 1.1rem 1.3rem;
  color: #fff;
  font-size: 1rem;
  line-height: 1.4;
  text-align: left;
}

.marvin-interrupt-portrait {
  width: 56px;
  height: 56px;
  flex-shrink: 0;
  border: 2px solid var(--yellow);
  image-rendering: pixelated;
}

.marvin-interrupt-name {
  display: block;
  font-family: var(--font-head);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  color: var(--yellow);
  margin-bottom: 0.35rem;
}

.mascot-dance.dance-walk {
  background-image: url("../img/marvin-walk-sprite.png");
  background-size: 800% 100%;
  animation: marvin-mascot-walk 0.5s steps(8, jump-none) infinite;
}

.mascot-dance.dance-jump {
  aspect-ratio: 112 / 128;
  background-image: url("../img/marvin-jump-sprite.png");
  background-size: 1100% 100%;
  animation: marvin-mascot-jump 1.1s steps(11, jump-none) 1;
}

@keyframes marvin-mascot-walk {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

@keyframes marvin-mascot-jump {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

/* Typewriter effect (used on short "// TAG" style lines, and on every
   H1/H2). Hidden until assets/js/animations.js actually starts typing it
   — otherwise the full text renders normally first (it's real HTML), then
   gets wiped and retyped the moment it scrolls into view, which reads as
   an unwanted "flash then rebuild." Same hide-then-reveal approach as
   .reveal below. */
[data-typewriter] {
  opacity: 0;
}

.type-cursor {
  color: var(--mint-text);
  animation: caret-blink 0.9s steps(1) infinite;
}

@keyframes caret-blink { 50% { opacity: 0; } }

/* Scroll reveal — slide + fade in from alternating sides */
.reveal {
  opacity: 0;
  transform: translateX(-32px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.reveal-right { transform: translateX(32px); }
.reveal.in-view { opacity: 1; transform: translateX(0); }

/* Note: the mascot idle, typewriter and scroll-reveal animations run
   unconditionally (not gated behind prefers-reduced-motion) so they're
   always visible — see assets/js/animations.js. */

/* Marvin corner widget — a living character, not a static click-toy.
   Styled as a real Game Boy-style handheld screen: pale-green LCD with
   dark-green duotone art, dark housing, twin accent stripe under the
   title bar. Idle / walk / talk use Marvin's real sprite frames from the
   original game project, recolored into the two-tone GB green — no
   invented poses. He rests at the bottom-right or bottom-left corner
   (never in between, so he's never sitting over page text) and
   occasionally walks across to the other side — assets/js/marvin-widget.js
   drives the transform, always ending up facing back in toward the page. */
.marvin-widget {
  position: fixed;
  left: 0;
  bottom: 1.1rem;
  z-index: 60;
  transition-property: transform;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.6rem;
  transform: translateX(0);
  /* Fixed to match .marvin-widget-bubble's width so opening/closing the
     panel never changes the container's own size — that used to shift
     the whole widget (positioned via JS-measured width) left or right
     depending on which child happened to be visible. */
  width: 200px;
}

/* At the left corner, everything hugs the container's left edge instead
   of the right — otherwise the panel/toast would grow off-screen past
   the left edge of the viewport. */
.marvin-widget.at-left {
  align-items: flex-start;
}

.marvin-widget-bubble {
  width: 200px;
  margin-bottom: 0;
  background: var(--ink-fixed);
  overflow: hidden;
}

.marvin-widget-bubble[hidden] { display: none; }

.marvin-widget-bubble .dialog-box-title {
  background: var(--gb-screen);
  color: var(--gb-pixel-dark);
}

.marvin-widget-bubble .dialog-box-title .dialog-box-close {
  border-color: var(--gb-pixel-dark);
  color: var(--gb-pixel-dark);
}

.marvin-widget-bubble .dialog-box-body {
  position: relative;
  background: var(--gb-screen);
  color: var(--gb-pixel-dark);
  text-shadow: none;
}

/* On the GB-screen background the typing caret should match the dark-green
   text there, not the site-wide mint used for the light-background
   typewriter headings. */
.marvin-toast .type-cursor,
.marvin-widget-bubble .type-cursor {
  color: var(--gb-pixel-dark);
}

/* CRT glass: scanlines + a soft vignette toward the corners, tinted with
   the screen's own dark green rather than neutral black. */
.marvin-widget-bubble .dialog-box-body::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, transparent 55%, rgba(15, 56, 15, 0.35) 100%),
    repeating-linear-gradient(to bottom, rgba(15, 56, 15, 0.15) 0, rgba(15, 56, 15, 0.15) 1px, transparent 1px, transparent 3px);
  pointer-events: none;
}

.marvin-widget-avatar {
  position: relative;
  display: block;
  line-height: 0;
  overflow: hidden;
  border: 3px solid var(--ink);
  background: var(--gb-screen);
  padding: 10px 12px 4px;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform 0.15s ease;
}

.marvin-widget-avatar:hover { transform: scale(1.08); }
.marvin-widget-avatar:active { transform: scale(0.97); }

/* Konami code payoff — same "runs unconditionally" precedent as the rest of
   the mascot's animations above, since this only ever fires from a
   deliberate 10-keypress sequence, never ambient. */
@keyframes konami-celebrate {
  0%, 100% { transform: scale(1) rotate(0deg); }
  25% { transform: scale(1.25) rotate(-8deg); }
  50% { transform: scale(1.1) rotate(8deg); }
  75% { transform: scale(1.25) rotate(-4deg); }
}
.marvin-widget-avatar.is-konami {
  animation: konami-celebrate 0.9s ease;
  box-shadow: var(--shadow), 0 0 0 4px var(--yellow);
}

/* Ground line — grounds Marvin on the little screen instead of floating. */
.marvin-widget-avatar::before {
  content: "";
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 5px;
  height: 2px;
  background: var(--gb-pixel-dark);
  opacity: 0.55;
}

/* CRT glass overlay, same treatment as the bubble screen. */
.marvin-widget-avatar::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, transparent 50%, rgba(15, 56, 15, 0.4) 100%),
    repeating-linear-gradient(to bottom, rgba(15, 56, 15, 0.15) 0, rgba(15, 56, 15, 0.15) 1px, transparent 1px, transparent 3px);
  pointer-events: none;
}

/* Idle by default: Marvin's real idle-animation frames, gently looping.
   Facing (the `scale` property, set from JS) and any small in-frame
   fidget position (the `translate` property) are kept independent of
   each other on purpose — only `translate` is transitioned, so a facing
   flip snaps instantly like a classic sprite flip instead of visibly
   rotating through itself. The frame (.marvin-widget-avatar) itself never
   moves for a fidget — only the sprite shifts inside it. */
.marvin-widget-avatar .mascot-sprite {
  aspect-ratio: 112 / 128;
  background-image: url("../img/marvin-idle-gb-sprite.png");
  background-size: 900% 100%;
  animation: marvin-idle-frames 1.5s steps(9, jump-none) infinite;
  transition-property: translate;
}

/* Walking: Marvin's real walk-cycle frames, used both for the corner-to-
   corner shuttle and for the small in-frame fidget shuffle. Facing (set
   from JS via the sprite's `scale`) matches direction of travel while
   this plays, then gets re-set for "facing inward" once at rest again. */
.marvin-widget-avatar.is-walking .mascot-sprite {
  background-image: url("../img/marvin-walk-gb-sprite.png");
  background-size: 800% 100%;
  animation: marvin-walk-frames 0.7s steps(8, jump-none) infinite;
}

/* Talking state (screen open): Marvin's real jump + land frames, paired
   with a matching hop/shimmy so he visibly jumps rather than just
   swapping pose in place. */
.marvin-widget-avatar.is-talking .mascot-sprite {
  background-image: url("../img/marvin-talk-gb-sprite.png");
  background-size: 1100% 100%;
  animation: marvin-talk-frames 1.1s steps(11, jump-none) infinite, marvin-talk-hop 1.1s ease-in-out infinite;
}

@keyframes marvin-idle-frames {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

@keyframes marvin-talk-frames {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

@keyframes marvin-walk-frames {
  from { background-position-x: 0%; }
  to { background-position-x: 100%; }
}

@keyframes marvin-talk-hop {
  0% { transform: translate(0, 0); }
  30% { transform: translate(-2px, -5px); }
  55% { transform: translate(2px, -4px); }
  80% { transform: translate(-1px, 0); }
  100% { transform: translate(0, 0); }
}

/* "Marvin Reports" — a small recurring fun-facts segment, Marvin
   presenting from the same dark GB-phosphor screen as the homepage boot
   sequence, so the two read as the same hardware. Cycles forever while
   in view (assets/js/marvin-reports.js pauses it off-screen/backgrounded)
   and gives prefers-reduced-motion visitors the full fact list as a
   plain static panel instead — same fallback pattern as the boot
   screen's glitch effect, reusing its .boot-glitch-static styling. */
.marvin-report-screen {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1.4rem;
  background: var(--gb-pixel-dark);
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  padding: 1.4rem 1.6rem;
  overflow: hidden;
}

.marvin-report-sprite {
  width: 140px;
  aspect-ratio: 112 / 128;
  flex-shrink: 0;
  background-image: url("../img/marvin-talk-gb-sprite.png");
  background-repeat: no-repeat;
  background-size: 1100% 100%;
  image-rendering: pixelated;
  animation: marvin-talk-frames 1.1s steps(11, jump-none) infinite, marvin-talk-hop 1.1s ease-in-out infinite;
}

.marvin-report-feed {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--gb-screen);
  position: relative;
  z-index: 1;
}

.marvin-report-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-head);
  font-size: 0.7rem;
  opacity: 0.85;
  margin-bottom: 0.7rem;
}

.marvin-report-label::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--coral);
  animation: marvin-report-blink 1.4s steps(1) infinite;
}

@keyframes marvin-report-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.25; }
}

.marvin-report-text {
  font-family: var(--font-body);
  font-size: 1.3rem;
  line-height: 1.5;
  margin: 0;
  min-height: 3.9em;
}

.marvin-report-text .type-cursor { color: var(--gb-screen); }

/* Same CRT glass (scanlines + vignette) as the boot screen. */
.marvin-report-screen::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.45) 100%),
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 3px);
  pointer-events: none;
}

.marvin-report-screen .boot-glitch-static { position: relative; z-index: 1; width: 100%; }

@media (max-width: 640px) {
  .marvin-report-screen { flex-direction: column; align-items: flex-start; gap: 1rem; }
  .marvin-report-sprite { width: 100px; }
}

/* Compact auto-dismissing toast — spontaneous comments (welcome, jokes,
   tips, trivia, idle nudges, section remarks). Deliberately smaller and
   plainer than the click-triggered panel: no title bar, short text only,
   fades in/out on its own via .is-visible rather than being toggled with
   [hidden], so the dismissal transition can actually play. */
.marvin-toast {
  position: absolute;
  right: 0;
  bottom: 105px;
  max-width: 175px;
  padding: 0.5rem 0.65rem;
  background: var(--gb-screen);
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
  color: var(--gb-pixel-dark);
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.25;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.3s ease, transform 0.3s ease;
  pointer-events: none;
}

/* At the left corner the toast anchors from the left instead, so it grows
   rightward into the page rather than off the left edge of the viewport. */
.marvin-widget.at-left .marvin-toast {
  right: auto;
  left: 0;
}

.marvin-toast.is-visible { opacity: 1; transform: translateY(0); }

@media (max-width: 600px) {
  .marvin-widget { bottom: 0.8rem; width: 190px; }
  .marvin-widget-bubble { width: 190px; }
  .marvin-widget-avatar .mascot-sprite { width: 48px; }
  .marvin-toast { max-width: 145px; font-size: 0.85rem; bottom: 84px; }
}

/* Advergame Estimator */
.estimator { max-width: 700px; margin: 0 auto; }

.estimator-progress {
  font-family: var(--font-head);
  font-size: 0.95rem;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 1.2rem;
}

/* No font-size override here on purpose — inherits the site's default h2
   clamp() sizing instead of an arbitrary smaller fixed value. */
.estimator-steps h2 { margin: 0 0 1rem; }

.estimator-options {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
  margin-bottom: 1rem;
}

@media (max-width: 620px) {
  .estimator-options { grid-template-columns: 1fr; }
}

.estimator-option {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--card-bg);
  border: 3px solid var(--ink);
  box-shadow: var(--shadow);
  padding: 0.9rem 1rem;
  cursor: pointer;
  font-family: var(--font-body);
  color: var(--ink);
  transition: transform 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}

.estimator-option:hover { transform: translate(-2px, -2px); }

.estimator-option.is-selected {
  border-color: var(--mint);
  background: var(--tag-bg, var(--bg-alt));
  box-shadow: 4px 4px 0 var(--mint);
}

.estimator-option .opt-label {
  display: block;
  font-family: var(--font-head);
  font-size: 1.05rem;
  margin-bottom: 0.35rem;
}

.estimator-option .opt-desc { display: block; color: var(--muted); font-size: 1.1rem; }

.estimator-options--check {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
}

.estimator-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border: 2px solid var(--ink);
  background: var(--card-bg);
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  font-size: 1.1rem;
}

.estimator-check.is-selected { background: var(--yellow); color: var(--ink-fixed); }
.estimator-check input { width: 18px; height: 18px; accent-color: var(--mint); cursor: pointer; }

.estimator-field { margin-bottom: 1.1rem; }
.estimator-field label {
  display: block;
  font-family: var(--font-head);
  font-size: 0.95rem;
  margin-bottom: 0.4rem;
}
.estimator-field input,
.estimator-field textarea {
  width: 100%;
  border: 3px solid var(--ink);
  background: var(--card-bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.2rem;
  padding: 0.6rem 0.7rem;
}
.estimator-field textarea { min-height: 90px; resize: vertical; }

.estimator-nav { display: flex; gap: 1rem; margin-top: 1.6rem; }
.estimator-nav [disabled] { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

.estimator-success {
  border: 3px solid var(--ink);
  border-top: 6px solid var(--mint);
  background: var(--card-bg);
  box-shadow: var(--shadow);
  padding: 1.4rem 1.5rem;
}
.estimator-success h2 { margin-top: 0; }

/* Fallback for visitors with no default mail app configured (common on
   desktop) — mailto: silently does nothing for them, so this gives a
   copy-able version of the exact same message as a way out. Always shown
   on step 5, harmless bonus for anyone whose mail app did open. */
.estimator-fallback {
  margin-top: 1.4rem;
  padding-top: 1.2rem;
  border-top: 2px dashed var(--muted);
}
.estimator-fallback p { font-size: 0.95rem; color: var(--muted); }
.estimator-fallback-text {
  width: 100%;
  min-height: 160px;
  border: 3px solid var(--ink);
  background: var(--bg-alt);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.05rem;
  padding: 0.7rem;
  resize: vertical;
  margin-bottom: 0.8rem;
}
