:root {
  --bg-primary: #050608;
  --bg-secondary: #0b0d12;
  --glass-bg: rgba(255, 255, 255, 0.04);
  --glass-border: rgba(255, 255, 255, 0.09);
  --glass-bg-hover: rgba(255, 255, 255, 0.07);
  --text-primary: #ffffff;
  --text-secondary: #9b9ba6;
  --accent-purple: #2f6fed;
  --accent-cyan: #60a5fa;
  --accent-magenta: #1d4ed8;
  --accent-gradient: linear-gradient(90deg, #60a5fa, var(--accent-purple));
  --accent-text-on-gradient: #05070d;
  --btn-radius: 10px;
  --glass-shadow: 0 24px 60px rgba(14, 8, 32, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --glass-shadow-sm: 0 10px 26px rgba(14, 8, 32, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.04);
  --field-bg: rgba(255, 255, 255, 0.03);
  --scrim: rgba(5, 6, 8, 0.65);
  --nav-bg: rgba(5, 6, 8, 0.72);
  --nav-mobile-bg: rgba(5, 6, 8, 0.97);
  --nav-dropdown-bg: rgba(9, 10, 15, 0.98);
  --aurora-opacity: 0.22;
  --radius: 14px;
  --radius-sm: 10px;
  --container-width: 1200px;
  /* San Francisco first: this is a Mac software site, so most visitors are
     on a Mac/iPhone/iPad and get the actual system font Apple's own pages
     use, not a Google-Fonts lookalike. Inter (already loaded for the rest
     of the visit) is the fallback everywhere else — the two are close
     enough in proportions that nothing reflows swapping between them. */
  --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif;
  --ease-premium: cubic-bezier(0.16, 1, 0.3, 1);
  /* 2026-07-18 visual polish pass */
  --card-bg: rgba(255, 255, 255, 0.05);
  --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.28);
  --card-shadow-hover: 0 2px 4px rgba(0, 0, 0, 0.4), 0 14px 36px rgba(0, 0, 0, 0.38);
  --shot-glow: rgba(47, 111, 237, 0.16);
  --space-section: clamp(64px, 8vw, 104px);
  /* Depicter hero slider's decorative background circles — see
     .depicter-blob in the Depicter hero section below. Sourced from the
     original export's own shape fill (a solid linear-gradient(0deg,
     #DDF1FF 0%, #FFF 100%) circle, designed to sit on a white card and
     fade into it) translated into a fade-to-transparent gradient so it
     works over any page background; dark theme needs a lower alpha since
     a blue tint reads far more vivid against near-black than it ever did
     fading into white. */
  --depicter-blob-color: rgba(96, 165, 250, 0.3); /* was 0.55 - too sharp/defined at that alpha, brought down for a softer fade */
  /* 2026 bold redesign — second accent used only for the new hero's glow
     and eyebrow label, a deliberate departure from the single-blue palette
     everywhere else. Warm coral against the cool blue gives the hero one
     bold color moment without recoloring the whole site. */
  --accent-coral: #ff6b4a;
  color-scheme: dark;
}

/* Keyboard-only "skip to content" link — every page (root + all 16
   homepage templates) has this as the first focusable element right after
   <body>, jumping straight to <main id="main"> so keyboard/screen-reader
   users don't have to tab through the full navbar (incl. two dropdown
   submenus) to reach page content. Hidden off-screen until focused, per
   the standard pattern, rather than display:none (which would also hide
   it from focus). */
.skip-link {
  position: absolute;
  top: -100px;
  left: 12px;
  z-index: 999;
  padding: 10px 18px;
  background: var(--accent-purple);
  color: #fff;
  border-radius: var(--radius-sm);
  font-weight: 600;
  font-size: 0.9rem;
  text-decoration: none;
  transition: top 0.15s ease;
}

.skip-link:focus {
  top: 12px;
}

[data-theme="light"] {
  /* Off-white page (was pure #ffffff) — softer on the eyes and lets white
     cards read as elevated surfaces instead of disappearing into the page. */
  --bg-primary: #fafafa;
  --bg-secondary: #f0f0f3;
  --glass-bg: rgba(18, 22, 40, 0.03);
  --glass-border: rgba(18, 22, 40, 0.1);
  --glass-bg-hover: rgba(18, 22, 40, 0.06);
  --text-primary: #000000;
  --text-secondary: #6b6b76;
  --accent-cyan: #60a5fa;
  --accent-magenta: #1d4ed8;
  --glass-shadow: 0 20px 50px rgba(30, 20, 60, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.7);
  --glass-shadow-sm: 0 8px 22px rgba(30, 20, 60, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.6);
  --field-bg: rgba(18, 22, 40, 0.025);
  --scrim: rgba(255, 255, 255, 0.75);
  --nav-bg: rgba(255, 255, 255, 0.82);
  --nav-mobile-bg: rgba(255, 255, 255, 0.97);
  --nav-dropdown-bg: rgba(255, 255, 255, 0.98);
  --aurora-opacity: 0.1;
  --card-bg: #ffffff;
  --card-shadow: 0 1px 2px rgba(30, 20, 60, 0.06), 0 8px 24px rgba(30, 20, 60, 0.06);
  --card-shadow-hover: 0 2px 4px rgba(30, 20, 60, 0.08), 0 14px 36px rgba(30, 20, 60, 0.1);
  --shot-glow: rgba(47, 111, 237, 0.1);
  /* The source export's literal fill color (#DDF1FF) is so pale that even
     at full opacity it barely registers against a white/off-white page —
     tried that first and it read as washed out. Using the same accent
     blue as dark theme instead, at a higher alpha than dark theme needs,
     since a lighter background takes more saturation to show a clear tint. */
  --depicter-blob-color: rgba(96, 165, 250, 0.36); /* was 0.62 - too sharp/defined at that alpha, brought down for a softer fade */
  /* Darker/more saturated than the dark-theme value — the lighter one reads
     fine glowing against near-black but is too washed-out-contrast for the
     eyebrow label's small bold text against a near-white page. */
  --accent-coral: #e8502e;
  color-scheme: light;
}

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  height: 100%;
  scroll-behavior: smooth;
  overflow-y: auto;
  scrollbar-gutter: stable;
}

body {
  background: linear-gradient(160deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
  background-attachment: fixed;
  color: var(--text-primary);
  font-family: var(--font-sans);
  line-height: 1.6;
  display: flex;
  flex-direction: column;
  transition: background 0.25s ease, color 0.25s ease;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

main {
  flex: 1 0 auto;
}

/* Post pages: the outer body flex column already pushes the footer to the
   bottom of short pages (main { flex: 1 0 auto }), but that only stretches
   main's own box — content inside main still stacks from the top, leaving
   the Previous/Next nav sitting right under a short article with a dead
   gap before the footer. Making main itself a flex column and pushing its
   last section to the bottom with margin-top: auto anchors Previous/Next
   directly above the footer on every post, regardless of article length. */
.post-page-main {
  display: flex;
  flex-direction: column;
}

.post-page-main > section:last-of-type {
  margin-top: auto;
}

/* Single restrained glow (was 3 stacked purple/cyan/magenta blobs) — one
   accent, used sparingly, per the monochrome-plus-one-accent direction. */
html::before {
  content: '';
  position: fixed;
  inset: -10%;
  z-index: -1;
  pointer-events: none;
  filter: blur(90px);
  opacity: var(--aurora-opacity);
  transition: opacity 0.25s ease;
  background: radial-gradient(circle at 20% 15%, var(--accent-purple), transparent 50%);
  animation: aurora-drift-a 34s ease-in-out infinite alternate;
}

@keyframes aurora-drift-a {
  from { transform: translate(0, 0) scale(1); }
  to { transform: translate(6%, 4%) scale(1.15); }
}

/* Pauses every CSS @keyframes animation inside a hero once it's scrolled
   well out of view — js/main.js's heroInViewport IntersectionObserver
   toggles this class on `main > section:first-of-type` (every template's
   hero) the moment it leaves the viewport (plus a 200px runway either
   side). Several templates run continuous, never-stopping animations
   there (Orbit Launcher's 8 rotating rings + 8 counter-rotating planets +
   2 hub pulses + starfield/nebula drift; Aurora Wave's 3 morphing blobs;
   Spotlight Grid's sweeping beams + rising dust motes; etc.) that have no
   reason to keep costing CPU/GPU while the user reads an article three
   screens further down — a foregrounded tab doesn't throttle any of this
   on its own just because the element is off-screen, only the browser's
   own background-TAB throttling does that, and this is the same page, still
   in the foreground. `animation-play-state` only affects @keyframes
   animations, never hover/focus transitions, so nothing interactive here
   is affected once scrolled back into view. */
.anim-offscreen,
.anim-offscreen * {
  animation-play-state: paused !important;
}

@media (prefers-reduced-motion: reduce) {
  html::before,
  .accent-text,
  .nav-logo-text,
  .hero-title,
  .hero-subtitle,
  .hero-actions,
  .hero-visual,
  .hero-visual::before,
  .hero-screenshot {
    animation: none;
  }

  .hero-title,
  .hero-subtitle,
  .hero-actions,
  .hero-visual {
    opacity: 1;
    filter: none;
    transform: none;
  }

  .hero-screenshot {
    transform: none;
  }

  .hero,
  .hero .container {
    transition: none !important;
    transform: none !important;
    opacity: 1 !important;
  }
}

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

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

ul {
  list-style: none;
}

h1, h2, h3 {
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.container {
  max-width: var(--container-width);
  margin: 0 auto;
  padding: 0 24px;
}

.accent-text {
  color: var(--accent-purple);
  background: linear-gradient(90deg, #38bdf8, #a78bfa, #f472b6, #fb923c, #38bdf8);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: accent-shimmer 8s ease-in-out infinite;
}

/* .hero-title and .nav-logo-text already paint the identical gradient
   continuously across their ENTIRE text (see their own rules below). If
   the nested "macOS"/"Me" .accent-text span keeps its own separate
   background-clip, it paints a second, independently-sized gradient layer
   scoped to just that word — which travels at a different apparent speed
   than the surrounding text (same animation, much narrower box) and shows
   as a visible seam/mismatch where the word meets the rest of the
   sentence. Dropping the span's own background lets the parent's single
   gradient show through its transparent glyphs instead, so the color
   sweeps across the whole line as one continuous motion. */
.hero-title .accent-text,
.nav-logo-text .accent-text {
  background: none;
  -webkit-text-fill-color: inherit;
  animation: none;
}

@keyframes accent-shimmer {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

.section-title {
  /* Bumped from clamp(1.5rem,...,2rem) as part of the 2026 bold-editorial
     pass — bigger section headings read more confident/less "template,"
     matching the new hero's much larger type scale. Only used on the
     homepage (All apps / Featured Articles / More to explore) and the
     About page, so this is a small, contained change. */
  font-size: clamp(1.75rem, 3.2vw, 2.75rem);
  letter-spacing: -0.03em;
  margin-bottom: 48px;
}

.badge {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent-purple);
  background: rgba(47, 111, 237, 0.12);
  border: 1px solid rgba(47, 111, 237, 0.3);
  padding: 4px 12px;
  border-radius: 999px;
  margin-bottom: 12px;
  cursor: pointer;
  transition: opacity 0.2s;
}
.badge:hover {
  opacity: 0.8;
}

.download-count {
  color: var(--text-secondary);
  font-size: 0.85rem;
}

.btn {
  display: inline-block;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 0.95rem;
  padding: 12px 28px;
  border-radius: var(--btn-radius);
  border: none;
  cursor: pointer;
  transition: transform 0.2s var(--ease-premium), box-shadow 0.3s var(--ease-premium), opacity 0.15s ease;
}

.btn-primary {
  /* Flat single color, not the gradient-plus-shine-sweep this used to be —
     that combination (moving light streak on hover, colored glow shadow,
     a lift) is exactly the "agency template" look Avestive explicitly
     steered away from for this site (Apple/Stripe minimal, not Linear/
     Vercel bold-gradient). Apple's own CTAs are a solid fill that just
     brightens slightly on hover — no motion, no glow, no sweep. */
  background: var(--accent-purple);
  color: #ffffff;
  transition: filter 0.2s var(--ease-premium), transform 0.2s var(--ease-premium);
}

.btn-primary:hover {
  filter: brightness(1.12);
}

.btn:active {
  transform: scale(0.96);
  box-shadow: none;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0%;
  background: var(--accent-gradient);
  z-index: 200;
  pointer-events: none;
  transition: width 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .scroll-progress {
    transition: none;
  }
}

.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--nav-bg);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--glass-border);
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0);
  transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.4s var(--ease-premium);
}

.navbar--scrolled {
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
}

/* Compact navbar style (navbar_style === 'compact', admin/inc/theme.php)
   renders as a short (60px), inset, rounded pill — visually much lighter
   than the 72px edge-to-edge full bar every hero's own top padding was
   originally tuned to clear. Against the smaller floating pill that
   padding reads as leftover dead space between the pill and the hero
   content, rather than a bug. Pulling each template's hero section up a
   bit compensates without hand-tuning ~15 different per-template
   padding-top values individually. `--navbar-mode: compact` is set on
   .navbar only for this style; a small snippet in main.js reads it back
   once on load and mirrors it onto <body> as `.compact-navbar-active`,
   the same pattern dock mode already uses for `.dock-navbar-active`.
   Every template's markup is `<main><section class="...hero...">`, so
   this single selector reaches all of them without per-template rules. */
body.compact-navbar-active main > section:first-of-type {
  margin-top: -24px;
}

@media (max-width: 640px) {
  body.compact-navbar-active main > section:first-of-type {
    margin-top: -14px;
  }
}

.nav-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 24px;
  height: 72px;
}

.nav-logo {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 1.55rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1;
}

.nav-logo-mark {
  width: 40px;
  height: 40px;
  object-fit: contain;
  /* logo.png isn't perfectly square (128x161), so object-fit:contain in a
     40x40 box letterboxes horizontally and centers by default — leaving a
     few px of invisible inset before the visible mark on its left edge.
     Pinning left keeps the visible logo flush with the box's (and the
     navbar's) true left edge. */
  object-position: left center;
  flex-shrink: 0;
}

.nav-logo-text {
  display: inline-block;
  line-height: 1;
  padding-bottom: 2px;
}

/* Full-text animated shimmer — the whole hero tagline and the whole
   "MacMeUp" wordmark cycle through the same gradient .accent-text uses,
   rather than just the single highlighted word/syllable. Always on (not
   hover-gated) so it reads consistently on touch devices too. Nested
   .accent-text spans inside .hero-title keep their own copy of the same
   gradient/animation — harmless, since the colors match. */
.nav-logo-text {
  color: transparent;
  background-image: linear-gradient(90deg, #38bdf8, #a78bfa, #f472b6, #fb923c, #38bdf8);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: accent-shimmer 8s ease-in-out infinite;
}

.nav-links {
  display: flex;
  align-items: center;
  justify-self: center;
  gap: 32px;
  font-weight: 500;
  color: var(--text-secondary);
}

/* Tablet range — below this the links are still rendered inline (the
   hamburger swap doesn't happen until 720px below), but centering them in
   the middle grid track reads as awkward, unbalanced whitespace at these
   narrower widths. Push them over next to the search/donate/theme icons
   instead, so everything but the logo clusters on the right. Below 720px
   .nav-links switches to the absolute-positioned mobile dropdown panel
   (see that block below) where justify-self no longer applies, so this
   naturally only affects the tablet gap. */
@media (max-width: 860px) and (min-width: 721px) {
  .nav-links {
    justify-self: end;
  }
}

.nav-links a {
  position: relative;
  padding-bottom: 4px;
}

.nav-links a::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 0;
  height: 2px;
  background: var(--accent-gradient);
  box-shadow: 0 0 8px rgba(47, 111, 237, 0.6);
  transform: translateX(-50%);
  transition: width 0.3s var(--ease-premium);
}

.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--text-primary);
}

.nav-links a:hover::after,
.nav-links a:focus-visible::after {
  width: 100%;
}

/* Current-page highlight — main.js adds .nav-link--active to whichever
   top-level link (or dropdown toggle, when the current page lives inside
   that dropdown's menu) matches location.pathname. Same treatment as
   hover, just persistent. */
.nav-links a.nav-link--active,
.nav-links .nav-dropdown-toggle.nav-link--active {
  color: var(--text-primary);
}

.nav-links a.nav-link--active::after {
  width: 100%;
}

.nav-dropdown {
  position: relative;
}

.nav-dropdown-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-sans);
  font-size: inherit;
  font-weight: inherit;
  color: var(--text-secondary);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0 0 4px;
}

.nav-dropdown-toggle:hover,
.nav-dropdown.open .nav-dropdown-toggle {
  color: var(--text-primary);
}

.nav-dropdown-chevron {
  width: 13px;
  height: 13px;
  transition: transform 0.25s var(--ease-premium);
}

.nav-dropdown.open .nav-dropdown-chevron {
  transform: rotate(180deg);
}

.nav-dropdown-menu {
  position: absolute;
  top: calc(100% + 14px);
  left: 50%;
  transform: translate(-50%, -6px);
  min-width: 260px;
  white-space: nowrap;
  background: var(--nav-dropdown-bg);
  backdrop-filter: blur(20px);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--glass-shadow);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.25s var(--ease-premium), transform 0.25s var(--ease-premium), visibility 0.25s;
  z-index: 150;
}

.nav-dropdown.open .nav-dropdown-menu {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  pointer-events: auto;
}

.nav-dropdown-menu a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  color: var(--text-primary);
  transition: background 0.2s ease;
}

.nav-dropdown-icon {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  object-fit: contain;
  flex-shrink: 0;
}

.nav-dropdown-icon--svg {
  border-radius: 0;
  color: var(--text-secondary);
  stroke-width: 1.8;
}

.nav-dropdown-menu a::after {
  display: none;
}

.nav-dropdown-menu a:hover {
  background: var(--glass-bg-hover);
}

@media (prefers-reduced-motion: reduce) {
  .nav-dropdown-menu,
  .nav-dropdown-chevron {
    transition: none;
  }
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 20px;
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 0;
  border: none;
  background: none;
  color: var(--text-primary);
  cursor: pointer;
  flex-shrink: 0;
  transition: color 0.15s ease, transform 0.15s ease;
}

