/* ==========================================================================
   San Pedro — Luxury Editorial Minimalist Ghost theme
   Two modes on <html data-theme="human|cyber">: a clean high-contrast
   editorial "Human" mode and a neon "Cyber / Robot" mode, switched via the
   robot toggle in partials/header.hbs (behavior in assets/js/main.js).
   ========================================================================== */

:root[data-theme="human"] {
    --bg-primary: #ffffff;
    --bg-elevated: #fafaf8;
    --surface-border: #e6e3da;
    --text-primary: #111111;
    --text-secondary: #47453f;
    --text-muted: #8b8677;
    /* Unshadowed aliases of the three above — .components-page-inner
       (when Components Bg Transparent is on) redefines --text-primary/etc
       to a fixed card-appropriate color, so a rule inside it can't get
       back to "whatever the sitewide mode's real text color is" through
       var(--text-primary) itself. These give it something to reference. */
    --site-text-primary: #111111;
    --site-text-secondary: #47453f;
    --site-text-muted: #8b8677;
    --accent-color: #111111;
    --accent-soft: rgba(17, 17, 17, 0.06);
    --code-bg: #f3f1ea;
    --shadow-sm: 0 1px 2px rgba(17, 17, 17, 0.06);
    --shadow-md: 0 12px 32px rgba(17, 17, 17, 0.08);
    --shadow-lg: 0 30px 70px rgba(17, 17, 17, 0.14);
    --font-heading: 'Playfair Display', 'Georgia', serif;
    --scanline-opacity: 0;
}

:root[data-theme="cyber"] {
    --bg-primary: #0a0c10;
    --bg-elevated: #10141b;
    --surface-border: #1d2733;
    --text-primary: #e6edf3;
    --text-secondary: #a7b4c2;
    --text-muted: #67788c;
    --site-text-primary: #e6edf3;
    --site-text-secondary: #a7b4c2;
    --site-text-muted: #67788c;
    --accent-color: #00f0ff;
    --accent-soft: rgba(0, 240, 255, 0.12);
    --code-bg: #0a0e14;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-md: 0 12px 32px rgba(0, 240, 255, 0.12);
    --shadow-lg: 0 30px 80px rgba(0, 240, 255, 0.18);
    --font-heading: 'Space Grotesk', 'Inter', sans-serif;
    --scanline-opacity: 0.05;
}

:root {
    --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
    --content-width: 1180px;
    --radius-md: 14px;
    --ease: cubic-bezier(0.25, 1, 0.5, 1);
}

/* == Reset & base == */
*, *::before, *::after { box-sizing: border-box; }
html {
    background: var(--bg-primary);
    color: var(--text-primary);
    font-family: var(--font-body);
    line-height: 1.7;
    scroll-behavior: smooth;
    transition: background-color 0.4s var(--ease), color 0.4s var(--ease);
}
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; position: relative; }
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: 700; line-height: 1.15; margin: 0; }
p { margin: 0; }

.wrap { width: 100%; max-width: var(--content-width); margin: 0 auto; padding: 0 28px; }
.wrap--narrow { max-width: 780px; }

.eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.eyebrow i { color: var(--accent-color); }

/* == Cyber-mode scanline overlay ==
   Very low-opacity moving horizontal lines across the whole viewport —
   subtle in "cyber" mode, invisible (opacity 0) in "human" mode via the
   --scanline-opacity token, so this one rule covers both states. */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 200;
    background: repeating-linear-gradient(
        0deg,
        rgba(0, 240, 255, 1) 0px,
        rgba(0, 240, 255, 1) 1px,
        transparent 1px,
        transparent 3px
    );
    opacity: var(--scanline-opacity);
    animation: scanline-drift 9s linear infinite;
    transition: opacity 0.4s var(--ease);
}
@keyframes scanline-drift {
    from { background-position-y: 0; }
    to { background-position-y: 200px; }
}

/* == Scroll reveal (generic — see assets/js/main.js) ==
   .reveal-text is applied directly in markup (section headings, archive
   headers, the cheatsheet content wrapper). .post-card's own reveal state
   lives with its base rules further down, since it needs to coexist with
   the hover-lift transform. */
.reveal-text { transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); }
.reveal-text:not(.is-revealed) { opacity: 0; transform: translateY(20px); }

/* == Reading progress bar (single post) == */
.reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0%;
    background: var(--accent-color);
    z-index: 300;
    transition: width 0.1s linear;
}
:root[data-theme="cyber"] .reading-progress { box-shadow: 0 0 8px var(--accent-color); }

/* == Header / nav == */
.site-header {
    position: sticky;
    top: 0;
    z-index: 100;
    padding: 28px 0;
    background: color-mix(in srgb, var(--bg-primary) 85%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--surface-border);
}
/* 3-column layout (menu+search / logo / robot toggle), Louis Vuitton-style */
.site-header-grid { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px; }
.site-logo { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 700; letter-spacing: 0.02em; justify-self: center; }
.header-left { display: flex; align-items: center; gap: 12px; justify-self: start; }
.header-right { display: flex; align-items: center; gap: 14px; justify-self: end; }

/* == Header buttons: menu + search, shared pill style (Louis Vuitton-style:
   no persistent nav bar, just triggers that open an overlay — at every
   breakpoint, not only on mobile) == */
.header-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    border: 1px solid var(--surface-border);
    padding: 8px 16px;
    border-radius: 999px;
    color: var(--text-secondary);
    font-size: 1rem;
    transition: border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.header-btn:hover { border-color: var(--accent-color); color: var(--accent-color); }
.header-btn-label {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}
.menu-toggle-icon { display: flex; flex-direction: column; gap: 4px; width: 18px; }
.menu-toggle-icon span {
    display: block;
    height: 1.5px;
    background: var(--text-primary);
    transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.menu-toggle.is-open .menu-toggle-icon span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.menu-toggle.is-open .menu-toggle-icon span:nth-child(2) { opacity: 0; }
.menu-toggle.is-open .menu-toggle-icon span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

/* == Slide-out menu drawer (left side, Louis Vuitton-style) == */
.site-menu-overlay { position: fixed; inset: 0; z-index: 150; pointer-events: none; }
.site-menu-overlay.is-open { pointer-events: auto; }

.site-menu-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity 0.35s var(--ease);
}
.site-menu-overlay.is-open .site-menu-backdrop { opacity: 1; }

.site-menu-panel {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: min(420px, 88vw);
    background: var(--bg-primary);
    box-shadow: var(--shadow-lg);
    padding: 32px 36px;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 0.4s var(--ease);
}
.site-menu-overlay.is-open .site-menu-panel { transform: translateX(0); }

.site-menu-close {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    color: var(--text-primary);
    margin-bottom: 48px;
}
.site-menu-close i { font-size: 1rem; }

/* Horizontal, wrapping — Home | Tech Blog | Cheatsheets | About, per
   Tyler's 2026-08-19 feedback (was a vertical stack).

   Ghost's {{#navigation}} helper renders its own <ul class="nav"><li>
   wrapper around whatever markup goes inside the block — it's not a bare
   iterator, so .site-menu-nav's actual direct child is one <ul>, not four
   <a> tags. The flex row (and the divider styling, moved from a:last-child
   to li:last-child since every <a> is now the only/"last" child of its
   own <li>) has to target that real structure. Found 2026-08-19 after
   this exact bug shipped silently here, on ITOps's main nav, and on
   tyler-tech-theme too. */
.site-menu-nav .nav { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; row-gap: 12px; list-style: none; margin: 0; padding: 0; }
.site-menu-nav li {
    padding-right: 16px;
    margin-right: 16px;
    border-right: 1px solid var(--surface-border);
}
.site-menu-nav li:last-child { padding-right: 0; margin-right: 0; border-right: 0; }
.site-menu-nav a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-body);
    font-size: 1.05rem;
    font-weight: 500;
    color: var(--text-primary);
    transition: color 0.2s var(--ease);
}
.site-menu-nav a:hover { color: var(--accent-color); }
.site-menu-nav a i { width: 1.1em; text-align: center; color: var(--accent-color); font-size: 0.75em; }

/* == Tag cloud (drawer menu) == */
.tag-cloud { margin-top: 40px; padding-top: 32px; border-top: 1px solid var(--surface-border); }
.tag-cloud-heading {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 18px;
}
.tag-cloud-heading i { color: var(--accent-color); }
.tag-cloud-items { display: flex; flex-wrap: wrap; gap: 10px; }
.tag-cloud-item {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-secondary);
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    padding: 7px 14px;
    border-radius: 999px;
    transition: color 0.2s var(--ease), background-color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.tag-cloud-item:hover { color: var(--bg-primary); background: var(--accent-color); border-color: var(--accent-color); }
.tag-cloud-item i { font-size: 0.85em; }
body.menu-open { overflow: hidden; }

/* == Robot theme toggle ("The Cybernetic Core") == */
.robot-toggle-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    border: 1px solid var(--surface-border);
    padding: 8px 14px;
    border-radius: 999px;
    transition: all 0.3s var(--ease);
}
.robot-head { position: relative; display: flex; align-items: center; justify-content: center; }

