:root {
  --color-primary: #D4366A;
  --color-primary-dark: #8E1E4D;
  --color-primary-glow: rgba(212, 54, 106, 0.35);
  --color-primary-glow-strong: rgba(212, 54, 106, 0.55);
  --color-dark: #0D111F;
  --color-dark-secondary: #171c2e;
  --color-dark-elevated: #1b2338;
  --color-dark-border: #262c40;
  --color-accent-green: #4CAF50;
  --color-accent-green-glow: rgba(76, 175, 80, 0.35);
  --color-light: #E6E7EB;
  --color-white: #FFFFFF;
  --color-muted-on-dark: #b7bccb;

  --font-sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --hud-grid-lines:
    repeating-linear-gradient(0deg, rgba(230, 231, 235, 0.045) 0, rgba(230, 231, 235, 0.045) 1px, transparent 1px, transparent 42px),
    repeating-linear-gradient(90deg, rgba(230, 231, 235, 0.045) 0, rgba(230, 231, 235, 0.045) 1px, transparent 1px, transparent 42px);

  --radius: 10px;
  --container-width: 1600px;
  --header-height: 72px;
  --footer-height: 40px;
  --service-nav-height: 60px;
  --transition-base: 0.3s ease;
  --transition-slow: 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
}

/* Hide the scrollbar everywhere, site-wide — scrolling itself stays
   fully functional (wheel, touch, keyboard, paginate.js's glide), this
   only removes the visible track/thumb. Needs all three vendor hooks:
   Firefox reads scrollbar-width, old Edge/IE reads -ms-overflow-style,
   and WebKit/Blink (Chrome, Safari, new Edge) need the ::-webkit-
   scrollbar pseudo-element hidden explicitly — neither property alone
   covers every engine this site supports. */
html {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

html::-webkit-scrollbar {
  display: none;
}

html.nav-open {
  overflow: hidden;
}

body {
  /* Primary/default typeface is now the console/terminal stack per the
     client's request — headings, nav, buttons and UI chrome all inherit
     this. Long-form paragraph copy is deliberately opted back to
     --font-sans below (see the `p` rule) for readability; see the
     .form-field input/textarea rule for that same judgment call applied
     to form fields (kept mono, since those are short UI fields, not
     long-form copy). */
  font-family: var(--font-mono);
  color: var(--color-light);
  background: var(--color-dark);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* Clears the fixed, always-visible .site-footer strip (see Footer
     below) so the last line of ordinary in-flow content on plain-scroll
     pages (e.g. legal/privacy.html, legal/terms.html) never sits behind
     it. Paginated (.snap-scroll) screens handle their own clearance via
     their own padding-bottom rule further down, since each screen must
     stay clear of the footer individually, not just the very end of the
     document. */
  padding-bottom: var(--footer-height);
}

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

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  color: var(--color-primary-dark);
}

h1, h2, h3 {
  color: var(--color-white);
  line-height: 1.2;
  margin: 0 0 0.6em;
}

h1 {
  font-size: 2.4rem;
}

h2 {
  font-size: 1.7rem;
}

h3 {
  font-size: 1.2rem;
}

p {
  margin: 0 0 1em;
  color: var(--color-muted-on-dark);
  /* Long-form paragraph copy stays on the sans stack even though body's
     default is now --font-mono — full monospace across service
     descriptions and (especially) the privacy/terms legal pages hurts
     readability enough to carve out this exception. Everything else
     (headings, nav, buttons, labels, form fields) goes mono via
     inheritance. */
  font-family: var(--font-sans);
}

.container {
  width: 100%;
  max-width: var(--container-width);
  margin: 0 auto;
  padding-inline: clamp(24px, 6vw, 120px);
}

/* Visually-hidden but still announced — used to keep a real heading in
   the accessibility tree where the visible equivalent lives as
   decorative SVG text (e.g. the home page's berry-diagram section
   title) instead of a normal DOM heading. */
.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;
}

/* Eyebrow / section marker (monospace tech labels) */

.eyebrow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 16px;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--color-primary);
}

.eyebrow::before {
  content: "";
  width: 28px;
  height: 1px;
  background: var(--color-primary);
  box-shadow: 0 0 8px var(--color-primary-glow-strong);
}

.section-marker {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-bottom: 12px;
}

.section-marker-index {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 2px;
  color: var(--color-primary);
}

.section-marker-line {
  width: 60px;
  height: 1px;
  background: linear-gradient(90deg, var(--color-primary), transparent);
  box-shadow: 0 0 8px var(--color-primary-glow);
}

/* Header / Nav */

.site-header {
  /* Fully transparent per client request — no fill, no HUD grid image,
     no border, no shadow. The persistent .circuit-backdrop (fixed,
     behind everything) shows through directly. */
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 72px;
}

/* Wraps Back + the logo on the header's left side — Back sits right
   next to the logo (the standard "go back" spot, top-left) instead of
   grouped in with Home/language/menu on the right, where client
   feedback found it unintuitive. */
.header-start {
  display: flex;
  align-items: center;
  gap: 14px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--color-white);
}

.brand:hover {
  color: var(--color-white);
}

.brand-logo {
  height: 40px;
}

.brand-name {
  font-weight: 700;
  font-size: 1.15rem;
}

/* Fallback nav — shown only when JS never runs (see the no-js/js class
   swap in base.html). The menu-button overlay is the primary pattern. */
.site-nav-fallback {
  display: flex;
  align-items: center;
  gap: 24px;
}

.js .site-nav-fallback {
  display: none;
}

.site-nav-fallback a {
  color: var(--color-light);
  font-weight: 500;
}

.site-nav-fallback a:hover {
  color: var(--color-primary);
}

.no-js .menu-btn,
.no-js .nav-overlay {
  display: none;
}

/* The header's Home control is a plain <a href>, so it degrades to a
   normal working link with no-js (landing on "/" naturally starts at
   screen 0 anyway). The Back control has no href at all — it only means
   anything once header-nav.js can run — so it's hidden with no-js rather
   than left as a dead button, same treatment as .menu-btn above. */
.no-js .header-nav-btn--back {
  display: none;
}

.nav-cta {
  background: var(--color-primary);
  color: var(--color-white) !important;
  padding: 8px 18px;
  border-radius: 999px;
  font-weight: 600;
}

.nav-cta:hover {
  background: var(--color-primary-dark);
  box-shadow: 0 8px 20px var(--color-primary-glow);
}

/* Home icon control, grouped with the language toggle and menu button
   on the right side of the header — icon-only per client request
   ("solo como íconos"), reachable from every screen without opening
   the nav overlay. Back lives instead in .header-start, next to the
   logo (see above). Home glows pink (matches the primary brand color
   used everywhere else); Back glows green — the two color-coded
   distinctly at a glance, and it's the "detallito en verde" the client
   asked for used with actual purpose rather than decoration for its
   own sake. */
.header-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.header-nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  background: transparent;
  border: 1px solid var(--color-dark-border);
  border-radius: 999px;
  padding: 0;
  margin: 0;
  color: var(--color-light);
  cursor: pointer;
  transition: border-color var(--transition-base), box-shadow var(--transition-base), color var(--transition-base);
}

.header-nav-btn-icon {
  flex-shrink: 0;
}

.header-nav-btn--home:hover,
.header-nav-btn--home:focus-visible {
  border-color: var(--color-primary);
  color: var(--color-primary);
  box-shadow: 0 0 12px var(--color-primary-glow);
}

.header-nav-btn--back:hover,
.header-nav-btn--back:focus-visible {
  border-color: var(--color-accent-green);
  color: var(--color-accent-green);
  box-shadow: 0 0 12px var(--color-accent-green-glow);
}

/* Language toggle (ES/EN) — a client-side-only i18n switch (see
   static/js/i18n.js), reusing the .header-nav-btn pill shape but with a
   short text label (the language you'd switch TO) instead of an icon
   glyph. One instance lives in .header-controls, a second, larger
   instance lives in the mobile nav overlay's head (.nav-overlay-panel),
   so the toggle is reachable everywhere the Home/Back/Menu controls are.
   Both instances share the .lang-toggle-btn class so JS can keep them in
   sync with a single querySelectorAll pass. */
.lang-toggle-btn {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.5px;
}

.lang-toggle-btn:hover,
.lang-toggle-btn:focus-visible {
  border-color: var(--color-primary);
  color: var(--color-primary);
  box-shadow: 0 0 12px var(--color-primary-glow);
}

.no-js .lang-toggle-btn {
  display: none;
}

/* Menu button (opens the full-screen nav overlay) */

.menu-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  background: transparent;
  border: 1px solid var(--color-dark-border);
  border-radius: 999px;
  padding: 9px 20px 9px 16px;
  color: var(--color-light);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 2px;
  transition: border-color var(--transition-base), box-shadow var(--transition-base), color var(--transition-base);
}

.menu-btn:hover,
.menu-btn:focus-visible,
.menu-btn[aria-expanded="true"] {
  border-color: var(--color-primary);
  color: var(--color-primary);
  box-shadow: 0 0 18px var(--color-primary-glow);
}

.menu-btn-icon {
  display: flex;
  flex-direction: column;
  gap: 5px;
  width: 20px;
}