.theme-toggle:hover {
  color: var(--accent-purple);
  transform: translateY(-1px);
}

.theme-toggle svg {
  width: 18px;
  height: 18px;
}

.theme-toggle .icon-moon {
  display: none;
}

[data-theme="light"] .theme-toggle .icon-sun {
  display: none;
}

[data-theme="light"] .theme-toggle .icon-moon {
  display: block;
}

.search-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 0;
  border: none;
  background: none;
  color: var(--text-primary);
  cursor: pointer;
  flex-shrink: 0;
  transition: color 0.15s ease, transform 0.15s ease;
}

.search-toggle:hover {
  color: var(--accent-purple);
  transform: translateY(-1px);
}

.search-toggle svg {
  width: 18px;
  height: 18px;
}

.donate-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 0;
  border: none;
  background: none;
  color: var(--text-primary);
  cursor: pointer;
  flex-shrink: 0;
  transition: color 0.15s ease, transform 0.15s ease;
}

.donate-toggle:hover {
  color: #f472b6;
  transform: translateY(-1px);
}

.donate-toggle svg {
  width: 18px;
  height: 18px;
}

.search-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 12vh 20px 20px;
  background: rgba(10, 12, 18, 0.55);
  backdrop-filter: blur(6px);
}

.search-overlay[hidden] {
  display: none;
}

.search-panel {
  width: 100%;
  max-width: 640px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--surface, var(--bg-primary));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
  overflow: hidden;
  animation: search-panel-in 0.2s var(--ease-premium);
}

@keyframes search-panel-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}

.search-panel-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--glass-border);
  flex-shrink: 0;
}

.search-panel-icon {
  width: 18px;
  height: 18px;
  color: var(--text-secondary);
  flex-shrink: 0;
}

.search-panel-input {
  flex: 1;
  border: none;
  background: none;
  outline: none;
  font: inherit;
  font-size: 1rem;
  color: var(--text-primary);
}

/* type="search" inputs render their own native clear/cancel button in
   Chrome, Safari, and Edge — left on, it sits right next to our own
   .search-panel-close "x" button and reads as two close buttons. */
.search-panel-input::-webkit-search-cancel-button,
.search-panel-input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

.search-panel-close {
  border: none;
  background: none;
  color: var(--text-secondary);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
  flex-shrink: 0;
}

.search-panel-close:hover {
  color: var(--text-primary);
}

.search-panel-results {
  overflow-y: auto;
  padding: 8px;
}

.search-panel-empty {
  padding: 20px 12px;
  color: var(--text-secondary);
  font-size: 0.92rem;
  text-align: center;
}

.search-result {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: inherit;
}

.search-result:hover,
.search-result:focus-visible {
  background: var(--glass-bg-hover);
}

.search-result-thumb {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  flex-shrink: 0;
  background: var(--glass-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

.search-result-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.search-result-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.search-result-type {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent-purple);
  font-weight: 600;
}

.search-result-title {
  font-size: 0.98rem;
  font-weight: 600;
  color: var(--text-primary);
}

.search-result-desc {
  font-size: 0.85rem;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

body.search-open {
  overflow: hidden;
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  cursor: pointer;
}

.nav-toggle span {
  display: block;
  height: 2px;
  width: 100%;
  background: var(--text-primary);
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.nav-toggle--active span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav-toggle--active span:nth-child(2) {
  opacity: 0;
}

.nav-toggle--active span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

@media (max-width: 720px) {
  .nav-toggle {
    display: flex;
    /* Burger leads the icon cluster on mobile (was last in DOM order —
       search/donate/theme, then burger). DOM stays as-is for desktop;
       flex order flips it visually here only. */
    order: -1;
  }

  .nav-right {
    gap: 12px;
  }

  .nav-links {
    position: absolute;
    top: 72px;
    left: 0;
    right: 0;
    width: 100%;
    /* Base .nav-links sets align-items:center for the desktop horizontal
       row — left uncorrected here, that centers/shrink-wraps each direct
       child (the plain Home/About/Support/Contact links, since the
       .nav-dropdown wrappers below force their own width back to 100%),
       instead of every row spanning edge-to-edge like it should in this
       column layout. */
    align-items: stretch;
    background: var(--nav-mobile-bg);
    flex-direction: column;
    gap: 0;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.25s ease, background 0.25s ease, border-color 0.25s ease;
    /* border-bottom is fully OFF here (width 0, not just a transparent
       color) and only turned on in .nav-links--open below. An earlier fix
       set `border-bottom: 1px solid transparent` instead of removing it —
       that looked right (transparent = invisible) but wasn't: this element
       still has `background: var(--nav-mobile-bg)` and the default
       `background-clip: border-box`, which paints the box's OWN background
       color underneath the border area regardless of the border's own
       color. So a "transparent" 1px border over an opaque background isn't
       actually invisible — it just shows that background color bleeding
       through, which in light mode is a near-white --nav-mobile-bg tint,
       i.e. exactly the persistent "white line under the navbar" that kept
       being reported. overflow:hidden + max-height:0 clips CONTENT, not the
       box's own border/padding area, so that 1px strip was never actually
       collapsed away by either of those. The only real fix is zero border
       WIDTH when closed — border-bottom-color alone can't undo it. */
    border-bottom: none;
  }

  .nav-links a {
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 16px 24px;
    border-top: 1px solid var(--glass-border);
  }

  .nav-dropdown {
    width: 100%;
    box-sizing: border-box;
  }

  .nav-dropdown-toggle {
    width: 100%;
    box-sizing: border-box;
    justify-content: space-between;
    padding: 16px 24px;
    border-top: 1px solid var(--glass-border);
  }

  .nav-dropdown-menu {
    position: static;
    transform: none;
    width: 100%;
    box-sizing: border-box;
    min-width: 0;
    max-width: none;
    white-space: normal;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    /* Base rule (desktop) sets backdrop-filter: blur(20px) for the floating
       popover look — left active here, a transparent background would blur
       whatever page content sits behind the open mobile menu instead of
       showing the dropdown rows cleanly. Explicitly killed below. */
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--glass-bg-hover);
    border: none;
    box-shadow: none;
    border-radius: 0;
    padding: 0;
    margin: 0;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.25s ease;
  }

  .nav-dropdown.open .nav-dropdown-menu {
    /* Was 480px — the Posts submenu alone (All Posts + 10 categories at
       ~44px/row) exceeds that, so its last item was clipped mid-row. This
       is only a transition cap, not layout sizing, so it just needs to be
       comfortably taller than the tallest realistic menu; the outer
       .nav-links--open panel already scrolls at viewport height. */
    max-height: 1000px;
    /* The base (desktop) rule for this exact selector — .nav-dropdown.open
       .nav-dropdown-menu — sets transform: translate(-50%, 0) to slide the
       absolutely-positioned popover into place. That rule has higher
       specificity (two classes) than the mobile-only plain
       ".nav-dropdown-menu { transform: none }" override above, so it kept
       winning the instant a dropdown opened — shifting the whole panel
       left by half its own width and pushing its content out of view.
       This is the actual, long-standing cause of "dropdown opens but
       shows nothing." Needs the same specificity to override it. */
    transform: none;
  }

  .nav-dropdown-menu a {
    display: flex;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    padding: 12px 24px 12px 40px;
    border-top: 1px solid var(--glass-border);
    border-radius: 0;
    font-size: 0.95rem;
    color: var(--text-primary);
    opacity: 1;
  }

  .nav-links--open {
    max-height: calc(100vh - 72px);
    overflow-y: auto;
    /* Full border-bottom (width+style+color), not just -color: the closed
       state above now has NO border at all (width 0), so there's nothing
       for a color-only override to apply to here. */
    border-bottom: 1px solid var(--glass-border);
  }
}

.site-footer {
  position: relative;
  margin-top: auto;
  padding: 16px 0;
}

.site-footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(47, 111, 237, 0.6), transparent);
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  color: var(--text-secondary);
  font-size: 0.78rem;
}

.footer-links {
  display: flex;
  gap: 8px;
}

.footer-links a:not(:last-child)::after {
  content: '|';
  margin-left: 8px;
  color: var(--text-secondary);
  opacity: 0.5;
}

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

/* Social icons — appended by js/main.js right after .footer-links (About |
   Support | Contact | Privacy | Terms), one row per page via a single JS
   block rather than static markup in every footer. Each link carries its
   own inline color (that platform's brand color, or a gradient for
   Instagram) set by main.js from the admin config; --accent-purple here is
   only a fallback for the rare case a platform has no color defined. */
.footer-social {
  display: flex;
  align-items: center;
  gap: 14px;
}

.footer-social-link {
  display: flex;
  color: var(--accent-purple);
  opacity: 0.9;
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.footer-social-link:hover {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .footer-social-link:hover {
    transform: translateY(-1px);
  }
}

.footer-social-link svg {
  width: 16px;
  height: 16px;
}

/* Platforms whose brand color is pure black (X) — rendered in the theme's
   text color instead of an inline black, matching X's own adaptive
   black-on-light / white-on-dark branding. Without this the icon was
   invisible on the dark theme. */
.footer-social-link--adaptive {
  color: var(--text-primary);
}

/* Mobile: .footer-inner's justify-content:space-between only reads as
   "spread out" when its 2-3 children (copyright text, links, social icons)
   fit on one line. Once they wrap to their own stacked rows each row still
   only has one item, so space-between leaves it flush left instead of
   centered — switch to a centered column instead. */
@media (max-width: 720px) {
  .footer-inner {
    flex-direction: column;
    justify-content: center;
    text-align: center;
  }

  .footer-links {
    justify-content: center;
    flex-wrap: wrap;
    row-gap: 6px;
  }

  .footer-social {
    justify-content: center;
  }
}

.hero {
  padding: 140px 0 90px;
  will-change: transform, opacity;
}

/* Subpages (About, Contact, etc.) just use .hero without .hero--home */
.hero:not(.hero--home) {
  text-align: center;
}
.hero:not(.hero--home) .hero-subtitle {
  margin-left: auto;
  margin-right: auto;
}

/* .hero-layout--* variants only apply to #heroClassic (.hero-variant--classic)
   — selected via admin Homepage > Hero Style > Classic, then the Hero
   Layout dropdown beneath it. The new Depicter slider (.hero-variant--
   depicter, its own dedicated rules further down this file) is a fixed
   design with no layout variants of its own; see the .hero-variant--*
   visibility toggle below for how the two coexist in the same markup. */
.hero-layout--classic {
  text-align: center;
}
.hero-layout--classic .hero-content {
  text-align: center;
  margin-bottom: 48px;
}
.hero-layout--classic .hero-subtitle {
  margin-left: auto;
  margin-right: auto;
}
.hero-layout--classic .hero-actions {
  justify-content: center;
}
.hero-layout--classic .hero-media {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
}

.hero-layout--split .container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 48px;
}
.hero-layout--split .hero-content {
  text-align: left;
}
.hero-layout--split .hero-subtitle {
  margin-left: 0;
  margin-right: 0;
}
.hero-layout--split .hero-actions {
  justify-content: flex-start;
}
.hero-layout--split .hero-media {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
}

.hero-layout--bento .container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}
.hero-layout--bento .hero-content,
.hero-layout--bento .hero-media {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  padding: 40px;
  box-shadow: var(--glass-shadow-sm);
  backdrop-filter: blur(12px);
}
.hero-layout--bento .hero-content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: left;
}
.hero-layout--bento .hero-subtitle {
  margin-left: 0;
  margin-right: 0;
}
.hero-layout--bento .hero-actions {
  justify-content: flex-start;
}
.hero-layout--bento .hero-media {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
}

/* 4) Floating Glass (Card) */
.hero-layout--glass .container {
  max-width: 1000px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  padding: 48px;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  text-align: center;
  box-shadow: 0 24px 64px -16px rgba(0,0,0,0.3);
}
.hero-layout--glass .hero-content {
  text-align: center;
  margin-bottom: 48px;
}
.hero-layout--glass .hero-subtitle {
  margin: 24px auto 32px;
}
.hero-layout--glass .hero-actions {
  justify-content: center;
}
.hero-layout--glass .hero-media {
  margin: 0 auto;
}

/* 5) Minimalist (Typography only) */
.hero-layout--minimal {
  text-align: center;
  padding-top: 60px;
  padding-bottom: 60px;
}
.hero-layout--minimal .hero-content {
  text-align: center;
}
.hero-layout--minimal .hero-title {
  font-size: 5rem;
  letter-spacing: -0.04em;
  line-height: 1.05;
}
.hero-layout--minimal .hero-subtitle {
  margin: 32px auto 48px;
  font-size: 1.3rem;
  max-width: 700px;
}
.hero-layout--minimal .hero-actions {
  justify-content: center;
}
.hero-layout--minimal .hero-media {
  display: none; /* Hide the app carousel entirely */
}

/* 6) Asymmetric Grid */
.hero-layout--asymmetric .container {
  display: grid;
  grid-template-columns: 2fr 3fr;
  gap: 48px;
  align-items: end;
}
.hero-layout--asymmetric .hero-content {
  text-align: right;
  padding-bottom: 40px;
}
.hero-layout--asymmetric .hero-subtitle {
  margin: 24px 0 32px auto;
}
.hero-layout--asymmetric .hero-actions {
  justify-content: flex-end;
}
.hero-layout--asymmetric .hero-media {
  margin-top: -60px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  padding: 24px;
}

@media (max-width: 960px) {
  .hero-layout--split .container,
  .hero-layout--bento .container,
  .hero-layout--asymmetric .container {
    grid-template-columns: 1fr;
    gap: 32px;
  }
  .hero-layout--split .hero-content,
  .hero-layout--bento .hero-content,
  .hero-layout--asymmetric .hero-content {
    text-align: center;
  }
  .hero-layout--split .hero-subtitle,
  .hero-layout--bento .hero-subtitle,
  .hero-layout--asymmetric .hero-subtitle {
    margin: 16px auto 32px;
  }
  .hero-layout--split .hero-actions,
  .hero-layout--bento .hero-actions,
  .hero-layout--asymmetric .hero-actions {
    justify-content: center;
  }
  .hero-layout--bento .hero-content,
  .hero-layout--bento .hero-media {
    padding: 24px;
  }
}

/* Fullscreen, vertically-centered treatment. This used to cause a shift bug:
   the per-slide subtitle text rewraps to a different number of lines as the
   hero rotates through apps, and centering the whole block in the viewport
   meant any height change lower down shifted the entire block — including
   the title above it — up or down. That's now fixed at the source instead
   of by avoiding centering: .hero-subtitle reserves a constant 2-line
   min-height and .hero-visual has a fixed (not min) height regardless of
   which screenshot is showing, so the block's total height never changes
   between slides and centering it is safe again. */
.hero--home {
  /* Capped with min() so this never runs away on unusually tall/narrow
     viewports (e.g. a desktop monitor rotated to portrait, ~1080x1920) —
     100vh - 72px on a 1920px-tall viewport is ~1848px, far taller than the
     hero's actual content, which left a large empty flex-centered gap
     above whatever follows (the stats banner, inserted right after the
     hero). 880px is comfortably taller than the hero's natural content
     height at any normal desktop width, so this only ever kicks in to
     rein in excess height, never to shrink the intended fullscreen feel
     on typical 16:9 displays. */
  min-height: min(calc(100vh - 72px), 880px);
  display: flex;
  align-items: center;
  padding: 60px 0;
}

.hero--home .container {
  width: 100%;
}

/* ============================================================================
   2026 hero redesign — one unified hero replacing the old selectable
   Classic/Depicter pair below. Bold direction per explicit request: richer
   second accent color, grain texture, oversized editorial type, and an
   asymmetric (not centered) layout — a deliberate departure from this
   site's earlier "restrained Apple/Stripe minimal" hero treatment.
   Deliberately does NOT carry .hero--home (see that rule above) — this
   hero's height is fully content-driven (padding + the stack's own
   height), never viewport-height-based, specifically so it can never
   reproduce the "doesn't fit on a 1080p window" bug the old hero had. */
.hero--2026 {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: 88px 0 72px;
  text-align: left;
}

.hero-2026-noise {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.05;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Removed on request ("single image, no glow, no animation") — the coral
   radial glow behind the screenshot stack. .hero-2026-noise (grain) stays;
   this is just the glow blob. */
.hero-2026-glow {
  display: none;
}

.hero-2026-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 48px;
  align-items: center;
}

.hero-2026-copy {
  text-align: left;
}

.hero-2026-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-coral);
  margin: 0 0 20px;
}

.hero-2026-eyebrow::before {
  content: '';
  width: 24px;
  height: 2px;
  background: var(--accent-coral);
  display: inline-block;
}

/* Bigger/bolder than the old hero-title scale (was clamp(...,5.5rem) at
   most) — the "editorial type" half of the bold direction. */
.hero-2026-title {
  font-size: clamp(2.75rem, 5.6vw, 4.9rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0 0 24px;
}

.hero-2026-subtitle {
  max-width: 460px;
  margin: 0 0 32px;
}

.hero-2026-actions {
  justify-content: flex-start;
}

/* Single rotating screenshot (replaced the 4-card floating fan on request —
   "the three screenshots floating isn't working for me, I'd rather the
   images change one at the time"). js/main.js crossfades in the next app's
   dark/light screenshot pair on a timer, same rotator pattern as the
   Classic hero's #heroVisual, plus a small dot row to jump to a slide
   directly. The glow/grain/asymmetric-grid/editorial-type ingredients of
   the bold direction are unaffected — only the screenshot display changed. */
.hero-2026-stack {
  position: relative;
  height: 460px;
  /* Bottom strip reserved for .hero-2026-dots below, so the dot row has its
     own space instead of sitting on top of the image — the previous
     absolute-at-bottom-of-a-flex-centered-image layout let a tall
     screenshot's card grow right up under the dots. box-sizing: border-box
     means this padding comes out of the 460px total, not on top of it. */
  padding-bottom: 34px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-2026-card {
  /* No card frame — border/background/box-shadow/border-radius/overflow
     removed on request ("remove the frame from screenshots"). The
     screenshot now floats directly on the hero background, same as the
     Classic and Depicter heroes' screenshots, with a plain drop-shadow on
     the image itself instead of a boxed card behind it. */
  position: relative;
  width: 88%;
  max-width: 460px;
  height: 100%;
  transition: opacity 0.38s ease;
}

.hero-2026-card--swapping {
  opacity: 0;
}

.hero-2026-card img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Same subtle shadow as .hero-screenshot/.depicter-image img, now that
     there's no card frame providing its own depth. */
  filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.16));
}