/* Inline SVG robot (partials/header.hbs). Replaced the Font Awesome glyph +
   .robot-status-dot so the head, antenna and eyes animate independently; the
   eyes now carry the theme-state colour the dot used to.

   IMPORTANT — transform-box: the head and antenna use view-box, NOT fill-box.
   Under fill-box a transform-origin is measured from the element's OWN bbox
   corner, so the viewBox coordinates below would resolve to (20.2, 26.5) for
   the head (below the chin) and (30.3, 8.9) for the antenna (13px clear of it),
   hinging both off the wrong point. view-box makes the coordinates mean what
   they say. .robot-eye deliberately keeps fill-box, because "center" is
   correct there for the blink. */
.robot-svg {
    width: 28px;
    height: 28px;
    overflow: visible;           /* hover tilt + antenna sway exceed the viewBox */
    color: var(--text-secondary);
    display: block;
}
.robot-eye {
    fill: #3b82f6;               /* same blue the old .robot-status-dot used */
    transform-origin: center;
    transform-box: fill-box;
    animation: robot-blink 4.8s ease-in-out infinite;
}
.robot-eye-right { animation-delay: 0.12s; }
.robot-visor, .robot-scan { opacity: 0; fill: #00f0ff; }
.robot-mark {
    transform-origin: 16px 18px;
    transform-box: view-box;
    animation: robot-idle 3.4s ease-in-out infinite;
}
.robot-antenna {
    transform-origin: 16px 8px;
    transform-box: view-box;
    animation: robot-antenna-idle 2.2s ease-in-out infinite;
}

/* Hover: the mascot leans and the antenna wiggles faster. */
.robot-toggle-btn:hover .robot-mark { animation: robot-hover 0.7s ease-in-out infinite; }
.robot-toggle-btn:hover .robot-antenna { animation: robot-antenna-hover 0.45s ease-in-out infinite; }

@keyframes robot-idle {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-1.2px); }
}
@keyframes robot-hover {
    0%, 100% { transform: rotate(-8deg) translateY(-1px); }
    50% { transform: rotate(8deg) translateY(-2px); }
}
@keyframes robot-antenna-idle {
    0%, 100% { transform: rotate(-4deg); }
    50% { transform: rotate(4deg); }
}
@keyframes robot-antenna-hover {
    0%, 100% { transform: rotate(-14deg); }
    50% { transform: rotate(14deg); }
}
@keyframes robot-blink {
    0%, 92%, 100% { transform: scaleY(1); }
    94% { transform: scaleY(0.12); }
}
@keyframes robot-scan {
    0% { opacity: 0.15; transform: translateY(-6px); }
    50% { opacity: 0.85; }
    100% { opacity: 0.15; transform: translateY(8px); }
}
@keyframes robot-tip-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

/* Ambient loops only. The .robot-activate click pulse is deliberately left
   alone — it is user-initiated feedback, not ambient motion. */
@media (prefers-reduced-motion: reduce) {
    .robot-mark, .robot-antenna, .robot-eye, .robot-scan, .robot-antenna-tip { animation: none; }
}
.theme-label { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }

:root[data-theme="cyber"] .robot-toggle-btn {
    border-color: var(--accent-color);
    box-shadow: 0 0 14px rgba(0, 240, 255, 0.45);
}
:root[data-theme="cyber"] .robot-svg { color: var(--accent-color); }
:root[data-theme="cyber"] .robot-eye { fill: var(--accent-color); }
:root[data-theme="cyber"] .robot-visor { opacity: 0.35; }
:root[data-theme="cyber"] .robot-scan { animation: robot-scan 1.6s linear infinite; }
:root[data-theme="cyber"] .robot-antenna-tip { animation: robot-tip-pulse 1.1s ease-in-out infinite; }
.robot-toggle-btn.robot-activate { animation: robot-pulse 0.4s var(--ease); }
@keyframes robot-pulse {
    0% { transform: scale(1); }
    40% { transform: scale(1.12); }
    100% { transform: scale(1); }
}

/* == Kinetic text (generic, not tied to any specific page — see
   [data-kinetic-text] in assets/js/main.js. Currently unused since the
   homepage hero was removed 2026-08-19, but kept as a ready-to-use theme
   capability: add data-kinetic-text to any heading to enable it.) == */
.kinetic-letter {
    display: inline-block;
    opacity: 0;
    transform: translateY(0.6em);
    animation: kinetic-rise 0.7s var(--ease) forwards;
    /* Stagger is a token so a page can tune it: a long title on a
       frequently-revisited page wants a tighter cascade than a one-off
       hero. Default stays 35ms so the About page is unchanged. */
    animation-delay: calc(var(--i, 0) * var(--kinetic-stagger, 35ms));
}

/* Anti-flash (2026-08-22). main.js builds this effect by reading the
   heading's text and then clearing it — so without this the title paints
   normally, blanks, then animates back in, which reads as a glitch.
   Hiding it until JS adds .kinetic-ready removes that.
   The failsafe animation matters: if main.js never runs (JS disabled, a
   script error, a failed asset fetch) the heading would otherwise stay
   invisible forever. This reveals it after 1.5s regardless, so the worst
   case is a late title rather than a missing one. */
[data-kinetic-text] { visibility: hidden; animation: kinetic-failsafe 0s 1.5s forwards; }
[data-kinetic-text].kinetic-ready { visibility: visible; animation: none; }
@keyframes kinetic-failsafe { to { visibility: visible; } }
.kinetic-letter.is-space { width: 0.3em; }
@keyframes kinetic-rise {
    to { opacity: 1; transform: translateY(0); }
}

/* == Curtain Text (2026-08-22) ==
   The sharper sibling of Kinetic Text above. Ported from the pattern
   proven on portfolio.ty1er.com's hero.

   The trick is the mask: each WORD is an inline-block with
   overflow:hidden, and each letter starts pushed fully below it at
   translateY(140%). Letters therefore rise out from behind a hidden edge
   rather than fading in on the spot — which is what makes it read as
   deliberate rather than soft.

   Two deliberate differences from .kinetic-letter:
   1. Transform only, no opacity. A letter is fully solid the instant it
      clears the mask, which looks crisp; cross-fading looks mushy.
   2. Letters are grouped into words, so a heading can never wrap
      mid-word — a real flaw in the flat .kinetic-letter approach.

   Descender clearance matters here: overflow:hidden would otherwise slice
   the tails off g/p/y/j/q. The padding-bottom opens the box enough to
   clear them and the equal negative margin gives the space back, so line
   height is unaffected. */
[data-curtain-text] { visibility: hidden; animation: curtain-failsafe 0s 1.5s forwards; }
[data-curtain-text].curtain-ready { visibility: visible; animation: none; }
@keyframes curtain-failsafe { to { visibility: visible; } }

.curtain-line { display: block; line-height: inherit; }
.curtain-word {
    display: inline-block; white-space: nowrap; overflow: hidden;
    vertical-align: bottom; line-height: inherit;
    padding-top: 0.05em;
    padding-bottom: 0.22em;
    margin-bottom: -0.22em;
}
.curtain-char {
    display: inline-block;
    transform: translateY(140%);
    will-change: transform;
    animation: curtain-rise var(--curtain-duration, 0.95s) cubic-bezier(0.16, 1, 0.3, 1) forwards;
    animation-delay: calc(var(--i, 0) * var(--curtain-stagger, 26ms) + var(--curtain-offset, 100ms));
}
@keyframes curtain-rise { from { transform: translateY(140%); } to { transform: translateY(0); } }

/* Reduced motion: show the text immediately, no movement at all. */
@media (prefers-reduced-motion: reduce) {
    .curtain-char { animation: none !important; transform: none !important; }
}

.btn {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 14px 28px;
    border: 1px solid var(--accent-color);
    font-family: var(--font-mono);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    transition: all 0.25s var(--ease);
}
.btn-primary { background: var(--accent-color); color: var(--bg-primary); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.btn-outline { background: transparent; color: var(--text-primary); }
.btn-outline:hover { background: var(--accent-soft); }

/* == Post grid / cards (scroll-reveal targets: .reveal-text / .post-card) == */
.section-heading { display: flex; align-items: center; justify-content: space-between; margin: 80px 0 32px; }
.section-heading h2 { display: flex; align-items: center; gap: 12px; font-size: 1.9rem; }
.section-heading h2 i { color: var(--accent-color); font-size: 0.85em; }

.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 40px 32px; margin-bottom: 60px; }

/* Boxed card with a hover-lift highlight and a rounded, clipped image —
   matching the ITOps theme's card treatment, per Tyler's 2026-08-19 feedback. */
.post-card {
    display: flex;
    flex-direction: column;
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease), border-color 0.3s var(--ease), opacity 0.3s var(--ease);
}
/* Scroll-reveal initial state — see assets/js/main.js. Uses :not() rather
   than a separate hidden-state rule so once .is-revealed lands, .post-card
   falls back to its normal base/hover rules with no lingering override. */