.menu-btn-icon span {
  display: block;
  height: 2px;
  width: 100%;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--transition-base), opacity var(--transition-base);
}

.menu-btn[aria-expanded="true"] .menu-btn-icon span:first-child {
  transform: translateY(3.5px) rotate(45deg);
}

.menu-btn[aria-expanded="true"] .menu-btn-icon span:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

/* Full-screen nav overlay (game-menu style) */

.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}

.nav-overlay.is-open {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.35s ease;
}

.nav-overlay-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  background: rgba(6, 8, 16, 0.86);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  cursor: pointer;
}

.nav-overlay-panel {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 720px;
  margin: 0 auto;
  padding: 40px max(24px, 6vw);
}

.nav-overlay-head {
  position: absolute;
  top: 24px;
  left: max(24px, 6vw);
  right: max(24px, 6vw);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav-overlay-kicker {
  font-family: var(--font-mono);
  letter-spacing: 3px;
  font-size: 0.8rem;
  color: var(--color-muted-on-dark);
}

.nav-overlay-head-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.nav-overlay-lang-btn {
  width: 44px;
  height: 44px;
}

.nav-overlay-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: transparent;
  border: 1px solid var(--color-dark-border);
  border-radius: 999px;
  color: var(--color-light);
  cursor: pointer;
  transition: border-color var(--transition-base), color var(--transition-base), box-shadow var(--transition-base);
}

.nav-overlay-close:hover,
.nav-overlay-close:focus-visible {
  border-color: var(--color-primary);
  color: var(--color-primary);
  box-shadow: 0 0 18px var(--color-primary-glow);
}

.nav-overlay-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-overlay-link {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 18px 8px;
  border-bottom: 1px solid var(--color-dark-border);
  color: var(--color-light);
  font-size: clamp(1.5rem, 4.2vw, 2.4rem);
  font-weight: 600;
  opacity: 0;
  transform: translateY(24px);
  transition: color var(--transition-base);
}

.nav-overlay.is-open .nav-overlay-link {
  animation: nav-link-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.nav-overlay.is-open .nav-overlay-link:nth-child(1) { animation-delay: 0.08s; }
.nav-overlay.is-open .nav-overlay-link:nth-child(2) { animation-delay: 0.16s; }
.nav-overlay.is-open .nav-overlay-link:nth-child(3) { animation-delay: 0.24s; }
.nav-overlay.is-open .nav-overlay-link:nth-child(4) { animation-delay: 0.32s; }
.nav-overlay.is-open .nav-overlay-link:nth-child(5) { animation-delay: 0.4s; }

@keyframes nav-link-in {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: translateY(0); }
}

.nav-overlay-index {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--color-primary);
  min-width: 2.4ch;
}

.nav-overlay-chevron {
  margin-left: auto;
  color: var(--color-primary);
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity var(--transition-base), transform var(--transition-base);
  flex-shrink: 0;
}

.nav-overlay-link:hover,
.nav-overlay-link:focus-visible {
  color: var(--color-primary);
}

.nav-overlay-link:hover .nav-overlay-chevron,
.nav-overlay-link:focus-visible .nav-overlay-chevron {
  opacity: 1;
  transform: translateX(0);
}

.nav-overlay-link--cta {
  color: var(--color-primary);
}

/* Buttons */

.btn {
  position: relative;
  display: inline-block;
  overflow: hidden;
  padding: 12px 26px;
  border-radius: 999px;
  font-weight: 600;
  border: 2px solid transparent;
  cursor: pointer;
  font-size: 1rem;
  transition: transform var(--transition-base), box-shadow var(--transition-base),
    background var(--transition-base), color var(--transition-base), border-color var(--transition-base);
}

.btn::before {
  content: "";
  position: absolute;
  top: 0;
  left: -60%;
  width: 40%;
  height: 100%;
  background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.55), transparent);
  transform: skewX(-20deg) translateX(0);
  transition: transform 0.65s ease;
  pointer-events: none;
}

.btn:hover::before {
  transform: skewX(-20deg) translateX(320%);
}

.btn-primary {
  background: var(--color-primary);
  color: var(--color-white);
  box-shadow: 0 0 16px 1px var(--color-primary-glow);
}

.btn-primary:hover {
  background: var(--color-primary-dark);
  color: var(--color-white);
  box-shadow: 0 0 26px 4px var(--color-primary-glow-strong), 0 10px 30px rgba(212, 54, 106, 0.3);
  transform: translateY(-2px);
}

.btn-ghost {
  background: transparent;
  border-color: var(--color-dark-border);
  color: var(--color-white);
}

.btn-ghost:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
  box-shadow: 0 0 18px var(--color-primary-glow);
  transform: translateY(-2px);
}

/* Hero */

.hero {
  /* No background-image and no per-section fill anymore — the persistent
     .circuit-backdrop (fixed behind the whole page) is the only
     background, everywhere, so the "stage" never changes as you move
     between sections; only the foreground content scrolls. position:relative
     + overflow:hidden are kept for the ::before glow and the .container
     z-index stacking below, not for a background of their own. */
  color: var(--color-white);
  padding: 120px 0 110px;
  position: relative;
  overflow: hidden;
}

.hero::before {
  content: "";
  position: absolute;
  top: -140px;
  right: -140px;
  width: 520px;
  height: 520px;
  background: radial-gradient(circle, var(--color-primary-glow) 0%, rgba(212, 54, 106, 0) 70%);
  z-index: 0;
  pointer-events: none;
  /* Parallax: static/js/main.js writes this custom property on the
     .hero element itself (a pseudo-element can't receive an inline
     style directly), so only the glow drifts — the section and its
     text content never move. */
  transform: translateY(var(--parallax-shift, 0px));
}

.hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .hero-canvas {
    display: none;
  }
}

.hero-inner {
  max-width: 780px;
}

/* Merged hero + "Nuestros Servicios" screen: hero copy left, interactive
   berry diagram right, so both fit on one snap-scroll screen instead of
   two ("no volver a hacer scroll" per client request). Collapses to a
   single stacked column at the same 720px breakpoint the other 2-column
   grids (.grid-2 etc.) already collapse at, see Responsive below. */
.hero-split {
  display: grid;
  /* Diagram column now gets more real estate than the text column — the
     client wants the berry to be the dominant element, not the copy.
     Pushed further still (was 0.85fr/1.15fr) per follow-up feedback that
     the "Creamos el software..." copy was still pulling too much focus. */
  grid-template-columns: 0.75fr 1.25fr;
  gap: 48px;
  align-items: center;
}

.hero h1 {
  color: var(--color-white);
  /* Toned down from the original 2.9rem, then again from 2rem, per
     client request ("no tan llamativo") — the services diagram is meant
     to be the dominant element on this screen, not the copy. */
  font-size: 1.75rem;
  margin-bottom: 0.4em;
}

.hero h1 .word,
.page-hero h1 .word {
  display: inline-block;
  opacity: 0;
  transform: translateY(24px);
  animation: word-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes word-in {
  to { opacity: 1; transform: none; }
}

.hero-subtitle {
  font-size: 1.15rem;
  color: var(--color-muted-on-dark);
  max-width: 640px;
}

.hero-actions {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 28px;
}

/* Quiet "see everything" escape hatch for users who want the full
   services list rather than one specific node — deliberately not a
   .btn so it doesn't compete with the two primary CTAs above it. */
.hero-services-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 22px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.5px;
  color: var(--color-muted-on-dark);
  text-decoration: none;
  transition: color var(--transition-base);
}

.hero-services-link:hover,
.hero-services-link:focus-visible {
  color: var(--color-primary);
}

.hero-services-link::after {
  content: "\2192";
}

/* Interactive berry-shaped services diagram (right column), simplified
   after a fuller two-ring mesh + background watermarks didn't read
   clearly as a berry. Now: ONE ring of 9 nodes, evenly spaced by arc
   length along the exact berry-silhouette outline curve, uniformly
   sized, each one a real clickable service, connected only to its two
   ring-neighbors in a single closed loop. .berry-diagram-decor-nodes
   below is unused on this simplified pass but left defined in case a
   future pass reintroduces non-clickable filler nodes. */
.hero-diagram-col {
  display: flex;
  justify-content: center;
}

.berry-diagram {
  display: block;
  width: 100%;
  /* Bumped from 620px so the berry (and every label/font-size inside it,
     since it's all one scaled vector) reads as the screen's dominant
     element instead of the hero copy beside it. */
  max-width: 780px;
  height: auto;
  /* The service labels sit close to the viewBox edges by design (routed
     outward past the berry silhouette) and grow further on hover/focus
     (see .circuit-node-label's scale transform below) — overflow:visible
     is a safety net so that growth is never hard-clipped at the SVG's own
     box, on top of the viewBox margins already sized to fit it. */
  overflow: visible;
}

/* No enclosing disc this pass (the client's own node 12 sits too close
   to the center for one) — a dark halo behind the letters (same
   paint-order trick as .circuit-node-tooltip) keeps the title legible
   against the mesh/lines behind it instead. */
/* Font-size/stroke here (and throughout the rest of the diagram's px
   values) are scaled ~2.14x versus earlier passes, to match the
   client's reference SVG's own coordinate scale (a ~1200-wide viewBox
   instead of ~560) — same relative proportions, bigger absolute units. */