/* .hero-2026-card img's class+type selector above (0,1,1) outranks the
   plain single-class theme toggle rules (.theme-img--light, 0,1,0) — same
   specificity bug already documented and fixed for .app-icon and
   .depicter-image. Without this, both the dark and light screenshot show
   at once in dark mode (light mode "worked" only because the
   [data-theme="light"] rules happen to have higher specificity than
   .hero-2026-card img, 0,2,0 vs 0,1,1). Scope the toggle to
   .hero-2026-card specifically so it wins in both themes. */
.hero-2026-card .theme-img--light {
  display: none;
}

[data-theme="light"] .hero-2026-card .theme-img--dark {
  display: none;
}

[data-theme="light"] .hero-2026-card .theme-img--light {
  display: block;
}

.hero-2026-dots {
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  z-index: 5;
}

.hero-2026-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: var(--glass-border);
  cursor: pointer;
  transition: background 0.25s ease, transform 0.25s ease;
}

.hero-2026-dot:hover {
  background: var(--text-secondary);
}

.hero-2026-dot--active {
  /* Was --accent-coral (read as "red") — the coral token was only ever
     meant for the now-removed glow, so switch the active dot to the same
     purple used everywhere else on the site (stats banner, accent-text,
     filter pills) for consistency. */
  background: var(--accent-purple);
  transform: scale(1.3);
}

@media (max-width: 900px) {
  .hero--2026 {
    padding: 56px 0 48px;
  }

  .hero-2026-grid {
    grid-template-columns: 1fr;
    gap: 36px;
  }

  .hero-2026-copy {
    text-align: center;
  }

  .hero-2026-eyebrow {
    justify-content: center;
  }

  .hero-2026-subtitle {
    margin-left: auto;
    margin-right: auto;
  }

  .hero-2026-actions {
    justify-content: center;
  }

  .hero-2026-stack {
    height: 300px;
    max-width: 420px;
    margin: 0 auto;
  }
}

@media (max-width: 520px) {
  .hero-2026-stack {
    height: 260px;
  }
}

/* Three selectable homepage hero variants live in the same markup —
   #heroClassic (.hero-variant--classic, the original text+rotating-
   screenshot hero), #depicterHero (.hero-variant--depicter, the
   reconstructed Depicter slider), and #hero2026 (.hero--2026, the bold 2026
   redesign). js/main.js toggles which one is visible based on admin
   Homepage > Hero Style (homeConfig.heroStyle); these defaults (depicter
   shown, the other two hidden) only apply before that JS runs / if it never
   loads, so pick the one you actually want as the no-JS fallback.
   #hero2026 previously had no default here at all — meaning it rendered
   visible by default same as #depicterHero, so the two were stacked and
   both showing (each with its own <h1>, a real duplicate-heading SEO issue
   too) for as long as it took main.js to load and apply the real toggle,
   and permanently for any visitor/crawler that never runs JS at all. */
.hero-variant--classic,
.hero--2026 {
  display: none;
}

.hero-variant--depicter {
  display: block;
}

.hero-title {
  position: relative;
  display: inline-block;
  font-size: clamp(3rem, 7.5vw, 5.5rem);
  font-weight: 900;
  letter-spacing: -0.035em;
  margin-bottom: 20px;
  opacity: 0;
  color: transparent;
  background-image: linear-gradient(90deg, #38bdf8, #a78bfa, #f472b6, #fb923c, #38bdf8);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: hero-fade-up 0.9s var(--ease-premium) 0.1s forwards, accent-shimmer 8s ease-in-out infinite;
}

.hero-subtitle {
  max-width: 560px;
  margin: 0 0 36px;
  color: var(--text-secondary);
  font-size: 1.1rem;
  line-height: 1.55;
  /* Reserves space for 2 lines regardless of which app's tagline is
     showing — taglines range from ~65 to ~90 characters, some wrap to one
     line and some to two at this width, so without a fixed min-height
     everything below (the screenshot, progress bar, buttons) would hop up
     or down each time the hero rotates. */
  min-height: calc(1.55em * 2);
  opacity: 0;
  animation: hero-fade-up 0.9s var(--ease-premium) 0.3s forwards;
  transition: opacity 0.3s ease;
}

@keyframes hero-fade-up {
  from {
    opacity: 0;
    transform: translateY(22px);
    filter: blur(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

.hero-actions {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 14px;
  margin: 0;
  opacity: 0;
  animation: hero-fade-up 0.9s var(--ease-premium) 0.6s forwards;
}

.btn-secondary {
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--glass-border);
}

.btn-secondary:hover {
  /* No lift — matches .btn-primary's hover now being color-only, not
     motion-based (Apple's own secondary/"Learn more" buttons don't move
     on hover either). */
  border-color: var(--text-secondary);
  background: var(--glass-bg-hover);
}

/* --- #heroClassic only: old rotating single-screenshot hero visual ------- */

.hero-visual {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Fixed height (not min-height) on purpose: the 8 rotating app
     screenshots all have different natural aspect ratios, so if this box
     sized itself to each image's content, swapping screenshots would grow
     or shrink the box and push the progress bar/buttons below it up or
     down. A fixed height + object-fit: contain on the image keeps this
     box's footprint identical no matter which screenshot is showing —
     .hero-subtitle above it has its own min-height for the same reason,
     since that's the piece that actually changes per slide now. */
  height: clamp(380px, 50vh, 720px);
  opacity: 0;
  animation: hero-fade-up 1s var(--ease-premium) 0.45s forwards;
}

.hero-visual--interactive {
  cursor: pointer;
  touch-action: pan-x pan-y;
}

.hero-visual--interactive:focus-visible {
  outline: 2px solid var(--accent-purple);
  outline-offset: 6px;
  border-radius: var(--radius);
}

.hero-visual--interactive:hover .hero-screenshot {
  transform: scale(1.015);
}

.hero-bg--neon .hero-visual::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 90%;
  height: 90%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 30% 30%, rgba(96, 165, 250, 0.4), transparent 50%),
              radial-gradient(circle at 70% 70%, rgba(47, 111, 237, 0.4), transparent 50%),
              radial-gradient(circle at 50% 50%, rgba(29, 78, 216, 0.3), transparent 60%);
  filter: blur(80px);
  z-index: -1;
  pointer-events: none;
  animation: hero-glow-pulse 10s ease-in-out infinite alternate;
}

[data-theme="light"] .hero-bg--neon .hero-visual::before {
  background: radial-gradient(circle at 30% 30%, rgba(96, 165, 250, 0.25), transparent 50%),
              radial-gradient(circle at 70% 70%, rgba(47, 111, 237, 0.25), transparent 50%);
  filter: blur(60px);
}

.hero-bg--glow .hero-visual::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 68%;
  height: 68%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(47, 111, 237, 0.28), transparent 70%);
  filter: blur(70px);
  z-index: -1;
  pointer-events: none;
  animation: hero-glow-pulse-simple 7s ease-in-out infinite;
}

@keyframes hero-glow-pulse-simple {
  0%, 100% { opacity: 0.55; transform: translate(-50%, -50%) scale(1); }
  50% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
}

@keyframes hero-glow-pulse {
  0% { opacity: 0.6; transform: translate(-50%, -50%) scale(1) rotate(0deg); }
  50% { opacity: 0.9; transform: translate(-48%, -52%) scale(1.05) rotate(5deg); }
  100% { opacity: 0.6; transform: translate(-52%, -48%) scale(1) rotate(-5deg); }
}

.hero-screenshot {
  position: relative;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius);
  /* Same subtle shadow as .app-hero-visual img on the app pages. */
  filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.16));
  transition: opacity 0.4s ease, transform 0.35s var(--ease-premium);
}

.hero-visual--swapping.hero-visual--transition-fade .hero-screenshot {
  opacity: 0;
}

.hero-visual--swapping.hero-visual--transition-slide-left .hero-screenshot {
  opacity: 0;
  transform: translateX(-40px);
}

.hero-visual--swapping.hero-visual--transition-slide-right .hero-screenshot {
  opacity: 0;
  transform: translateX(40px);
}

.hero-visual--swapping.hero-visual--transition-scale-down .hero-screenshot {
  opacity: 0;
  transform: scale(0.92);
}

.hero-visual--swapping.hero-visual--transition-scale-up .hero-screenshot {
  opacity: 0;
  transform: scale(1.08);
}

.hero-visual--swapping.hero-visual--transition-none .hero-screenshot {
  transition: none;
  opacity: 0;
}

/* Dot-and-line style — matches .app-gallery-progress on the app pages
   exactly (see there): small dots for inactive slides, the active one
   stretches into a short line. Was previously its own segmented
   fill-over-4.5s-timer style; simplified to match, per explicit request. */
.hero-visual-progress {
  /* Outer row: prev arrow, dots, next arrow — .hero-visual-dots below holds
     the dots themselves (was this element directly). */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: 22px 0 36px;
  opacity: 0;
  animation: hero-fade-up 1s var(--ease-premium) 0.5s forwards;
}

.hero-visual-dots {
  display: flex;
  align-items: center;
  gap: 8px;
}

.hero-visual-progress-bar {
  display: block;
  width: 6px;
  height: 6px;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--glass-border);
  cursor: pointer;
  transition: width 0.25s var(--ease-premium), background 0.25s ease;
}

.hero-visual-progress-bar--active {
  width: 22px;
  background: var(--accent-gradient);
}

/* Prev/next arrows — same circular icon-button treatment as
   .app-gallery-arrow on the app pages, just its own class since this row
   has its own entrance animation/margins already. */
.hero-visual-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  padding: 0;
  border-radius: 50%;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
}

.hero-visual-arrow:hover {
  background: var(--glass-bg-hover);
  color: var(--text-primary);
  transform: translateY(-1px);
}

.hero-visual-arrow svg {
  width: 15px;
  height: 15px;
}

@media (prefers-reduced-motion: reduce) {
  .hero-visual-arrow:hover {
    transform: none;
  }
}

@media (max-width: 640px) {
  /* Side-by-side two columns instead of stacked rows on mobile — was
     flex-direction: column, changed on request. */
  .hero-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }

  .hero-actions .btn {
    width: 100%;
    text-align: center;
    /* Smaller than the site-wide .btn default on mobile specifically —
       scoped to .hero-actions so app-card/post-card buttons elsewhere
       aren't affected. */
    padding: 10px 16px;
    font-size: 0.85rem;
  }

  .hero-visual {
    height: 320px;
  }
}

.app-icon {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 1.25rem;
  color: var(--accent-text-on-gradient);
  flex-shrink: 0;
}

.app-icon--lg {
  width: 128px;
  height: 128px;
  font-size: 2.6rem;
  border-radius: var(--radius);
}