.post-card:not(.is-revealed) { opacity: 0; transform: translateY(24px); }
.post-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); border-color: var(--accent-color); }
.post-card-image-link { display: block; aspect-ratio: 16/9; overflow: hidden; background: var(--bg-elevated); }
.post-card-image { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s var(--ease); }
.post-card:hover .post-card-image { transform: scale(1.05); }
.post-card-content { padding: 22px 22px 24px; display: flex; flex-direction: column; gap: 4px; flex: 1; }
.post-card-tag {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    align-self: flex-start;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent-color);
    background: var(--accent-soft);
    padding: 4px 10px;
    border-radius: 999px;
}
.post-card-title { font-size: 1.35rem; margin: 10px 0 8px; line-height: 1.3; }
.post-card:hover .post-card-title { text-decoration: underline; text-underline-offset: 4px; }
.post-card-excerpt { color: var(--text-secondary); font-size: 0.98rem; flex: 1; }
.post-card-meta { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--surface-border); font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); display: flex; gap: 14px; }
.post-card-meta span { display: inline-flex; align-items: center; gap: 6px; }
.post-card-meta i { color: var(--accent-color); }

.pagination { display: flex; align-items: center; justify-content: space-between; padding: 32px 0 72px; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }
.pagination a { color: var(--accent-color); }

/* == Recent-posts widget (custom-*.hbs templates) == */
.post-list { margin-top: 60px; padding-top: 40px; border-top: 1px solid var(--surface-border); }
.post-list-heading { display: flex; align-items: center; gap: 10px; font-size: 1.3rem; margin-bottom: 24px; }
.post-list-items { display: flex; flex-direction: column; gap: 2px; }
.post-list-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px 4px;
    border-bottom: 1px solid var(--surface-border);
    transition: padding-left 0.25s var(--ease);
}
.post-list-item:hover { padding-left: 10px; }
.post-list-item-main { display: flex; align-items: center; gap: 16px; min-width: 0; }
.post-list-thumb { flex-shrink: 0; width: 64px; height: 44px; overflow: hidden; background: var(--bg-elevated); }
.post-list-thumb img { width: 100%; height: 100%; object-fit: cover; }
.post-list-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.post-list-date { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); white-space: nowrap; }
.post-list-empty { color: var(--text-muted); font-size: 0.9rem; }

/* == Single post == */
.post-header, .page-header { padding: 70px 0 40px; text-align: center; }
.post-tag, .version-tag {
    display: inline-flex; align-items: center; gap: 6px;
    font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600;
    letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-color);
    padding: 6px 14px; border: 1px solid var(--surface-border); margin-bottom: 20px;
}
.post-title { font-size: clamp(2.2rem, 5vw, 3.4rem); }
.post-custom-excerpt { margin-top: 16px; color: var(--text-secondary); font-style: italic; max-width: 620px; margin-left: auto; margin-right: auto; }
.post-meta { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 24px; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }
.post-meta span { display: inline-flex; align-items: center; gap: 6px; }
.post-meta i { color: var(--accent-color); }
.author-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }

.parallax-hero { position: relative; max-width: 1400px; margin: 0 auto 48px; overflow: hidden; height: 60vh; min-height: 340px; }
.parallax-hero-img { width: 100%; height: 130%; object-fit: cover; position: absolute; top: -15%; left: 0; will-change: transform; }

.post-content, .page-content { font-size: 1.15rem; color: var(--text-primary); }
.post-content h2, .page-content h2 { font-family: var(--font-heading); font-size: 1.8rem; margin: 2.2em 0 0.7em; }
.post-content h3, .page-content h3 { font-family: var(--font-heading); font-size: 1.4rem; margin: 1.8em 0 0.6em; }
.post-content p, .page-content p { margin: 1.3em 0; color: var(--text-secondary); line-height: 1.85; }
.post-content a, .page-content a { color: var(--accent-color); text-decoration: underline; text-underline-offset: 3px; }
.post-content img, .page-content img { margin: 1.8em 0; }
.post-content blockquote, .page-content blockquote {
    border-left: 2px solid var(--accent-color);
    margin: 2em 0; padding: 4px 0 4px 24px;
    font-family: var(--font-heading); font-size: 1.4rem; font-style: italic; color: var(--text-primary);
}
.post-content code, .page-content code {
    background: var(--code-bg); border: 1px solid var(--surface-border);
    border-radius: 4px; padding: 0.2rem 0.5rem; font-family: var(--font-mono); font-size: 0.9em; color: var(--accent-color);
}
.post-content pre, .page-content pre { background: var(--code-bg); border: 1px solid var(--surface-border); padding: 20px; overflow-x: auto; margin: 1.8em 0; }
.post-content pre code, .page-content pre code { background: none; border: none; padding: 0; color: var(--text-primary); }
.post-content table, .page-content table { width: 100%; border-collapse: collapse; margin: 2em 0; border: 1px solid var(--surface-border); }
.post-content th, .page-content th { background: var(--bg-elevated); padding: 12px 16px; text-align: left; font-weight: 700; border-bottom: 1px solid var(--surface-border); }
.post-content td, .page-content td { padding: 12px 16px; border-bottom: 1px solid var(--surface-border); color: var(--text-secondary); }

/* Cheatsheet-specific styles now live in assets/css/cheatsheet.css,
   loaded only by custom-cheatsheet.hbs / custom-cheatsheet-index.hbs. */

/* == About page (custom-about.hbs) ==
   Flex two-column bio: rounded-square framed photo (matches
   portfolio.ty1er.com's #contact "contact-bio-photo" treatment) beside the
   body text, replacing the old float:left <img> that caused text to wrap
   unevenly once the image ran out mid-paragraph. */
/* Bounded content card for the About page — same rationale as
   .components-page-inner (see that rule's comment): @custom.About_BG_Color
   paints only this box, not the full <article>, so the sitewide background
   pattern still shows through around it. */
.about-page-inner {
    max-width: 1040px; margin: 48px auto; padding: 8px 0 56px;
    border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
}
/* Same fixed-background-vs-mode-flipping-tokens problem as
   .components-page-inner (see that rule's comment for the full
   explanation) — make this subtree mode-independent too, derived from the
   two colors Tyler controls here. */
.about-page-inner {
    background-color: var(--about-bg-color);
    --bg-primary: var(--about-bg-color);
    --bg-elevated: color-mix(in srgb, var(--about-bg-color) 92%, var(--about-text-human) 8%);
    --surface-border: color-mix(in srgb, var(--about-bg-color) 80%, var(--about-text-human) 20%);
    --code-bg: color-mix(in srgb, var(--about-bg-color) 90%, var(--about-text-human) 10%);
    --text-primary: var(--about-text-human);
    --text-secondary: color-mix(in srgb, var(--about-text-human) 80%, transparent);
    --text-muted: color-mix(in srgb, var(--about-text-human) 60%, transparent);
    color: var(--text-primary);
}
:root[data-theme="cyber"] .about-page-inner {
    --bg-elevated: color-mix(in srgb, var(--about-bg-color) 92%, var(--about-text-cyber) 8%);
    --surface-border: color-mix(in srgb, var(--about-bg-color) 80%, var(--about-text-cyber) 20%);
    --code-bg: color-mix(in srgb, var(--about-bg-color) 90%, var(--about-text-cyber) 10%);
    --text-primary: var(--about-text-cyber);
    --text-secondary: color-mix(in srgb, var(--about-text-cyber) 80%, transparent);
    --text-muted: color-mix(in srgb, var(--about-text-cyber) 60%, transparent);
}
@media (max-width: 600px) {
    .about-page-inner { margin: 24px 12px; }
}

/* Tighter header-to-bio gap on the About page specifically (was the shared
   .page-header's 40px bottom padding, which read as too disconnected from
   the photo/text row right below it) — scoped to .about-page so this
   doesn't affect page.hbs's shared .page-header elsewhere. */
.about-page .page-header { padding-bottom: 16px; }
.about-bio { display: flex; gap: 48px; align-items: flex-start; padding-bottom: 80px; }
.about-bio-photo {
    flex-shrink: 0; width: 220px; height: 220px;
    border-radius: var(--radius-md); overflow: hidden;
    background: var(--bg-elevated); box-shadow: var(--shadow-md);
}
.about-bio-photo img { width: 100%; height: 100%; object-fit: cover; object-position: center 20%; display: block; }
.about-bio-text { flex: 1; min-width: 0; }
/* .about-bio-text carries .page-content too, whose p rule gives every
   paragraph a 1.3em top margin — on the first paragraph that pushed the
   actual visible text ~24px below the photo's top edge, even though the
   flex row itself (align-items: flex-start) already lines up correctly. */
.about-bio-text p:first-child { margin-top: 0; }
@media (max-width: 680px) {
    .about-bio { flex-direction: column; align-items: center; text-align: center; }
    .about-bio-photo { width: 180px; height: 180px; }
}

/* == Components & Elements page (custom-components.hbs) ==
   A design-system reference page: 50 sitewide background-pattern swatches
   (assets/css/bg-patterns.css supplies the actual --pattern-* gradients via
   .bg-swatch-preview) plus an Effects & Interactions tab cataloging the
   theme's real interactive features. The tab system itself is
   cheatsheet.css's .tab-nav/.tab-pane, loaded on this page too rather than
   duplicated — see the file's own header comment for why. */
/* Bounded content card for the Components page — NOT full-bleed, so
   @custom.Components_BG_Color (applied inline via style=) only paints this
   box. Margin above/below and max-width narrower than the viewport leave
   the sitewide background pattern (body::before) visible around it. */