.berry-diagram-title {
  font-family: var(--font-mono);
  font-size: 34px;
  letter-spacing: 3px;
  text-transform: uppercase;
  fill: var(--color-white);
  paint-order: stroke;
  stroke: var(--color-dark);
  stroke-width: 10px;
  stroke-linejoin: round;
}

/* The client's own brand leaf artwork (static/images/BerryByte leaves.png,
   a transparent-background PNG with its own gradient shading) replaces
   the hand-drawn stroke leaves/stem entirely — no CSS treatment needed
   beyond disabling pointer events, since it's a decorative raster asset
   sitting above the node cluster, not an interactive part of it. */
.berry-diagram-leaves {
  pointer-events: none;
}

/* Short straight connectors between adjacent nodes — a beaded mesh, not
   the elbow-bend traces used for the page's ambient background circuitry
   (.circuit-decor), since here every line runs directly between two real
   spheres sitting close together rather than across open background
   space. */
.berry-diagram-traces line {
  stroke: var(--color-primary);
  stroke-width: 4;
  opacity: 0.6;
  filter: drop-shadow(0 0 6px var(--color-primary-glow));
}

/* Plain filler seeds — same cluster, but dim and inert (no link, no
   flicker) so the 9 real service nodes below clearly read as "the
   special, clickable ones" rather than every dot looking equally
   important. Reuses the ambient backdrop's own node-pulse keyframe (the
   slower, softer of the two — see .circuit-decor .circuit-nodes circle)
   instead of led-blink, so they visibly recede behind the brighter,
   faster-flickering interactive dots. */
.berry-diagram-decor-nodes circle {
  fill: var(--color-accent-green);
  opacity: 0.65;
  filter: drop-shadow(0 0 8px var(--color-accent-green-glow));
  animation: node-pulse 4.5s ease-in-out infinite;
}

.berry-diagram-decor-nodes circle:nth-child(even) {
  animation-delay: 2.2s;
}

.circuit-node {
  cursor: pointer;
}

/* No default browser focus ring on click — the node already gets its own
   glow/brighten treatment on :focus-visible below (for real keyboard
   focus), so the native outline on top of that just reads as a stray
   blue box around the node. */
.circuit-node:focus {
  outline: none;
}

/* Larger invisible tap target around the small visible dot, for a
   comfortable click/touch area without inflating the visible glow. */
.circuit-node-hit {
  fill: transparent;
}

/* Deliberately NOT reusing the ambient backdrop's led-blink keyframe —
   that one spends 60% of its cycle at 0.22 opacity, which is correct
   for a tiny background LED but made these much bigger service nodes
   read as "mostly invisible, blending into the backdrop" at almost any
   given instant (per client feedback: "se ve como puntos y ya"). This
   node-flicker keyframe stays bright the whole time and only dips
   slightly, so the dot is always clearly a solid, defined sphere. */
.circuit-node-dot {
  fill: var(--color-primary);
  filter: drop-shadow(0 0 8px var(--color-primary)) drop-shadow(0 0 22px var(--color-primary-glow-strong));
  animation: node-flicker 2.6s ease-in-out infinite;
}

/* Nine nodes now (one per service) instead of four — stagger the flicker
   across three offsets instead of two so the cluster twinkles like a
   living circuit rather than pulsing in two rigid alternating halves. */
.circuit-node:nth-of-type(3n+2) .circuit-node-dot {
  animation-delay: 0.9s;
}

.circuit-node:nth-of-type(3n) .circuit-node-dot {
  animation-delay: 1.8s;
}

/* Hover/click title reveal — hidden by default, shown on hover/focus.
   Anchored toward the inside of the cluster (left-column nodes open
   rightward, right-column nodes open leftward, center-column nodes stay
   centered) and away from the row above/below it, so the label never
   runs off the diagram's edge or straight through a neighboring node.
   paint-order+stroke fakes a soft dark halo behind the letters (rather
   than a background chip) so the text stays readable even where it
   crosses in front of a neighboring dot or connector line — nodes sit
   close together here, unlike the four-node layout this replaced. */
.circuit-node:hover .circuit-node-dot,
.circuit-node:focus-visible .circuit-node-dot {
  /* Pause the flicker while hovered/focused so it doesn't fight with
     reading the label, and brighten well past the passive ambient
     dots' hover state. */
  animation-play-state: paused;
  opacity: 1;
  filter: drop-shadow(0 0 17px var(--color-primary)) drop-shadow(0 0 47px var(--color-primary-glow-strong)) drop-shadow(0 0 68px var(--color-primary-glow));
}

/* The callout line/tick/label now live inside the same <a> as the dot
   (see templates/home/index.html), so hovering the dot OR the name —
   either one — highlights the whole pairing together. */
.circuit-node:hover .circuit-node-callout,
.circuit-node:focus-visible .circuit-node-callout,
.circuit-node:hover .circuit-node-callout-tick,
.circuit-node:focus-visible .circuit-node-callout-tick {
  stroke: var(--color-white);
  opacity: 1;
  stroke-width: 3;
}

.circuit-node:hover .circuit-node-label,
.circuit-node:focus-visible .circuit-node-label {
  opacity: 1;
  filter: drop-shadow(0 0 6px var(--color-primary)) drop-shadow(0 0 16px var(--color-primary-glow-strong));
  transform: scale(1.35);
}

/* Blueprint-style callouts, permanently visible (not hover-only): a
   leader line from each real node out past the silhouette, ending in a
   short perpendicular "dimension" tick, with the service name beyond
   it — same visual language as an engineering drawing's measurement
   call-outs, per the client's "como si la fresa fuera un plano" ask.
   Routed outward on purpose so the always-on labels never sit on top
   of the berry's own nodes/lines. */
.circuit-node-callout,
.circuit-node-callout-tick {
  stroke: var(--color-primary);
  stroke-width: 2;
  opacity: 0.55;
}

.circuit-node-label {
  font-family: var(--font-mono);
  font-size: 22px;
  fill: var(--color-white);
  paint-order: stroke;
  stroke: var(--color-dark);
  stroke-width: 6px;
  stroke-linejoin: round;
  opacity: 0.92;
  /* fill-box makes `transform-origin` resolve against the text's own
     rendered box (in %) instead of the SVG viewport's origin, so the
     hover scale below grows the label in place instead of sliding it
     toward a corner. */
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.2s ease, opacity 0.2s ease, filter 0.2s ease;
}

.page-hero {
  /* See .hero above re: no background-image / per-section fill anymore. */
  color: var(--color-white);
  padding: 64px 0;
  position: relative;
  overflow: hidden;
}

.page-hero::before {
  content: "";
  position: absolute;
  top: -100px;
  right: -100px;
  width: 340px;
  height: 340px;
  background: radial-gradient(circle, rgba(212, 54, 106, 0.22) 0%, rgba(212, 54, 106, 0) 70%);
  z-index: 0;
  pointer-events: none;
  transform: translateY(var(--parallax-shift, 0px));
}

.page-hero h1 {
  color: var(--color-white);
  margin-bottom: 0.3em;
}

.hero .container,
.page-hero .container,
.section-dark .container,
.berry-decorated .container {
  position: relative;
  z-index: 1;
}

/* Sections */

.section {
  padding: 70px 0;
  /* No background of its own — see .hero above. The persistent
     .circuit-backdrop shows through uninterrupted. */
}

.section-dark {
  /* No background of its own — see .hero above. position:relative +
     overflow:hidden kept for this section's own decorative children
     (berry watermarks, sparks), not for a background fill. */
  color: var(--color-white);
  position: relative;
  overflow: hidden;
}

.section-dark .section-title,
.section-dark h2,
.section-dark h3 {
  color: var(--color-white);
}

.section-title {
  position: relative;
  text-align: center;
  margin-bottom: 40px;
  padding-bottom: 18px;
}

.section-title::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 70px;
  height: 2px;
  background: var(--color-primary);
  box-shadow: 0 0 12px var(--color-primary-glow-strong), 0 0 26px var(--color-primary-glow);
}

.center-cta {
  text-align: center;
  margin-top: 36px;
}

/* Splash / title screen — home page's first screen (see
   templates/home/index.html), a game-title-card treatment where the
   BerryByte logo is the dominant element. It's a plain `.section` (so it
   picks up the body.snap-scroll min-height/scroll-snap-align/flex-center
   treatment below for free, and the mandatory `.container` child for
   paginate.js's isAtInterceptEdge() overflow check) plus this
   splash-specific look layered on top. No background of its own, same as
   every other section — the persistent .circuit-backdrop shows through. */
.splash-screen {
  position: relative;
  overflow: hidden;
  text-align: center;
}

.splash-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 26px;
}

/* Corner-bracket frame around the logo, same construction as .card::before
   (a stack of short linear-gradient "L" ticks per corner) but transparent
   in the middle — this isn't a boxed panel, just a HUD readout frame
   around the hero element. Top ticks pink (the site's primary), bottom
   ticks green: the other "detallito en verde" the client asked for, tied
   to a real structural element instead of sprinkled at random. */
.splash-logo-frame {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 40px 56px;
}