.icon-bootabler { background: linear-gradient(135deg, #7c5cff, #38bdf8); box-shadow: 0 8px 24px rgba(124, 92, 255, 0.35); }
.icon-esp-mounter { background: linear-gradient(135deg, #22d3ee, #34d399); box-shadow: 0 8px 24px rgba(34, 211, 238, 0.35); }
.icon-mac-tweaks { background: linear-gradient(135deg, #fb923c, #f472b6); box-shadow: 0 8px 24px rgba(251, 146, 60, 0.35); }
.icon-gatekeeper-toggler { background: linear-gradient(135deg, #f87171, #fb923c); box-shadow: 0 8px 24px rgba(248, 113, 113, 0.35); }
.icon-iso-image-creator { background: linear-gradient(135deg, #60a5fa, #818cf8); box-shadow: 0 8px 24px rgba(96, 165, 250, 0.35); }
.icon-sparkler { background: linear-gradient(135deg, #fde047, #f59e0b); box-shadow: 0 8px 24px rgba(253, 224, 71, 0.35); }
.icon-mac-optimizer { background: linear-gradient(135deg, #4ade80, #a3e635); box-shadow: 0 8px 24px rgba(74, 222, 128, 0.35); }
.icon-enable-boot-manager { background: linear-gradient(135deg, #a78bfa, #e879f9); box-shadow: 0 8px 24px rgba(167, 139, 250, 0.35); }

.app-icon img {
  width: 82%;
  height: 82%;
  object-fit: contain;
  display: block;
}

/* Every .app-icon spot now always has a real app icon image in it — the
   gradient .icon-* classes above were the placeholder treatment for before
   real icons existed. Neutralize just the icon-box background/shadow (not
   .app-hero-blob, which still wants its own color) and let the actual icon
   artwork fill the box, since macOS app icons already come with their own
   shape/padding baked in and don't need a colored backdrop behind them. */
.app-icon[class*="icon-"] {
  background: none;
  box-shadow: none;
}

.app-icon[class*="icon-"] img {
  width: 100%;
  height: 100%;
}

.theme-img--light { display: none; }
[data-theme="light"] .theme-img--dark { display: none; }
[data-theme="light"] .theme-img--light { display: block; }

/* Was an accidental duplicate of .btn-primary (filled gradient) — since
   .btn-plain is always used as the secondary/"back" action next to a real
   primary CTA (see app-cta sections), two filled gradient buttons side by
   side competed for attention. Now matches .btn-secondary's treatment. */
.btn-plain {
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--glass-border);
}

.btn-plain:hover {
  border-color: var(--text-secondary);
  transform: translateY(-2px);
}

.reveal {
  opacity: 0;
  /* Reversed on request — was translateY(24px) (cards start below their
     resting spot and move UP into place on scroll-reveal); now starts
     above and moves DOWN into place instead. */
  transform: translateY(-24px);
  transition: opacity 0.7s var(--ease-premium), transform 0.7s var(--ease-premium);
}

.reveal--visible {
  opacity: 1;
  transform: translateY(0);
}

.apps-section {
  padding: 56px 0 150px;
  scroll-margin-top: 88px;
}

/* Divider between homepage sections (All apps / Featured / More to
   explore) — same edge-to-edge fading-gradient line used above the footer
   (.site-footer::before). Uses the general sibling combinator (~), not the
   adjacent one (+): #featuredSentinel/#extraSentinel lazy-load markers sit
   directly between each pair of sections in the DOM (see index.html), so
   no two .apps-section elements are ever truly adjacent siblings — + never
   matched here. ~ only needs an earlier .apps-section sibling, sentinels
   in between or not, and only lands on sections after the first regardless
   of which ones main.js's sectionOrder reordering hides/shows/moves. */
.apps-section ~ .apps-section {
  position: relative;
  /* The active top-padding for .apps-section is var(--space-section)
     (64-104px, set later in the cascade under "Apps section rhythm") —
     tuned for the FIRST section on the page (no divider line above it, so
     that space reads as breathing room under the hero). Sections after the
     first also have the divider line from ::before below sitting right at
     their own top edge, so that same 64-104px reads as a much bigger gap
     between the line and its own section's title than intended. This
     2-class selector already outranks the plain .apps-section rule on
     specificity, so it wins regardless of where either sits in the file. */
  padding-top: 32px;
}

.apps-section ~ .apps-section::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(47, 111, 237, 0.6), transparent);
}

.filter-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 32px;
}

.filter-pill {
  font-family: var(--font-sans);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  padding: 8px 18px;
  cursor: pointer;
  transition: color 0.2s ease, background 0.3s var(--ease-premium), border-color 0.2s ease, box-shadow 0.3s ease;
}

.filter-pill:hover {
  /* Border color change instead of a colored glow ring — same reasoning
     as the button flattening above: no tinted drop-shadows on hover. */
  color: var(--text-primary);
  border-color: rgba(47, 111, 237, 0.4);
}

.filter-pill--active {
  color: #ffffff;
  background: var(--accent-purple);
  border-color: transparent;
}

.app-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 20px;
}

/* Optional fixed-column overrides, applied via js/homepage-config.js when
   Appearance settings request a specific column count instead of the
   default responsive auto-fill above. */
.app-grid--cols-2 { grid-template-columns: repeat(2, 1fr); }
.app-grid--cols-3 { grid-template-columns: repeat(3, 1fr); }
.app-grid--cols-4 { grid-template-columns: repeat(4, 1fr); }

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

@media (max-width: 560px) {
  .app-grid--cols-2,
  .app-grid--cols-3,
  .app-grid--cols-4 { grid-template-columns: 1fr; }
}

.app-card {
  --stagger: 0s;
  display: block;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 24px;
  box-shadow: var(--glass-shadow-sm);
  transition: transform 0.2s var(--ease-premium), background 0.25s ease, box-shadow 0.3s ease,
    opacity 0.7s var(--ease-premium) var(--stagger);
}

.app-card:hover {
  transform: translateY(-4px);
  background: var(--glass-bg-hover);
  border-color: rgba(47, 111, 237, 0.35);
  box-shadow: 0 16px 40px rgba(47, 111, 237, 0.2);
}

.app-grid .app-card:nth-child(2) { --stagger: 0.06s; }
.app-grid .app-card:nth-child(3) { --stagger: 0.12s; }
.app-grid .app-card:nth-child(4) { --stagger: 0.18s; }
.app-grid .app-card:nth-child(5) { --stagger: 0.24s; }
.app-grid .app-card:nth-child(6) { --stagger: 0.3s; }
.app-grid .app-card:nth-child(7) { --stagger: 0.36s; }
.app-grid .app-card:nth-child(8) { --stagger: 0.42s; }

.app-card .app-icon {
  margin-bottom: 16px;
  transition: transform 0.4s var(--ease-premium);
}

.app-card:hover .app-icon {
  transform: scale(1.08) translateY(-2px);
}

.app-card-tag {
  margin-bottom: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.app-card h3 {
  font-size: 1.05rem;
  margin-bottom: 8px;
}

.app-card p {
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.contact-section .container {
  display: flex;
  justify-content: center;
}

.contact-form {
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 32px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  box-shadow: var(--glass-shadow);
}

.form-field label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 8px;
}

.form-field input,
.form-field textarea,
.form-field select {
  width: 100%;
  font-family: var(--font-sans);
  font-size: 0.95rem;
  color: var(--text-primary);
  background: var(--field-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  resize: vertical;
  box-shadow: var(--glass-shadow-sm);
}

.form-field select {
  appearance: none;
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-secondary) 50%),
    linear-gradient(135deg, var(--text-secondary) 50%, transparent 50%);
  background-position: calc(100% - 22px) center, calc(100% - 17px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 40px;
}

.form-field select:invalid {
  color: var(--text-secondary);
}

.form-field option {
  color: #12141c;
}

.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
  outline: none;
  border-color: var(--accent-purple);
}

.form-field input:required:invalid,
.form-field select:required:invalid,
.form-field textarea:required:invalid {
  box-shadow: var(--glass-shadow-sm);
}

.form-honeypot {
  position: absolute;
  left: -9999px;
  top: -9999px;
}

.form-status {
  font-size: 0.9rem;
  min-height: 1.2em;
}

.form-status--success {
  color: #4ade80;
}

.form-status--error {
  color: #f87171;
}

.app-note {
  max-width: 640px;
  margin-bottom: 32px;
  padding: 14px 18px;
  border-radius: var(--radius-sm);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-left: 3px solid var(--accent-cyan);
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.app-note strong {
  color: var(--text-primary);
}

.app-note a {
  color: var(--accent-cyan);
  text-decoration: underline;
}

/* Numbered step timeline — CSS-counter based, no HTML/JS changes needed
   since this only restyles the existing <ol class="howto-list"><li> markup
   that's already on all 10 app detail pages. Solid single-accent dot + a
   thin connector line (not a gradient, not a glow) to stay inside the
   site's flat monochrome-plus-one-accent direction while still reading as
   more considered than a plain numbered list. */
.howto-list {
  list-style: none;
  counter-reset: howto-step;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 22px;
  max-width: 640px;
  padding-left: 40px;
  margin: 0;
}

.howto-list::before {
  content: '';
  position: absolute;
  left: 13px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--glass-border);
}

.howto-list li {
  position: relative;
  color: var(--text-secondary);
  padding-top: 2px;
  counter-increment: howto-step;
}

.howto-list li::before {
  content: counter(howto-step);
  position: absolute;
  left: -40px;
  top: -3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent-purple);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* ============================================================
   App detail page v2 — hero / highlights / features / reqs / CTA
   ============================================================ */

.app-hero {
  position: relative;
  overflow: hidden;
  padding: 48px 0 64px;
}

/* Previously zeroed out .container's usual 24px left/right padding here so
   hero content ran flush to the container's outer edge — but that made the
   app hero ~48px wider than the details/features/highlights sections below
   it on the same page, and wider than the homepage hero and posts/articles
   pages too. Reverted (2026-07-18) so every page shares the same effective
   content width. */

.app-hero-glow {
  position: absolute;
  inset: -15% -10% auto -10%;
  height: 480px;
  z-index: 0;
  pointer-events: none;
  filter: blur(90px);
  opacity: 0.28;
  border-radius: 50%;
}

[data-theme="light"] .app-hero-glow {
  opacity: 0.16;
}

/* Back to 0.95fr 1.3fr (was briefly unified with .app-details-grid's 1.1fr
   1fr, but that shrank the visual column ~18% and made several apps'
   screenshots — Bootabler, Sparkler, Mac Tweaks — look noticeably smaller). */
.app-hero-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 0.85fr 1.4fr;
  gap: 48px;
  align-items: start;
}

.app-hero-icon {
  margin-bottom: 20px;
}

.app-hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 14px;
}

.app-hero-title {
  font-size: clamp(2.25rem, 4.5vw, 3.25rem);
  margin-bottom: 16px;
}

.app-hero-tagline {
  color: var(--text-secondary);
  font-size: 1.1rem;
  max-width: 480px;
  margin-bottom: 28px;
}

.app-hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin-bottom: 22px;
}

.app-hero-actions .btn {
  /* Was hardcoded to 999px, silently overriding the admin "Button shape"
     setting for the app-page hero's Download/etc. buttons — this is the
     "button settings don't apply on the Applications page" bug. */
  border-radius: var(--btn-radius);
}

.app-hero-actions .download-count {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-primary);
}

.app-hero-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.app-hero-badge {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  color: var(--text-secondary);
}

.app-hero-badge--free {
  color: #34d399;
  border-color: rgba(52, 211, 153, 0.35);
  background: rgba(52, 211, 153, 0.1);
}

/* Matches the classic homepage hero's .hero-visual/.hero-screenshot
   treatment exactly (natural size, centered via flex, object-fit:contain,
   no forced width/edge-alignment). The mask-fade/--shot-trim right-
   alignment attempt was removed at the user's request. */
.app-hero-visual {
  /* Column, not row — .app-hero-visual-frame (the image) and
     .app-gallery-progress (the dots/arrows row) are now two separate,
     independent sibling containers stacked vertically, not one nested
     flex block. Frame has its own fixed height below with the image
     centered inside it, so it never changes size between slides — which
     means the gap above progress (margin-top, in normal flow) is always
     constant too, without needing to bind the two together or fix either
     one's position artificially. Main axis flipped to vertical, so the
     "screenshot position" (left/center/right) admin setting now maps to
     align-items instead of justify-content — see the modifier classes
     below. */
  position: relative;
  display: flex;
  flex-direction: column;
  /* Right is the default (flex-end) — the
     admin "Screenshot position" setting's left/center options apply via
     the modifier classes below, added by main.js. */
  align-items: flex-end;
}

/* Admin > App screenshots > "Screenshot position" — 'right' is the base
   rule above and needs no modifier class; main.js adds one of these two
   for the other options. */
.app-hero-visual--pos-left {
  align-items: flex-start;
}

.app-hero-visual--pos-center {
  align-items: center;
}

.app-hero-visual--zoomable {
  cursor: zoom-in;
}

.app-hero-blob {
  display: none;
}

/* js/main.js wraps the dark/light <img> pair in this on load. Fixed height
   (was shrink-wrapped to whichever image happened to be visible) with
   align-items/justify-content: center so the image sits vertically (and
   horizontally) centered inside its own constant-size "stage" regardless
   of its own natural dimensions — a short, wide screenshot and a tall,
   narrow one both just center in the same box instead of one leaving a
   lopsided gap. This is what actually fixes the "way off" alignment
   complaint: previously the frame shrink-wrapped to the image (so its own
   size varied) and the dots row lived inside it, so the two were bound
   together as one variable-height unit no matter how the outer box was
   positioned. Now frame is a fixed, independent box, and .app-gallery-
   progress (a separate sibling, not nested in here — see its own rule
   below) sits at a genuinely constant offset below it. */
.app-hero-visual-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 100%;
}

.app-hero-visual-frame {
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  justify-content: center;
  max-width: 100%;
  height: 560px;
}

.app-hero-visual img {
  /* Matches .app-hero-visual-frame's fixed height directly above — the
     tallest a screenshot can render is exactly the frame's own height, so
     it always has room to be fully visible, vertically centered by the
     frame, with no cropping. */
  position: relative;
  z-index: 1;
  max-width: 100%;
  max-height: 560px;
  width: auto;
  height: auto;
  object-fit: contain;
  filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.16));
  transition: opacity 0.4s var(--ease-premium), transform 0.4s var(--ease-premium);
}

/* Multi-screenshot gallery — only activated by main.js when an app has more
   than one screenshot configured in admin/app-screenshots.php. With exactly
   one screenshot (the default), this is all dormant and the page behaves
   exactly like a static image, same as before. Cursor is inherited from
   .app-hero-visual--zoomable (applied to both single- and multi-shot apps)
   so every screenshot shows the same zoom-in affordance. */
.app-hero-visual--gallery:focus-visible {
  outline: 2px solid var(--accent-purple);
  outline-offset: 6px;
  border-radius: var(--radius);
}

.app-gallery-transition-fade.app-gallery--swapping img {
  opacity: 0;
}

.app-gallery-transition-zoom.app-gallery--swapping img {
  opacity: 0;
  transform: scale(0.95);
}

.app-gallery-transition-slide.app-gallery--swapping img {
  opacity: 0;
  transform: translateX(-20px);
}

.app-gallery-transition-flip.app-gallery--swapping img {
  opacity: 0;
  transform: perspective(1000px) rotateY(30deg) scale(0.95);
}

.app-gallery-transition-bounce img {
  transition: opacity 0.4s var(--ease-premium), transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.app-gallery-transition-bounce.app-gallery--swapping img {
  opacity: 0;
  transform: scale(0.9) translateY(20px);
}

/* js/main.js appends this into .app-hero-visual directly (NOT into
   .app-hero-visual-frame) — a separate, independent container, a sibling
   of frame rather than nested inside it. Since frame above now has a
   fixed height of its own (image vertically centered inside it, not
   shrink-wrapped), this row's position — right after frame, margin-top
   below — stays genuinely constant across every slide, without needing
   any of the absolute-positioning or shared-block-centering workarounds
   tried earlier. */
.app-gallery-progress {
  /* Outer row: prev arrow, dots, next arrow (main.js appends all three as
     children — see buildGalleryRow). .app-gallery-dots below holds just
     the dots. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 22px;
  /* .app-hero-visual's align-items (flex-end/center/flex-start, driven by
     the admin "Screenshot position" setting) governs frame's horizontal
     position. */
  align-self: center;
}

.app-gallery-dots {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Dot-and-line style: inactive slides are small round dots, the active
   slide's indicator stretches into a short line/pill — same idea as iOS
   page controls. */
.app-gallery-progress-bar {
  display: block;
  width: 6px;
  height: 6px;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--glass-border);
  cursor: pointer;
  transition: width 0.25s var(--ease-premium, ease), background 0.25s ease;
}

.app-gallery-progress-bar--active {
  width: 22px;
  background: var(--accent-gradient);
}

/* Prev/next arrows flanking the dots — same circular icon-button treatment
   as .screenshot-lightbox-close, just smaller. Shared class names, so this
   automatically styles both the inline row (.app-gallery-progress) and the
   matching row inside the zoom lightbox (.screenshot-lightbox-progress). */
.app-gallery-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  padding: 0;
  border-radius: 50%;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
}

.app-gallery-arrow:hover {
  background: var(--glass-bg-hover);
  color: var(--text-primary);
  transform: translateY(-1px);
}

.app-gallery-arrow svg {
  width: 15px;
  height: 15px;
}

@media (prefers-reduced-motion: reduce) {
  .app-gallery-arrow:hover {
    transform: none;
  }
}


/* Zoom overlay for app screenshots — one shared instance appended to
   <body> by main.js and reused across dark/light theme and (for
   multi-screenshot apps) whichever gallery slide is currently showing. */
.screenshot-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 40px;
  background: rgba(5, 7, 13, 0.75);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  visibility: hidden;
  cursor: zoom-out;
  transition: opacity 0.3s var(--ease-premium), visibility 0.3s;
}

.screenshot-lightbox.is-open {
  opacity: 1;
  visibility: visible;
}

.screenshot-lightbox-image {
  max-width: min(92vw, 1400px);
  max-height: 82vh;
  width: auto;
  height: auto;
  border-radius: var(--radius-sm);
  cursor: default;
  transform: scale(0.96);
  transition: transform 0.3s var(--ease-premium), opacity 0.3s ease;
}

.screenshot-lightbox.is-open .screenshot-lightbox-image {
  transform: scale(1);
}

/* Fades out/in as main.js swaps the zoomed image between slides — same
   crossfade the normal (non-zoomed) view already gets via
   .app-gallery--swapping, just scoped to the lightbox's own image. */
.screenshot-lightbox-image--swapping {
  opacity: 0;
}

/* Slide indicator inside the zoomed view — same dot-and-line-plus-arrows
   row as .app-gallery-progress (main.js gives these the same
   .app-gallery-progress-bar / .app-gallery-dots / .app-gallery-arrow
   classes), just in its own row below the zoomed image instead of below
   the inline screenshot. */
.screenshot-lightbox-progress {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  cursor: default;
}

/* Single-screenshot apps never populate this — collapse it so it doesn't
   leave a stray gap under the zoomed image (the lightbox is a gap:18px
   flex column, so an empty-but-present div would still eat that gap). */
.screenshot-lightbox-progress:empty {
  display: none;
}

.screenshot-lightbox-close {
  position: absolute;
  top: 24px;
  right: 24px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  color: var(--text-primary);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.2s ease;
}

.screenshot-lightbox-close:hover {
  background: var(--glass-bg-hover);
}

@media (prefers-reduced-motion: reduce) {
  .screenshot-lightbox,
  .screenshot-lightbox-image {
    transition: none;
  }
}

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

  .app-hero-icon {
    margin-left: auto;
    margin-right: auto;
  }

  .app-hero-tagline {
    margin-left: auto;
    margin-right: auto;
  }

  .app-hero-actions,
  .app-hero-badges {
    justify-content: center;
  }

  .app-hero-visual,
  .app-hero-visual--pos-left,
  .app-hero-visual--pos-right {
    /* Admin's "Screenshot position" (left/right/center) only makes sense
       against the wide, asymmetric desktop grid — .app-hero-grid collapses
       to a single centered column above, but this box kept whatever
       align-items the desktop position setting gave it (flex-end by
       default), so the screenshot sat pinned to one edge instead of
       centered under the now-centered icon/title/tagline. Force center
       here regardless of which position class is applied. */
    align-items: center;
  }

  .app-hero-visual-frame {
    /* Fixed height (was min-height: 260px on .app-hero-visual, back when
       frame shrink-wrapped to the image) — same idea as the desktop rule
       above, just at mobile/tablet's smaller sizes: the image centers
       inside this constant-size box instead of the box changing size with
       the image. */
    height: 400px;
  }

  .app-hero-visual img {
    max-width: 100%;
    /* Matches .app-hero-visual-frame's height directly above. */
    max-height: 400px;
  }
}

/* Section header used by Details / Features / Requirements sections */

.app-section-head {
  text-align: center;
  max-width: 620px;
  margin: 0 auto 40px;
}

.app-section-eyebrow {
  /* Now an <h2> (was a <span>) — the actual heading tag for this block, so
     it takes .app-section-title's old font-size (was 0.85rem). Reversed
     with .app-section-title below at the user's request. */
  display: inline-block;
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 12px;
}

.app-section-title {
  /* Takes .app-section-eyebrow's old, smaller font-size (was clamp(1.5rem,
     3vw, 2.1rem)) — see note above. */
  font-size: 0.85rem;
  margin-bottom: 12px;
}

.app-section-sub {
  color: var(--text-secondary);
}

/* Details section: About text + highlight stat cards */

.app-details-grid {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 48px;
  align-items: start;
}

.app-details-text h2 {
  font-size: clamp(1.5rem, 3vw, 2rem);
  margin-bottom: 16px;
}

.app-details-text p {
  color: var(--text-secondary);
  margin-bottom: 14px;
}

.app-details-text code {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 0.9em;
}

.app-details-text pre {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  padding: 18px 20px;
  margin: 20px 0;
  overflow-x: auto;
  white-space: pre-wrap;
  word-wrap: break-word;
  font-size: 0.88em;
  line-height: 1.65;
}

.app-details-text pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  white-space: inherit;
}

.app-highlights {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}

.app-highlight-card {
  padding: 20px;
  border-radius: var(--radius-sm);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}

.app-highlight-num {
  font-size: 1.2rem;
  font-weight: 800;
  margin-bottom: 6px;
}

.app-highlight-label {
  font-size: 0.8rem;
  color: var(--text-secondary);
}

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

  .app-reqs-grid {
    /* No justify-content at all in the base rule — it's a wrapping flex
       row that defaults to flex-start, so the pills sit left-aligned under
       .app-section-head's centered "System requirements" title. Barely
       noticeable on wide desktop rows; on mobile/tablet, where fewer pills
       fit per line, the left-hugging block reads visibly off-center. */
    justify-content: center;
  }
}

/* Features grid */

.app-features-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

.feature-card {
  padding: 24px;
  border-radius: var(--radius);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  transition: transform 0.3s var(--ease-premium), background 0.25s ease;
}

.feature-card:hover {
  transform: translateY(-3px);
  background: var(--glass-bg-hover);
}

.feature-icon {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}

.feature-icon svg {
  width: 20px;
  height: 20px;
}

.feature-title {
  font-weight: 700;
  margin-bottom: 8px;
}

.feature-desc {
  font-size: 0.88rem;
  color: var(--text-secondary);
}

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

@media (max-width: 560px) {
  .app-features-grid {
    grid-template-columns: 1fr;
  }
}

/* Requirements spec-sheet grid */

.app-reqs-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.req-card {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}

.req-lbl {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-secondary);
}

.req-lbl::after {
  content: ':';
}

.req-val {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--text-primary);
}

.req-val.free {
  color: #34d399;
}

/* CTA section */

.app-cta {
  text-align: center;
  padding: 64px 24px;
  border-radius: var(--radius);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}

.app-cta-eyebrow {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 14px;
}

.app-cta h2 {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  margin-bottom: 16px;
}

.app-cta p {
  color: var(--text-secondary);
  max-width: 480px;
  margin: 0 auto 28px;
}

.app-cta-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px;
}

.app-cta-actions .btn {
  /* Same fix as .app-hero-actions .btn above — this is the bottom-of-page
     "Download now" CTA row on app pages. */
  border-radius: var(--btn-radius);
}

.app-detail-body-v2 {
  /* Was padding: 64px 0 — this element also carries .container's class
     (padding: 0 24px), and since both rules set the same shorthand
     property with equal specificity, this one (later in the stylesheet)
     won outright and wiped out .container's horizontal 24px entirely.
     On wide desktop viewports that went unnoticed because the 1200px box
     already sits comfortably inset from the browser edge; on mobile/tablet,
     where the box fills the actual viewport width, it left Details/
     Highlights/Features running flush to the screen edge. padding-top/
     -bottom only, so .container's left/right padding survives. */
  padding-top: 64px;
  padding-bottom: 64px;
  display: flex;
  flex-direction: column;
  gap: 72px;
}