.components-page-inner {
    max-width: 1300px; margin: 48px auto; padding: 8px 0 56px;
    border-radius: var(--radius-md);
}
.components-page-inner { box-shadow: var(--shadow-lg); }
/* cheatsheet.css's .tab-btn reads --cs-surface/--cs-border/--cs-text-2/etc,
   normally supplied by its own .cheatsheet wrapper — this page uses the
   same tab system (see comment above) but isn't a .cheatsheet, so without
   these it fell back to no border/hover fill and just a bare glow. Mirror
   .cheatsheet's token set here (badge/code tokens tied to
   --components-icon-color, which itself follows --cs-accent — see that
   token's own comment below for the Components Theme brand-reuse chain)
   so the tabs render identically to a real cheatsheet page's .tab-btn,
   e.g. https://build.ty1er.com/cloudflare/. */
/* @custom.Components_BG_Color is one fixed color regardless of Human/Cyber
   mode, but --text-primary/-secondary/-muted (and --bg-elevated/
   --surface-border/--code-bg, which style every "card inside the card" —
   the pattern pager, effects-demo boxes, inline <code>, tab-btn's inactive
   pill fill) still flip per mode. Cyber's --text-primary is a near-white
   meant for Cyber's own dark --bg-primary, so against the fixed light card
   it went near-invisible — and forcing text dark alone would then break
   AGAIN wherever a child box still used Cyber's dark --bg-elevated (dark
   text on dark pill = invisible). Fix is to make the whole subtree
   mode-independent: override every surface/text token this page's
   descendants read, all derived from the two colors Tyler actually
   controls (--components-bg-color, --components-text-human/-cyber), so
   everything nested in .components-page-inner stays internally consistent
   regardless of which mode the sitewide toggle is in. */
.components-page-inner {
    background-color: var(--components-bg-color);
    --bg-primary: var(--components-bg-color);
    --bg-elevated: color-mix(in srgb, var(--components-bg-color) 92%, var(--components-text-human) 8%);
    --surface-border: color-mix(in srgb, var(--components-bg-color) 80%, var(--components-text-human) 20%);
    --code-bg: color-mix(in srgb, var(--components-bg-color) 90%, var(--components-text-human) 10%);
    --text-primary: var(--components-text-human);
    --text-secondary: color-mix(in srgb, var(--components-text-human) 80%, transparent);
    --text-muted: color-mix(in srgb, var(--components-text-human) 60%, transparent);
    color: var(--text-primary);
}
:root[data-theme="cyber"] .components-page-inner {
    --bg-elevated: color-mix(in srgb, var(--components-bg-color) 92%, var(--components-text-cyber) 8%);
    --surface-border: color-mix(in srgb, var(--components-bg-color) 80%, var(--components-text-cyber) 20%);
    --code-bg: color-mix(in srgb, var(--components-bg-color) 90%, var(--components-text-cyber) 10%);
    --text-primary: var(--components-text-cyber);
    --text-secondary: color-mix(in srgb, var(--components-text-cyber) 80%, transparent);
    --text-muted: color-mix(in srgb, var(--components-text-cyber) 60%, transparent);
}
/* The whole --components-text-human/-cyber system above assumes the card
   background is the fixed --components-bg-color regardless of mode — true
   normally, but not when Components Bg Transparent is on: the "background"
   is then whatever the sitewide pattern looks like for the active mode, so
   dark Cyber-mode text (its default) went invisible against Cyber's own
   dark backdrop. --components-page-inner--transparent (set via a class,
   only when the checkbox is on) resets text back to the real sitewide
   mode colors instead — --site-text-* aliases (top of file) exist because
   var(--text-primary) here would just resolve to this same override's own
   value, not the sitewide one it's trying to restore. Higher specificity
   (two classes) than the two rules above so it wins in both modes without
   needing its own :root[data-theme="cyber"] variant. */
:root .components-page-inner.components-page-inner--transparent {
    --text-primary: var(--site-text-primary);
    --text-secondary: var(--site-text-secondary);
    --text-muted: var(--site-text-muted);
    color: var(--text-primary);
}
/* The reset above is correct for text sitting directly on the now-see-
   through card (h1, intro paragraphs) — but .pattern-pager/.effects-demo-
   card/.favorites-summary/.table-card are their OWN light boxes (still
   painted from --bg-elevated, which stays derived from the fixed
   Components Bg Color regardless of transparency or mode), and `color`
   inherits, so their text was inheriting the reset's sitewide-Cyber WHITE
   straight onto a light green card — invisible. Un-reset it back to
   --components-text-human specifically inside these boxes, since their
   background is always light regardless of mode, same reasoning as the
   original (pre-transparent) design. */
.components-page-inner.components-page-inner--transparent .pattern-pager,
.components-page-inner.components-page-inner--transparent .effects-demo-card,
.components-page-inner.components-page-inner--transparent .favorites-summary,
.components-page-inner.components-page-inner--transparent .table-card {
    --text-primary: var(--components-text-human);
    --text-secondary: color-mix(in srgb, var(--components-text-human) 80%, transparent);
    --text-muted: color-mix(in srgb, var(--components-text-human) 60%, transparent);
    color: var(--text-primary);
}
/* Tab buttons under Components Bg Transparent.
   The transparent reset above hands text back to the sitewide palette
   (--site-text-*, near-WHITE in Cyber), but --cs-surface still derives from
   --bg-elevated, which stays keyed to the fixed LIGHT Components Bg Color.
   cheatsheet.css paints .tab-btn with `background: var(--cs-surface);
   color: var(--cs-text-2)` and its :hover with `--cs-surface-hover` /
   `--cs-text`, so in Cyber the pills came out light with near-white text —
   measured 1.71:1 at rest and 1.04:1 on hover, i.e. invisible.

   The un-reset used for .table-card & friends below cannot be reused here:
   --cs-text is read by BOTH :hover (light pill) and .active (which paints a
   TRANSPARENT accent mix, so it is dark over the see-through card). Forcing
   --cs-text dark would fix hover and break .active.

   So fix the surface instead of the text: make the resting and hover pills
   translucent accent mixes, the same technique .active already uses. They
   then composite over whatever backdrop is really behind them and stay in
   the same palette as the text, in either mode. Scoped to --transparent
   because the opaque path is already self-consistent. */
:root .components-page-inner.components-page-inner--transparent .tab-nav {
    --cs-surface: color-mix(in srgb, var(--cs-accent) 8%, transparent);
    --cs-surface-hover: color-mix(in srgb, var(--cs-accent) 22%, transparent);
}

.components-page-inner {
    --cs-surface: var(--bg-elevated);
    --cs-surface-hover: color-mix(in srgb, var(--bg-elevated) 88%, var(--text-primary) 12%);
    --cs-border: var(--surface-border);
    /* A lighter tint of --cs-accent (whichever brand Components Theme
       picked, or the manual icon-color fallback if "none") rather than a
       flat --cs-border, so .table-card/.search-input/.stats-badge/.tab-btn
       (all of which read --cs-tab-border, same as a real cheatsheet page)
       actually reflect the selected theme instead of staying neutral gray
       regardless of it. References --cs-accent, defined further down in
       this same rule — safe, custom-property resolution uses the final
       cascaded value for the element, not source order. */
    --cs-tab-border: color-mix(in srgb, var(--cs-accent) 75%, white 25%);
    --cs-text: var(--text-primary);
    --cs-text-2: var(--text-secondary);
    --cs-text-muted: var(--text-muted);
    /* Default (Components Theme = "none"): --cs-accent/-2 fall back to the
       manual Components Bg Icon Color field. When @custom.Components_Theme
       matches a cheatsheet.css brand slug, the more specific
       .components-page-inner[data-cheatsheet-brand="X"] rule there
       overrides these two with that cheatsheet's real colors instead —
       --components-icon-color (read by this page's own pill-button/badge
       rules further down) always follows whichever one wins. */
    --cs-accent: var(--components-manual-accent);
    --cs-accent-2: var(--components-manual-accent);
    --components-icon-color: var(--cs-accent);
    --cs-code-bg: color-mix(in srgb, var(--bg-primary) 92%, var(--text-primary) 8%);
    --cs-code-border: color-mix(in srgb, var(--components-icon-color) 25%, transparent);
    --cs-badge-bg: color-mix(in srgb, var(--components-icon-color) 12%, transparent);
    --cs-badge-text: var(--components-icon-color);
    --cs-radius: 16px;
}
/* Tyler asked for the crisp bordered-pill + hover look without the glow
   the active tab otherwise gets from cheatsheet.css's .tab-btn.active. */
.components-page-inner .tab-nav > .tab-btn.active { box-shadow: none; }
/* The "Design System" eyebrow badge uses the shared sitewide .version-tag
   class (same one page.hbs/post.hbs use for their own tag badges), which
   colors itself from --accent-color — the sitewide Human/Cyber toggle
   accent, not this page's own --cs-accent. Override just here so it
   follows Components Theme like everything else on this page, without
   touching .version-tag's color for every other page that uses it. */