.splash-logo-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(var(--color-primary), var(--color-primary)) top left / 24px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top left / 2px 24px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top right / 24px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top right / 2px 24px no-repeat,
    linear-gradient(var(--color-accent-green), var(--color-accent-green)) bottom left / 24px 2px no-repeat,
    linear-gradient(var(--color-accent-green), var(--color-accent-green)) bottom left / 2px 24px no-repeat,
    linear-gradient(var(--color-accent-green), var(--color-accent-green)) bottom right / 24px 2px no-repeat,
    linear-gradient(var(--color-accent-green), var(--color-accent-green)) bottom right / 2px 24px no-repeat;
  opacity: 0.9;
  pointer-events: none;
}

.splash-logo-frame::after {
  content: "";
  position: absolute;
  inset: -60px;
  background: radial-gradient(circle, var(--color-primary-glow) 0%, rgba(212, 54, 106, 0) 70%);
  z-index: -1;
  pointer-events: none;
}

/* Real PNG exports (static/images/berry.png + BerryByte.png) replace the
   old hand-traced SVG here — no more scale-dependent trace artifacts, so
   these can run as large as the layout allows. */
.splash-icon {
  height: clamp(110px, 15vw, 190px);
  width: auto;
}

.splash-wordmark {
  height: clamp(56px, 8vw, 100px);
  width: auto;
}

.splash-tagline {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--color-muted-on-dark);
  margin: 0;
}

/* "Press to continue" prompt — a tasteful nod to a game title screen,
   pinned near the bottom of the screen (clear of the fixed footer) at
   low opacity so it reads as a subtle hint, not a call to action. */
.splash-prompt {
  position: absolute;
  left: 50%;
  bottom: calc(var(--footer-height) + 36px);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--color-muted-on-dark);
  opacity: 0.6;
  white-space: nowrap;
}

.splash-cursor {
  animation: blink-cursor 1.1s steps(1) infinite;
}

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

.splash-chevron {
  animation: chevron-bob 1.8s ease-in-out infinite;
}

@keyframes chevron-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(4px); }
}

@media (max-width: 720px) {
  .splash-prompt-text {
    display: none;
  }
}

/* Screen-snap section transitions ("pantallas" the client asked for).
   Opt-in per page via body.snap-scroll (see base.html's body_class
   block) — only the marketing pages (home/services/about/portfolio/
   contact) set it; legal/privacy.html and legal/terms.html deliberately
   never do, since forcing snap-scroll through long-form legal text is
   bad UX.

   The actual "never rest halfway between two screens" guarantee and the
   eased glide between them are now driven by static/js/paginate.js
   (wheel/touch/keyboard, with a hand-rolled rAF tween — native smooth
   scroll-snap alone can't guarantee no resting mid-screen and reads as
   too abrupt/linear for the requested "game map" feel). This CSS rule
   stays as a lightweight backstop underneath it: `proximity`, not
   `mandatory`, so it never fights paginate.js's own overflow handling
   for a tall screen (e.g. the contact page), and so browser-native
   scroll paths paginate.js doesn't listen for (scrollbar dragging,
   in-page anchor jumps) still settle roughly on a screen boundary. It is
   also the *entire* screen-transition behaviour for prefers-reduced-motion
   users, since paginate.js disables itself completely for them. */
body.snap-scroll {
  scroll-snap-type: y proximity;
}

body.snap-scroll .hero,
body.snap-scroll .page-hero,
body.snap-scroll .section {
  /* svh (small viewport height) avoids the jump-on-scroll caused by
     mobile browser chrome showing/hiding; 100vh is declared first as a
     fallback for browsers that don't support svh yet — the svh rule
     simply overrides it where supported.
     Subtracting --header-height: the sticky header still occupies real
     space in normal flow (it's sticky, not fixed), so once scrolled past
     its natural position it sits stuck on top of whatever the paginated
     glide lands on, covering --header-height worth of that screen. A
     bare 100svh screen therefore always extends --header-height *past*
     the actual visible viewport — confirmed concretely on the splash
     screen, where content anchored to the section's own bottom edge
     (e.g. .splash-prompt) landed underneath the fixed footer instead of
     above it, because the section's true bottom was 72px lower than the
     viewport's visible bottom. Sizing to the real visible area fixes
     that for every screen, not just this one. */
  min-height: calc(100vh - var(--header-height));
  min-height: calc(100svh - var(--header-height));
  scroll-snap-align: start;
  /* Without this, a native anchor jump (e.g. templates/home/index.html's
     #hero-screen, used by the "Volver a la fresa" node-warp return trip)
     lands this section's top flush with the viewport's own top edge — but
     the sticky header then sits on top of that, covering its own
     --header-height worth of the section and leaving a sliver of the
     PREVIOUS section (the splash screen, in the #hero-screen case)
     peeking out above the header instead. scroll-margin-top shifts the
     landing point up by that same amount so the section's true visible
     top (below the header) is what actually aligns to the header instead. */
  scroll-margin-top: var(--header-height);
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Clears the fixed, always-visible .site-footer strip (see Footer
     below). Added on top of each variant's own bottom padding via the
     cascade (this selector is more specific than the bare .hero /
     .page-hero / .section rules, so this longhand wins over their
     shorthand `padding` without !important) — it does NOT change the
     section's total rendered height (still exactly one viewport via
     min-height), it just nudges the flex-centered content up a bit so
     nothing sits under the footer on any screen, not just the last one. */
  padding-bottom: calc(var(--footer-height) + 20px);
}

/* "Reality warp" content transition — synced to paginate.js's ~750ms
   glide (see GLIDE_DURATION in paginate.js). goToScreen() there toggles
   .screen-exiting on the outgoing screen and .screen-entering (plus the
   instant-apply .screen-entering-start sub-state, removed on the next
   tick so the browser has something to transition *from*) on the
   incoming screen at the start of a jump, and removes both when the
   glide's onDone fires. Purely additive to the existing scroll-position
   glide — this only touches these three visual properties, never
   transform/opacity/filter used elsewhere for layout.
   Entirely inert for prefers-reduced-motion users: paginate.js returns
   early and never runs at all for them (see the guard near the top of
   that file), so these classes are simply never added — they keep the
   already-reduced-motion-safe plain scroll + CSS proximity-snap. */
body.snap-scroll main > section {
  transition: transform 0.75s cubic-bezier(0.65, 0, 0.35, 1),
    filter 0.75s cubic-bezier(0.65, 0, 0.35, 1),
    opacity 0.75s cubic-bezier(0.65, 0, 0.35, 1);
}

body.snap-scroll main > section.screen-exiting {
  transform: scaleY(1.14);
  filter: blur(5px);
  opacity: 0;
}

body.snap-scroll main > section.screen-entering.screen-entering-start {
  /* Instant snap to the distorted "from" state, no transition — this
     class is added, flushed, and removed again within the same tick
     (see goToScreen()), so the section never actually renders this way;
     it just gives the browser a starting point to animate away from
     once .screen-entering-start is removed a moment later. */
  transition: none;
  transform: scaleY(1.14);
  filter: blur(5px);
  opacity: 0;
}

/* Node warp — full-document "falling into a black hole" transition used
   when navigating from a services-diagram node (or a service detail
   page's own back link) to another page entirely. Unlike the "reality
   warp" above (which animates between snap-scroll SCREENS within one
   document), this crosses a real page load, so it's driven by
   static/js/node-warp.js instead of paginate.js: that script sets
   --warp-x/--warp-y (viewport px) to the exact point the user clicked,
   then toggles the classes below on <body> to shrink+tilt #node-warp-stage
   into that point before navigating away. It also stores that same point
   (as viewport fractions, so it survives a resize) in sessionStorage; the
   NEXT page's pre-paint <head> script (see base.html) reads it back and
   sets the same --warp-x/--warp-y before first paint, so the entrance
   converges/emerges at the identical spot instead of jumping to center —
   an earlier version defaulted to center on arrival on the theory that
   matching the exact point across two separate documents wasn't reliable
   enough to bother with, but skipping it made the handoff between the two
   halves read as a visible jump/blink instead of one continuous motion.
   #node-warp-overlay (see base.html) adds the glowing "event horizon"
   radial-gradient at that point, which is doing most of the "black hole"
   visual work — the stage itself only scales down to 82% (not a dramatic
   near-zero shrink): an early pass tried scaling it down to almost
   nothing, but animating that large a transform delta made the browser
   visibly interpolate/smear the page's own fine detail (the berry
   diagram's small nodes, the hero's dot-constellation canvas) into a
   blurry mess for the ~480ms the transition runs — a real rendering
   artifact, not just a filter choice, so dialing back *how much* scales
   was the fix, not any particular filter property. */

/* Clips the viewport to exactly what's currently visible for the
   duration of the transition — necessary because scaling #node-warp-stage
   down around --warp-x/--warp-y (a point that's normally somewhere ON
   SCREEN, e.g. the "Volver a la fresa" link) geometrically pulls
   OFF-SCREEN content toward that point too: on the home page specifically,
   arriving back from a service page shrinks the hero section toward the
   click point, and since the splash screen sits just above the hero in
   the document (scrolled out of view, not out of existence), shrinking
   visibly drags a slice of it down into view for the moment the scale is
   below 100% — a real, expected consequence of animating a transform
   around an on-screen origin when off-screen siblings exist nearby, not
   a scroll-position bug (the scroll position itself was verified correct
   throughout). overflow:hidden here doesn't stop that geometric pull, but
   it does stop it from being visible: anything the shrink would have
   otherwise exposed beyond the current viewport is simply clipped away
   instead, same as it already is at rest. html:has() (not a class
   directly on <html>, to keep the JS side of this to only ever touching
   <body>'s classes) is broad browser support at this point (Chrome/Edge
   105+, Firefox 121+, Safari 15.4+); the failure mode for any browser
   that doesn't support it is just this specific reveal being visible
   again, not anything actually broken. */
