* { box-sizing: border-box; }

/* Android Chrome reported jumping the scroll position backward while
   scrolling into the origin/video section and the gallery section — both
   contain content that changes size right as it scrolls into view (lazy
   images gaining their real height once loaded, the portrait video
   starting to play). Chrome's scroll-anchoring feature tries to "fix" the
   scroll position when it detects nearby content shifting, and gets it
   wrong here, producing the visible jump. The gallery images now reserve
   their real aspect ratio via width/height attributes (see index.html) so
   they shouldn't shift at all, but this is a broader safety net: it
   disables the auto-correction itself, so no layout change near the
   viewport can trigger a scroll jump, regardless of the cause. */
html {
  overflow-anchor: none;
}

/* Fluid root font-size: since almost everything on the site (type, padding,
   gaps) is already in rem, scaling the root scales nearly the whole layout
   proportionally from one place. 0.952vw = 16px exactly at 1680px — the
   fixed recording resolution — so nothing changes there; it eases down to
   14px by ~1024px and up to 19px by ~2560px for other monitors. */
html {
  font-size: clamp(14px, 0.9524vw, 19px);
}

/* Path is relative to THIS file (css/style.css), not the project root — same
   as the @font-face url below — so it needs ../assets, not assets. That's
   why the cursor never actually appeared before. Hotspot (6,6) is the wand's
   tip, not the image's corner, matching where a normal pointer's own tip
   sits. */
html, body, a, button, .cta-button, .nav-cta, .lang-switch {
  cursor: url('/html-static/elf/assets/cursor-wand.png') 7 7, auto;
}

@font-face {
  font-family: 'HarryP';
  src: url('/html-static/elf/harry-p-font/HarryP-MVZ6w.ttf') format('truetype');
  font-display: swap;
}

/* Self-hosted (was Google Fonts, loaded via an external <link rel=stylesheet>
   in index.html) — that meant 2 extra third-party origins (fonts.googleapis.com
   + fonts.gstatic.com) on the render-blocking critical path, each its own
   DNS+TLS round trip, on top of our own CSS. On a slow mobile connection that
   was measured to be several seconds of a fully blank/black screen before
   anything could paint. Same fix as html-static/generalkivitelezodemo already
   uses. Cormorant Garamond intentionally dropped — grep confirmed it's no
   longer referenced anywhere (see the memoriam-main-text comment below for
   why it was swapped out). */
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cinzel Decorative';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-decorative-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel Decorative';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/html-static/elf/fonts/cinzel-decorative-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/html-static/elf/fonts/work-sans-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/html-static/elf/fonts/work-sans-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/html-static/elf/fonts/work-sans-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/html-static/elf/fonts/work-sans-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --font-hp: 'HarryP', 'Cinzel Decorative', 'Georgia', serif;
  --font-display: 'Cinzel Decorative', 'Georgia', serif;
  --font-title: 'Cinzel', 'Georgia', serif;
  --font-sans: 'Work Sans', sans-serif;
  --font-serif-premium: 'Cormorant Garamond', 'Georgia', serif;
  --gold: #c99a3d;
  --gold-dim: #a3792f;
}

html {
  scroll-behavior: smooth;
  /* Firefox — thumb/track color only, no shape control available. */
  scrollbar-color: var(--gold) #241a0f;
  scrollbar-width: thin;
}

/* Chrome/Edge/Safari — full control, unlike Firefox above. Dark track +
   gold thumb with rounded ends, matching the site's badge/button palette. */
html::-webkit-scrollbar {
  width: 12px;
}

html::-webkit-scrollbar-track {
  background: #241a0f;
}

html::-webkit-scrollbar-thumb {
  background: var(--gold);
  border-radius: 6px;
  border: 2px solid #241a0f;
}

html::-webkit-scrollbar-thumb:hover {
  background: #e0b354;
}

html, body {
  margin: 0;
  background: #c9b48a;
  color: #4a3c2a;
  font-family: var(--font-title);
  font-weight: 500;
  letter-spacing: 0.03em;
}

/* Hidden until the web fonts are confirmed ready (see the page-ready gate
   in main.js, same pattern as html-static/index.html's FOUT guard) —
   otherwise the page flashes the fallback serif before swapping to
   Cinzel/HarryP. Short (~400ms-capped) like the plain-HTML page, since this
   only waits on fonts now — the canvas has its own separate fade-in below
   so a slow first frame can't stall the whole page behind a blank tan
   screen. */
body {
  position: relative;
  opacity: 0;
  transition: opacity 0.4s ease;
}

body.page-ready {
  opacity: 1;
}

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

.hero-glow {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  background: #c9b48a;
}

/* APPROVED CHECKPOINT (2026-08-09) — straight vertical seam confirmed correct
   at 29.6%. That drifted to 29.8% at some later edit (likely during the
   clip-path-div → SVG polygon conversion) and reopened a small gap at the
   bolt's bottom tip — reported and re-derived precisely from the actual
   polygon geometry (point 45.86,100 inside the 25vw–35vw bolt box = 25 +
   0.4586*10 = 29.586vw), which lands right back near the original approved
   value. Do not change this left value without checking with the user
   first. */
.hero-dark-fill {
  position: absolute;
  top: 0;
  left: 29.586%;
  right: 0;
  height: 100%;
  background: #241a0f;
}

.hero-castle-img {
  position: absolute;
  top: 0;
  left: 29.586%;
  right: 0;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0.75;
}

/* Scroll-driven again, but fast now (see HERO_IMAGE_FADE_END in main.js):
   0 at rest, so the hero looks clean and undarkened before you touch the
   page, but ramping to full strength within the first ~8% of the hero's own
   scroll-through — because the Rólam seam starts peeking into view almost
   immediately (the hero is exactly one viewport tall). The earlier attempt
   used the same slow ~60% ramp as the text/nav fade and stayed nearly
   invisible right when the seam first appeared, showing as a hard edge. */