.components-page-inner .version-tag { color: var(--cs-accent); }
/* cheatsheet.css's table/thead/th/tbody-tr/td rules (the Custom Pages
   tab's table reuses .table-card/table) are scoped to .cheatsheet, which
   this page isn't — same gap as the tab-nav tokens above, just discovered
   later. Mirror them here so the table isn't left with unstyled browser
   defaults. Header/zebra-row/hover are tints of --cs-accent (this page's
   own --components-bg-icon-color, set inline above) — same approach as
   cheatsheet.css's real .cheatsheet tables, which tint from each
   cheatsheet's own brand color instead of one flat color sitewide. */
/* .table-card's own background is cheatsheet.css's shared var(--cs-surface)
   — neutral there, since editing it directly would also retint every real
   cheatsheet page's table card. Scoped override here instead, tinted the
   same way as .pattern-pager/.effects-demo-card/.favorites-summary below. */
.components-page-inner .table-card { background: color-mix(in srgb, var(--cs-accent) 6%, var(--cs-surface)); }
.components-page-inner table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.92rem; table-layout: auto; }
.components-page-inner thead { background: color-mix(in srgb, var(--cs-accent) 14%, transparent); border-bottom: 2px solid var(--cs-border); }
.components-page-inner th {
    padding: 16px 18px; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--cs-text-2); white-space: nowrap;
}
.components-page-inner th i { margin-right: 6px; color: var(--cs-accent); }
.components-page-inner tbody tr { border-bottom: 1px solid var(--cs-border); transition: background-color 0.15s ease; }
.components-page-inner tbody tr:last-child { border-bottom: none; }
.components-page-inner tbody tr:nth-child(even) { background: color-mix(in srgb, var(--cs-accent) 6%, transparent); }
.components-page-inner tbody tr:hover { background: color-mix(in srgb, var(--cs-accent) 20%, transparent); }
.components-page-inner td { padding: 16px 18px; vertical-align: middle; word-break: break-word; overflow-wrap: break-word; max-width: 100%; }
@media (max-width: 768px) {
    .components-page-inner table, .components-page-inner thead, .components-page-inner tbody,
    .components-page-inner th, .components-page-inner td, .components-page-inner tr { display: block; }
    .components-page-inner thead { display: none; }
    .components-page-inner tbody tr {
        background: var(--cs-surface); border: 1px solid var(--cs-border); border-radius: 14px;
        margin-bottom: 16px; padding: 16px; box-shadow: var(--shadow-sm);
    }
    .components-page-inner tbody tr:hover { background: var(--cs-surface); }
    .components-page-inner td { padding: 6px 0; border: none; display: flex; flex-direction: column; align-items: flex-start; gap: 4px; max-width: 100%; }
    .components-page-inner td::before {
        content: attr(data-label); font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
        letter-spacing: 0.08em; color: var(--cs-text-muted); margin-bottom: 2px;
    }
    .components-page-inner td:first-child { padding-top: 0; }
}
@media (max-width: 600px) {
    .components-page-inner { margin: 24px 12px; }
}

.components-section { padding-bottom: 80px; }
.components-section h2 { font-family: var(--font-heading); font-size: 1.8rem; margin: 0 0 12px; }
.components-intro { color: var(--text-secondary); line-height: 1.8; max-width: 640px; margin: 0 0 32px; }
.components-intro code { background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.15rem 0.4rem; font-family: var(--font-mono); font-size: 0.9em; }
/* Custom Pages tab only: Tyler asked for this tab's intro and the screenshot
   caption to run the full width of .components-page-inner rather than sit in
   a 640px measure. The other three tabs keep the capped .components-intro,
   which is the right default for multi-paragraph prose. */
#tab-custom-page .components-intro { max-width: none; }

.components-subheading { font-family: var(--font-heading); font-size: 1.2rem; margin: 40px 0 20px; padding-bottom: 8px; border-bottom: 1px solid var(--surface-border); }
.components-subheading:first-of-type { margin-top: 0; }

/* Background Patterns pagination (50-at-a-time, custom-components.hbs). */
.pattern-pager {
    display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
    padding: 14px 20px; border: 1px solid var(--cs-tab-border); border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--cs-accent) 6%, var(--bg-elevated)); margin-bottom: 28px;
}
.pattern-pager-label { font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-secondary); }

/* Action-button "pill" style shared across the Components page — pattern
   pager, effects-demo replay buttons, favorites copy/clear — matching the
   .cheatsheet-eyebrow badge look (accent-tinted bg, accent border+text,
   uppercase bold mono). Driven by --components-icon-color
   (@custom.Components_BG_Icon_Color, set inline on .components-page-inner)
   instead of --accent-color, so this page's buttons/icons follow their own
   setting rather than the sitewide Human/Cyber toggle accent. */
.components-page-inner .pattern-pager-btn,
.components-page-inner #robot-toggle-demo-btn,
.components-page-inner #drawer-demo-btn,
.components-page-inner #countup-replay-btn,
.components-page-inner #favorites-copy-btn,
.components-page-inner #favorites-clear-btn {
    all: revert; cursor: pointer; font-family: var(--font-mono); font-size: 0.78rem; font-weight: 700;
    padding: 8px 20px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.06em;
    background: color-mix(in srgb, var(--components-icon-color) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--components-icon-color) 25%, transparent);
    color: var(--components-icon-color);
    transition: border-color 0.2s var(--ease), background 0.2s var(--ease), opacity 0.2s var(--ease);
    white-space: nowrap;
}
.components-page-inner .pattern-pager-btn:hover:not(:disabled),
.components-page-inner #robot-toggle-demo-btn:hover,
.components-page-inner #drawer-demo-btn:hover,
.components-page-inner #countup-replay-btn:hover,
.components-page-inner #favorites-copy-btn:hover,
.components-page-inner #favorites-clear-btn:hover {
    background: color-mix(in srgb, var(--components-icon-color) 22%, transparent);
}
.components-page-inner #favorites-clear-btn:hover {
    color: #e5484d; border-color: color-mix(in srgb, #e5484d 40%, transparent);
    background: color-mix(in srgb, #e5484d 12%, transparent);
}
.pattern-pager-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.bg-swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 24px; }
.bg-swatch { text-align: center; }
.bg-swatch-preview {
    width: 100%; aspect-ratio: 4 / 3; border-radius: var(--radius-md);
    border: 1px solid var(--surface-border); background-color: var(--bg-elevated);
    box-shadow: var(--shadow-sm); margin-bottom: 12px;
}
.bg-swatch h3 { font-family: var(--font-heading); font-size: 1rem; margin: 0 0 6px; }
.bg-swatch code { font-family: var(--font-mono); font-size: 0.82rem; color: var(--text-muted); }

/* Effects & Interactions live-demo cards. */
.effects-demo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 24px; margin-top: 20px; }
.effects-demo-card {
    padding: 28px 24px; border: 1px solid var(--cs-tab-border); border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--cs-accent) 6%, var(--bg-elevated)); text-align: center;
}
.effects-demo-card h4 { font-family: var(--font-heading); font-size: 1.05rem; margin: 0 0 6px; }
.effects-demo-note { color: var(--text-muted); font-size: 0.85rem; margin: 0 0 20px; }
.effects-demo-glow-link {
    display: inline-block; padding: 14px 28px; font-family: var(--font-mono); font-size: 0.9rem;
    color: var(--text-primary); text-decoration: none; border: 1px solid var(--surface-border);
    border-radius: 999px; background: var(--bg-primary);
}
.effects-demo-replay-btn {
    all: unset; cursor: pointer; font-family: var(--font-heading); font-size: 1.3rem;
    color: var(--text-primary);
}
.effects-demo-card--wide { grid-column: span 2; }
@media (max-width: 640px) { .effects-demo-card--wide { grid-column: span 1; } }

#countup-replay-btn { margin-top: 12px; }

/* CountUp demo number. */
.effects-demo-countup { font-family: var(--font-heading); font-size: 2.4rem; color: var(--accent-color); }
/* Same demo elements above use the sitewide --accent-color by default,
   which is correct for their equivalents elsewhere (the real footer
   .glow-link, the real .post-card grid) but not theme-aware — override
   just the Components-page instances to follow --cs-accent/Components
   Theme instead, without touching the shared sitewide rules those other,
   real usages still rely on. */
.components-page-inner .effects-demo-countup { color: var(--cs-accent); }
.components-page-inner .post-card:hover { border-color: var(--cs-accent); }
.components-page-inner .glow-link::before { background: radial-gradient(circle, color-mix(in srgb, var(--cs-accent) 55%, transparent), transparent 70%); }

/* Post-card demo — reuses the real .post-card component, just constrained
   to the grid cell instead of a full post-grid column width. */
.effects-demo-post-card { max-width: 340px; margin: 0 auto; }
.effects-demo-post-card-image[data-bg-pattern],
.effects-demo-parallax-img[data-bg-pattern] {
    background-image: var(--pattern-bg-image);
    background-size: var(--pattern-bg-size, auto);
    background-position: var(--pattern-bg-position, 0 0);
}

/* Parallax demo — same .parallax-hero/.parallax-hero-img classes as
   page.hbs/post.hbs, just a shorter fixed height for a compact card. */
.effects-demo-parallax-hero { height: 180px; margin: 0; max-width: 100%; }