html:has(body.node-warp-exiting),
html:has(body.node-warp-entering),
html.node-warp-pending {
  overflow: hidden;
}

#node-warp-overlay {
  position: fixed;
  inset: 0;
  z-index: 9998;
  pointer-events: none;
  opacity: 0;
  /* Tight point-of-light falloff on purpose — these percentages are of
     the *viewport's* own box (this element is inset:0), so anything much
     past ~20% here reads as a screen-filling pink wash rather than a
     single bright point against a dark void. */
  background: radial-gradient(
    circle at var(--warp-x, 50%) var(--warp-y, 50%),
    var(--color-primary-glow-strong) 0%,
    var(--color-primary-glow) 4%,
    rgba(13, 17, 31, 0.92) 16%,
    var(--color-dark) 30%
  );
  transition: opacity 0.48s ease;
}

html.node-warp-pending #node-warp-overlay,
body.node-warp-exiting #node-warp-overlay,
body.node-warp-entering-start #node-warp-overlay {
  opacity: 1;
}

/* Pre-paint "collapsed" starting state for a page that's about to run its
   entrance animation — applied by the blocking inline script in
   base.html's <head> (before first paint, so there's no flash of the
   full-size page first) and picked up as the transition's `from` state
   the instant node-warp.js swaps this class for body.node-warp-entering
   on DOMContentLoaded. Targets #node-warp-stage (header/main — the
   fixed .site-footer and any page's fixed_bars block live outside it,
   see base.html), not <body> itself — see that div's comment for why.
   Deliberately no blur() — see the top-of-section comment above for
   why animating a much bigger scale delta than this made one necessary
   to avoid. Also deliberately no filter:brightness() any more, even
   though that dip read fine on its own: animating `filter` is markedly
   more expensive for the browser to composite every frame than
   `transform`/`opacity` alone, and it was the actual cause of this
   feeling like it "paused"/stuttered rather than glided. The overlay's
   own dark-at-the-edges radial-gradient (and the opacity fade itself,
   over the page's already-dark background) carries the darkening cue
   instead. */
html.node-warp-pending #node-warp-stage {
  transform-origin: var(--warp-x, 50%) var(--warp-y, 50%);
  transform: scale(0.82) rotate(-6deg);
  opacity: 0;
}

/* transform-origin lives in its own always-applied rule (every node-warp
   state needs the same point, and re-declaring it per state risks one
   getting out of sync and silently reintroducing the jump-to-center bug
   described above) — kept separate from the transition/transform/opacity
   rules below, which genuinely do differ per state. */
body.node-warp-exiting #node-warp-stage,
body.node-warp-entering #node-warp-stage,
body.node-warp-entering-start #node-warp-stage {
  transform-origin: var(--warp-x, 50%) var(--warp-y, 50%);
}

body.node-warp-exiting #node-warp-stage {
  will-change: transform, opacity;
  /* Ease-IN — starts slow, accelerates — for "falling/getting sucked
     into" the point. */
  transition: transform 0.48s cubic-bezier(0.4, 0, 1, 1),
    opacity 0.48s cubic-bezier(0.4, 0, 1, 1);
  transform: scale(0.82) rotate(6deg);
  opacity: 0;
}

/* Instant-apply starting point for the entrance transition (mirrors
   paginate.js's own .screen-entering-start trick): added, flushed and
   removed within the same tick by node-warp.js, so the browser has a
   `from` state to animate away from once it's gone.
   Selector requires BOTH classes (not just .node-warp-entering-start) on
   purpose: node-warp.js always sets them together, and the combined
   class list outweighs plain .node-warp-entering's specificity below —
   without that, both rules tie at one class + one ID, and since
   .node-warp-entering comes later in this file it would win the cascade
   even while -start is still present, so the "collapsed" starting point
   would never actually get painted and the page would just snap straight
   to full size with only the overlay glow left animating on top of it. */
body.node-warp-entering.node-warp-entering-start #node-warp-stage {
  transition: none;
  transform: scale(0.82) rotate(-6deg);
  opacity: 0;
}

body.node-warp-entering #node-warp-stage {
  will-change: transform, opacity;
  /* Ease-OUT — starts fast, decelerates into place — the mirror of the
     exit's ease-in above. Using the SAME curve both ways (an earlier
     pass did) makes the entrance spend most of its duration barely
     moving and then snap into its final size right at the end, which
     reads as a sudden pop/jump instead of a smooth grow. */
  transition: transform 0.48s cubic-bezier(0, 0, 0.2, 1),
    opacity 0.48s cubic-bezier(0, 0, 0.2, 1);
  transform: scale(1) rotate(0deg);
  opacity: 1;
}

/* Extra defense-in-depth alongside the smaller scale delta above:
   .hero-canvas's drifting-dot constellation (static/js/main.js) draws
   dots only a couple px across, fine detail that's more visible to any
   transform-interpolation softness than large shapes like text — simplest
   to just not show it for the ~480ms the transition runs. */
html.node-warp-pending .hero-canvas,
body.node-warp-exiting .hero-canvas,
body.node-warp-entering .hero-canvas {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  html.node-warp-pending #node-warp-stage,
  body.node-warp-exiting #node-warp-stage,
  body.node-warp-entering #node-warp-stage,
  body.node-warp-entering-start #node-warp-stage {
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
    transition: none !important;
  }

  html.node-warp-pending #node-warp-overlay,
  body.node-warp-exiting #node-warp-overlay,
  body.node-warp-entering-start #node-warp-overlay {
    opacity: 0 !important;
  }
}

/* Grid / Cards (HUD panel treatment) */

.grid {
  display: grid;
  gap: 24px;
}

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

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

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

/* Dense variant — for grids with many cards sharing one screen (the
   9-card services grid) that would otherwise tower past a single
   viewport. Same HUD-panel language as .card, just tighter padding,
   icon size and type scale so more rows fit without feeling cramped. */
.grid-compact {
  gap: 18px;
}

.grid-compact .card {
  padding: 20px 22px;
}

.grid-compact .service-icon {
  width: 32px;
  height: 32px;
  margin-bottom: 10px;
}

/* Service detail page (templates/services/detail.html) — two normal,
   naturally-scrolling sections (a .page-hero for the title/lead, then a
   plain .section for the cards/CTA/nav), same as any other inner page.
   An earlier pass forced this into one body.snap-scroll screen sized to
   never need a scrollbar; per client feedback it read as everything
   crammed toward the center instead, so this went back to normal flow.
   Still its own type scale/spacing rather than reusing .card/.grid's
   defaults wholesale — a little tighter than those, not as tight as the
   single-screen pass was — but this page doesn't share these classes
   with anything else, so there's no knock-on effect from tuning them. */

/* No custom max-width here on purpose (was 1080px) — that made this page
   read as a narrow column stranded in the middle of a much wider screen
   compared to every other inner page, which all just use .container's
   own full site-wide width (--container-width, 1600px) same as this now
   does. */

/* Vertical-only padding trims for this page's two sections — scoped to
   .service-detail-hero/.service-detail-section specifically (combined
   with the shared .page-hero/.section classes for higher specificity, so
   these win regardless of file order) rather than editing .page-hero's
   64px or .section's 70px directly: those two base rules are shared by
   every other inner page (services index, about, portfolio, contact,
   both legal pages) and the home page's splash screen, so a direct edit
   there would shrink all of those too. Horizontal spacing (.container's
   own side padding) is untouched — only top/bottom padding changes. */
.page-hero.service-detail-hero {
  padding-top: 40px;
  padding-bottom: 40px;
}

.section.service-detail-section {
  padding-top: 0;
  /* Clears the now-fixed .service-detail-nav (see below) plus the fixed
     .site-footer beneath it, so the last card/CTA content never ends up
     scrolled underneath either one. */
  padding-bottom: calc(var(--footer-height) + var(--service-nav-height) + 24px);
}

.service-detail-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 28px;
}

.service-detail-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.5px;
  color: var(--color-muted-on-dark);
  text-decoration: none;
  transition: color var(--transition-base);
}

.service-detail-back:focus {
  outline: none;
}

.service-detail-back:hover,
.service-detail-back:focus-visible {
  color: var(--color-primary);
}

.service-detail-badge {
  display: inline-block;
  padding: 3px 12px;
  border-radius: 999px;
  border: 1px solid var(--color-primary);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--color-primary);
  background: rgba(212, 54, 106, 0.1);
}

.service-detail-title {
  font-size: clamp(1.9rem, 1.5rem + 1.6vw, 2.4rem);
  margin-bottom: 0.35em;
}

.service-detail-lead {
  font-size: 1.05rem;
  line-height: 1.6;
  color: var(--color-muted-on-dark);
  max-width: 680px;
}

.service-detail-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 24px;
  margin-bottom: 40px;
}