.hero--support {
  /* Support page only — the shared .hero's default bottom padding (90px,
     see .hero base rule) plus .support-section's own top padding below
     left a big gap between the tagline and the donation card. Shrunk on
     request to bring the donation box up closer to the tagline. */
  padding-bottom: 24px;
}

.support-section {
  /* Support page only — .apps-section's default top padding stacks with
     .hero--support's (already-reduced) bottom padding above and would
     still leave a bigger gap than intended. */
  padding-top: 0;
}

.support-section .container {
  display: flex;
  justify-content: center;
}

.support-card {
  width: 100%;
  max-width: 580px;
  margin: 0 auto;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 48px;
  text-align: center;
}

.support-card > p {
  color: var(--text-secondary);
  max-width: 460px;
  margin: 0 auto 28px;
}

.support-paypal-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 1.05rem;
  padding: 15px 34px;
}

.support-paypal-btn svg {
  width: 20px;
  height: 20px;
}

/* Needs to out-specificity ".support-card > p" above (0,1,1), which was
   silently winning over ".support-note" alone (0,1,0) and collapsing this
   margin-top down to that rule's 28px-bottom-only value — the button and
   this note were overlapping because of that specificity loss, not because
   40px was actually being applied and wasn't enough. */
.support-card > p.support-note {
  margin-top: 40px;
  font-size: 0.85rem;
  color: var(--text-secondary);
}

.support-ways {
  margin-top: 40px;
  padding-top: 32px;
  border-top: 1px solid var(--glass-border);
  text-align: left;
}

.support-ways h3 {
  font-size: 1rem;
  margin-bottom: 14px;
  color: var(--text-primary);
}

.support-ways ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.support-ways li {
  padding-left: 24px;
  position: relative;
  color: var(--text-secondary);
  font-size: 0.92rem;
}

.support-ways li::before {
  content: '\2713';
  position: absolute;
  left: 0;
  color: var(--accent-cyan);
  font-weight: 700;
}

.support-ways a {
  color: var(--accent-cyan);
  text-decoration: underline;
}

@media (max-width: 560px) {
  .support-card {
    padding: 32px 24px;
  }
}

/* --- Pre-download donation modal --------------------------------------- */

.donate-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(5, 7, 13, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease-premium), visibility 0.35s;
}

.donate-overlay.is-open {
  opacity: 1;
  visibility: visible;
}

.donate-modal {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--bg-primary);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--glass-shadow);
  padding: 40px 32px 32px;
  text-align: center;
  transform: translateY(16px) scale(0.97);
  transition: transform 0.35s var(--ease-premium);
}

.donate-overlay.is-open .donate-modal {
  transform: translateY(0) scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .donate-overlay,
  .donate-modal {
    transition: none;
  }
}

.donate-modal-close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: 50%;
  transition: background 0.2s ease, color 0.2s ease;
}

.donate-modal-close:hover {
  background: var(--glass-bg-hover);
  color: var(--text-primary);
}

.donate-modal-close svg {
  width: 18px;
  height: 18px;
}

.donate-modal-icon {
  width: 52px;
  height: 52px;
  margin: 0 auto 18px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-gradient);
  color: #ffffff;
}

.donate-modal-icon svg {
  width: 24px;
  height: 24px;
}

.donate-modal h2 {
  font-size: 1.3rem;
  margin-bottom: 10px;
}

.donate-modal p {
  color: var(--text-secondary);
  font-size: 0.92rem;
  max-width: 340px;
  margin: 0 auto 24px;
}

.donate-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.donate-modal-actions .btn {
  width: 100%;
  justify-content: center;
}

.btn-skip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: transparent;
  color: var(--text-secondary);
  border: 1px solid var(--glass-border);
}

.btn-skip:hover {
  background: var(--glass-bg-hover);
  color: var(--text-primary);
}

.btn-skip:disabled {
  cursor: not-allowed;
  opacity: 0.7;
}

.btn-skip:disabled:hover {
  background: transparent;
  color: var(--text-secondary);
}

.skip-timer-ring {
  flex-shrink: 0;
  transform: rotate(-90deg);
}

.skip-timer-ring-bg {
  stroke: var(--glass-border);
}

.skip-timer-ring-fill {
  stroke: currentColor;
  stroke-linecap: round;
  transition: stroke-dashoffset 0.1s linear, opacity 0.2s ease;
}

.donate-modal p.donate-modal-hint {
  font-size: 0.78rem;
  color: var(--text-secondary);
  opacity: 0.75;
  max-width: 320px;
  margin: 14px auto 0;
}

/* Stats Banner — restyled from a flat borders-top/bottom strip to a row of
   individual stat cards (same --glass-bg/--glass-border/--radius language
   as .app-card), each with a small accent-colored top rule and a quiet
   hover lift, so it reads as a set of distinct figures rather than one
   undifferentiated band of text. */
.stats-banner {
  padding: 56px 0 64px;
  position: relative;
  z-index: 10;
}

/* Section divider (hero → stats banner) — same fading-gradient line already
   used between apps/featured/extra sections and above the footer, reused
   here for visual consistency rather than a plain solid border. */
.stats-banner::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(47, 111, 237, 0.6), transparent);
}
.stats-banner-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}
.stats-banner-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 32px 20px 28px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: transform 0.25s var(--ease-premium), border-color 0.25s ease, background 0.25s ease;
}
.stats-banner-item::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 32px;
  height: 3px;
  border-radius: 0 0 3px 3px;
  background: var(--accent-purple);
  opacity: 0.7;
}
@media (hover: hover) and (pointer: fine) {
  .stats-banner-item:hover {
    transform: translateY(-4px);
    border-color: var(--accent-purple);
    background: var(--glass-bg-hover);
  }
}
.stats-banner-value {
  font-size: 3rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-bottom: 10px;
  line-height: 1;
  /* Ensure the gradient below applies properly here */
  display: inline-block;
}

/* Admin Homepage > "Stats banner color shift" adds this class to each value
   (see main.js), which also sets --stats-gradient on the .stats-banner
   container from the admin's 4 configurable colors — independent of
   .accent-text/the hero tagline's own hardcoded rainbow gradient, so the
   two can be recolored separately. Falls back to the tagline's original
   colors if --stats-gradient is ever missing (e.g. a settings file saved
   before this option existed). background-size is tighter than
   .accent-text's default (300%, tuned for a full sentence) since a short
   1-4 character stat would otherwise barely show any color variation
   before the text runs out. */
.stats-banner-value--gradient {
  color: var(--accent-purple);
  background-image: var(--stats-gradient, linear-gradient(90deg, #38bdf8, #a78bfa, #f472b6, #fb923c, #38bdf8));
  background-size: 150% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: accent-shimmer 8s ease-in-out infinite;
}
.stats-banner-label {
  font-size: 0.85rem;
  color: var(--text-secondary);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* Tablet and below: keep all 4 stats on one row (was wrapping to 2x2 under
   600px), just smaller — a single compact row reads better than a taller
   2-row block on a narrow screen, and there are only ever 4 stats so a row
   never gets crowded. Tiered by width since 4 items need to shrink further
   on a phone than on a tablet to keep fitting. */
@media (max-width: 860px) {
  .stats-banner {
    padding: 32px 0 40px;
  }
  .stats-banner-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
  }
  .stats-banner-item {
    padding: 14px 6px 12px;
    border-radius: 12px;
  }
  .stats-banner-item::before {
    width: 18px;
    height: 2px;
  }
  .stats-banner-value {
    font-size: 1.8rem;
    margin-bottom: 4px;
  }
  .stats-banner-label {
    font-size: 0.62rem;
    letter-spacing: 0.06em;
  }
}

@media (max-width: 480px) {
  .stats-banner {
    padding: 24px 0 28px;
  }
  .stats-banner-grid {
    gap: 4px;
  }
  .stats-banner-item {
    padding: 10px 4px 9px;
  }
  .stats-banner-value {
    font-size: 1.2rem;
  }
  .stats-banner-label {
    font-size: 0.5rem;
    letter-spacing: 0.02em;
  }
}

/* Very large displays (4K/5K) — hide the stats banner entirely per request;
   it was still rendering on these but 1080p/1440p and below are unaffected.
   2561px is chosen because standard 1440p (2560 logical px) monitors
   already report widths up to 2560 — this only fires above that, i.e. on
   actual 4K/5K screens running with little/no display scaling. */
@media (min-width: 2561px) {
  .stats-banner {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   2b. Custom homepage templates — hero-bottom Stats Banner + hidden
       Value Props (shared across every custom template; see
       templates/[name]/script.js for the relocation/toggle logic)
   -------------------------------------------------------------------------- */

/* Every custom homepage template's <body> carries data-homepage-layout="X"
   (baked into that template's own markup when it's registered/activated —
   see admin/inc/homepage.php); the original single-column "default"
   homepage carries no such attribute at all, so everything in this block
   only ever touches a custom template, never the default homepage. */

/* Value props hidden by default (Admin > Homepage > Template Settings >
   "Show value props section", config key showValueProps) — every custom
   template restates its "why MacMeUp" pitch inside the hero anyway (or, now
   that the real Stats Banner lives there too, its actual numbers), so the
   section is redundant on first load. One shared rule works across all 20
   templates because every one of them uses the same base .value-props class
   on this section (several also add a template-specific second class, e.g.
   .orbit-value-props, purely for their own extra cosmetic styling — this
   selector doesn't need to know about those). Templates each set
   data-show-value-props on <body> via config.showValueProps in their own
   script.js. */
.value-props {
  display: none;
}

body[data-show-value-props="true"] .value-props {
  display: block;
}

/* Stats Banner, relocated onto the hero's bottom edge (see the small
   "relocate onto the hero" snippet appended to each template's own
   script.js) — restyled from the full-width, card-per-stat section this
   file's earlier .stats-banner rules define into a single non-wrapping
   row, absolutely positioned near the bottom of whichever hero it's been
   moved into, above that hero's own scroll-cue chevron where one exists
   (those sit around bottom:22-36px across the templates that have one —
   64px comfortably clears all of them). Scoped to .hero-screen, the base
   class shared by 18 of the 20 custom templates' hero sections; the two
   that don't use it (magazine-grid's .mag-cover, app-store-showcase's
   .store-today) get an equivalent rule in their own style.css instead.
   Any template needing different colors/spacing than this default (Orbit
   Launcher already does, for its accent-tinted dark hero) can override it
   in its own style.css, which always loads after this shared stylesheet. */
body[data-homepage-layout] .hero-screen > .stats-banner {
  position: absolute;
  left: 50%;
  bottom: 64px;
  transform: translateX(-50%);
  z-index: 1;
  width: max-content;
  max-width: calc(100% - 48px);
  padding: 0;
}

body[data-homepage-layout] .hero-screen > .stats-banner::before {
  display: none;
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-grid {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: 0;
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item {
  align-items: center;
  text-align: center;
  padding: 0 28px;
  background: none;
  border: none;
  border-radius: 0;
  transition: none;
  flex: 0 0 auto;
}

/* Thin vertical rule between stats instead of the card borders they'd
   otherwise have — reads as one continuous strip. */
body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item:not(:first-child) {
  border-left: 1px solid rgba(255, 255, 255, 0.15);
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item::before {
  display: none;
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item:hover {
  transform: none;
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-value {
  font-size: 1.7rem;
  margin-bottom: 2px;
  color: #fff;
  white-space: nowrap;
}

body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-label {
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.68rem;
  white-space: nowrap;
}

@media (max-width: 980px) {
  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item {
    padding: 0 18px;
  }

  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-value {
    font-size: 1.4rem;
  }

  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-label {
    font-size: 0.6rem;
  }
}

@media (max-width: 640px) {
  /* Never wraps to a second line at any width (that's the whole point of
     this layout) — sized down further so a 3-4 stat row still fits on one
     line at phone widths. */
  body[data-homepage-layout] .hero-screen > .stats-banner {
    bottom: 48px;
  }

  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-item {
    padding: 0 10px;
  }

  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-value {
    font-size: 1.1rem;
  }

  body[data-homepage-layout] .hero-screen > .stats-banner .stats-banner-label {
    font-size: 0.52rem;
    letter-spacing: 0.06em;
  }
}

/* --------------------------------------------------------------------------
   3. Apps Section
   -------------------------------------------------------------------------- */

/* --- Back to top button -------------------------------------------------- */

.back-to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 500;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: var(--glass-shadow-sm);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity 0.3s ease, transform 0.3s var(--ease-premium), background 0.2s ease, visibility 0.3s;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.back-to-top:hover {
  background: var(--glass-bg-hover);
}

.back-to-top svg {
  width: 20px;
  height: 20px;
}

@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: opacity 0.2s ease, visibility 0.2s;
    transform: none;
  }
}

@media (max-width: 640px) {
  .back-to-top {
    right: 16px;
    bottom: 16px;
    width: 40px;
    height: 40px;
  }
}

/* --- Blog: post hero, content, listing grid ----------------------------- */

.post-hero {
  padding: 100px 0 24px;
}

.post-type-badge {
  position: relative;
  z-index: 2;
  text-decoration: none;
  display: inline-block;
  align-self: flex-start;
  padding: var(--post-type-badge-padding, 5px 14px);
  border-radius: 999px;
  font-size: var(--post-type-badge-font-size, 0.66rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 16px;
  background: var(--post-type-badge-bg, var(--glass-bg));
  border: var(--post-type-badge-border, 1px solid var(--glass-border));
  color: var(--post-type-badge-color, inherit);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Was three per-type color overrides here (guide=cyan, news=magenta,
   article=purple) — leftover from before the admin pill-style system
   (--post-type-badge-color, solid/outline in admin/inc/display.php)
   existed. Same specificity as .post-type-badge and later in the file, so
   they always won and fought the admin's actual chosen style: in "solid"
   mode --post-type-badge-color is white for a reason (contrast against
   the solid fill), but these three overrode it right back to a colored
   text — and --accent-purple is literally the same value as the solid
   background color, so every "article"-type pill (used for every category
   pill on the homepage's Featured Articles cards, not just literal
   "Article" posts — see renderFeaturedCard in main.js, which reuses
   .post-type-badge--<type> per category) rendered with invisible,
   blend-into-the-background text. Removing these makes every badge follow
   the one admin-configured style consistently, matching how the category
   listing pages' single type badge already looked. */

.post-title {
  font-size: clamp(1.5rem, 3.2vw, 2.2rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-bottom: 12px;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.post-meta-sep {
  opacity: 0.5;
}

.post-meta-cat {
  font-family: var(--font-sans);
  font-weight: 700;
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  font-size: 0.75rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  transition: color 0.2s ease, border-color 0.2s ease;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.post-meta-cat:hover {
  color: var(--text-primary);
  border-color: var(--accent-purple);
}

@media (prefers-reduced-motion: reduce) {
  .post-meta-cat {
    transition: none;
  }
}

.post-featured-image {
  max-width: var(--container-width);
  margin: 0 auto 32px;
}

.post-featured-image img {
  width: 100%;
  height: clamp(320px, 42vw, 620px);
  object-fit: cover;
  border-radius: var(--radius);
  display: block;
}

.post-content {
  /* Matches --container-width, same as the app pages' .container — was
     760px, which made post pages feel noticeably narrower than the app
     pages sitting right next to them in the Previous/Next nav. */
  max-width: var(--container-width);
  margin: 0 auto;
  padding-bottom: 48px;
  color: var(--text-secondary);
  font-size: 1.05rem;
  line-height: 1.75;
}

.post-content h2, .post-content h3, .post-content h4, .post-content h5, .post-content h6 {
  color: var(--text-primary);
  margin: 40px 0 16px;
  line-height: 1.3;
}

.post-content h2 { font-size: 1.5rem; }
.post-content h3 { font-size: 1.25rem; }
.post-content h4, .post-content h5, .post-content h6 { font-size: 1.05rem; }

.post-content p {
  margin-bottom: 20px;
}

.post-content a {
  color: var(--accent-cyan);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.post-content ul, .post-content ol {
  margin: 0 0 20px 1.3em;
  display: block;
}

.post-content ul { list-style: disc; }
.post-content ol { list-style: decimal; }

.post-content li {
  margin-bottom: 8px;
}

.post-content img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  margin: 24px 0;
}

.post-content blockquote {
  border-left: 3px solid var(--accent-cyan);
  padding: 4px 0 4px 20px;
  margin: 24px 0;
  color: var(--text-secondary);
  font-style: italic;
}

.post-content code {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 0.9em;
}

/* TinyMCE's codesample/code-block toolbar button outputs a <pre><code>
   block. With no rule of its own it fell through to the bare browser
   default for <pre> — white-space: pre with no wrapping — so any line
   longer than the content column just ran off the edge instead of
   flowing like a normal paragraph, which read as text chopped "line by
   line" rather than one readable block. pre-wrap keeps intentional line
   breaks/blank lines from the source while still wrapping long lines. */
.post-content pre {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  padding: 18px 20px;
  margin: 24px 0;
  overflow-x: auto;
  white-space: pre-wrap;
  word-wrap: break-word;
  font-size: 0.88em;
  line-height: 1.65;
}

/* Undo the inline-code chip styling (background/border/padding) when a
   <code> is nested inside <pre> — that styling is for short inline
   snippets, and doubling it up here just re-boxes every line. */
.post-content pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  white-space: inherit;
}

.post-content strong {
  color: var(--text-primary);
}

.post-content table {
  width: max-content;
  max-width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  margin: 32px 0;
  font-size: 0.95rem;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--glass-shadow-sm);
  display: block;
  overflow-x: auto;
}

.post-content th,
.post-content td {
  padding: 14px 20px;
  text-align: left;
  border-bottom: 1px solid var(--glass-border);
  color: var(--text-secondary);
}

.post-content th {
  background: rgba(255, 255, 255, 0.05);
  font-weight: 600;
  color: var(--text-primary);
}

[data-theme="light"] .post-content th {
  background: rgba(0, 0, 0, 0.03);
}

.post-content tr:last-child td {
  border-bottom: none;
}

.post-content tr:nth-child(even) td {
  background: rgba(255, 255, 255, 0.02);
}

[data-theme="light"] .post-content tr:nth-child(even) td {
  background: rgba(0, 0, 0, 0.02);
}

/* Deliberately a bare wrapper (no .container) — .post-adjacent-nav sets its
   own max-width + horizontal padding below so it lines up exactly with
   .post-content's box above it. Nesting it inside .container as well used
   to double up the padding and make this row ~48px narrower than the
   content above it. */
.post-adjacent-section {
  width: 100%;
}

.post-adjacent-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  max-width: var(--container-width);
  margin: 32px auto 0;
  padding: 20px 0;
  border-top: 1px solid var(--glass-border);
  position: relative;
}

/* Zero gap to the footer, scoped to post pages only — .site-footer's 80px
   top margin is a shared site-wide rule (breathing room after the Apps
   grid, About page content, etc.) and shouldn't change everywhere just
   because post pages want Previous/Next to sit flush against the footer. */
.post-page-main + .site-footer {
  margin-top: 0;
}

.post-adjacent-nav::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--glass-border);
}

.post-adjacent-link {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: 1 1 0;
  min-width: 0;
  transition: opacity 0.2s ease;
}

.post-adjacent-link:hover {
  opacity: 0.72;
}

.post-adjacent-link--next {
  /* blog-post-view.php's markup already puts .post-adjacent-copy before
     .post-adjacent-thumb for the "next" link (the mirror image of "prev",
     which is thumb-then-copy) — plain row order alone already renders
     text-then-icon. row-reverse here was flipping that a second time,
     putting the icon back on the LEFT (wrong — it belongs on the right,
     matching "prev"'s icon hugging the opposite/left edge). */
  justify-content: flex-end;
  text-align: right;
}

.post-adjacent-link--empty {
  flex: 0 1 auto;
  justify-content: center;
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.9rem;
}

.post-adjacent-thumb {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-secondary);
}

.post-adjacent-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.post-adjacent-thumb--placeholder {
  font-weight: 800;
  font-size: 1.1rem;
  color: var(--text-secondary);
  background: var(--glass-bg);
}

.post-adjacent-copy {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.post-adjacent-label {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
}

.post-adjacent-title {
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.92rem;
  line-height: 1.3;
}

@media (max-width: 640px) {
  .post-adjacent-nav {
    /* Row, not column — Previous and Next sit side by side as two compact
       cards, matching desktop's one-row layout instead of stacking. Was
       padding: 20px 0 (zero horizontal), so the row sat flush against the
       true viewport edge while .post-content above it (wrapped in
       .container, which carries its own 0 24px) reads with a visible
       inset; matching that same 24px here is what the section comment
       above (line ~3077) already claims this rule does. */
    flex-direction: row;
    align-items: stretch;
    gap: 10px;
    padding: 20px 24px;
  }

  .post-adjacent-nav::before {
    display: none;
  }

  .post-adjacent-link {
    /* Each link is its own compact card (was full-width, stacked) — flex:1
       from the base rule already splits the row evenly, so just tighten the
       padding/gap enough for thumb + short label to fit two-up on a phone
       screen without the label needing to truncate. */
    flex: 1 1 0;
    min-width: 0;
    box-sizing: border-box;
    padding: 14px 12px;
    border-radius: var(--radius-sm);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    gap: 8px;
  }

  /* .post-adjacent-link--next: base rule now sets justify-content:flex-end
     (see above) which still applies here — icon stays on the right, label
     on the left, matching Previous's icon-left/label-right on the opposite
     side within its own card, so the pair still reads as a mirrored set. */

  .post-adjacent-link--empty {
    justify-content: center;
    padding: 14px 12px;
  }

  .post-adjacent-thumb {
    width: 36px;
    height: 36px;
  }

  .post-adjacent-copy {
    /* Title hidden below — just the thumb + label remain, so center them
       on the thumb's height instead of top-aligning a now-single line. */
    justify-content: center;
    gap: 0;
    min-width: 0;
  }

  .post-adjacent-label {
    font-size: 0.62rem;
    line-height: 1.2;
  }

  .post-adjacent-title {
    /* Post titles at this width were wrapping to 2-3 lines and throwing
       off the row's height/alignment against the thumb — dropped in favor
       of just the label ("Previous post" / "Next post"), matching the
       "All <type>" and "Back to MacMeUp" empty-state links which were
       already label-only. */
    display: none;
  }
}

/* Related posts — post pages only, between the article and the
   Previous/Next nav. Cards themselves reuse the shared .post-card styles. */
.related-posts {
  margin: 16px 0 56px;
}

.related-posts-title {
  font-size: 1.35rem;
  margin-bottom: 20px;
}

.post-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

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

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

@media (max-width: 640px) {
  .post-grid,
  .post-grid--cols-2,
  .post-grid--cols-3,
  .post-grid--cols-4 { grid-template-columns: 1fr; }
}

.post-card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: transform 0.3s var(--ease-premium), box-shadow 0.3s var(--ease-premium);
}

.post-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--glass-shadow-sm);
}

.post-card-link-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* Real screenshot/photo cover — used when a post has one. Fills the card
   edge-to-edge (no letterbox gaps); tall/portrait source images crop at
   the sides, which reads better in a grid than empty margins. */
.post-card-image {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-secondary);
}

.post-card-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}

/* Generated card cover — fallback for posts with no image: gradient + type
   icon + title overlay, instead of leaving the card headless. */
.post-card-cover {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding: 16px;
  text-align: center;
  gap: 10px;
}

.post-card-cover::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent 60%);
}