.hero-fade-out {
  position: absolute;
  bottom: 0;
  left: 29.586%;
  right: 0;
  height: 35%;
  background: linear-gradient(to bottom, transparent, #241a0f);
  opacity: 0;
  transition: opacity 0.15s linear;
}

/* Approved reference shape (2026-08-09) — traced from assets/villam3.png via
   OpenCV findContours + approxPolyDP (see .claude/trace_bolt.py). Do not
   re-derive by hand or swap for a different extraction; ask before changing
   these coordinates, only fine-tune from here. Positioned at left:30%
   (unchanged from the checkpoint above) — it sits purely as a decorative
   overlay on top of the straight dark/light seam, not as the seam itself.
   Reusable motif (2026-08-09): renamed from .hero-bolt-divider to .bolt-divider
   so every section can reuse the same locked shape as its signature divider
   between the sticky animation and that section's own content/background,
   not just the hero. */
/* filter: drop-shadow produced no visible effect at all in testing, and the
   layered-duplicate-shape fallback gave an uneven stroke width (scaling the
   whole box doesn't offset every edge of an irregular polygon by the same
   amount). Now a real SVG polygon with a native stroke — vector-effect:
   non-scaling-stroke keeps the line a constant screen-pixel width on every
   edge even though the box itself is stretched non-uniformly (10vw x 100vh,
   preserveAspectRatio="none"). */
.bolt-divider {
  position: fixed;
  top: 0;
  left: 30vw;
  transform: translateX(-50%);
  width: 10vw;
  height: 100vh;
  height: 100svh;
  z-index: 3;
  pointer-events: none;
}

.bolt-divider-mobile {
  display: none;
}


/* left:34.47vw (not 0) — that's the bolt's own top-right tip position (same
   value content-col's padding uses to clear it), not the 29.586vw dark-panel
   seam further down the bolt — the bolt's top edge sits further right than
   its base, so the panel-seam value left the bar overlapping it. Grid, not
   flex+space-between: with 3 unevenly-sized children, space-between does not
   put the middle one on the true centre of the bar — only a grid centre
   column guarantees that regardless of wordmark/CTA width. */
.top-bar {
  position: fixed;
  top: 0;
  left: 34.47vw;
  right: 0;
  z-index: 6;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 1.8rem 2.5rem;
  transition: opacity 0.2s linear;
}

.top-bar .wordmark {
  justify-self: start;
  font-family: var(--font-hp);
  font-size: 1.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #f0e2c4;
}

.nav-links {
  justify-self: center;
  margin-right: 2rem;
  display: flex;
  gap: 1.4rem;
}

.nav-links a {
  font-family: var(--font-title);
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  white-space: nowrap;
  color: rgba(240, 226, 196, 0.75);
  text-decoration: none;
  transition: color 0.15s ease;
}

.nav-links a:hover {
  color: var(--gold);
}

.top-bar-actions {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* Dark brown, not the light cream this used to be — the button sits over
   the tan animation strip (#c9b48a), and light-on-light was nearly
   invisible there. Dark brown is the same tone used everywhere else on the
   site for text over the light/tan sections. */
.lang-switch {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0.3rem;
  color: #4a3c2a;
  font-family: var(--font-title);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  transition: color 0.15s ease;
}

/* Moved out of the top-bar entirely — sits over the Dobby animation column
   instead, top-left. Faded via JS alongside .top-bar (see main.js), same
   "gone after the hero" behaviour as the nav. */
.lang-switch-corner {
  position: fixed;
  top: 1.8rem;
  left: 2rem;
  z-index: 6;
}

.lang-switch svg {
  width: 18px;
  height: 18px;
}

.lang-switch:hover {
  color: var(--gold);
}

/* Both options shown at once (HU / EN) instead of just the current one —
   showing a single changing label didn't read as a toggle at all, it just
   looked like a static language tag. Both stay fully dark/opaque (same as
   the icon) so both stay legible on the tan strip — dimming the inactive
   one to 45% opacity (an earlier version) washed it out to the point of
   being unreadable, and gold-for-active turned out low-contrast against
   tan too. Bold weight marks the active one instead of a color/opacity
   change, so legibility never trades off against showing which is active. */
.lang-switch .lang-option {
  font-weight: 500;
}

.lang-switch .lang-option.active {
  font-weight: 800;
}

.lang-switch .lang-sep {
  opacity: 0.35;
}

.mobile-menu-toggle {
  display: none;
}

.nav-cta {
  font-family: var(--font-title);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: #241a0f;
  background: var(--gold);
  padding: 0.6rem 1.2rem;
  border-radius: 3px;
  text-decoration: none;
  transition: background 0.15s ease, transform 0.15s ease;
}

.nav-cta:hover {
  background: #e0b354;
  transform: translateY(-1px);
}

.scroll-cue-arrow {
  display: inline-block;
  color: var(--gold);
  animation: cue-bounce 1.6s ease-in-out infinite;
}

@keyframes cue-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(5px); }
}

.layout {
  display: grid;
  grid-template-columns: 30% 70%;
  position: relative;
  z-index: 2;
}

.sticky-col {
  position: relative;
}

.sticky-inner {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

#frameCanvas {
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 0.5s ease;
}

#frameCanvas.canvas-ready {
  opacity: 1;
}

.content-col {
  display: flex;
  flex-direction: column;
  padding-left: 4.47vw;
}

.hero-block {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 4rem 3rem;
  position: relative;
  transition: opacity 0.2s linear, transform 0.2s linear;
}

.hero-block .kicker {
  font-family: var(--font-title);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 1.2rem;
}

.hero-block h1 {
  font-family: var(--font-hp);
  font-size: clamp(5.5rem, 13vw, 11rem);
  line-height: 0.9;
  letter-spacing: 0.02em;
  margin: 0 0 1.5rem;
  color: #f7ead0;
  font-weight: 400;
  text-shadow: 0 2px 40px rgba(201, 154, 61, 0.35);
  -webkit-text-stroke: 1.5px #e8c165;
  text-stroke: 1.5px #e8c165;
}

.hero-block .tagline {
  font-family: var(--font-title);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #d9c6a0;
  font-style: italic;
  max-width: 22em;
  margin: 0 0 3rem;
  line-height: 1.5;
}

.hero-cta {
  display: inline-block;
  margin-top: 2rem;
  font-family: var(--font-title);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: #241a0f;
  background: var(--gold);
  padding: 0.85rem 1.8rem;
  border-radius: 3px;
  text-decoration: none;
  transition: background 0.15s ease, transform 0.15s ease;
}

.hero-cta:hover {
  background: #e0b354;
  transform: translateY(-2px);
}

.book-section {
  /* svh (with vh as a fallback for browsers that don't support it), not
     dvh: dvh actively tracks the address bar's current state, which means
     it recalculates — and visibly resizes the section — every time the bar
     shows/hides mid-scroll on Android Chrome (reported as the page
     "jumping"). svh is fixed at the smallest possible viewport (address bar
     always assumed visible) and never recalculates, trading a small unused
     gap at the bottom when the bar auto-hides for zero resize jumps.
     Fixed here at the source rather than as a mobile-only override so it
     doesn't fight the min-height:auto that .gallery-section/.skills-section
     set deliberately (that more specific rule still wins as before; this
     only corrects the height for sections that keep the full-height
     default). */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 4rem 1.5rem 4rem 3rem;
  border-top: 1px solid rgba(74, 60, 42, 0.15);
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.origin-section {
  position: relative;
  z-index: 1;
  border-top: none;
  opacity: 1 !important;
  transform: none !important;
}

/* .origin-section's own left edge sits at 34.47vw (30vw grid column + 4.47vw
   content-col padding), but this fill needs to start at the true viewport
   seam (29.6vw) like the hero panel does. Since `left` on an absolutely
   positioned element is relative to the containing block's edge (not the
   viewport), we offset by the difference: 29.6vw - 34.47vw = -4.87vw. */
.section-fill {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
  background: #241a0f;
  z-index: -2;
}

.section-glow {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(201, 154, 61, 0.16), transparent 72%);
  z-index: -1;
}


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

.book-section .book-num {
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #a3792f;
  margin-bottom: 0.6rem;
}

.book-section h2 {
  margin: 0 0 0.3rem;
  font-size: 2rem;
  letter-spacing: 0.03em;
  color: #2e2114;
  font-weight: 600;
}

.book-section h3 {
  margin: 0 0 1.2rem;
  font-size: 1.1rem;
  letter-spacing: 0.05em;
  color: #a3792f;
  font-weight: 500;
  font-style: italic;
}