.service-detail-card {
  background: var(--color-dark-elevated);
  border: 1px solid var(--color-dark-border);
  border-radius: var(--radius);
  padding: 26px 28px;
}

.service-detail-card h3 {
  font-size: 1.15rem;
  margin-bottom: 0.6em;
}

.service-detail-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.service-detail-list li {
  position: relative;
  padding-left: 20px;
  margin-bottom: 10px;
  color: var(--color-muted-on-dark);
  font-size: 0.95rem;
  line-height: 1.55;
}

.service-detail-list li:last-child {
  margin-bottom: 0;
}

.service-detail-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 6px var(--color-primary-glow-strong);
}

.service-detail-nav {
  display: flex;
  justify-content: space-between;
  gap: 32px;
  /* No-op now that the rule below takes this out of the document flow
     (a fixed element with top unset has nothing for margin-top to push
     against) — left as-is rather than removed since it's harmless and
     this is still the source of truth for the bar's own internal
     rhythm if it's ever put back in-flow. */
  margin-top: 32px;
  font-family: var(--font-mono);
  font-size: 0.85rem;

  /* Pinned just above the fixed .site-footer, at the same height on
     every service detail page regardless of how much (or little)
     content sits above it, so prev/next is always reachable without
     hunting for it — see .section.service-detail-section's matching
     padding-bottom above, which keeps the last card/CTA clear of it.
     Floats directly over the page like .site-footer already does (no
     panel/background of its own) — and, like .site-footer, lives
     outside #node-warp-stage (in base.html's fixed_bars block), so the
     node-warp collapse/rotate applied to the header/main during a
     prev/next jump never touches it: it stays visibly fixed through the
     whole transition, only its own label text and href changing once
     the next page has loaded. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--footer-height) + 16px);
  z-index: 150;
  height: var(--service-nav-height);
  align-items: center;
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: clamp(24px, 6vw, 120px);
}

.service-detail-nav-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--color-muted-on-dark);
  text-decoration: none;
  transition: color var(--transition-base), transform var(--transition-base);
}

.service-detail-nav-link:focus {
  outline: none;
}

.service-detail-nav-link:hover,
.service-detail-nav-link:focus-visible {
  color: var(--color-primary);
}

/* Arrow nudges toward the direction of travel on hover — a small,
   tasteful cue (on top of the node-warp transition every internal link
   gets automatically, see node-warp.js) that this bar moves you along a
   sequence, not just to two unrelated pages. */
.service-detail-nav-link[href]:first-child:hover .service-detail-nav-link-arrow,
.service-detail-nav-link[href]:first-child:focus-visible .service-detail-nav-link-arrow {
  transform: translateX(-4px);
}

.service-detail-nav-link[href]:last-child:hover .service-detail-nav-link-arrow,
.service-detail-nav-link[href]:last-child:focus-visible .service-detail-nav-link-arrow {
  transform: translateX(4px);
}

.service-detail-nav-link-arrow {
  transition: transform var(--transition-base);
}

@media (max-width: 640px) {
  /* Full labels plus two icons don't fit a fixed-width bar on narrow
     screens without wrapping — collapse to icon-only, matching the
     back-button/menu-button pattern already used elsewhere in the
     header for the same reason. */
  .service-detail-nav-link-label {
    display: none;
  }
}

@media (max-width: 720px) {
  .service-detail-grid {
    grid-template-columns: 1fr;
  }
}

.grid-compact .card h3 {
  font-size: 1.05rem;
  margin-bottom: 0.4em;
}

.grid-compact .card p {
  font-size: 0.92rem;
  line-height: 1.5;
}

.card {
  position: relative;
  isolation: isolate;
  display: block;
  background: var(--color-dark-elevated);
  border: 1px solid var(--color-dark-border);
  border-radius: var(--radius);
  padding: 28px;
  color: var(--color-light);
  transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
}

.card,
.card:hover {
  text-decoration: none;
}

.card h2,
.card h3 {
  color: var(--color-white);
}

.card p {
  color: var(--color-muted-on-dark);
  margin-bottom: 0;
}

/* Corner-bracket HUD frame, drawn as 8 short gradient strips forming
   an L in each corner. Intensifies on hover. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.55;
  transition: opacity var(--transition-base), filter var(--transition-base);
  background:
    linear-gradient(var(--color-primary), var(--color-primary)) top left / 18px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top left / 2px 18px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top right / 18px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) top right / 2px 18px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) bottom left / 18px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) bottom left / 2px 18px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) bottom right / 18px 2px no-repeat,
    linear-gradient(var(--color-primary), var(--color-primary)) bottom right / 2px 18px no-repeat;
}

/* Faint scanline sweep on hover */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 40%, rgba(212, 54, 106, 0.14) 50%, transparent 60%);
  background-size: 250% 250%;
  background-position: -60% -60%;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.card:hover {
  transform: translateY(-6px);
  border-color: var(--color-accent-green);
  box-shadow: 0 18px 40px rgba(212, 54, 106, 0.22);
  z-index: 999;
}

.card:hover::before {
  opacity: 1;
  filter: drop-shadow(0 0 6px var(--color-primary-glow));
}

.card:hover::after {
  opacity: 1;
  animation: card-sweep 1.1s ease forwards;
}

.card:hover .service-icon {
  filter: drop-shadow(0 0 8px var(--color-primary-glow-strong));
}

@keyframes card-sweep {
  from { background-position: -60% -60%; }
  to { background-position: 160% 160%; }
}

/* Brief highlight for a card just arrived at via an in-page anchor jump
   (the home page's services diagram nodes link to #<card-id> here).
   Peaks at the same border-color/box-shadow language as .card:hover,
   then fades back to the resting state over ~2s — a one-shot pulse, not
   a permanent state change. */
.card:target {
  animation: card-target-pulse 2s ease-out;
}

@keyframes card-target-pulse {
  0% {
    border-color: var(--color-accent-green);
    box-shadow: 0 18px 40px rgba(212, 54, 106, 0.22), 0 0 0 3px var(--color-accent-green-glow);
  }
  100% {
    border-color: var(--color-dark-border);
    box-shadow: none;
  }
}

.service-icon {
  display: block;
  width: 40px;
  height: 40px;
  color: var(--color-primary);
  margin-bottom: 14px;
  transition: filter var(--transition-base);
}

.icon-inline {
  width: 20px;
  height: 20px;
  color: var(--color-primary);
  vertical-align: -5px;
  margin-right: 6px;
}

/* Capability chips ("Lo que hacemos" on the About page) — a compact
   sibling of .card: same elevated-panel/border/radius/hover language,
   just lighter (smaller padding, no corner-bracket HUD frame, no scanline
   sweep) since these appear ten-at-a-time in scannable clusters rather
   than as a handful of hero cards. */
.capability-cluster {
  margin-bottom: 40px;
}

.capability-cluster:last-child {
  margin-bottom: 0;
}

.chip-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.capability-chip {
  flex: 1 1 220px;
  background: var(--color-dark-elevated);
  border: 1px solid var(--color-dark-border);
  border-radius: var(--radius);
  padding: 18px 20px;
  transition: transform var(--transition-base), border-color var(--transition-base);
}

.capability-chip:hover {
  transform: translateY(-4px);
  border-color: var(--color-accent-green);
}

.capability-chip h3 {
  font-size: 1.05rem;
  margin-bottom: 4px;
}

.capability-chip p {
  color: var(--color-muted-on-dark);
  font-size: 0.9rem;
  margin-bottom: 0;
}

/* Tech stack badge row */

.stack-stat {
  font-size: 1.05rem;
  color: var(--color-muted-on-dark);
  max-width: 640px;
  margin: 0 auto 32px;
  text-align: center;
}

.badge-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
}

/* Fits the same badge row inside a narrower .card column (see the
   compacted Stack Tecnológico card, templates/about/index.html) —
   left-aligned and tighter-gapped to match the card's own left-aligned
   heading/paragraph, instead of the standalone section's centered
   treatment. */
.badge-row--compact {
  justify-content: flex-start;
  gap: 8px;
  margin-top: 14px;
}

/* The 4-card Misión/Visión/Valores/Stack row sits directly under the
   page-hero's h1 (see templates/about/index.html) — needs its own gap
   since .page-hero h1's own margin-bottom is tuned for a bare title,
   not a title immediately followed by a card grid. */
.about-intro-grid {
  margin-top: 24px;
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 8px 18px;
  border: 1px solid var(--color-dark-border);
  border-radius: 999px;
  background: var(--color-dark-elevated);
  color: var(--color-light);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 1px;
}

/* Contact */

.contact-grid {
  align-items: start;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.form-field label {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--color-light);
}

.form-field input,
.form-field textarea {
  /* Short UI fields, not long-form copy — mono fits the console theme
     (see the `p` rule above for the opposite call on paragraph text). */
  font-family: var(--font-mono);
  font-size: 1rem;
  padding: 12px 14px;
  border: 1px solid var(--color-dark-border);
  border-radius: 8px;
  background: var(--color-dark-elevated);
  color: var(--color-light);
  transition: border-color var(--transition-base), box-shadow var(--transition-base);
}

.form-field input:focus,
.form-field textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-glow);
}

.form-error {
  color: var(--color-primary);
  font-size: 0.9rem;
  margin: 0;
}