.post-card-cover-icon {
  position: relative;
  z-index: 1;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: #ffffff;
  backdrop-filter: blur(4px);
  flex-shrink: 0;
}

.post-card-cover-icon svg {
  width: 22px;
  height: 22px;
}

.post-card-cover-title {
  position: relative;
  z-index: 1;
  color: #ffffff;
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.3;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.post-card-cover--1 { background: linear-gradient(135deg, #8f7cff, #5a3fd6); }
.post-card-cover--2 { background: linear-gradient(135deg, #6b4fd6, #2a2438); }
.post-card-cover--3 { background: linear-gradient(135deg, #a394ff, #4b3aa8); }
.post-card-cover--4 { background: linear-gradient(135deg, #3a3450, #7c5cff); }
.post-card-cover--5 { background: linear-gradient(135deg, #5a3fd6, #1a1626); }
.post-card-cover--6 { background: linear-gradient(135deg, #7c5cff, #2f2648); }

.post-card-body {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

.post-card-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.post-card-body h3 {
  font-size: 1.05rem;
  color: var(--text-primary);
}

.post-card-body p {
  color: var(--text-secondary);
  font-size: 0.9rem;
  flex: 1;
}

.post-card-body time {
  color: var(--text-secondary);
  font-size: 0.78rem;
}

@media (prefers-reduced-motion: reduce) {
  .post-card {
    transition: none;
  }
}

/* ============================================================================
   2026-07-18 visual polish pass — appended as one block so the cascade wins
   over the base rules above without editing them in place. Each sub-block
   notes which review item it implements.
   ========================================================================== */

/* --- Surface hierarchy (item 3 + light-theme audit, item 14) ---------------
   Cards get a real elevated surface (token --card-bg) and a soft two-layer
   shadow instead of leaning entirely on 1px glass borders for depth. */
.post-card,
.feature-card,
.app-highlight-card,
.support-card {
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
}

/* --- Card hover substance (item 5) -----------------------------------------
   Hover reads as lift: deeper shadow + border picking up a hint of accent.
   Composes with (doesn't replace) the admin-configured transform effects. */
.post-card,
.feature-card,
.app-card {
  transition-property: transform, background, border-color, box-shadow;
  transition-duration: 0.3s;
  transition-timing-function: var(--ease-premium);
}

.post-card:hover,
.feature-card:hover,
.app-card:hover {
  border-color: rgba(47, 111, 237, 0.35);
  box-shadow: var(--card-shadow-hover);
}

/* --- Screenshot glow (item 2) ----------------------------------------------
   Soft accent radial behind the hero/app screenshots — the "product on a
   lit stage" treatment. Pure gradient (no filter: blur) so it costs nothing
   to composite; sits behind the image via z-index layering that both
   containers already have. */
.hero-visual,
.app-hero-visual-frame {
  position: relative;
}

.hero-visual::before,
.app-hero-visual-frame::before {
  content: '';
  position: absolute;
  inset: -8% -12%;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 60% 55% at 50% 50%, var(--shot-glow), transparent 70%);
}

.hero-screenshot {
  z-index: 1;
}

/* --- Section rhythm (item 4) -----------------------------------------------
   One shared vertical cadence instead of per-section magic numbers. */
.site-footer {
}

.apps-section {
  padding: 56px 0 var(--space-section);
}

/* Category/hub listing pages (every /category/{slug}/ page — guides, news,
   applications, hackintosh, ios, macos, opencore-legacy-patcher,
   pre-release, release — plus the /posts/ hub) share one simple "title +
   one-line subtitle + card grid" layout via blog-listing-view.php /
   blog-hub-view.php, both of which set body.page-listing. The generic
   .hero/.apps-section padding above is tuned for feature-rich page heroes
   (screenshots, CTAs, badges) and left far too much empty space around a
   plain heading here. Scoped to body.page-listing so every other page that
   still uses .hero/.apps-section (homepage, about, contact, app pages) is
   untouched. Both selectors below match .hero:not(.hero--home)'s and
   .apps-section's own specificity (two classes), so being later in the
   cascade is what lets them win — moving this block earlier than either of
   those would silently stop working. */
body.page-listing .hero {
  padding: 64px 0 40px;
}

body.page-listing .apps-section {
  padding: 8px 0 clamp(48px, 6vw, 72px);
}

@media (max-width: 720px) {
  body.page-listing .hero {
    padding: 40px 0 28px;
  }

  body.page-listing .apps-section {
    padding: 0 0 40px;
  }
}

/* --- Type discipline (item 6) ----------------------------------------------
   Large headings get the tight tracking that makes Inter read like a
   display face; h3-level stays subtler. */
h1,
h2 {
  letter-spacing: -0.02em;
}

h3 {
  letter-spacing: -0.01em;
}

/* --- Button states (item 7) ------------------------------------------------ */
.btn:focus-visible {
  outline: 2px solid var(--accent-purple);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .btn {
    transition: transform 0.1s ease, background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, color 0.25s ease;
  }

  .btn:active {
    transform: scale(0.98);
  }
}

/* --- Applications mega-menu (item 8) ---------------------------------------
   Desktop only: two-column grid with a one-line descriptor under each app
   name (main.js supplies the sub-line from apps config when available).
   Mobile keeps the stacked single-column rows it already has. */
@media (min-width: 721px) {
  .nav-dropdown-menu--grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
    width: 480px;
    max-width: min(480px, calc(100vw - 48px));
    white-space: normal;
  }

  .nav-dropdown-menu--grid a {
    align-items: flex-start;
  }

  .nav-dropdown-menu--grid .nav-dropdown-icon {
    margin-top: 2px;
  }
}

.nav-dropdown-label {
  display: block;
  line-height: 1.3;
}

.nav-dropdown-sub {
  display: block;
  font-size: 0.72rem;
  line-height: 1.35;
  color: var(--text-secondary);
  font-weight: 400;
  margin-top: 1px;
}

/* Hidden on mobile on request — the stacked single-column rows are already
   tight (16px vertical padding per row, see .nav-links a), and a second
   line of descriptive text under every app name made the open menu much
   taller/denser to scroll through than just the name alone. */
@media (max-width: 720px) {
  .nav-dropdown-sub {
    display: none;
  }
}

/* --- Post table of contents (item 9) ---------------------------------------
   Built by main.js from the post's h2 headings when there are 3 or more. */
.post-toc {
  background: var(--card-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--card-shadow);
  padding: 20px 24px;
  margin: 0 0 32px;
  font-size: 0.92rem;
}

.post-toc-title {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
  margin-bottom: 10px;
}

.post-toc ol {
  margin: 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.post-toc a {
  color: var(--text-secondary);
}

.post-toc a:hover {
  color: var(--accent-purple);
}

/* --- Social proof strip (item 10) ------------------------------------------
   Hidden until main.js confirms a real total from the download counter. */
.social-proof {
  text-align: center;
  padding: 0 0 8px;
}

.social-proof-inner {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.95rem;
  color: var(--text-secondary);
}

.social-proof-count {
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  background: var(--accent-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

/* --- Reveal stagger (item 13) ----------------------------------------------
   Cards in a grid arrive with a slight cascade instead of all at once.
   transition-delay is inert when reduced-motion has disabled the reveal
   transition itself, so no extra gating needed.
   Previous approach used nth-child(4n+2/3/4) transition-delay rules to
   stagger cards — but .post-grid/.app-grid/.app-features-grid all use
   `grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))`, a FLUID
   column count that changes continuously with viewport width (2, 3, 4, 5+
   columns depending on exact width). The nth-child(4n+…) math hardcoded a
   4-column assumption, so it only ever lined up with real visual rows at
   whatever width happened to produce exactly 4 columns — at any other
   column count (most real widths), cards from different visual rows shared
   delays out of order (e.g. a 3-column grid's row-2-first-card would fire
   its animation immediately/first while its own row-mates were still
   waiting), which is what actually made the reveal read as glitchy/
   unsmooth rather than a clean cascade. Removed entirely — the JS-side
   revealObserver/observeReveal (js/main.js) now handles the one-by-one
   stagger itself via a shared reveal queue, timed independently of grid
   column count, so it's correct at every viewport width. */

/* --- Structured footer (item 1) --------------------------------------------
   Columns row built by main.js from the same admin config that drives the
   nav (brand + Apps + Posts + Company); the existing one-line bar becomes
   the bottom strip beneath it. No border/hard divider between the two —
   see feedback_two_footers_fix memory: a border-bottom + its own margin
   here made this read as two separate stacked footers instead of one, since
   .site-footer::before already draws a divider line above the whole thing.
   Bottom strip gets the divider instead (below), so there's exactly one
   line bracketing exactly one footer. */
.footer-columns {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1fr;
  gap: 40px;
  /* Horizontal padding comes from the .container class this row also
     carries — only vertical here, or the sides double up. */
  padding: 40px 0 28px;
}

.footer-brand {
  max-width: 280px;
}

.footer-brand-logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 800;
  font-size: 1.1rem;
  color: var(--text-primary);
  margin-bottom: 12px;
}

.footer-brand-logo img {
  width: 34px;
  height: 34px;
  object-fit: contain;
}

.footer-brand-blurb {
  font-size: 0.84rem;
  line-height: 1.6;
  color: var(--text-secondary);
  margin-bottom: 16px;
}

.footer-col h4 {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
  margin-bottom: 14px;
}

.footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-col li {
  margin-bottom: 8px;
}

.footer-col a {
  font-size: 0.86rem;
  color: var(--text-secondary);
  transition: color 0.15s ease;
}

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

/* Social row relocates into the brand column when columns exist — main.js
   appends it there instead of the bottom bar. */
.footer-brand .footer-social {
  margin-top: 4px;
}

@media (max-width: 860px) {
  .footer-columns {
    grid-template-columns: 1fr 1fr;
    gap: 32px;
  }

  .footer-brand {
    grid-column: 1 / -1;
    max-width: none;
  }
}

@media (max-width: 480px) {
  .footer-columns {
    grid-template-columns: 1fr;
  }
}

/* Desktop dropdown safety net — on short viewports (small laptops, browser
   at half height) a tall menu like Posts' 11 rows could run past the bottom
   of the screen with its last items unreachable. Scrolls within the popover
   instead; inert on tall screens where everything already fits. */
@media (min-width: 721px) {
  .nav-dropdown-menu {
    max-height: calc(100vh - 110px);
    overflow-y: auto;
  }
}

/* --- FAQ Section --- */
/* Was var(--border-color)/var(--surface-bg)/var(--text-color)/
   var(--text-muted) — none of those four custom properties are declared
   anywhere in this project (confirmed by scanning every var() usage
   against every declaration site-wide); an undefined custom property
   makes the whole declaration invalid, so .faq-item was rendering with no
   background and no border at all, in both themes, on every app page's
   FAQ accordion (this block is shared across all 9 app pages). Swapped in
   the site's real tokens: --glass-border/--card-bg/--text-primary/
   --text-secondary, the same ones every other card on the site uses. */
.faq-section {
  padding: 4rem 0;
  border-top: 1px solid var(--glass-border);
}
.faq-list {
  max-width: 800px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.faq-item {
  background: var(--card-bg);
  border: 1px solid var(--glass-border);
  border-radius: 12px;
  overflow: hidden;
  text-align: left;
}
.faq-item summary {
  padding: 1.25rem 1.5rem;
  font-weight: 600;
  font-size: 1.1rem;
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--text-primary);
}
.faq-item summary::-webkit-details-marker {
  display: none;
}
.faq-item summary::after {
  content: '+';
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
  color: var(--text-secondary);
  transition: transform 0.2s ease;
}
.faq-item[open] summary::after {
  content: '−';
}
.faq-answer {
  padding: 0 1.5rem 1.25rem 1.5rem;
  color: var(--text-secondary);
  line-height: 1.6;
}

/* ============================================================================
   2026-07-30 homepage redesign — everything below the hero restyled for a
   more premium, editorial feel (Apple/Stripe-minimal direction, see
   feedback_design_direction memory). Appended as its own block, same
   pattern as the 2026-07-18 pass above, so nothing earlier has to be
   touched in place.
   ========================================================================== */

.section-heading-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 40px;
}

.section-heading-row .section-title {
  margin-bottom: 0;
}

.section-heading-row p {
  color: var(--text-secondary);
  font-size: 0.95rem;
  max-width: 420px;
  margin: 8px 0 0;
}

@media (max-width: 640px) {
  .section-heading-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
}

/* --- Why MacMeUp / value props ------------------------------------------
   Sits between the stats banner (JS-injected right after .hero) and the
   app grid. Four short, honest claims — no invented testimonials/ratings,
   see feedback memory on the fabricated stats banner this replaces. */
.value-props {
  padding: var(--space-section) 0;
  position: relative;
}

/* Section dividers (stats banner → value-props → all-apps) — same
   fading-gradient line as .stats-banner::before above. Placed on
   .value-props itself (top + bottom) rather than on the apps-section side,
   since All apps/Featured/Extra all share the .apps-section class and
   main.js can reorder which one renders first — anchoring both dividers to
   the one fixed, always-present element between them avoids depending on
   that order. */
.value-props::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(47, 111, 237, 0.6), transparent);
}

.value-props::after {
  bottom: 0;
}

/* Went through two revisions: original strict 4-up icon-in-circle grid,
   flagged as reading "amateur" → 2026 redesign swapped in big asymmetric
   ordinal numerals (01/02/03/04) → walked back the asymmetric offset the
   same day ("value-props look bad") → and now, on request, back to icons
   (the numerals removed entirely, not just hidden) plus giving each item
   the same card treatment as .app-card (background/border/radius/padding/
   hover), so these read as a small grid of cards rather than bare text
   columns. */
.value-props-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 24px;
  /* align-items: start (the old value) let each card size to its own
     content, so a card with a two-line description sat visibly shorter
     than a neighboring three-line one in the same row — grid's default
     stretch instead makes every card in a row match the tallest one. */
  align-items: stretch;
}

.value-prop {
  --stagger: 0s;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 28px 24px;
  box-shadow: var(--glass-shadow-sm);
  transition: opacity 0.7s var(--ease-premium) var(--stagger), transform 0.25s var(--ease-premium),
    background 0.25s ease, border-color 0.25s ease, box-shadow 0.3s ease;
}

@media (hover: hover) and (pointer: fine) {
  .value-prop:hover {
    transform: translateY(-4px);
    background: var(--glass-bg-hover);
    border-color: rgba(47, 111, 237, 0.35);
    box-shadow: var(--card-shadow-hover);
  }
}

.value-props-grid .value-prop:nth-child(2) { --stagger: 0.06s; }
.value-props-grid .value-prop:nth-child(3) { --stagger: 0.12s; }
.value-props-grid .value-prop:nth-child(4) { --stagger: 0.18s; }

.value-prop-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: rgba(47, 111, 237, 0.12);
  color: var(--accent-purple);
  margin-bottom: 16px;
}

.value-prop-icon svg {
  display: block;
  width: 22px;
  height: 22px;
}

.value-prop h3 {
  font-size: 1.3rem;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}

.value-prop p {
  color: var(--text-secondary);
  font-size: 0.95rem;
  line-height: 1.6;
  max-width: 38ch;
  margin: 0;
}

@media (max-width: 640px) {
  .value-props-grid {
    grid-template-columns: 1fr;
    gap: 36px;
  }
}

/* --- Apps section rhythm -------------------------------------------------
   A faint radial wash behind the grid (reuses the aurora opacity token the
   hero already defines) gives the busiest section on the page a bit of
   depth instead of a flat panel, without resorting to a hard color block. */
.apps-section {
  position: relative;
  padding-top: var(--space-section);
}

.apps-section#all-apps::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse 70% 50% at 50% 0%, var(--shot-glow), transparent 70%);
  opacity: var(--aurora-opacity);
  pointer-events: none;
}

/* --- Filter pills, contained --------------------------------------------
   Wrapping the pill row in its own soft panel reads as one deliberate
   control instead of loose buttons floating above the grid. */
.filter-pills {
  padding: 10px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  margin-bottom: 40px;
  width: fit-content;
  max-width: 100%;
}

@media (max-width: 640px) {
  .filter-pills {
    /* One scrollable row instead of wrapping — with 4 category labels
       ("Boot & Installation", "EFI & System Access", etc.) wrapping left
       an uneven, tall 2-3 row block on narrow phones. A single row you can
       swipe through reads cleaner and matches the stats banner's own
       "always one row" treatment. */
    border-radius: var(--radius);
    width: auto;
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 6px;
    gap: 6px;
  }

  .filter-pills::-webkit-scrollbar {
    display: none;
  }

  .filter-pill {
    flex: 0 0 auto;
    white-space: nowrap;
    /* Smaller footprint on mobile — the desktop size (0.85rem / 8px 18px)
       ate too much of the scrollable row's width per pill, on request. */
    font-size: 0.75rem;
    padding: 6px 14px;
  }
}

/* --- App card refinement --------------------------------------------------
   App icon left exactly as it was (see feedback_app_icon_revert memory —
   the padded/boxed tile treatment tried here initially was reverted).
   Category tag shrunk to a quieter inline pill instead of a full-width
   badge; description grows to fill remaining space via flex so the
   download-count + top border stay pinned to the same baseline across
   cards with shorter or longer taglines. */
.app-card {
  display: flex;
  flex-direction: column;
  padding: 28px 24px 24px;
  position: relative;
}

.app-card .app-card-tag {
  font-size: 0.62rem;
  padding: 3px 9px;
  margin-bottom: 14px;
}

.app-card p {
  flex-grow: 1;
  line-height: 1.55;
}

.app-card .download-count {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--glass-border);
  font-size: 0.78rem;
}

@media (prefers-reduced-motion: no-preference) {
  .app-card {
    transition: transform 0.25s var(--ease-premium), background 0.25s ease, box-shadow 0.3s ease, border-color 0.25s ease;
  }

  .app-card:hover {
    transform: translateY(-6px);
  }
}

/* --- One footer, not two (site-wide) -------------------------------------
   With .footer-columns's own border-bottom removed above, the only
   remaining divider is this one hairline on the bottom copyright bar —
   sites with footer_columns off in Admin > Navbar & Footer never get this
   border at all (:has-less browsers still get the default columns-less
   look via the plain .footer-inner rule already in the base stylesheet). */
.footer-columns + .footer-inner {
  border-top: 1px solid var(--glass-border);
  padding-top: 20px;
}

/* Split layout: base .hero-title's clamp(3rem, 7.5vw, 5.5rem) is tuned for
   a full-width centered title (classic layout) — in a ~50%-width column it
   hits the same near-max size and wraps awkwardly tight. Scaled down and
   given a narrower vw coefficient so it settles at a size that comfortably
   wraps 2 lines in the narrower column instead of testing its edges. */
.hero-layout--split .hero-title {
  font-size: clamp(2.5rem, 4.4vw, 4.25rem);
}

.hero-layout--split .hero-subtitle {
  max-width: 480px;
}

/* --- App card: fix both theme icons showing at once ------------------------
   Pre-existing CSS specificity bug, not something introduced by this pass —
   surfaced now because the redesigned grid drew a closer look. `.app-icon
   img` (line ~1669, class+type = specificity 0,1,1) outranks the theme
   toggle rules `.theme-img--light`/`[data-theme="light"] .theme-img--dark`
   (single class = 0,1,0), so inside `.app-icon` specifically, both the dark
   and light <img> ended up visible side by side instead of one being
   display:none — two icons squeezed into one 56px box. Everywhere else on
   the site the theme-img toggle works because nothing else applies a
   type-selector rule directly to `img` at the same nesting level. Fixed with
   higher-specificity overrides scoped to .app-icon only, rather than
   touching the global .theme-img rules every other themed image relies on. */
.app-icon .theme-img--light {
  display: none;
}

[data-theme="light"] .app-icon .theme-img--dark {
  display: none;
}

[data-theme="light"] .app-icon .theme-img--light {
  display: block;
}

/* ==========================================================================
   2026-07-30 — Depicter homepage hero slider (imported from the old
   WordPress marketing site's "MacMeUp-Home" slider export). This is a
   faithful reconstruction: same 1700x870 canvas proportions (kept as an
   aspect-ratio box so it scales as one unit, same as the original "keep
   aspect" slider), same layout/typography/button styling, same gradient
   blob background, same per-element staggered entrance animation
   directions/delays (translated from Depicter's easeOutExpo/easeOutQuart
   move-in params to CSS transitions). Updated same day: the original export
   had a solid white card background (title/body text hardcoded navy/grey
   to sit on that white) — per explicit request the card frame is now
   transparent so it sits directly on the site's own dark/light background,
   which meant switching title/description/accent-bar colors to the site's
   theme-aware vars (--text-primary/--text-secondary) instead of the
   hardcoded navy/grey, since navy-on-near-black would otherwise be
   unreadable in dark mode. The gradient blobs now fade to transparent
   instead of to white, for the same reason. Below ~900px it collapses to a
   simple stacked layout (image above text) since a fixed-canvas absolute
   layout doesn't reflow safely at phone widths — see feedback memory for
   the full reconstruction writeup. This is one of two selectable hero
   variants (.hero-variant--classic / .hero-variant--depicter, see the
   toggle rules further down) — admin Homepage > Hero Style picks which one
   is visible. */

.hero--depicter {
  min-height: auto;
  display: block;
  /* Top padding halved on request (navbar-to-tagline gap felt too big). */
  padding: 24px 0 64px;
}

.hero--depicter .container {
  width: 100%;
}

.depicter-hero-content {
  max-width: 1100px;
  /* Tagline/buttons back above the slider (order-swap reverted on
     request). Negative bottom margin still pulls the slider up under this
     block rather than leaving a big gap; a touch of overlap is fine. */
  margin: 0 auto -20px;
  text-align: center;
  position: relative;
  z-index: 2;
}

/* Base .hero-title is sized for a full hero column (clamp up to 5.5rem).
   Widened the block above to 1100px so this can go back up to (roughly)
   that same full scale without re-triggering the 3-line wrap this once
   had at the narrower 720px width. */
.depicter-hero-content .hero-title {
  font-size: clamp(3.5rem, 7.2vw, 5.5rem);
  margin-bottom: 28px;
}

.depicter-hero-content .hero-actions {
  justify-content: center;
}

/* Short desktop-width viewports (typical 1080p browser window, once tab/
   address bar chrome eats into the real 1080px) don't have room for the
   full-size tagline block above AND the full-size slider below, both
   above the fold — on a 1920x1080 display the slider's own CTA row was
   landing right at or past the bottom edge of the window. Only this
   tagline block shrinks here; the slider itself is untouched, min-width
   keeps this from interacting with the separate max-width:900px mobile
   layout below, and taller monitors (1440p+, above this max-height) still
   get the full-size hero exactly as before.
   Threshold was originally 960px, tuned from an estimate of typical
   browser-chrome height — too tight in practice (a maximized 1080p window
   often still reports an innerHeight closer to 980-1030px depending on
   browser/OS, which slipped right past that cutoff and the rule never
   fired). Raised to 1100px for real margin against that variance; a
   1440p+ display's viewport is comfortably above 1100px regardless of
   chrome, so this still can't accidentally catch a taller monitor. */
@media (max-height: 1100px) and (min-width: 901px) {
  .hero--depicter {
    padding: 24px 0 32px;
  }

  .depicter-hero-content {
    margin-bottom: -12px;
  }

  .depicter-hero-content .hero-title {
    font-size: clamp(2.25rem, 4.5vw, 3.25rem);
    margin-bottom: 12px;
  }

  .depicter-hero-content .hero-subtitle {
    margin-bottom: 16px;
    min-height: 0;
  }
}

.depicter-slider {
  position: relative;
  /* Was a flat `width: 100%` — height followed from width via the aspect
     ratio below with no awareness of viewport HEIGHT at all, so on a wide
     but short window (typical 1080p-class browser) the slider could render
     taller than the visible viewport and get cut off. Fixed the
     aspect-ratio-safe way: derive width from whichever is smaller — the
     container's own width, or the width that would make the slider's
     height exactly fill the remaining space below navbar + the tagline
     block above it. 1.9548 ≈ 1700/870 flipped (multiplying a target height
     by it gives the width that produces that height at this aspect ratio);
     336px budgets the 72px navbar + this section's own ~24px top padding +
     the tagline/buttons block above the slider (title + subtitle + CTA
     row, roughly 220-260px at full size) + a little breathing room — an
     estimate, not pixel-measured, since it varies with how the title
     wraps; nudge this constant if the slider still runs long on a real
     short display. (Was 360px, assuming the ~48px top padding .hero--depicter
     had before it was independently halved in a later pass — re-tuned down
     by that same 24px so this stays accurate to the current padding rather
     than budgeting space that no longer exists, which was making the
     slider render smaller than necessary on short viewports.) Height is
     left to `aspect-ratio` (auto) so it always derives cleanly from
     whichever width wins, instead of being clamped independently and
     breaking the ratio (which would visually squish the per-slide
     screenshots — their positions are percentages of this exact
     1700x870-proportioned box). */
  width: min(100%, calc((100vh - 336px) * 1.9548));
  max-width: 1700px;
  margin: 0 auto;
  aspect-ratio: 1700 / 870;
  overflow: hidden;
  background: transparent;
  cursor: grab;
}

.depicter-slider--dragging {
  cursor: grabbing;
  user-select: none;
}

.depicter-slide {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.6s ease;
}

.depicter-slide--active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  z-index: 1;
}

/* Circular background shapes — re-derived directly from the source export
   (assets/Sliders/MacMeUp-Home/data.json, the three "Shape" elements per
   section) after actually reading its raw element data rather than
   guessing at the effect. The real design is NOT a blurred/translucent
   glow at all: each one is a fully opaque solid circle (corner-radius
   1000px on a square box = a perfect circle) filled with a plain
   linear-gradient(0deg, #DDF1FF <stop>%, #FFF 100%) — a pale ice-blue that
   ramps to solid white by the bottom of the shape, so it visually
   "dissolves" into the original white card's background. Translated here
   as the same solid-circle-with-gradient-fill construction, but fading to
   transparent instead of a hardcoded white so it dissolves into whatever
   the page background actually is (dark or light), with the start color
   itself swapped per theme via --depicter-blob-color since the same alpha
   reads very differently against near-black vs near-white (see the two
   var definitions above, in :root and [data-theme="light"]). Sizes/
   positions/rotations are exact conversions of the source's pixel values
   against its 1700x870 canvas (e.g. large: 933x933px at (753,-67) →
   54.88%/107.24% at 44.29%/-7.7%). */
.depicter-blob {
  display: block;
  position: absolute;
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
}

.depicter-blob--large {
  /* Source size (933x933px, a perfect circle) mapped straight onto this
     1700x870 canvas comes out to 54.88% wide but 107.24% tall — taller
     than the canvas itself, because the canvas is much wider than it is
     tall and the shape is a plain square. The source got away with this
     by bleeding the top off-canvas (top: -7.7%) so only the bottom stayed
     in frame; clamping that to 0% two rounds ago fixed the top-clip but
     just pushed the same overflow to the bottom edge instead (still
     107.24% tall from top:0). Sized to 90% of the slide height to
     eliminate that clipping entirely, then grown back up to 97% here on
     request — checked the actual numbers first: the shared tagline block
     above the slider renders at a near-fixed ~262px tall on any normal
     desktop viewport (its H1 hits its own max font-size by ~1220px
     viewport width and stops growing), while the slider itself keeps
     growing from ~655px tall (1280px-wide viewport) up to its own cap of
     870px (any viewport ≳1700px) — i.e. the slider is 2.5x to 3.3x the
     tagline block's height depending on screen width, so there's ample
     room for the blob to sit close to the slide's full height without
     ever competing with or overwhelming the fixed-size tagline above it.
     97% (not a flush 100%) keeps a hair of margin on both edges so it
     doesn't look like it's touching the frame exactly. */
  width: 49.64%;
  height: 97%;
  left: 46.92%;
  top: 1.5%;
  background: linear-gradient(0deg, var(--depicter-blob-color) 0%, transparent 100%);
}

.depicter-blob--small-1 {
  width: 7.18%;
  height: 14.02%;
  left: 50.2%;
  top: 32.3%;
  background: linear-gradient(0deg, var(--depicter-blob-color) 23%, transparent 100%);
  transform: rotate(27.9deg) translate(50px, -50px);
}

.depicter-blob--small-2 {
  width: 11.76%;
  height: 22.99%;
  left: 75.65%;
  top: 8.05%;
  background: linear-gradient(0deg, var(--depicter-blob-color) 20%, transparent 100%);
  transform: rotate(200deg) translate(-50px, 50px);
}

.depicter-text {
  position: absolute;
  left: 6.12%;
  top: 26.09%;
  width: 36.7%;
  z-index: 2;
}

.depicter-tagline,
.depicter-title,
.depicter-accent-bars,
.depicter-description,
.depicter-buttons {
  opacity: 0;
}

/* Typography below now matches the rest of the site (--font-sans / Inter,
   the same weights/letter-spacing as .section-eyebrow and the global
   h1/h2/h3 rule) instead of the imported Depicter export's own 'Exo'
   font-family + capitalize/tight-tracking treatment. */
.depicter-tagline {
  font-family: var(--font-sans);
  font-size: clamp(0.8rem, 1.05vw, 1rem);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  line-height: 1.2;
  margin: 0 0 12px;
  transform: translateX(40px);
}

.depicter-title {
  font-family: var(--font-sans);
  font-size: clamp(1.75rem, 3.6vw, 4rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  line-height: 1.15;
  margin: 0;
  transform: translateX(40px);
}

.depicter-accent-bars {
  display: flex;
  gap: 7px;
  margin: 18px 0;
  transform: translateX(-60px);
}

.depicter-bar {
  height: 6px;
  border-radius: 15px;
  display: block;
}

.depicter-bar--accent {
  width: 25px;
  background: var(--accent-cyan);
}

.depicter-bar--muted {
  width: 65px;
  background: var(--text-secondary);
  opacity: 0.5;
}

.depicter-description {
  font-family: var(--font-sans);
  font-size: clamp(0.85rem, 1vw, 1.0625rem);
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-secondary);
  margin: 0 0 28px;
  transform: translateY(40px);
}

.depicter-buttons {
  display: flex;
  gap: 14px;
  transform: translateX(40px);
}

/* Per-slide buttons now use the site's real .btn/.btn-primary/.btn-secondary
   classes directly (see the slide-builder in main.js) instead of a bespoke
   navy-pill style, so they read identically to every other CTA on the
   site. No .depicter-btn rule needed any more. */

.depicter-image {
  position: absolute;
  z-index: 1;
  opacity: 0;
  transform: translate(50px, 20px) scale(0.92);
}

.depicter-image img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Same subtle shadow as .app-hero-visual img on the app dedicated pages —
     applied here too so the homepage hero's screenshots match exactly. */
  filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.16));
}

/* .depicter-image img's class+type selector above (0,1,1) outranks the
   plain single-class theme toggle rules (.theme-img--light, 0,1,0), which
   would leave both the dark and light screenshot visible at once — the
   exact bug already documented and fixed for .app-icon. Same fix here:
   scope the toggle to .depicter-image specifically so it wins. */
.depicter-image .theme-img--light {
  display: none;
}

[data-theme="light"] .depicter-image .theme-img--dark {
  display: none;
}

[data-theme="light"] .depicter-image .theme-img--light {
  display: block;
}

/* Dot-and-line style — matches .hero-visual-progress-bar (the classic
   hero's own dots) exactly, per explicit request to use "the original
   slider's dots and lines" rather than the Depicter export's pill bullets:
   small dots for inactive slides, the active one stretches into a short
   line. */
/* Outer row: prev arrow, dots, next arrow — same three-piece layout as
   .hero-visual-progress on the classic hero, just its own class since this
   row sits directly under the slider instead of inside .hero-media. */
.depicter-visual-progress {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 24px;
}

.depicter-bullets {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.depicter-bullet {
  display: block;
  width: 8px;
  height: 8px;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--glass-border);
  cursor: pointer;
  transition: width 0.25s var(--ease-premium), background 0.25s ease;
}

.depicter-bullet--active {
  width: 26px;
  background: var(--accent-gradient);
}

/* Entrance animations — only when a slide is active, and only when the
   visitor hasn't asked for reduced motion. Delays/directions/durations are
   translated from the Depicter export's per-element move-in params. */
@media (prefers-reduced-motion: no-preference) {
  .depicter-slide--active .depicter-tagline,
  .depicter-slide--active .depicter-title,
  .depicter-slide--active .depicter-accent-bars,
  .depicter-slide--active .depicter-description,
  .depicter-slide--active .depicter-buttons,
  .depicter-slide--active .depicter-image {
    opacity: 1;
    transform: none;
  }

  .depicter-slide--active .depicter-tagline {
    transition: opacity 1.5s var(--ease-premium, ease) 0.3s, transform 1.5s var(--ease-premium, ease) 0.3s;
  }
  .depicter-slide--active .depicter-title {
    transition: opacity 1.6s var(--ease-premium, ease) 0.5s, transform 1.6s var(--ease-premium, ease) 0.5s;
  }
  .depicter-slide--active .depicter-accent-bars {
    transition: opacity 1.6s var(--ease-premium, ease) 0.4s, transform 1.6s var(--ease-premium, ease) 0.4s;
  }
  .depicter-slide--active .depicter-description {
    transition: opacity 1.6s var(--ease-premium, ease) 0.9s, transform 1.6s var(--ease-premium, ease) 0.9s;
  }
  .depicter-slide--active .depicter-buttons {
    transition: opacity 1.6s var(--ease-premium, ease) 1s, transform 1.6s var(--ease-premium, ease) 1s;
  }
  .depicter-slide--active .depicter-image {
    transition: opacity 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s, transform 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s;
  }
  /* Source export: large blob is a plain 2s fade (move type with 0
     movement — i.e. "Fade In" in all but name), the two small ones are a
     1.7s move-in that eases back to their resting rotation with no
     translate offset. */
  .depicter-slide--active .depicter-blob--large {
    opacity: 1;
    transition: opacity 2s var(--ease-premium, ease);
  }
  .depicter-slide--active .depicter-blob--small-1 {
    opacity: 1;
    transform: rotate(27.9deg) translate(0, 0);
    transition: opacity 1.7s var(--ease-premium, ease), transform 1.7s var(--ease-premium, ease);
  }
  .depicter-slide--active .depicter-blob--small-2 {
    opacity: 1;
    transform: rotate(200deg) translate(0, 0);
    transition: opacity 1.7s var(--ease-premium, ease), transform 1.7s var(--ease-premium, ease);
  }
}

@media (prefers-reduced-motion: reduce) {
  .depicter-tagline,
  .depicter-title,
  .depicter-accent-bars,
  .depicter-description,
  .depicter-buttons,
  .depicter-image {
    transform: none;
  }
  /* Keep each blob's resting rotation even with motion reduced — only the
     translate offset (the actual "motion") is dropped, same treatment as
     every other element here just fading straight to its final state. */
  .depicter-blob--small-1 {
    transform: rotate(27.9deg);
  }
  .depicter-blob--small-2 {
    transform: rotate(200deg);
  }
  .depicter-slide--active .depicter-tagline,
  .depicter-slide--active .depicter-title,
  .depicter-slide--active .depicter-accent-bars,
  .depicter-slide--active .depicter-description,
  .depicter-slide--active .depicter-buttons,
  .depicter-slide--active .depicter-image,
  .depicter-slide--active .depicter-blob {
    opacity: 1;
  }
}

/* Per-slide screenshot geometry — desktop only, matches each slide's
   original image size/position from the export. Wrapped so the mobile
   stacked layout below can cleanly reset .depicter-image without fighting
   these two-class selectors on specificity. */
@media (min-width: 901px) {
  /* These boxes were originally sized to match each Depicter export image's
     own native pixel dimensions exactly — box == image, no letterboxing at
     all (confirmed straight from assets/Sliders/MacMeUp-Home/data.json:
     every "image" element's size in px is identical to its box's size in
     px). Now that the real app screenshots (different aspect ratios than
     those old marketing crops) are wired in, keeping the old boxes made
     object-fit: contain shrink each image to fit — which never crops, but
     did make some of them noticeably smaller/oddly proportioned inside
     their box. Recomputed here the same way the source did it: width kept
     the same (so nothing shifts horizontally or risks overflowing the
     canvas), height recalculated from each real screenshot's actual pixel
     aspect ratio, and top re-centered within the original vertical slot so
     the image now fills its box exactly like the source convention, with
     no letterboxing. mactweaks' box width was also trimmed from 952px to
     922px (55.99% → 54.24%) — the original export bled 30px past the
     1700px canvas on this one slide specifically (faithful to source, but
     highly visible as a hard-cropped edge on a real screenshot rather than
     a stylized marketing image), so this also removes that clip. */
  /* Sizes here (width/height) are each screenshot's own aspect-correct fit.
     Reverted on request — comparing against the actual live macmeup.com
     Depicter slider (screenshots supplied directly), the images there are
     noticeably larger/more prominent than this reconstruction's. The
     previous values here were this exact sizing shrunk 15% "for breathing
     room around the bigger blob"; undoing that 15% shrink (divide by 0.85)
     restores each screenshot to its pre-shrink size, which is what the
     comparison screenshots show. left/top recomputed the same way the
     original shrink was undone — every slide's image re-centered on
     .depicter-blob--large's own center point (71.74% horizontal, 50%
     vertical) at its new, larger size — rather than scaling left/top too,
     which would have shifted the centering off-blob. */
  .depicter-slide--mactweaks .depicter-image { left: 47.33%; top: 16.72%; width: 48.82%; height: 66.56%; }
  .depicter-slide--bootabler .depicter-image { left: 47.33%; top: 16.20%; width: 48.82%; height: 67.60%; }
  .depicter-slide--espmounter .depicter-image { left: 54.21%; top: 10.14%; width: 35.05%; height: 79.71%; }
  .depicter-slide--gatekeeper .depicter-image { left: 53.05%; top: 29.44%; width: 37.38%; height: 41.12%; }
  /* Sparkler bumped further (35% bigger than the rest of this round's
     across-the-board enlargement, same aspect ratio kept) on request —
     it's an icon graphic rather than an app window screenshot, and still
     read as too small next to the others after the general fix above. */
  .depicter-slide--sparkler .depicter-image { left: 52.09%; top: 12.98%; width: 39.31%; height: 74.05%; }
  .depicter-slide--guides .depicter-image { left: 45.75%; top: 21.44%; width: 51.98%; height: 57.11%; }
}

@media (max-width: 900px) {
  .hero--depicter { padding: 32px 0 40px; }

  /* Mobile-specific: the desktop clamp's 3.5rem FLOOR (clamp(3.5rem, 7.2vw,
     5.5rem)) never actually shrinks on a phone — 7.2vw is well under 3.5rem
     at any phone width, so it just sits pinned at the 56px floor, wrapping
     across 2-3 lines. Sized down for mobile specifically here. Also
     replaces the desktop's -20px "slight overlap is fine" bottom margin
     with a real positive gap: that overlap was tuned for the compact
     desktop text block, but on mobile the taller wrapped heading pushes
     the buttons row down further, so the same -20px was overlapping the
     slider by much more than intended (buttons visibly overlapping the
     screenshot below them). */
  .depicter-hero-content {
    margin-bottom: 28px;
  }

  .depicter-hero-content .hero-title {
    font-size: clamp(2rem, 8vw, 2.75rem);
    margin-bottom: 16px;
  }

  .depicter-slider {
    /* Reset the desktop viewport-height-driven width formula — on mobile
       aspect-ratio is off and each slide flows to its own natural height
       (see .depicter-slide below), so there's no fixed-canvas percentage
       layout left to preserve, and the height formula would otherwise
       needlessly narrow the slider on short/landscape phone viewports. */
    width: 100%;
    aspect-ratio: auto;
    height: auto;
    border-radius: 20px;
    /* No card-level box-shadow here (was 0 20px 50px -25px rgba(15,23,42,.4))
       — the desktop layout never puts a shadow on the slider itself, only
       on the screenshot inside it (.depicter-image img's drop-shadow
       filter), so this stacked shadow-on-top-of-shadow only ever showed up
       on mobile and looked like a stray dark smudge under the whole card. */
  }

  .depicter-slide {
    display: none;
    position: static;
    flex-direction: column;
    padding: 36px 24px 32px;
  }

  .depicter-slide--active {
    display: flex;
  }

  .depicter-blob--small-1,
  .depicter-blob--small-2 {
    display: none;
  }

  .depicter-blob--large {
    width: 160%;
    height: 50%;
    left: -30%;
    top: -15%;
  }

  .depicter-text {
    position: static;
    width: auto;
    text-align: center;
    order: 1;
  }

  .depicter-image {
    position: static;
    width: 100%;
    max-width: 320px;
    height: auto;
    margin: 0 auto 24px;
    order: 0;
  }

  .depicter-image img {
    position: static;
    width: 100%;
    height: auto;
  }

  .depicter-accent-bars,
  .depicter-buttons {
    justify-content: center;
  }

  .depicter-title {
    font-size: clamp(1.5rem, 7vw, 2.25rem);
  }

  .depicter-tagline {
    font-size: 0.8rem;
  }

  .depicter-description {
    font-size: 0.85rem;
    max-width: 46ch;
    margin-left: auto;
    margin-right: auto;
  }
}


/* Dock mode spacing adjustments to prevent the dock from covering content */
body.dock-navbar-active .faq-section {
  padding-bottom: 84px;
}


body.dock-navbar-active main:not(:has(~ .faq-section)) {
  padding-bottom: 84px;
}


/* Trademark / non-affiliation notice under the footer bottom bar (Admin > Navbar & Footer). */
.footer-legal {
  flex: 1 1 100%;
  width: 100%;
  margin: 4px 0 0;
  font-size: 0.7rem;
  line-height: 1.55;
  color: var(--text-secondary);
  opacity: 0.85;
  text-align: center;
}

/* Legal pages (Terms, Privacy, EULA, Refund Policy). */
.post-content .legal-updated { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 1.5em; }
.post-content .legal-related { margin-top: 2.5em; padding-top: 1em; border-top: 1px solid var(--border-color, rgba(127,127,127,.25)); color: var(--text-secondary); font-size: 0.85rem; }

/* Contact form: inline hint under a field (e.g. what to include in a refund request). */
.form-field .form-hint { margin: 8px 0 0; font-size: 0.8rem; line-height: 1.5; color: var(--text-secondary); }
.form-field .form-hint code { font-size: 0.78rem; }

/* Cookie consent banner (js/main.js analytics block; Admin > Analytics). */
.cookie-banner {
  position: fixed; z-index: 2000;
  left: 50%; bottom: 20px; transform: translateX(-50%);
  width: min(720px, calc(100% - 24px));
  display: flex; align-items: center; gap: 18px;
  padding: 16px 18px;
  border-radius: 14px;
  background: var(--bg-secondary, #fff);
  color: var(--text-primary);
  border: 1px solid color-mix(in srgb, var(--text-primary) 14%, transparent);
  box-shadow: 0 18px 50px -12px rgba(0, 0, 0, 0.45);
  font-size: 0.85rem; line-height: 1.5;
  animation: cookie-in 0.35s ease;
}
.cookie-banner-text strong { display: block; font-size: 0.92rem; margin-bottom: 2px; }
.cookie-banner-text p { margin: 0; color: var(--text-secondary); }
.cookie-banner-text a { color: var(--accent-cyan, #2563eb); }
.cookie-banner-actions { display: flex; gap: 8px; flex: none; }
.cookie-btn {
  font: inherit; font-weight: 600; cursor: pointer;
  padding: 9px 14px; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--text-primary) 22%, transparent);
  background: transparent; color: var(--text-primary);
}
.cookie-btn--primary { background: var(--text-primary); color: var(--bg-primary); border-color: var(--text-primary); }
.cookie-btn:focus-visible { outline: 2px solid var(--accent-cyan, #2563eb); outline-offset: 2px; }
@media (max-width: 640px) {
  .cookie-banner { flex-direction: column; align-items: stretch; bottom: 12px; }
  .cookie-banner-actions .cookie-btn { flex: 1; }
}
@keyframes cookie-in { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
@media (prefers-reduced-motion: reduce) { .cookie-banner { animation: none; } }

/* Legal hub list + "Change cookie settings" button. */
.post-content .legal-index { list-style: none; padding: 0; margin: 0 0 1.5em; display: grid; gap: 10px; }
.post-content .legal-index li { padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent); border-radius: 12px; color: var(--text-secondary); font-size: 0.92rem; }
.post-content .legal-index li a { text-decoration: none; }
.post-content .legal-index li strong { color: var(--text-primary); font-size: 1rem; }
.post-content .legal-cookie-btn { display: inline-flex; text-decoration: none; }

/* ==========================================================================
   Dock + iOS bar on small screens (any navbar style that builds them).
   theme-overrides.css (generated, loaded after this file) holds the desktop
   rules, so these use "html body" for the extra specificity they need.
   ========================================================================== */

/* Dock: the "Apps" button only exists for small screens; on wide screens the
   app icons sit in the dock directly. */
.navbar-dock-apps { display: contents; }
html body .navbar-dock-apps-toggle { display: none; }
.navbar-dock-apps-btn {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; padding: 0; border: 0; background: none;
  color: inherit; cursor: pointer; border-radius: inherit;
}
.navbar-dock-apps-btn svg { width: 24px; height: 24px; stroke: currentColor; stroke-width: 1.5; fill: none; }
.navbar-dock-apps-panel { left: 0; right: auto; }
.navbar-dock-apps-panel a { display: flex; align-items: center; gap: 10px; }
.navbar-dock-apps-panel img { width: 26px; height: 26px; border-radius: 7px; flex: none; object-fit: cover; }

/* Touch screens: no hover magnification or hover labels (they'd stick after a tap). */
@media (hover: none) {
  html body .navbar-dock-item:hover,
  html body .navbar-dock-item:focus-visible { transform: none; }
  html body .navbar-dock-tooltip { display: none; }
  html body .navbar-ios-item:hover .nav-label-grid,
  html body .navbar-ios-item:focus-visible .nav-label-grid { grid-template-columns: 0fr; opacity: 0; margin-left: 0; }
}

/* Tablets and below: collapse the app icons into the Apps button. */
@media (max-width: 900px) {
  html body .navbar-dock-apps { display: none; }
  html body .navbar-dock-apps-toggle { display: flex; }
}

/* Phones */
@media (max-width: 640px) {
  /* Top pill: full width, icons spread evenly, scrolls sideways if a site
     has more links than fit. */
  html body .navbar-ios {
    top: calc(10px + env(safe-area-inset-top, 0px));
    left: 10px; right: 10px;
    transform: none;
    height: 54px;
    padding: 0 6px;
    gap: 0;
    justify-content: space-between;
    border-radius: 27px;
    overflow-x: auto;
    scrollbar-width: none;
  }
  html body .navbar-ios::-webkit-scrollbar { display: none; }
  html body.dock-scrolled .navbar-ios { transform: translateY(-20px); }
  html body .navbar-ios-item { min-width: 40px; height: 40px; padding: 0 8px; flex: 0 0 auto; }
  html body .navbar-ios-item svg { width: 20px; height: 20px; }
  html body .navbar-ios-item .nav-label-grid { display: none; }

  /* Bottom dock: full width, smaller icons, clear of the home indicator. */
  html body .navbar-dock {
    left: 10px; right: 10px;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    transform: translateY(20px);
    justify-content: space-between;
    gap: 2px; padding: 6px;
    border-radius: 20px;
  }
  html body.dock-scrolled .navbar-dock { transform: none; }
  html body .navbar-dock--docked { transform: none; }
  html body .navbar-dock-item { width: 42px; height: 42px; flex: 0 0 auto; }
  html body .navbar-dock-item img { width: 32px; height: 32px; }
  html body .navbar-dock-item--logo img { width: 26px; }
  html body .navbar-dock-item svg,
  html body .navbar-dock-apps-btn svg { width: 22px; height: 22px; }
  html body .navbar-dock-divider { height: 26px; margin: 8px 1px; }
  html body .navbar-dock-apps-panel { max-width: calc(100vw - 40px); max-height: 60vh; }

  /* Room for the fixed pill above app-page heroes. */
  html body.ios-dock-navbar-active .app-hero,
  html body.ios-navbar-active .app-hero { padding-top: 96px; }
  html body.dock-scrolled .site-footer { margin-top: 0; padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); }
}

/* Small phones (iPhone SE / mini): tighter still. */
@media (max-width: 380px) {
  html body .navbar-ios { padding: 0 4px; }
  html body .navbar-ios-item { min-width: 34px; height: 38px; padding: 0 5px; }
  html body .navbar-ios-item svg { width: 18px; height: 18px; }
  html body .navbar-dock { padding: 5px; }
  html body .navbar-dock-item { width: 36px; height: 36px; }
  html body .navbar-dock-item img { width: 28px; height: 28px; }
  html body .navbar-dock-item svg,
  html body .navbar-dock-apps-btn svg { width: 20px; height: 20px; }
  html body .navbar-dock-divider { display: none; }
}