.book-section p {
  font-family: var(--font-title);
  margin: 0;
  font-size: 1.3rem;
  font-weight: 500;
  letter-spacing: 0.015em;
  line-height: 1.65;
  color: #4a3c2a;
  max-width: 34em;
}

.origin-layout {
  display: flex;
  align-items: center;
  gap: 4rem;
  max-width: 61em;
}

.origin-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* Below this, the portrait's fixed 340px width eats a growing share of a
   shrinking row, squeezing origin-text into a narrow, very tall column of
   wrapped text. Stacks instead — image under the text (matches existing DOM
   order, so no reordering needed) — once the row can no longer give the
   text a reasonably comfortable measure. */
@media (max-width: 1500px) {
  .origin-layout {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 2.5rem;
  }
}

.origin-path {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  width: 90px;
  height: 420px;
  gap: 0;
}

/* This is a TikTok video — nobody reads the paragraph text, so the visual
   needs to do the work by itself. "Living portrait" (a HP-canon idea: framed
   portraits move) via a slow sway, plus a periodic light sweep across the
   glass, both pure CSS on the existing single image — no new asset needed. */
/* Sway animation removed on purpose: the living video inside already carries
   the movement, and the next section is about to get its own floating
   animation on the gallery items — two swaying things in a row would read
   as repetition instead of two distinct effects. */
.portrait-frame {
  position: relative;
  flex: 0 0 auto;
  width: 340px;
  filter: drop-shadow(10px 18px 22px rgba(0, 0, 0, 0.55));
}

/* Real ornate frame asset (user-supplied, already cleanly cut out with a
   transparent window — no extraction needed this time). aspect-ratio matches
   frame.png itself (700x787) so the overlay never distorts; the video sits
   in a slightly inset box so it lands inside the frame's actual opening
   instead of running full-bleed under the thick moulding. */
/* No background fill here: frame.png's outer silhouette is irregular (not a
   plain rectangle), so a background on this whole box shows as a dark
   rectangular halo wherever the ornate edge doesn't reach the box's actual
   corners. The small openwork piercings stay transparent to the page behind
   instead — a lesser issue than a visible halo around the whole frame. */
.portrait-frame-inner {
  position: relative;
  overflow: hidden;
  aspect-ratio: 700 / 787;
}

/* Inset values measured directly from frame.png's alpha channel (the actual
   transparent-window bounds), not eyeballed — see the mid-line run scan. Box
   fills the FULL window — shrinking it earlier just exposed the plain
   background fill as a bare stripe instead of showing video content there.
   The source video already has empty headroom above Dobby's head; object-
   position shifts which part of that footage shows instead of shrinking the
   box, so the gap above him is actual video, not a solid-colour gap.
   width/height:100% are load-bearing: <video> is a replaced element with its
   own intrinsic size, so with inset alone (no explicit size) it renders at
   its native resolution instead of filling the inset box — same trap as the
   background <img>s earlier today. */
.portrait-video {
  position: absolute;
  inset: 16% 17.6% 16.3% 17.9%;
  width: 64.5%;
  height: 67.7%;
  object-fit: cover;
  object-position: center 20%;
  transform: scaleX(-1);
}

.portrait-frame-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Wider swing (was 1.5°, barely visible), pivoting from the bottom edge like
   a frame hanging from a nail, not the centre — reads as "hanging and
   swaying" instead of "rotating in place". */
@keyframes portrait-sway {
  0%, 100% { transform: rotate(-4.5deg); }
  50% { transform: rotate(1deg); }
}

.origin-path-line {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}

.origin-path-snitch-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 2;
}

.origin-path-track {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.5;
  stroke-dasharray: 3 7;
  stroke-linecap: round;
  opacity: 0.45;
  vector-effect: non-scaling-stroke;
}

.snitch {
  offset-path: path('M 30,55 Q 85,150 45,240 Q 5,330 55,375');
  offset-rotate: 0deg;
  animation: fly-snitch 6.5s linear infinite alternate;
  transform: scale(1.05);
}

/* Section 4 reuses the snitch component but its track container has a very
   different aspect ratio (narrow list column vs. the 90x420 hero column), so
   it needs its own offset-path spanning the full viewBox height — kept
   separate from .snitch above so section 2 isn't affected. */
.skills-path-snitch-layer .snitch {
  offset-path: path('M 30,0 Q 85,150 45,240 Q 5,330 55,480');
  transform: scale(1.7);
  filter: drop-shadow(0 0 2.5px rgba(240, 201, 117, 0.85));
  animation-duration: 4.2s;
}

.skills-path-line .origin-path-track {
  stroke-width: 2.6;
  stroke-dasharray: 4 8;
  opacity: 0.6;
}

.snitch-body {
  fill: #f0c975;
  stroke: #241a0f;
  stroke-width: 0.5;
}

.snitch-wing {
  fill: var(--gold);
  stroke: #241a0f;
  stroke-width: 0.6;
  transform-origin: 0px 0px;
  animation: flap 0.14s ease-in-out infinite alternate;
}

.snitch-wing-vein {
  fill: none;
  stroke: #241a0f;
  stroke-width: 0.45;
  stroke-linecap: round;
  opacity: 0.7;
  transform-origin: 0px 0px;
  animation: flap 0.14s ease-in-out infinite alternate;
}

.snitch-stud {
  fill: #e0b354;
}

@keyframes fly-snitch {
  0% { offset-distance: 0%; }
  100% { offset-distance: 100%; }
}

@keyframes flap {
  0%, 100% { transform: scaleX(1) scaleY(1); }
  50% { transform: scaleX(0.35) scaleY(0.85); }
}

.origin-stop {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
}

.origin-icon {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1.5px solid var(--gold);
  background: #241a0f;
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
}

.origin-icon svg {
  width: 26px;
  height: 26px;
}

.origin-label {
  font-family: var(--font-title);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold-dim);
  text-align: center;
}

/* Placed last on purpose: .book-section h2/h3/p (equal specificity, declared
   earlier) were winning the cascade over these and silencing the light text
   color on the dark origin-section background. Keep these overrides at the
   end of the file, after the generic .book-section rules, or re-apply this
   fix if the dark-on-dark bug reappears. */
.origin-section .book-num {
  color: var(--gold);
}

.origin-section h2 {
  color: #f7ead0;
}

.origin-section p {
  color: #f7ead0;
  font-weight: 400;
}

.origin-section .origin-label {
  color: rgba(240, 226, 196, 0.85);
}

/* Section 3 ("A Zoknigyűjtemény") sits on the plain tan body background
   (no dark .section-fill needed — that IS the "lighter" tone), just a
   fade-in transition at the top blending down from section 2's dark panel,
   same technique as .hero-fade-out but inverted (dark to transparent). */
/* min-height:auto overrides .book-section's forced 100vh — the gallery's
   content is shorter than a viewport, and the forced height left a large
   dead gap between the last row of items and the section end. */
/* opacity/transform pinned on the SECTION itself (not removed) so its direct
   child .gallery-fade-in — the transition gradient — is never dimmed by the
   generic scroll-reveal. The reveal-on-scroll effect moves to
   .gallery-content-wrap below instead, so the actual items still fade/slide
   in — just without dragging the transition gradient along with them. */