/* Auto Icon Matching demo. */
.effects-demo-icon-input {
    width: 100%; padding: 10px 14px; border: 1px solid var(--surface-border); border-radius: 8px;
    background: var(--bg-primary); color: var(--text-primary); font-family: var(--font-body); font-size: 0.92rem;
    margin-bottom: 12px;
}
.effects-demo-icon-output {
    display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 0.92rem;
    color: var(--text-primary); text-decoration: none;
}
.effects-demo-icon-output i { color: var(--components-icon-color); }

/* Pick Favorites tab — a clickable version of the swatch grid that builds a
   shortlist (capped at 10) for @custom.Site_BG_Favorites. Selection state
   lives in localStorage (draft only, this browser) until copied into Ghost
   Admin's real theme setting, which is the only thing that actually
   persists sitewide. */
.favorites-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 12px; margin-bottom: 28px; }
.favorites-picker-swatch {
    position: relative; cursor: pointer; border-radius: var(--radius-md);
    border: 2px solid var(--surface-border); overflow: hidden; transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
    background-color: var(--bg-elevated);
}
.favorites-picker-swatch:hover { border-color: var(--components-icon-color); transform: translateY(-2px); }
.favorites-picker-swatch.is-selected { border-color: var(--components-icon-color); box-shadow: 0 0 0 2px color-mix(in srgb, var(--components-icon-color) 30%, transparent); }
.favorites-picker-swatch .bg-swatch-preview { aspect-ratio: 1 / 1; margin: 0; border: none; border-radius: 0; box-shadow: none; }
.favorites-picker-swatch-num {
    position: absolute; bottom: 4px; left: 6px; font-family: var(--font-mono); font-size: 0.68rem;
    color: var(--text-muted); background: color-mix(in srgb, var(--bg-primary) 75%, transparent); padding: 1px 5px; border-radius: 4px;
}
.favorites-picker-swatch-badge {
    position: absolute; top: 6px; right: 6px; width: 20px; height: 20px; border-radius: 50%;
    background: var(--components-icon-color); color: var(--bg-primary); display: none;
    align-items: center; justify-content: center; font-size: 0.7rem;
}
.favorites-picker-swatch.is-selected .favorites-picker-swatch-badge { display: flex; }
.favorites-summary {
    padding: 24px; border: 1px solid var(--cs-tab-border); border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--cs-accent) 6%, var(--bg-elevated));
}
.favorites-summary-count { font-family: var(--font-heading); font-size: 1.1rem; margin: 0 0 12px; }
.favorites-summary-count span { color: var(--accent-color); }
.favorites-summary-output {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 12px 16px; background: var(--code-bg); border: 1px solid var(--surface-border);
    border-radius: 8px; font-family: var(--font-mono); font-size: 0.88rem; margin-bottom: 16px; min-height: 20px;
}
.favorites-summary-note { color: var(--text-secondary); line-height: 1.7; font-size: 0.92rem; margin: 0; }
.favorites-summary-note code { background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.15rem 0.4rem; font-family: var(--font-mono); font-size: 0.9em; }
.favorites-summary-screenshot {
    display: block; max-width: 100%; height: auto; margin-top: 20px;
    border: 1px solid var(--surface-border); border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}

/* Ghost Admin panel capture on the Components page's Custom Pages tab.
   Native width is 437px (a portrait screenshot of the Design > Theme
   sidebar), so it is capped rather than stretched — upscaling a UI
   screenshot just blurs the text it exists to show. */
.theme-settings-screenshot {
    display: block; width: 100%; max-width: 437px; height: auto; margin: 22px 0 4px;
    border: 1px solid var(--surface-border); border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}
.theme-settings-screenshot-caption {
    font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted);
    margin: 0 0 24px;
}

/* == Koenig editor cards (required for gscan) == */
.kg-width-wide { margin-left: calc(50% - 50vw + 8px); margin-right: calc(50% - 50vw + 8px); max-width: none; width: calc(100vw - 16px); }
.kg-width-full { margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); max-width: none; width: 100vw; }
.kg-gallery-container { display: flex; flex-direction: column; margin: 1.8em 0; }
.kg-gallery-row { display: flex; flex-direction: row; justify-content: center; }
.kg-gallery-image img { display: block; margin: 0; width: 100%; height: 100%; object-fit: cover; }
.kg-gallery-row:not(:first-of-type) .kg-gallery-image img { margin-top: 4px; }
.kg-gallery-image:not(:first-of-type) img { margin-left: 4px; }
.kg-bookmark-card { width: 100%; margin: 1.8em 0; }
.kg-bookmark-container { display: flex; min-height: 148px; color: var(--text-primary); border: 1px solid var(--surface-border); overflow: hidden; }
.kg-bookmark-content { flex: 1 1 auto; padding: 20px; order: 1; }
.kg-bookmark-title { font-weight: 700; }
.kg-bookmark-description { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: 0.85rem; color: var(--text-muted); margin-top: 6px; }
.kg-bookmark-metadata { display: flex; align-items: center; gap: 8px; font-size: 0.8rem; color: var(--text-muted); margin-top: 16px; }
.kg-bookmark-icon { width: 18px; height: 18px; }
.kg-bookmark-author::before { content: "\2022"; margin: 0 4px; }
.kg-bookmark-publisher { font-weight: 600; }
.kg-bookmark-thumbnail { flex: 0 1 30%; order: 2; }
.kg-bookmark-thumbnail img { width: 100%; height: 100%; object-fit: cover; }

/* == Archive header == */
.archive-header { padding: 70px 0 8px; text-align: center; }
.archive-title { font-size: clamp(2.2rem, 4.4vw, 3rem); }
.archive-description { margin-top: 14px; color: var(--text-secondary); max-width: 560px; margin-left: auto; margin-right: auto; }

/* == Footer == */
.site-footer { margin-top: auto; border-top: 1px solid var(--surface-border); padding: 56px 0; }
.footer-top { display: flex; justify-content: space-between; gap: 40px; flex-wrap: wrap; margin-bottom: 40px; }
.footer-brand { font-family: var(--font-heading); font-size: 1.5rem; }
.footer-tagline { margin-top: 10px; color: var(--text-muted); max-width: 320px; }
.footer-subscribe { flex: 1 1 320px; max-width: 420px; }
.footer-subscribe form { display: flex; gap: 8px; margin-top: 12px; }
.footer-subscribe input[type="email"] {
    flex: 1; padding: 12px 14px; background: var(--bg-elevated); border: 1px solid var(--surface-border);
    color: var(--text-primary); font-family: var(--font-body); font-size: 0.9rem;
}
.footer-bottom { display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; padding-top: 28px; border-top: 1px solid var(--surface-border); }
/* Ghost's {{#navigation}} helper renders its own <ul class="nav"><li>
   wrapper around whatever markup goes inside the block — it's not a bare
   iterator. .footer-links is the <nav> wrapper; .footer-links .nav (the
   actual <ul> Ghost renders) needs to be the flex row, not .footer-links
   itself, otherwise the <li>s inside just stack as plain blocks. Found
   2026-08-19 after this exact bug shipped silently here, on ITOps's nav,
   and (undiscovered) on tyler-tech-theme too. */
.footer-links .nav { display: flex; flex-wrap: wrap; gap: 20px; list-style: none; margin: 0; padding: 0; }

/* Signature block — same font-family/size/color as .eyebrow (footer text
   elsewhere), just without the uppercase transform / wide tracking, since
   this copy's casing and punctuation are intentional. */
/* Legal notice above the signature block (2026-08-22). Deliberately
   smaller and more muted than .footer-signature — it should be findable
   and legible without competing with the brand line. The link keeps the
   accent colour so it reads as clickable. */
.footer-legal {
    margin-top: 28px;
    padding-top: 18px;
    border-top: 1px solid var(--surface-border);
    text-align: center;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    line-height: 1.8;
    color: var(--text-muted);
}
.footer-legal p { max-width: 640px; margin: 0 auto; }
.footer-legal a { color: var(--accent-color); text-decoration: underline; text-underline-offset: 3px; }
.footer-legal a:hover { opacity: 0.75; }

.footer-signature {
    margin-top: 20px;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.9;
}
.footer-heart { color: var(--accent-color); }
.footer-signature a { transition: color 0.25s var(--ease), text-shadow 0.25s var(--ease); }
.footer-signature a:hover { color: var(--accent-color); text-shadow: 0 0 10px var(--accent-color); }

/* Glow-link — cursor-tracking hover glow, reusable on any inline link/button.
   --glow-x/--glow-y are set on mousemove in main.js; the blob fades in via
   opacity on :hover so it costs nothing when idle. See main.js for the
   codepen source this was adapted from. */