/* Contact form honeypot trap (see templates/contact/index.html and
   static/js/contact.js). Unlike .sr-only above, this must be invisible to
   assistive tech too — it's paired with aria-hidden="true" in the markup
   — since the point is that no real visitor, sighted or not, ever reaches
   or fills it; only a bot blindly filling every field would. */
.form-field--trap {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-aside .card {
  height: 100%;
}

.link-external {
  font-weight: 600;
}

.placeholder-note {
  color: var(--color-muted-on-dark);
  font-size: 0.85rem;
}

/* Messages */

.messages {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
}

.message {
  padding: 14px 18px;
  border-radius: 8px;
  margin-bottom: 10px;
  font-weight: 500;
}

.message-success {
  background: rgba(76, 175, 80, 0.12);
  color: var(--color-light);
  border: 1px solid var(--color-accent-green);
}

.message-error {
  background: rgba(212, 54, 106, 0.12);
  color: var(--color-light);
  border: 1px solid var(--color-primary);
}

/* Portfolio placeholder thumbnails — a stand-in image slot (dashed
   border, faint diagonal hatch, centered icon) for portfolio cards until
   a real screenshot replaces it. The corner label makes clear it's a
   placeholder rather than a broken image. */
.portfolio-thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--color-dark-border);
  border-radius: calc(var(--radius) - 4px);
  background:
    repeating-linear-gradient(45deg, rgba(212, 54, 106, 0.05) 0 2px, transparent 2px 12px),
    var(--color-dark-elevated);
  margin-bottom: 12px;
  overflow: hidden;
}

.portfolio-thumb svg {
  width: 40px;
  height: 40px;
  color: var(--color-primary);
  opacity: 0.5;
}

/* Real media preview (the Galería project's autoplay clip, Aluna's
   screenshot carousel, BerryByte's own live iframe) instead of the
   dashed placeholder look — drop the hatch/dashed border and let the
   media fill the same 16/10 frame edge-to-edge. */
.portfolio-thumb--video,
.portfolio-thumb--carousel,
.portfolio-thumb--iframe {
  border-style: solid;
  background: var(--color-dark-elevated);
}

.portfolio-thumb-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Live mini-window onto the real homepage (not a screenshot — an actual
   iframe, canvas hero and all). Sized to 4x the thumb's own box then
   scaled back down by 0.25 from the top-left corner: since the iframe
   is absolutely positioned, those percentages resolve against
   .portfolio-thumb--iframe's own current size, so this stays correct
   at any card width with no JS or media queries. pointer-events:none
   keeps it a picture, not a second copy of the site someone can
   scroll/click into. */
.portfolio-thumb-iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 400%;
  height: 400%;
  border: 0;
  pointer-events: none;
  transform: scale(0.25);
  transform-origin: top left;
}

/* Screenshot carousel (Aluna) — slides stack in the same spot. The
   resting (inactive) pose is shrunk and shifted right; main.js's
   initCarousels() toggles .is-active on a timer, so the outgoing slide
   shrinks + slides back into that resting pose while the incoming one
   grows + slides in from it — a little "pop in from the side" motion
   instead of a flat crossfade. Frozen on the first slide for
   prefers-reduced-motion. */
.portfolio-carousel-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(0.8) translateX(28px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

.portfolio-carousel-slide.is-active {
  opacity: 1;
  transform: scale(1) translateX(0);
  z-index: 1;
}

.portfolio-tag {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.5px;
  color: var(--color-accent-green);
  margin-bottom: 6px;
}

/* Brand leaf crown on portfolio cards — reuses the same leaf artwork as
   the homepage berry diagram (static/images/berry-leaves-trim.png, a
   pre-cropped version with the transparent padding removed) so each
   project card reads as the "fruit" with its own little stem/leaves on
   top. Positioned to straddle the card's top edge, and left as a plain
   child of .card so it rides along with the card's existing hover lift
   (.card:hover's translateY) with no extra JS. */
.portfolio-leaves {
  position: absolute;
  top: -22px;
  left: 50%;
  transform: translateX(-50%);
  width: 96px;
  height: auto;
  z-index: 2;
  pointer-events: none;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.35));
}

/* Legal content */

.legal-content h2 {
  margin-top: 1.6em;
}

.legal-content ul {
  padding-left: 1.3em;
  color: var(--color-muted-on-dark);
}

/* Footer — stripped to a single always-visible thin line: logo + short
   copyright on the left, the two legal links on the right. No
   background (same transparent treatment as .site-header above), pinned
   to the viewport bottom so it survives every paginated "screen"
   transition and stays put on the plain-scroll legal pages too. The
   previous "Navegación" link column is gone — it duplicated the
   menu-button overlay, which is the real nav now.
   z-index 200 sits above ordinary page content (cards etc. top out
   around z-index 1, .site-header at 100) but below .nav-overlay (300),
   per the brief, so the full-screen menu always wins if it were ever to
   overlap this strip. See body/section padding-bottom rules elsewhere
   in this file for how page content stays clear of this fixed strip. */
.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  background: transparent;
  color: var(--color-light);
}

.footer-inner {
  height: var(--footer-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.footer-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.footer-logo {
  height: 20px;
  flex-shrink: 0;
}

.footer-copy {
  font-size: 0.72rem;
  color: var(--color-muted-on-dark);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.footer-legal {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-shrink: 0;
}

.footer-legal a {
  color: var(--color-muted-on-dark);
  font-size: 0.75rem;
  white-space: nowrap;
}

.footer-legal a:hover {
  color: var(--color-primary);
}

/* Persistent full-page circuit/HUD background — included once in
   base.html right after <body> (see partials/circuit_decor.html) and
   fixed to the viewport, not the document, so it stays put behind every
   "screen" as static/js/paginate.js glides the page between sections
   instead of scrolling away. z-index:-1 tucks it behind ordinary in-flow
   content without needing a z-index bump anywhere else on the page. */
.circuit-backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background-color: var(--color-dark);
  background-image: var(--hud-grid-lines);
}

/* Ambient drifting glow — the "farthest" depth plane, pure gradient, no
   per-frame cost beyond the transform main.js already applies to every
   [data-parallax-speed] layer. */
.circuit-backdrop-glow {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(circle at 28% 22%, var(--color-primary-glow) 0%, rgba(212, 54, 106, 0) 45%),
    radial-gradient(circle at 76% 82%, var(--color-accent-green-glow) 0%, rgba(76, 175, 80, 0) 40%);
  opacity: 0.5;
}

/* Both remaining depth layers (starfield, circuit traces) are oversized
   relative to the fixed backdrop (inset by a negative percentage) so the
   small scroll-driven internal drift main.js applies to them never
   reveals a transparent edge at the top/bottom of the viewport — a fixed
   element can't rely on scrolling out of frame to hide that seam the way
   a normal in-flow layer would. */
.circuit-backdrop-stars {
  position: absolute;
  inset: -10%;
}

.circuit-backdrop-stars circle {
  fill: var(--color-light);
  opacity: 0.35;
}

.circuit-decor {
  position: absolute;
  inset: -10%;
  pointer-events: none;
}

/* Second, further-back PCB plane: dimmer and blurred so the sharp
   .circuit-decor layer on top of it reads as nearer, giving the whole
   backdrop a stacked, multi-layer-depth feel instead of one flat plane.
   It shares every other .circuit-decor rule below (color, base opacity,
   glow) — this modifier only mutes and softens the SVG as a whole. Its
   data-parallax-speed (0.07, set in circuit_decor.html) is slower than
   the front layer's (0.1), so it also drifts less on scroll, same
   "moves less = reads as farther away" logic already used for the glow
   (0.03) and starfield (0.05) layers. */
.circuit-decor--back {
  opacity: 0.55;
  filter: blur(3px);
}

.circuit-decor .circuit-lines path {
  fill: none;
  stroke: var(--color-primary);
  stroke-width: 1;
  opacity: 0.14;
  filter: drop-shadow(0 0 2px var(--color-primary-glow));
}

.circuit-decor .circuit-nodes circle {
  fill: var(--color-primary);
  opacity: 0.3;
  filter: drop-shadow(0 0 5px var(--color-primary-glow-strong));
  animation: node-pulse 4.5s ease-in-out infinite;
}

.circuit-decor .circuit-nodes circle:nth-child(even) {
  animation-delay: 2.2s;
}

.circuit-decor .led-dot {
  fill: var(--color-primary);
  opacity: 0.3;
  filter: drop-shadow(0 0 4px var(--color-primary)) drop-shadow(0 0 12px var(--color-primary-glow-strong));
  animation: led-blink 2.6s ease-in-out infinite;
  animation-delay: var(--led-delay, 0s);
}

/* A couple of the backdrop's LEDs run green instead of pink — the
   "detallitos en verde" the client asked for, applied to circuitry that
   was already there rather than new elements bolted on. See
   partials/circuit_decor.html for which two carry this modifier. */
.circuit-decor .led-dot--green {
  fill: var(--color-accent-green);
  filter: drop-shadow(0 0 4px var(--color-accent-green)) drop-shadow(0 0 12px var(--color-accent-green-glow));
}

.circuit-decor .circuit-readout text {
  font-family: var(--font-mono);
  font-size: 13px;
  fill: var(--color-primary);
  opacity: 0.35;
  letter-spacing: 1px;
  filter: drop-shadow(0 0 4px var(--color-primary-glow));
}

@keyframes node-pulse {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 0.7; }
}

@keyframes led-blink {
  0%, 100% { opacity: 0.22; }
  8% { opacity: 1; }
  18% { opacity: 0.28; }
  26% { opacity: 0.9; }
  40%, 100% { opacity: 0.22; }
}

/* Used by .circuit-node-dot (the services diagram's clickable nodes) —
   stays bright the entire cycle, only dipping to 0.8 briefly, so the
   node is always legible as a solid sphere rather than mostly-off like
   led-blink. */
@keyframes node-flicker {
  0%, 100% { opacity: 0.85; }
  30% { opacity: 1; }
  55% { opacity: 0.8; }
  75% { opacity: 1; }
}

/* CRT/HUD scanline texture — a thin repeating horizontal-line overlay
   above every other backdrop layer, evoking a damaged/old readout
   screen. Kept very low-alpha (baked into the line color itself) so it
   reads as texture, not as lines someone would consciously notice while
   reading body copy. The slow background-position drift ("refreshing"
   feel) is subtle enough in isolation, and is fully frozen anyway by the
   prefers-reduced-motion rule near the bottom of this file (that rule
   collapses animation-duration for every element, this one included). */
.circuit-backdrop-scanlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(230, 231, 235, 0.05) 0,
    rgba(230, 231, 235, 0.05) 1px,
    transparent 1px,
    transparent 3px
  );
  animation: scanline-drift 11s linear infinite;
}