.gallery-section {
  position: relative;
  border-top: none;
  min-height: auto;
  padding-top: 7rem;
  padding-bottom: 7rem;
  opacity: 1 !important;
  transform: none !important;
}

/* .gallery-fade-in is a direct child of the section, so it must NOT be
   opacity:1'd away on the section itself (that would also kill the nice
   scroll-reveal on the actual content) — instead the reveal fade moves onto
   this wrapper, which contains everything EXCEPT the transition gradient. */
.gallery-content-wrap {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.gallery-section.visible .gallery-content-wrap {
  opacity: 1;
  transform: translateY(0);
}

.gallery-fade-in {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 46%;
  background: linear-gradient(to bottom, #241a0f 0%, rgba(36, 26, 15, 0.55) 45%, transparent 100%);
  z-index: -1;
}

/* Explicit columns holding 1 / 2 / 3 items respectively — NOT css multi-column
   or a single row, both of which distribute evenly and read as tidy. The
   uneven item counts, differing column widths, differing start offsets and
   per-item --rot/--x/--w are what make it look tipped-out rather than laid
   out. Item size is set inline per item (--w), so no size classes here. */
/* No rows, no columns: a fixed-height canvas with every item free-placed at
   its own --l/--t coordinate. Column layouts kept looking tidy no matter how
   much they were nudged, and they also made the section grow tall — a fixed
   canvas height solves both. */
.gallery-grid {
  position: relative;
  height: 28rem;
  margin-top: 2.5rem;
  max-width: none;
}

.gallery-item {
  position: absolute;
  left: var(--l, 0);
  top: var(--t, 0);
  width: var(--w, 20%);
  z-index: var(--z, 1);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  transform: rotate(var(--rot, 0deg));
}

/* Enchanted-objects float, not a hover effect: this is a TikTok video, so
   nobody will actually hover anything during a passive scroll/screen
   recording — the animation has to run on its own to be seen at all. Applied
   to the image, not .gallery-item, so it layers on top of that element's
   own static --rot without fighting over the transform property. Each item
   gets its own duration/delay via nth-child so all six don't bob in sync. */
.gallery-img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
  filter: drop-shadow(6px 10px 12px rgba(43, 30, 15, 0.35));
  animation: item-float 5s ease-in-out infinite;
}

.gallery-item:nth-child(1) .gallery-img { animation-duration: 4.6s; animation-delay: 0s; }
.gallery-item:nth-child(2) .gallery-img { animation-duration: 5.3s; animation-delay: 0.5s; }
.gallery-item:nth-child(4) .gallery-img { animation-duration: 5.7s; animation-delay: 0.3s; }
.gallery-item:nth-child(6) .gallery-img { animation-duration: 5.1s; animation-delay: 1.4s; }

/* The two small sock items spin instead of just bobbing — a bit of variety
   so not everything moves the same way. */
.gallery-item:nth-child(3) .gallery-img,
.gallery-item:nth-child(5) .gallery-img {
  animation-name: item-spin-float;
}
.gallery-item:nth-child(3) .gallery-img { animation-duration: 7s; animation-delay: 1.1s; animation-direction: reverse; }
.gallery-item:nth-child(5) .gallery-img { animation-duration: 8.4s; animation-delay: 0.8s; animation-direction: alternate; }

@keyframes item-float {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-9px) rotate(2.5deg); }
}

@keyframes item-spin-float {
  0% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-9px) rotate(180deg); }
  100% { transform: translateY(0) rotate(360deg); }
}

.gallery-caption {
  font-family: var(--font-title);
  font-size: 1rem;
  font-style: italic;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: #4a3c2a;
  text-align: center;
}

/* Placed last on purpose (see the .origin-section note above) so it wins
   over the equal-specificity .book-section h2/h3 rules — the heading sits
   inside .gallery-fade-in's dark zone, needs a light color, not a glow hack. */
.gallery-section .book-num {
  color: var(--gold);
}

.gallery-section h2 {
  color: #f7ead0;
}

.gallery-section h3 {
  color: #d9c6a0;
}

/* Section 4 ("Képességek és szolgáltatások") starts light (continuing the
   gallery's tone) and darkens toward the bottom, timed to land on the third
   ("Védelmezés") item — the visual foreshadowing of the drama in section 5. */
.skills-section {
  position: relative;
  justify-content: flex-start;
  padding-top: 6rem;
  padding-bottom: 10rem;
  border-top: none;
  /* Same reason as .gallery-section above — owns .skills-fade-out. */
  opacity: 1 !important;
  transform: none !important;
}