.glow-link { position: relative; isolation: isolate; }
.glow-link::before {
    content: "";
    position: absolute;
    left: var(--glow-x, 50%);
    top: var(--glow-y, 50%);
    width: 90px;
    height: 90px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(circle, color-mix(in srgb, var(--accent-color) 55%, transparent), transparent 70%);
    filter: blur(14px);
    opacity: 0;
    transition: opacity 0.3s var(--ease);
    pointer-events: none;
    z-index: -1;
}
.glow-link:hover::before { opacity: 1; }
.footer-links a { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.footer-links a:hover { color: var(--accent-color); }
.footer-links a i { font-size: 0.9em; }

.error-content { text-align: center; padding: 130px 0; }
.error-content h1 { font-family: var(--font-heading); font-size: clamp(4rem, 10vw, 7.5rem); }
.error-content p { margin-top: 12px; color: var(--text-secondary); font-size: 1.1rem; }
.error-content .btn { margin-top: 30px; display: inline-flex; }

/* == Back to top button (sitewide, default.hbs) ==
   Hidden by default; main.js toggles .is-visible past a scroll threshold.
   Kept below .site-menu-overlay's z-index (150) so the slide-out drawer
   still layers above it. */
.back-to-top-btn {
    position: fixed; right: 24px; bottom: 24px; z-index: 120;
    width: 48px; height: 48px; border-radius: 50%; border: 1px solid var(--surface-border);
    background: var(--bg-primary); color: var(--text-primary); box-shadow: var(--shadow-md);
    display: flex; align-items: center; justify-content: center; font-size: 1.05rem;
    cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(12px);
    transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s, border-color 0.2s var(--ease);
}
.back-to-top-btn.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }
.back-to-top-btn:hover { border-color: var(--accent-color); color: var(--accent-color); }
@media (max-width: 560px) {
    .back-to-top-btn { right: 16px; bottom: 16px; width: 44px; height: 44px; }
}

/* == Dashboard page (custom-dashboard.hbs) ==
   Internal quick-links index, meant to be Members-only-gated in Ghost
   Admin rather than public. Every accent-colored bit below reads
   var(--cs-accent, var(--accent-color)) — --cs-accent only exists once
   cheatsheet.css's bare [data-cheatsheet-brand="X"] attribute selector
   matches (Dashboard Theme picked something other than "none"), so the
   fallback (var()'s 2nd argument) is what's actually in effect normally,
   keeping this page on the sitewide Human/Cyber accent same as before
   Dashboard Theme existed. No --cs-surface/--cs-border/etc bridging
   needed here (unlike Components/Custom Pages' table) since this page
   doesn't reuse any of cheatsheet.css's own components, just its
   brand-color variables. */
.dashboard-section { padding-bottom: 80px; }
.dashboard-page .version-tag { color: var(--cs-accent, var(--accent-color)); }
.dashboard-stats { display: flex; gap: 40px; flex-wrap: wrap; margin-bottom: 48px; padding-bottom: 32px; border-bottom: 1px solid var(--surface-border); }
.dashboard-stat { display: flex; flex-direction: column; gap: 4px; }
.dashboard-stat strong { font-family: var(--font-heading); font-size: 2.2rem; color: var(--cs-accent, var(--accent-color)); line-height: 1; }
/* :not(.count-up-number) — a bare `span` selector here was also matching
   the CountUp number span nested inside .dashboard-stat strong (added
   2026-08-21), overriding its inherited heading font/size/color with this
   label styling instead. The count itself should just inherit from its
   <strong> parent above, unchanged from before CountUp was wired in. */