@keyframes scanline-drift {
  from { background-position-y: 0; }
  to { background-position-y: 240px; }
}

/* Rare glitch-flicker accent — a thin band that briefly appears with a
   red/cyan channel-split (via box-shadow duplicates of the same band,
   offset left/right) and jitters position, then vanishes. The keyframe
   spends ~99% of its cycle at opacity:0 so the flicker is a genuinely
   rare accent rather than a loop the eye keeps catching. The 12.7s
   duration is deliberately non-round so it never lines up with the
   page's other timed animations (node-pulse 4.5s, led-blink 2.6s,
   logo-pulse 4.5s) and reads as un-mechanical/organic rather than a
   ticking metronome, without resorting to JS setInterval+Math.random. */
.circuit-backdrop-glitch {
  position: absolute;
  left: 0;
  right: 0;
  top: 30%;
  height: 3px;
  background: rgba(230, 231, 235, 0.55);
  box-shadow: -2px 0 0 rgba(255, 40, 90, 0.55), 2px 0 0 rgba(60, 220, 255, 0.55);
  opacity: 0;
  pointer-events: none;
  animation: glitch-pulse 12.7s ease-in-out infinite;
}

@keyframes glitch-pulse {
  0%, 96.5%, 100% { opacity: 0; transform: translate(0, 0); }
  96.7% { opacity: 0.85; transform: translate(-6px, 120px); }
  96.85% { opacity: 0; transform: translate(4px, -260px); }
  97.05% { opacity: 0.7; transform: translate(3px, 300px); }
  97.3% { opacity: 0; transform: translate(0, 0); }
}

/* Explicit opt-out rather than relying on the blanket
   animation-duration:0.001ms rule below. That blanket rule would already
   make each cycle's flash sub-perceptible, but a sudden flicker/RGB-split
   displacement is the kind of effect that can read as uncomfortable even
   when technically brief, and its whole point (a "damaged HUD" surprise)
   is motion-driven in a way the static scanline texture isn't — so it
   gets removed outright rather than trusting "fast enough to not
   count". The static scanline texture stays: its own drift has no
   opacity/position jump to begin with, it's just frozen in place. */
@media (prefers-reduced-motion: reduce) {
  .circuit-backdrop-glitch {
    display: none;
  }
}

/* Berry accent glyph — a small schematic/blueprint-style berry outline
   (thin strokes, brand pink + a tiny green leaf accent), used sparingly
   as a quiet brand wink alongside the circuit/HUD motif. Never the
   dominant visual element. */

.berry-glyph {
  display: block;
  color: var(--color-primary);
}

/* Tiny marker glyph standing in for the plain dash/line next to a
   section's HUD index marker (see .section-marker in the page
   templates). Small and legible, not meant to be near-invisible. */
.section-marker-berry {
  width: 14px;
  height: 18px;
  color: var(--color-primary);
  opacity: 0.7;
}

/* Quiet corner/background watermark — deliberately faint (0.15-0.22)
   and easy to miss on first glance ("fresitas sutiles"). Drifts with
   the other decorative layers via [data-parallax-speed] in main.js. */
.berry-accent {
  position: absolute;
  z-index: 0;
  color: var(--color-primary);
  pointer-events: none;
}

.berry-accent--br {
  right: 6%;
  bottom: 8%;
  width: 64px;
  opacity: 0.18;
}

.berry-accent--tl {
  left: 5%;
  top: 12%;
  width: 46px;
  opacity: 0.15;
}

.berry-accent--tr {
  right: 8%;
  top: 10%;
  width: 50px;
  opacity: 0.16;
}

/* Local positioning context for sections that host a .berry-accent
   watermark but aren't already position:relative/overflow:hidden
   (.hero, .page-hero and .section-dark already are). */
.berry-decorated {
  position: relative;
  overflow: hidden;
}

/* Portfolio's merged header + project grid + CTA (3 rich-media cards:
   video, carousel, live iframe) runs taller than one viewport at most
   sizes — unlike every other one-screen .berry-decorated section. Left
   as overflow:hidden, that combination (a scroll-snap-aligned section
   shorter than its own content) can hit the same scroll-snap quirk
   node-warp.js's alignToHash() comment documents: the section settles
   with a nonzero internal scrollTop on load, which permanently hides
   its own header above the fold with no way to scroll back to it.
   overflow:visible removes the precondition entirely — the section is
   no longer a scroll container, so it can't develop that stray offset,
   and .berry-decorated's own overflow:hidden loses to this pairing's
   higher specificity. */
.berry-decorated.portfolio-showcase {
  overflow: visible;
}

/* Spark accents — small blurred glowing dots that scroll WITH a
   section's content (no [data-parallax-speed] by default = an exact 1:1
   rate with the page, the same convention normal in-flow content already
   follows — see main.js). Placed sparsely (a handful site-wide, never
   per-section) inside .hero/.page-hero/.section-dark, which are already
   position:relative/overflow:hidden. Against the fully static
   .circuit-backdrop behind them, these few sparks rising with the
   content are the parallax effect the client asked for. */
.spark {
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
  filter: blur(8px);
}

.spark--pink {
  background: radial-gradient(circle, var(--color-primary-glow-strong) 0%, rgba(212, 54, 106, 0) 70%);
}

.spark--green {
  background: radial-gradient(circle, var(--color-accent-green-glow) 0%, rgba(76, 175, 80, 0) 70%);
}

.spark--sm {
  width: 60px;
  height: 60px;
  opacity: 0.35;
}

.spark--md {
  width: 100px;
  height: 100px;
  opacity: 0.28;
}

.spark--lg {
  width: 150px;
  height: 150px;
  opacity: 0.22;
}

.spark--tl {
  top: 14%;
  left: 6%;
}

.spark--tr {
  top: 12%;
  right: 10%;
}

.spark--mr {
  top: 46%;
  right: 14%;
}

.spark--bl {
  bottom: 12%;
  left: 8%;
}

/* Scroll-reveal (progressive enhancement, driven by static/js/main.js).
   More pronounced than a simple fade: larger travel distance, slight
   scale-in, and an ease-out-expo curve for a snappier, more dramatic
   entrance. Bumped up slightly (was 44px/0.98) now that sections arrive
   as discrete "screens" via scroll-snap rather than continuous
   scrolling — a bit more travel reads as a more intentional entrance. */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(60px) scale(0.95);
  transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1), transform 1s cubic-bezier(0.16, 1, 0.3, 1);
}

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

/* Responsive */

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

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

@media (max-width: 720px) {
  .hero h1 {
    font-size: 1.5rem;
  }

  .grid-2,
  .grid-3,
  .grid-4,
  .hero-split {
    grid-template-columns: 1fr;
  }

  .hero-split {
    gap: 32px;
  }

  .berry-diagram {
    max-width: 480px;
    margin: 0 auto;
  }

  /* Footer stays a single row at every width (it's a fixed thin strip,
     not a stacked block anymore) — the copyright text is dropped
     instead, the same "hide the least essential label" pattern already
     used for .brand-name/.menu-btn-label below, so logo + legal links
     always fit on one line. */
  .footer-copy {
    display: none;
  }

  .footer-inner {
    gap: 16px;
  }

  .footer-legal {
    gap: 14px;
  }

  .header-controls {
    gap: 6px;
  }

  .header-start {
    gap: 6px;
  }

  .brand-name {
    display: none;
  }

  .menu-btn-label {
    display: none;
  }
}

/* Motion accessibility: collapse every animation/transition to near-instant.
   Reveal targets, the logo, the nav overlay, and the circuit decor still
   change state via JS class toggles or CSS, they just do so without
   perceptible motion. The hero canvas is skipped entirely (see main.js)
   and hidden via the rule above. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