.skills-fade-out {
  position: absolute;
  bottom: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 26%;
  background: linear-gradient(to bottom, transparent, #241a0f 90%);
  z-index: -1;
}

/* CSS Grid on purpose: the track needs to span exactly as tall as the 3
   text rows actually render (variable, text-length-dependent), and Grid's
   multi-row-span sizing computes that for real — no guessed pixel heights,
   the trap that broke every earlier version of this component. */
.skills-layout {
  --skills-icon-size: 54px;
  --skills-row-gap: 3.5rem;
  display: grid;
  grid-template-columns: var(--skills-icon-size) 1fr;
  column-gap: 1.6rem;
  row-gap: var(--skills-row-gap);
  position: relative;
  margin-top: 3rem;
  max-width: 38em;
}

.skills-row-1 { grid-row: 1; }
.skills-row-2 { grid-row: 2; }
.skills-row-3 { grid-row: 3; }

/* Spans rows 1-2, then the negative bottom margin bridges the row gap so the
   track's bottom edge lands exactly on the row-3 grid line — which is exactly
   where icon 3's top edge is (icons are align-self: start). A stretched grid
   item's height accounts for margins, so a negative one extends it. Row 3's
   own height is text-dependent and must never be part of this calculation. */
.skills-track {
  grid-column: 1;
  grid-row: 1 / 3;
  margin-bottom: calc(-1 * var(--skills-row-gap));
  position: relative;
}

/* Explicit width/height are load-bearing: <svg> is a replaced element, so with
   only `inset: 0` and auto sizing it resolves its height from the viewBox
   aspect ratio instead of stretching to the track — that is why the dashed
   line kept stopping short of the third icon. */
.skills-path-line {
  position: absolute;
  top: var(--skills-icon-size);
  left: 0;
  width: 100%;
  height: calc(100% - var(--skills-icon-size));
  display: block;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}

.skills-path-snitch-layer {
  position: absolute;
  top: var(--skills-icon-size);
  left: 0;
  width: 100%;
  height: calc(100% - var(--skills-icon-size));
  display: block;
  overflow: visible;
  pointer-events: none;
  z-index: 3;
}

.skills-icon {
  grid-column: 1;
  align-self: start;
  position: relative;
  z-index: 1;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1.5px solid var(--gold);
  background: #241a0f;
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
}

.skills-icon svg {
  width: 26px;
  height: 26px;
}

.skills-icon img {
  width: 34px;
  height: 34px;
  object-fit: contain;
  display: block;
}

.skills-body {
  grid-column: 2;
}

.skills-body h3 {
  margin: 0 0 0.5rem;
}

.skills-body p {
  margin: 0;
}

/* Section 5 ("A Végső Hőstett") — the dramatic low point. Fully dark, no
   light band; the darkening that starts at the bottom of section 4 lands
   here. Deliberately sparse: one narrow centred column, lots of air, the
   epitaph as the only ornament. */
.finale-section {
  position: relative;
  z-index: 1;
  border-top: none;
  align-items: center;
  text-align: center;
  padding-top: 8rem;
  padding-bottom: 8rem;
  /* same reason as .origin-section: this section carries its own dark
     background, so the scroll-in opacity fade would flash the light body
     colour behind it */
  opacity: 1 !important;
  transform: none !important;
}

.finale-fill,
.finale-bg-img,
.finale-vignette,
.finale-fade-top,
.finale-fade-bottom {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
}

.finale-fill {
  background: #150f08;
  z-index: -3;
}

/* The source image is already very dark with its own natural vignette, so it
   needs near-full opacity and only a light extra vignette — the original
   0.55/heavy-vignette pairing was tuned blind and would crush it to black. */
/* width:100% is load-bearing: with width:auto an <img> (a replaced element)
   sizes its box from its own aspect ratio even with left+right both set, so
   the box matched the image exactly, cover had nothing to crop, and
   object-position silently did nothing. Same trap as the SVG track earlier. */
/* An absolutely positioned box with left + right + width is over-constrained,
   so `right` is dropped. width:100% resolves against the section, but this
   image starts 4.67vw further left than the section does — hence it fell
   4.67vw short of the right edge. Add that overhang back explicitly. */
.finale-bg-img {
  right: auto;
  width: calc(100% + (34.47vw - 29.586vw));
  object-fit: cover;
  /* Was 62%, with .finale-content shifted -22% left to keep the grave
     marker clear — that margin was razor-thin (measured: the grave's left
     edge landed right where centered text's right edge would be). 55%
     instead moves the crop enough that the grave sits comfortably clear of
     centered text without needing to move the text at all. */
  object-position: 55% center;
  opacity: 0.9;
  z-index: -2;
}

.finale-vignette {
  background: radial-gradient(ellipse 70% 60% at 50% 45%, transparent 35%, rgba(10, 7, 4, 0.6) 100%);
  z-index: -1;
}

/* Meets .skills-fade-out on matching flat colour (#241a0f, the same dark
   .skills-fade-out ends on) — same reasoning as .finale-fade-bottom /
   .memoriam-fade-top: don't try to blend two unrelated images, taper both
   sides to the same flat colour and let them meet there. */
.finale-fade-top {
  height: 20%;
  background: linear-gradient(to bottom, #241a0f 0%, transparent 100%);
  z-index: 0;
}

/* Bridges into section 6 (In Memoriam) — both dark, but different background
   photos (storm vs. candles); rather than try to cross-blend two unrelated
   images, fade THIS one down to flat near-black at the seam, and fade the
   next section's image UP from flat near-black at its own top (see
   .memoriam-fade-top) — they meet on matching solid colour, not on each
   other's imagery. */
.finale-fade-bottom {
  top: auto;
  bottom: 0;
  height: 22%;
  background: linear-gradient(to bottom, transparent, #0a0704 92%);
  z-index: 0;
}

/* Halfway between fully centred and hard-left: centred, then nudged left so
   the grave marker on the right side of the background stays clear. */
.finale-content {
  max-width: 28em;
  margin: 0 auto;
}

.finale-content p {
  margin: 0 auto 1.4rem;
}

.epitaph {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.6rem;
  margin-top: 5rem;
}

.epitaph-line {
  width: 90px;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--gold), transparent);
  opacity: 0.7;
}

.epitaph p.epitaph-text {
  font-family: var(--font-title);
  font-weight: 600;
  font-size: 1.9rem;
  line-height: 1.3;
  letter-spacing: 0.03em;
  color: var(--gold);
  margin: 0 !important;
  text-shadow: 0 2px 30px rgba(201, 154, 61, 0.3);
}

.finale-section .book-num {
  color: var(--gold);
}

.finale-section h2 {
  color: #f7ead0;
}

.finale-section p {
  color: #d9c6a0;
  font-weight: 400;
}

/* Section 6 ("In Memoriam") — dark, but a different character of dark from
   the storm in section 5: warm, candlelit, dignified rather than bleak.
   Repurposes what would be a testimonials section for the same visual role
   (short attributed quotes) without any of the usual star-rating/card
   trappings. */
.memoriam-section {
  position: relative;
  z-index: 1;
  border-top: none;
  align-items: center;
  text-align: center;
  padding-top: 8rem;
  padding-bottom: 8rem;
  opacity: 1 !important;
  transform: none !important;
}

.memoriam-fill {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
  background: #150f08;
  z-index: -3;
}

/* Same over-constrained-box fix as .finale-bg-img: left+right+width together
   drop `right`, and this box starts 4.67vw left of the section, so width:100%
   alone falls 4.67vw short of the right edge. */
.memoriam-bg-img {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: auto;
  width: calc(100% + (34.47vw - 29.586vw));
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0.6;
  z-index: -2;
}

/* Darkens the CENTRE, not the edges — the text sits centred, and the previous
   radial gradient (dark edges, clear middle) darkened exactly the part
   nobody reads and left the text's own backdrop at full brightness. */
.memoriam-vignette {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
  background: radial-gradient(ellipse 55% 50% at 50% 50%, rgba(8, 5, 3, 0.8) 0%, rgba(8, 5, 3, 0.4) 60%, rgba(8, 5, 3, 0.65) 100%);
  z-index: -1;
}

/* Meets .finale-fade-bottom on matching flat near-black — see the comment
   there. */
.memoriam-fade-top {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 20%;
  background: linear-gradient(to bottom, #0a0704 0%, transparent 100%);
  z-index: 0;
}

/* Bridges into section 7 (Záró CTA), the first light section since the
   hero — same transparent-to-tan technique used at every other dark→light
   seam on the page (.hero-fade-out, the gallery-section top fade). */
.memoriam-fade-bottom {
  position: absolute;
  bottom: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 12%;
  background: linear-gradient(to bottom, transparent, #c9b48a);
  z-index: 0;
}

.memoriam-content {
  max-width: 40em;
  margin: 0 auto;
}

.memoriam-quote {
  margin: 0;
}

.memoriam-quote-main {
  margin-top: 2rem;
}

/* Simplified again: Cormorant italic + giant decorative quote mark read as
   flimsy against the busy candle-engraving texture and came across cheap,
   not elegant. Switched to Cinzel (the typeface already proven everywhere
   else on the site) at a heavy weight with a hard, high-contrast shadow —
   plain, legible, no ornament — instead of a second decorative device. */
p.memoriam-main-text {
  font-family: var(--font-title);
  font-weight: 700;
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: #f7ead0;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.9), 0 4px 24px rgba(0, 0, 0, 0.85);
  margin: 0 0 1.4rem;
}

.memoriam-quote-main cite {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-style: normal;
  color: var(--gold);
}

/* The horizontal lines are on this element, widened past the text via
   negative margin + matching padding so they visibly overhang the quotes on
   both sides instead of ending flush with them. The vertical divider is a
   pseudo-element on THIS box (top:0/bottom:0 of the full padded height), not
   on the second quote — anchoring it to the quote left it short of both
   horizontal lines, since the quote's own box doesn't reach the padding. */
.memoriam-supporting {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 3rem;
  margin-top: 4.5rem;
  margin-left: -2.5rem;
  margin-right: -2.5rem;
  padding: 3rem 2.5rem;
  border-top: 1px solid rgba(201, 154, 61, 0.2);
  border-bottom: 1px solid rgba(201, 154, 61, 0.2);
}

.memoriam-supporting::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: rgba(201, 154, 61, 0.2);
}

.memoriam-supporting .memoriam-quote {
  max-width: 26em;
}

.memoriam-supporting p {
  font-family: var(--font-title);
  font-size: 1rem;
  font-style: italic;
  font-weight: 400;
  line-height: 1.5;
  color: #e8d9b8;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
  margin: 0 0 0.6rem;
}

.memoriam-supporting cite {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-style: normal;
  color: #d9b866;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}

/* Small "stamp" portraits next to each attributed name — minimal line-art
   badges echoing the skills-icon circles, sized down since these are a
   secondary flourish, not a primary content icon. */
.memoriam-attribution {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
}

/* Bumped up a second time — this is a TikTok scroll-recording, so a badge
   that reads clearly at a glance matters more than restraint; the original
   28px/34px sizing was too subtle to register during a fast scroll. */
.memoriam-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(201, 154, 61, 0.55);
  background: rgba(36, 26, 15, 0.6);
  color: var(--gold);
}

.memoriam-icon svg {
  width: 16px;
  height: 16px;
}

.memoriam-icon img {
  width: 40px;
  height: 40px;
  object-fit: contain;
  display: block;
}

.memoriam-icon-lg {
  width: 64px;
  height: 64px;
}

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

.memoriam-icon-lg img {
  width: 50px;
  height: 50px;
}

/* Placed last (see the .origin-section note above): .book-section .book-num
   is equal specificity and declared earlier, so without this the muted
   brown meant for the light sections would win here too. */
.memoriam-section .book-num {
  color: var(--gold);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}

/* Section 7 — the closing CTA. Returns to the light/warm tone (no dark fill,
   just the plain body background, same as section 3) to end on hope and
   resolution rather than grief — the last beat of the light/dark rhythm. */
/* Back to a full 100vh (wanted for clean video/photo framing), but
   justify-content:flex-start instead of the inherited centre — that keeps
   the text where it already sits near the top, and pushes the leftover
   empty space to the BOTTOM only, instead of splitting it above and below. */
/* Wraps the CTA + footer sections so the two together occupy one screen
   (100vh) instead of the CTA alone claiming a full 100vh and the footer
   tacking on extra scroll length below it. min-height, not height: on a
   short/small monitor the CTA's own content (candle + heading + paragraph +
   button) can need more room than "100vh minus footer" — height:100vh plus
   min-height:0 on .cta-section let that content overflow behind the footer's
   opaque background instead of the frame just growing taller to fit it. */
.closing-frame {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

.cta-section {
  flex: 1 1 auto;
  /* auto, not the default (unset falls back to .book-section's inherited
     min-height:100vh, which is the bug this comment is fixing) and not 0
     (that reintroduces the earlier clipping bug) — auto floors this at the
     content's own natural height, letting it actually shrink to share the
     100vh with the footer instead of always claiming a full screen itself. */
  min-height: auto;
  /* was flex-start (pinned to top, all leftover space pooling below the
     button) — now that CTA+footer together are locked to exactly 100vh,
     that read as a big dead gap right above the footer. Centered instead,
     so the leftover space splits evenly above/below. */
  justify-content: center;
  align-items: center;
  text-align: center;
  padding-top: 6rem;
  border-top: none;
  /* Same reason as the sections above — keeps the memoriam→cta seam (which
     lives on the PREVIOUS section, .memoriam-fade-bottom) from appearing to
     "catch up" after this section's own scroll-reveal finishes. */
  opacity: 1 !important;
  transform: none !important;
}

.cta-section .book-num {
  color: var(--gold-dim);
}

.cta-section h2 {
  max-width: 20em;
}

.cta-section p {
  max-width: 30em;
  margin-left: auto;
  margin-right: auto;
}

/* Deliberately restrained, not the loud solid-gold hero CTA — an outline
   button so the closing note reads as dignified, not salesy. */
.cta-button {
  display: inline-block;
  margin-top: 2.5rem;
  font-family: var(--font-title);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: #4a3c2a;
  background: transparent;
  border: 1.5px solid #4a3c2a;
  padding: 0.9rem 2.2rem;
  border-radius: 3px;
  text-decoration: none;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.cta-button:hover {
  background: var(--gold);
  border-color: var(--gold);
  color: #241a0f;
  transform: translateY(-1px);
}

/* Section 8 — closing legal/promo bar. A normal book-section like every
   other one, just short (min-height:auto) since it's a footer, not a beat
   in the story. */
.site-footer-section {
  position: relative;
  z-index: 1;
  min-height: auto;
  border-top: none;
  padding: 2.2rem 1.5rem;
  text-align: center;
  opacity: 1 !important;
  transform: none !important;
}

/* Same bleed technique as .finale-fill / .memoriam-fill — pulls the dark
   panel past the section's own left padding so it reaches the bolt-divider
   tip instead of stopping short at the content column's edge. */
.site-footer-fill {
  position: absolute;
  top: 0;
  left: calc(29.586vw - 34.47vw);
  right: 0;
  height: 100%;
  background: #241a0f;
  z-index: -3;
}

p.footer-disclaimer {
  max-width: 40em;
  margin: 0 auto 0.8rem;
  font-size: 0.75rem;
  line-height: 1.6;
  color: #8a7a5e;
}

p.footer-promo {
  max-width: none;
  margin: 0 auto;
  font-size: 0.85rem;
  font-weight: 500;
  color: #d9c6a0;
}

.footer-promo a {
  color: var(--gold);
  text-decoration: none;
  border-bottom: 1px solid rgba(201, 154, 61, 0.5);
}

.footer-promo a:hover {
  color: #e0b354;
  border-bottom-color: #e0b354;
}

.cta-layout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.cta-candle {
  flex: 0 0 auto;
  width: 46px;
  filter: drop-shadow(0 6px 14px rgba(74, 60, 42, 0.3));
  animation: candle-float 5s ease-in-out infinite;
}

.cta-candle svg {
  display: block;
  width: 100%;
  height: auto;
}

.cta-candle-flame {
  transform-origin: 40px 46px;
  animation: candle-flicker-flame 2.2s ease-in-out infinite;
}

.cta-candle-flame-inner {
  transform-origin: 40px 45px;
  animation: candle-flicker-flame 1.7s ease-in-out infinite reverse;
}

@keyframes candle-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

@keyframes candle-flicker-flame {
  0%, 100% { transform: scaleY(1) scaleX(1) rotate(0deg); }
  30% { transform: scaleY(0.94) scaleX(1.05) rotate(-1.5deg); }
  60% { transform: scaleY(1.05) scaleX(0.96) rotate(1.5deg); }
}

/* ==========================================================================
   GLOBAL: section-boundary seam fix (all viewport sizes)
   Found via pixel-sampling actual screenshots: a genuine 1-device-pixel
   line at several section boundaries — the gradient/fill already at its
   target color one row before the edge, then one off-color row, then
   correct again. A subpixel rounding gap between two abutting absolutely-
   positioned layers. Originally patched as a mobile-only fix on the
   assumption the diagonal bolt-divider was masking the same seam on
   desktop by crossing straight through it — but that only holds at the one
   viewport width (1680px) it was measured at; at other desktop sizes
   (1440px "Laptop L" reported specifically) the same subpixel gap shows up
   because the math doesn't happen to land on a whole pixel there either.
   Extending each fade/fill 2px past the section edge it sits on guarantees
   overlap so there's no exact seam left to gap at ANY width — not
   re-derived per breakpoint every time someone tests a new screen size. */
.hero-fade-out,
.skills-fade-out,
.finale-fade-bottom,
.memoriam-fade-bottom {
  bottom: -2px;
}

.gallery-fade-in,
.finale-fade-top,
.memoriam-fade-top {
  top: -2px;
}

.section-fill,
.section-glow {
  top: -2px;
  height: calc(100% + 2px);
}

/* ==========================================================================
   NARROW DESKTOP (1201px–1470px): graceful compression
   Between the golden 1680px reference and true mobile (768px), the split-
   screen layout stays put but .top-bar's width (100vw - 34.47vw) is
   vw-based, while its content (wordmark + 6 nav links + CTA) is rem-based
   and the fluid rem stops shrinking below ~1470px viewport width (hits its
   14px floor). Below that point the bar keeps shrinking but the content
   doesn't, so they collide — confirmed at 1920x1080 @150% browser zoom
   (1280px effective width): ~17px short, enough to overlap "Rólam" into
   the wordmark and clip the CTA button's edge. This tier trims gaps/
   padding/font-size to buy back room before the hamburger fallback below
   takes over (deficit grows too large for trimming alone under ~1200px). */
@media (max-width: 1470px) and (min-width: 1201px) {
  .top-bar {
    padding: 1.8rem 1.5rem;
  }

  .nav-links {
    gap: 1rem;
    margin-right: 1rem;
  }

  .nav-links a {
    font-size: 0.82rem;
  }

  .nav-cta {
    padding: 0.6rem 0.9rem;
  }
}

/* ==========================================================================
   NARROW DESKTOP (769px–1200px): hamburger fallback
   Reuses the hamburger already built for the 768px mobile tier below, but
   keeps the desktop split-screen layout intact (.top-bar stays at its
   34.47vw offset) instead of switching to the full portrait redesign.
   .nav-links.mobile-open is positioned absolute against .top-bar itself
   (a fixed, and therefore positioned, ancestor) so left:0/width:100% fills
   the bar's own 34.47vw–100vw span automatically, with no need to repeat
   that offset here. (right:0 instead of width:100% measured wrong here —
   the flex column shrink-wrapped to a centered auto-width box instead of
   stretching, so width:100% is used explicitly.) */
@media (max-width: 1200px) and (min-width: 769px) {
  .nav-links {
    display: none;
  }

  /* display:none removes .nav-links from grid auto-placement entirely, so
     without this, .top-bar-actions slides up into the now-vacant middle
     ("auto") column instead of the last one — still justify-self:end, but
     pinned to that column's own right edge, which sits in the middle of the
     bar instead of at its actual right edge. Forcing it back into the
     explicit last column keeps it flush right regardless of what nav-links
     is doing. */
  .top-bar-actions {
    grid-column: 3;
  }

  .nav-links.mobile-open {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    margin-right: 0;
    background: #241a0f;
    padding: 1.25rem 2.5rem;
    gap: 1rem;
  }

  .nav-links.mobile-open a {
    color: #f0e2c4;
    font-size: 0.95rem;
  }

  .mobile-menu-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    background: transparent;
    border: none;
    cursor: pointer;
    color: rgba(240, 226, 196, 0.85);
  }

  .mobile-menu-toggle svg {
    width: 24px;
    height: 24px;
  }

  .mobile-menu-toggle:hover {
    color: var(--gold);
  }
}

/* ==========================================================================
   MOBILE (max-width: 768px)
   The desktop layout is a side-by-side split: a sticky canvas column (30%)
   next to a scrolling text column (70%), with every section's dark fill
   hand-aligned to the bolt-divider that sits on that column boundary. None
   of that works in portrait — there's no room for a side column, and the
   bolt-divider is a decoration for a boundary that no longer exists. Instead:
   the canvas becomes a fixed strip pinned to the top of the viewport, the
   text scrolls underneath it at full width, and the bolt + every fill/fade
   element that was aligned to it collapses to a plain full-width dark panel.
   ========================================================================== */
@media (max-width: 768px) {
  .bolt-divider {
    display: none;
  }

  /* Canvas is a fixed strip pinned to the top of the viewport instead of a
     sticky side column — simpler than trying to keep position:sticky
     working once .sticky-col and .content-col are no longer two columns of
     equal height in the same grid row. */
  .layout {
    display: block;
  }

  /* Solid background is load-bearing: the canvas is centered/sized by
     resizeCanvas() with empty space around it, and without an opaque strip
     background that empty space was see-through. Tan (#c9b48a), not dark —
     the original animation background, same tone as the desktop sticky
     column. */
  .sticky-col {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--mobile-strip-h, 16vh);
    background: #c9b48a;
    z-index: 4;
  }

  .sticky-inner {
    position: static;
    height: 100%;
    justify-content: flex-start;
    padding-left: 1.25rem;
  }

  .content-col {
    padding-left: 0;
    margin-top: var(--mobile-strip-h, 16vh);
  }

  /* Horizontal counterpart to .bolt-divider, centred on the strip/content
     boundary the same way the desktop divider centres on the column
     boundary (translateX(-50%) there, translateY(-50%) here). */
  .bolt-divider-mobile {
    display: block;
    position: fixed;
    top: var(--mobile-strip-h, 16vh);
    left: 0;
    width: 100vw;
    height: 4vh;
    transform: translateY(-50%);
    z-index: 5;
    pointer-events: none;
  }

  /* Every one of these was offset to align with the bolt-divider's position
     on the desktop column boundary (29.586vw/34.47vw math) — with no side
     column left to align to, they just become plain full-width panels.
     (section-fill/section-glow and skills-fade-out were missed in the first
     pass — same calc(), same fix, just easy to miss since they're not named
     like the others.) */
  .hero-dark-fill,
  .hero-castle-img,
  .hero-fade-out,
  .section-fill,
  .section-glow,
  .gallery-fade-in,
  .skills-fade-out,
  .finale-fill,
  .finale-bg-img,
  .finale-vignette,
  .finale-fade-top,
  .finale-fade-bottom,
  .memoriam-fill,
  .memoriam-bg-img,
  .memoriam-vignette,
  .memoriam-fade-top,
  .memoriam-fade-bottom,
  .site-footer-fill {
    left: 0 !important;
    width: 100% !important;
  }

  /* Folded into the same strip as the canvas instead of a separate
     overlapping bar — transparent background, same height as .sticky-col
     so it shares that one strip rather than reading as a second layer on
     top of it. Keeps the desktop 1fr/auto/auto grid but repurposes it:
     col1 stays empty (balances the hamburger so the wordmark is genuinely
     centred, not just left-of-center), wordmark moves into the centre
     column, hamburger takes the end column. */
  .top-bar {
    left: 0;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    height: var(--mobile-strip-h, 16vh);
    padding: 0 1.25rem;
    background: transparent;
    z-index: 7;
  }

  .top-bar .wordmark {
    grid-column: 2;
    justify-self: center;
    font-size: 4.5rem;
  }

  /* Full nav-links list doesn't fit a phone-width bar — hidden by default,
     toggled open as a full-width dropdown by .mobile-menu-toggle below. */
  .nav-links {
    display: none;
  }

  .nav-links.mobile-open {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: var(--mobile-strip-h, 16vh);
    left: 0;
    right: 0;
    width: 100vw;
    margin-right: 0;
    grid-column: 1 / -1;
    background: #241a0f;
    padding: 1.5rem 1.75rem;
    gap: 1.1rem;
    z-index: 8;
  }

  .nav-links a {
    color: #f0e2c4;
    font-size: 1.05rem;
    text-align: left;
    width: 100%;
  }

  .top-bar-actions {
    grid-column: 3;
    justify-self: end;
  }

  /* CTA button dropped — no room for a third element alongside the
     hamburger, and it's a duplicate of the finale/CTA section's own
     button a couple of scrolls away. */
  .top-bar-actions .nav-cta {
    display: none;
  }

  .mobile-menu-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    background: none;
    border: none;
    color: #f0e2c4;
  }

  .mobile-menu-toggle svg {
    width: 33px;
    height: 33px;
  }

  /* Stacked in the same right-hand column now, not side by side: language
     switcher pinned to the true top-right corner, hamburger fixed
     independently (not a flex child of .top-bar any more, so it needs its
     own top offset) roughly centred in the strip space left below it. */
  .lang-switch-corner {
    top: 0.9rem;
    left: auto;
    right: 1.25rem;
    z-index: 7;
  }

  /* Centred on the whole strip's own vertical midpoint, not the space left
     below the lang-switch — independent of wherever that happens to sit. */
  .mobile-menu-toggle {
    position: fixed;
    top: calc(var(--mobile-strip-h, 16vh) / 2);
    right: 1.25rem;
    transform: translateY(-50%);
    z-index: 7;
  }

  .hero-block {
    padding: 3rem 1.5rem;
  }

  .book-section {
    padding: 3rem 1.25rem;
  }

  .origin-layout {
    gap: 2rem;
  }

  .portrait-frame {
    width: min(340px, 78vw);
  }

  /* Desktop nudges this 22% left to dodge a grave-marker element in the
     background art on a wide canvas — on a narrow single-column phone
     layout that just pushes the text off-centre/toward the edge instead. */
  .finale-content {
    transform: none;
  }

  /* Desktop's 55% was tuned for a wide, short box (cover-scales to ~1.1x,
     most of the source stays visible). Mobile's box is narrow and tall —
     cover has to scale to ~0.9x the source HEIGHT, so only about a quarter
     of the source's width is ever visible at all, and 55% left that sliver
     centred over open ocean, nowhere near the grave (right side of the
     source). Pushed further right so the visible sliver actually contains
     the grave and cottage instead. */
  .finale-bg-img {
    object-position: 72% center;
  }

  /* Desktop's gallery/skills headings are left-aligned (paragraph-style
     columns) — the other sections (finale/memoriam/cta) are centered on
     desktop already, and single-column mobile reads more consistent with
     everything centered to match them, gallery and skills included. */
  .gallery-content-wrap {
    text-align: center;
  }

  /* The desktop gallery is free-placed items scattered across a fixed-height
     canvas, tuned for a wide column — on a narrow phone that either crushes
     the items or lets them collide. A plain 2-column grid instead; the float
     animation on .gallery-img still runs either way. */
  .gallery-grid {
    height: auto;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
  }

  .gallery-item {
    position: static;
    width: 100% !important;
    transform: none !important;
  }

  .gallery-img {
    animation: item-float 5s ease-in-out infinite;
  }

  /* Was grid-template-columns:1fr, but the icon/body pairs keep their
     desktop grid-row assignments (row-1/2/3) — in a single column that put
     each icon and its own body text in the same cell, stacked on top of
     each other. Flex-column instead: DOM order alone (icon, body, icon,
     body, icon, body) gives correct vertical stacking with no row/column
     bookkeeping needed. */
  /* Desktop's padding-bottom:10rem leaves room for the decorative track
     graphic (hidden on mobile) below the last item — without that graphic
     it was just dead space, and combined with .skills-fade-out's bottom 26%
     it pushed the actual bottom edge far enough down that real paragraph
     text (3 stacked icon/heading/paragraph groups need real height) ended
     up inside the fade-out zone, reading as "faded out mid-sentence". */
  /* min-height:auto — unlike gallery-section, skills-section never had this
     override; it always inherited .book-section's full min-height. Fine on
     desktop (2-column layout, content fits within one screen easily), but
     mobile's stacked layout (3x icon+heading+paragraph) genuinely needs more
     room than min-height:100svh minus the fade-out zone provides — forcing
     the fixed height pushed real text into that fade zone instead of
     leaving it in the (now correctly content-sized) padding below. */
  .skills-section {
    min-height: auto;
    text-align: center;
    padding-bottom: 5.5rem;
  }

  /* height:26% (desktop) is sized against the section's own total height —
     harmless when content is short relative to that height, but mobile's
     stacked content occupies most of it, so ANY percentage-based fade
     height overlaps real text somewhere. Fixed pixels instead, sized to sit
     within the padding-bottom above (4rem) regardless of how tall the
     actual content ends up being. */
  .skills-fade-out {
    height: 5rem !important;
  }

  .skills-layout {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.75rem;
    /* align-items:center here only centers skills-layout's OWN children —
       it says nothing about how skills-layout itself sits inside
       .skills-section. Desktop's max-width:38em (532px) still applies and
       is narrower than the viewport starting around iPad width (768px),
       so without an explicit margin the whole box (icons, headings, body
       text, all of it) sat flush left instead of centered — invisible on
       a phone where 532px is wider than the screen anyway. */
    margin-left: auto;
    margin-right: auto;
  }

  .skills-track {
    display: none;
  }

  .skills-icon {
    /* Desktop sets align-self:start (icon pinned to the top of its grid
       row) — that overrides the container's align-items:center for this
       one item specifically, which is why it stayed left-aligned instead
       of centering with everything else. */
    align-self: center;
    margin-top: 1.5rem;
  }

  .skills-icon:first-child {
    margin-top: 0;
  }

  .memoriam-supporting {
    flex-direction: column;
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding: 2rem 1.25rem;
  }

  .memoriam-supporting::after {
    display: none;
  }

  /* The 100vh-combined CTA+footer sizing was calibrated for the fixed
     1680x1050 recording setup, not the wide range of phone screen heights —
     let both sections just size to their own content on mobile instead. */
  .closing-frame {
    min-height: auto;
  }

  .cta-section {
    padding-top: 3rem;
  }
}