.dashboard-stat span:not(.count-up-number) { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.dashboard-subheading { font-family: var(--font-heading); font-size: 1.4rem; margin: 0 0 20px; }
/* Fixed 2 columns (not auto-fit) so a 4th card — Quick Links, always
   authored last — lands deterministically bottom-right of a 2x2 grid
   regardless of viewport width, rather than however auto-fit happens to
   wrap at a given screen size. */
.dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
@media (max-width: 640px) {
    .dashboard-grid { grid-template-columns: 1fr; }
}
.dashboard-card {
    display: block; padding: 24px; border: 1px solid var(--surface-border); border-radius: var(--radius-md);
    background: var(--bg-elevated); text-decoration: none; color: inherit;
    transition: transform 0.2s var(--ease), border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.dashboard-card:hover { transform: translateY(-3px); border-color: var(--cs-accent, var(--accent-color)); box-shadow: var(--shadow-md); }
.dashboard-card-header { display: flex; align-items: center; gap: 10px; font-family: var(--font-heading); font-size: 1.15rem; color: var(--text-primary); margin-bottom: 10px; }
.dashboard-card-header i { color: var(--cs-accent, var(--accent-color)); }
.dashboard-card p { margin: 0; color: var(--text-secondary); font-size: 0.92rem; line-height: 1.6; }
/* Quick Links card — a container of several distinct links (repo,
   analytics, etc), unlike the other cards which are themselves one big
   link. .dashboard-card--static drops the whole-card hover-lift (nothing
   to click on the card itself, only the rows inside), and each row gets
   its own hover feedback instead. */
.dashboard-card--static:hover { transform: none; border-color: var(--surface-border); box-shadow: none; }
.dashboard-quicklinks-list { display: flex; flex-direction: column; gap: 4px; }
.dashboard-quicklinks-list a {
    display: flex; align-items: center; gap: 10px; padding: 10px 12px; margin: 0 -12px;
    border-radius: 8px; color: var(--text-secondary); text-decoration: none; font-size: 0.92rem;
    transition: background 0.2s var(--ease), color 0.2s var(--ease);
}
.dashboard-quicklinks-list a:hover { background: var(--code-bg); color: var(--text-primary); }
.dashboard-quicklinks-list a i { width: 18px; text-align: center; color: var(--cs-accent, var(--accent-color)); }

/* == Internal pages — shared cross-link nav (2026-08-21) ==
   Used by custom-dashboard.hbs, custom-components.hbs, and
   custom-style-guide.hbs's headers to link to the other two of the three;
   each page's own copy omits its self-link rather than showing/disabling
   it, since Ghost's Handlebars has no string-equality helper to drive an
   "active page" state cleanly. Colored via
   var(--cs-accent, var(--accent-color)) so it follows whichever brand
   theme (Components/Dashboard/Style Guide Theme) that specific page has
   set, falling back to the sitewide accent when unset. */
.internal-pages-nav { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 20px; }
.internal-pages-nav a {
    display: inline-flex; align-items: center; gap: 8px; padding: 8px 18px;
    border: 1px solid var(--surface-border); border-radius: 999px;
    color: var(--text-secondary); text-decoration: none; font-family: var(--font-mono); font-size: 0.82rem;
    transition: border-color 0.2s var(--ease), color 0.2s var(--ease), transform 0.2s var(--ease);
}
.internal-pages-nav a:hover { border-color: var(--cs-accent, var(--accent-color)); color: var(--cs-accent, var(--accent-color)); transform: translateY(-2px); }

/* == Style Guide page (custom-style-guide.hbs) ==
   Documents the real sitewide Human/Cyber tokens in the Color/Typography
   sections regardless of the field below — Style Guide Theme
   (@custom.Style_Guide_Theme, added 2026-08-21) only colors this page's
   own accent-tinted chrome (sub-nav hover, eyebrows, CTA borders, the
   internal-pages-nav above, a11y check icons) via
   var(--cs-accent, var(--accent-color)), same fallback pattern Dashboard
   uses — see custom-style-guide.hbs's header comment for the full story. */
.style-guide-page .version-tag { color: var(--cs-accent, var(--accent-color)); }
.sg-subnav {
    display: flex; flex-wrap: wrap; gap: 24px; padding: 20px 0; margin-bottom: 48px;
    border-top: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
}
.sg-subnav a {
    font-family: var(--font-mono); font-size: 0.82rem; color: var(--text-secondary);
    text-decoration: none; text-transform: uppercase; letter-spacing: 0.05em;
    transition: color 0.2s var(--ease);
}
.sg-subnav a:hover { color: var(--cs-accent, var(--accent-color)); }
.sg-section { padding-bottom: 88px; scroll-margin-top: 24px; }
.sg-eyebrow {
    display: inline-block; font-family: var(--font-mono); font-size: 0.78rem; font-weight: 700;
    color: var(--cs-accent, var(--accent-color)); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px;
}
.sg-section h2 { font-family: var(--font-heading); font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 16px; }
.sg-lede { color: var(--text-secondary); line-height: 1.8; max-width: 640px; margin: 0 0 32px; }
.sg-lede code { background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.15rem 0.4rem; font-family: var(--font-mono); font-size: 0.88em; }

/* Color */
.sg-color-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; }
.sg-color-column { padding: 28px; border: 1px solid var(--surface-border); border-radius: var(--radius-md); background: var(--bg-elevated); }
.sg-color-column--cyber { background: #0a0c10; border-color: #1d2733; }
.sg-color-column h3 { display: flex; align-items: center; gap: 10px; font-family: var(--font-heading); font-size: 1.1rem; margin: 0 0 20px; color: var(--text-primary); }
.sg-color-column--cyber h3 { color: #e6edf3; }
.sg-color-column--cyber h3 i { color: #00f0ff; }
.sg-swatch-row { display: flex; align-items: center; gap: 14px; padding: 10px 0; border-bottom: 1px solid var(--surface-border); }
.sg-color-column--cyber .sg-swatch-row { border-color: #1d2733; }
.sg-swatch-row:last-child { border-bottom: none; }
.sg-swatch { width: 36px; height: 36px; flex-shrink: 0; border-radius: 8px; border: 1px solid rgba(128,128,128,0.25); }
.sg-swatch-row strong { display: block; font-size: 0.9rem; color: var(--text-primary); }
.sg-color-column--cyber .sg-swatch-row strong { color: #e6edf3; }
.sg-swatch-row code { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); }
.sg-color-column--cyber .sg-swatch-row code { color: #67788c; }
.sg-swatch-row p { margin: 2px 0 0; font-size: 0.82rem; color: var(--text-secondary); }
.sg-color-column--cyber .sg-swatch-row p { color: #a7b4c2; }

/* Typography */
.sg-type-sample { padding: 24px 0; border-bottom: 1px solid var(--surface-border); }
.sg-type-sample:last-child { border-bottom: none; }
.sg-type-label { display: block; font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; }
.sg-type-heading { font-family: var(--font-heading); font-size: clamp(1.8rem, 5vw, 2.6rem); margin: 0; }
.sg-type-body { font-size: 1.05rem; line-height: 1.8; color: var(--text-secondary); margin: 0; max-width: 620px; }
.sg-type-mono { font-family: var(--font-mono); font-size: 0.95rem; background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 8px; padding: 14px 18px; margin: 0; display: inline-block; }

/* Components / Cheatsheet Brands CTAs */
.sg-cta {
    display: inline-flex; align-items: center; gap: 10px; padding: 12px 22px;
    border: 1px solid var(--surface-border); border-radius: 999px; background: var(--bg-elevated);
    color: var(--text-primary); text-decoration: none; font-family: var(--font-mono); font-size: 0.88rem;
    transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.sg-cta:hover { border-color: var(--cs-accent, var(--accent-color)); transform: translateY(-2px); }
.sg-brand-sample { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 24px; }
.sg-brand-swatch { width: 32px; height: 32px; border-radius: 50%; border: 1px solid rgba(128,128,128,0.25); }

/* Theme Switching */
.sg-theming-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
.sg-theming-card { padding: 24px; border: 1px solid var(--surface-border); border-radius: var(--radius-md); background: var(--bg-elevated); }
.sg-theming-card h3 { display: flex; align-items: center; gap: 10px; font-family: var(--font-heading); font-size: 1.05rem; margin: 0 0 10px; }
.sg-theming-card h3 i { color: var(--cs-accent, var(--accent-color)); }
.sg-theming-card p { margin: 0; color: var(--text-secondary); font-size: 0.92rem; line-height: 1.7; }
.sg-theming-card code { background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.1rem 0.35rem; font-family: var(--font-mono); font-size: 0.85em; }

/* Accessibility */
.sg-a11y-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 16px; max-width: 720px; }
.sg-a11y-list li { padding-left: 28px; position: relative; color: var(--text-secondary); line-height: 1.7; }
.sg-a11y-list li::before { content: "\f00c"; font-family: "Font Awesome 6 Free"; font-weight: 900; position: absolute; left: 0; top: 2px; color: var(--cs-accent, var(--accent-color)); font-size: 0.85rem; }
.sg-a11y-list strong { color: var(--text-primary); }
.sg-a11y-list code { background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.1rem 0.35rem; font-family: var(--font-mono); font-size: 0.88em; }

/* == Changelog page (custom-changelog.hbs) ==
   A Preline-style release timeline (reference Tyler pointed at:
   preline.co/docs/changelog.html) — a two-column grid per milestone:
   a left meta rail carrying the date/version and the timeline dot, and a
   right body column holding one or more entry cards.

   The vertical line is drawn on .changelog-entry::before rather than as a
   single line on the container, so it segments per entry and naturally
   stops after the last one (:last-child kills it) — no separate "line
   end" element and no fixed height to keep in sync with the content.

   Accent: var(--cs-accent, var(--accent-color)) throughout, the same
   fallback chain Dashboard/Components/Style Guide use — so
   @custom.Changelog_Theme repaints this page's chrome, and "none" falls
   back to the sitewide Human/Cyber accent. See custom-changelog.hbs.

   The entry cards themselves are authored in Ghost's editor via an HTML
   card ({{content}}), NOT hardcoded in the template — same pattern as
   custom-dashboard.hbs's card grid, so Tyler can log a new release
   without a theme deploy. Markup for one milestone:
     <div class="changelog-entry">
       <div class="changelog-meta">
         <span class="changelog-version">v1.4</span>
         <time class="changelog-date">22 Aug 2026</time>
       </div>
       <div class="changelog-body">
         <div class="changelog-card"> ... </div>
       </div>
     </div> */
.changelog-timeline { margin: 0; padding: 0; }
.changelog-entry {
    position: relative;
    display: grid; grid-template-columns: 190px 1fr; gap: 40px;
    padding: 0 0 56px 0;
}
/* Timeline rail: vertical line + dot, drawn off the meta column's edge. */
.changelog-entry::before {
    content: ""; position: absolute; top: 22px; bottom: -4px; left: 190px;
    width: 1px; background: var(--surface-border);
}
.changelog-entry:last-child::before { display: none; }
.changelog-entry::after {
    /* left = line centre (190.5) minus half the dot box (11px + 2*2px border = 15px). */
    content: ""; position: absolute; top: 14px; left: 183px;
    width: 11px; height: 11px; border-radius: 50%;
    background: var(--bg-primary);
    border: 2px solid var(--cs-accent, var(--accent-color));
}
.changelog-meta {
    display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
    padding-right: 30px; text-align: right;
}
.changelog-version {
    font-family: var(--font-mono); font-size: 1rem; font-weight: 700;
    color: var(--cs-accent, var(--accent-color)); letter-spacing: 0.02em;
}
.changelog-date {
    font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted);
}
.changelog-body { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.changelog-card {
    padding: 26px 28px; border: 1px solid var(--surface-border);
    border-radius: var(--radius-md); background: var(--bg-elevated);
    transition: border-color 0.2s var(--ease);
}
.changelog-card:hover { border-color: var(--cs-accent, var(--accent-color)); }
.changelog-card-title {
    font-family: var(--font-heading); font-size: 1.15rem; line-height: 1.4;
    margin: 0 0 12px; color: var(--text-primary);
}
.changelog-summary { margin: 0 0 16px; color: var(--text-secondary); line-height: 1.75; font-size: 0.95rem; }
.changelog-summary:last-child { margin-bottom: 0; }

/* Tag pills — one per change class, colored by role rather than by the
   page accent so New/Fixed/Removed stay distinguishable under any brand. */
.changelog-tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.changelog-tag {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 11px; border-radius: 999px;
    font-family: var(--font-mono); font-size: 0.72rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.05em;
    border: 1px solid currentColor; color: var(--text-muted);
}
.changelog-tag--new { color: #1a7f4b; }
.changelog-tag--improved { color: #175cd3; }  /* 5.73:1 on --bg-elevated; #1f6feb was 4.43 and failed AA */
.changelog-tag--fixed { color: #a06508; }     /* 4.61:1 on --bg-elevated; #b5730a was 3.71 and failed AA */
.changelog-tag--removed { color: #b42318; }
.changelog-tag--docs { color: #7c3aed; }
[data-theme="cyber"] .changelog-tag--new { color: #3fb950; }
[data-theme="cyber"] .changelog-tag--improved { color: #58a6ff; }
[data-theme="cyber"] .changelog-tag--fixed { color: #d29922; }
[data-theme="cyber"] .changelog-tag--removed { color: #f85149; }
[data-theme="cyber"] .changelog-tag--docs { color: #bc8cff; }

.changelog-list { list-style: none; padding: 0; margin: 0 0 16px; display: flex; flex-direction: column; gap: 9px; }
.changelog-list:last-child { margin-bottom: 0; }
.changelog-list li { position: relative; padding-left: 22px; color: var(--text-secondary); line-height: 1.7; font-size: 0.93rem; }
.changelog-list li::before {
    content: "\f105"; font-family: "Font Awesome 6 Free"; font-weight: 900;
    position: absolute; left: 4px; top: 1px;
    color: var(--cs-accent, var(--accent-color)); font-size: 0.8rem;
}
.changelog-list strong { color: var(--text-primary); }
.changelog-card code, .changelog-list code, .changelog-summary code {
    background: var(--code-bg); border: 1px solid var(--surface-border); border-radius: 4px;
    padding: 0.1rem 0.4rem; font-family: var(--font-mono); font-size: 0.85em;
    overflow-wrap: break-word;
}
/* Files-touched footer chips */
.changelog-files { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--surface-border); }
.changelog-files code { font-size: 0.76rem; color: var(--text-muted); }

@media (max-width: 640px) {
    .sg-subnav { gap: 14px; }
}

/* == Responsive == */
@media (max-width: 900px) {
    .header-btn-label { display: none; }
    .theme-label { display: none; }
}
@media (max-width: 820px) {
    /* Collapse the two-column timeline: meta rail moves above the cards and
       the line/dot shift to the far left so the rail still reads as a
       timeline rather than a stack of unrelated cards. */
    .changelog-entry { grid-template-columns: 1fr; gap: 14px; padding-left: 26px; padding-bottom: 40px; }
    .changelog-entry::before { left: 7px; top: 20px; }  /* centre 7.5 == dot centre below */
    .changelog-entry::after { left: 0; top: 5px; }
    .changelog-meta { flex-direction: row; align-items: baseline; gap: 12px; padding-right: 0; text-align: left; }
}

@media (max-width: 560px) {
    .hero { padding: 88px 0 56px; }
    .hero-cta { flex-direction: column; align-items: stretch; }
    .post-grid { grid-template-columns: 1fr; }
    .footer-subscribe form { flex-direction: column; }
}
