/* ==========================================================
   JOEL+ — site theme  ::  "amber phosphor terminal"
   Port of joel-kalich-theme.css (PeerTube) onto Ghost/Solo.

   Install:
     1. Ghost Admin > Settings > Theme > ... > Edit code
     2. Create  assets/css/jk-theme.css  and paste this in
     3. In default.hbs, immediately AFTER the existing
        built/screen.css <link>, add:
          <link rel="stylesheet" href="{{asset "css/jk-theme.css"}}">
     4. Save > Replace theme

   Why a separate file and not assets/css/screen.css:
   Solo compiles assets/css/screen.css -> assets/built/screen.css
   with Gulp/PostCSS. The in-browser editor has no build step, so
   edits to the source CSS do nothing, and edits to the built file
   get blown away the next time anyone runs `yarn dev`. A standalone
   file loaded after screen.css sidesteps both problems and survives
   theme updates as long as you re-add the one <link> line.

   Ghost-side settings this file assumes (Design & branding):
     Accent color  ....... your amber. Drives --ghost-accent-color,
                           which everything here derives from.
     Solo > background_color ... near-black (#0d0b08 suggested).
     Solo > typography ......... "Consistent mono"  (body face)
     Solo > header_section_layout / post_feed_layout: your call,
                                 all three of each are handled.

   Verified against:
     Solo main (assets/css/screen.css, default.hbs, partials/loop.hbs,
     partials/content-cta.hbs) and the compiled
     @tryghost/shared-theme-assets v1 screen.css.

   NOT verified — Portal (signup/login modal) is a cross-origin
   iframe. It takes the accent color and nothing else. Sections
   marked [PORTAL] below are the closest you can get from outside.
   ========================================================== */

@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
/* Self-hosting note: you have theme file access now, so you can drop
   a VT323 woff2 into assets/fonts/ and swap this @import for a local
   @font-face. Kills the Google Fonts round-trip and the third-party
   request. Left as an @import here so this file is paste-and-go. */


/* ---------- 1. Tokens ----------
   Same derivation chain as the PeerTube file, different root source.
   There, everything came off --primary. Here it comes off
   --ghost-accent-color, which Ghost sets from the Design settings
   accent picker. Change the accent, the whole site recolors. One knob.

   --jk-dark replaces PeerTube's --is-dark. Solo doesn't expose a
   dark flag, but default.hbs runs a YIQ contrast check on the
   background_color setting and puts .has-light-text on <html> when
   the background is dark. Same trick, different hook: glow strength
   is multiplied by --jk-dark so it goes to 0 on a light background
   instead of muddying it. */
:root {
    --jk-display: 'VT323', 'Courier New', monospace;
    --jk-amber: var(--ghost-accent-color);
    --jk-glow: color-mix(in srgb, var(--jk-amber) 50%, transparent);
    --jk-hairline: color-mix(in srgb, var(--jk-amber) 24%, transparent);
    --jk-tint: color-mix(in srgb, var(--jk-amber) 12%, transparent);
    --jk-dark: 0;
    --jk-radius: 2px;
    /* Dark halo behind display text sitting over a photo. Unlike the
       accent tokens this is deliberately not derived from the accent —
       its job is contrast, not colour. */
    --jk-scrim: rgba(0, 0, 0, 0.7);

    /* VT323 has a small x-height and sits light next to a sans at the
       same nominal size. Every display rule below multiplies by this
       rather than hardcoding, so you can retune the whole scale here. */
    --jk-display-scale: 1.22;

    /* Header glow, factored out of the inline drop-shadows so the whole
       site's bloom tunes from one place. Same accent colour and tight
       core as before; the halo blur was dialled down over a few passes
       (started at inline 16px) so headers read crisp rather than hazy.
       Turn these up and every header glow widens together. Post titles
       no longer use these — they're deliberately flat now (see §7). */
    --jk-glow-core: 2px;   /* hot inner core, kept tight */
    --jk-glow-bloom: 5px;  /* header halo */

    /* Composite filter reused by every header below. Custom properties
       resolve at use-time, not here, so --jk-dark and --jk-glow are
       read wherever this is applied — which is why the glow still
       collapses to 0 on a light background. */
    --jk-header-glow:
        drop-shadow(0 0 calc(var(--jk-glow-core) * var(--jk-dark)) var(--jk-glow))
        drop-shadow(0 0 calc(var(--jk-glow-bloom) * var(--jk-dark)) var(--jk-glow));
}

:root.has-light-text {
    --jk-dark: 1;
    --color-border: var(--jk-hairline);
}


/* ---------- 2. Display face ----------
   VT323 on display elements ONLY — same rule as the PeerTube theme.
   Body copy, code blocks and anything you actually read at length
   stay on Solo's chosen face. A pixel bitmap font at 17px for a
   1200-word post is a readability problem, not a vibe. */
.gh-head-logo,
.gh-head-menu a,
.gh-head-link,
.gh-btn,
.gh-about-primary,
.gh-about-secondary,
.gh-card-title,
.gh-card-meta,
.gh-article-title,
.gh-article-tag,
.gh-article-meta,
.gh-content h1,
.gh-content h2,
.gh-content h3,
.gh-content h4,
.gh-cta-title,
.gh-cta-link,
.gh-comments-title,
.gh-navigation-text,
.gh-foot-menu a,
.gh-copyright,
.gh-subscribe-input {
    font-family: var(--jk-display);
    letter-spacing: 0.04em;
}

/* Uppercase is applied separately from the font swap — titles set by
   you (post titles, the about header) keep their own casing so a
   long headline doesn't turn into a wall of caps. */
.gh-head-menu a,
.gh-head-link,
.gh-btn,
.gh-card-meta,
.gh-article-tag,
.gh-cta-title,
.gh-comments-title,
.gh-navigation-text,
.gh-foot-menu a {
    text-transform: uppercase;
    letter-spacing: 0.06em;
}


/* ---------- 3. Geometry ----------
   Solo ships pill buttons (100px), rounded cards (24px), round
   avatars (50%) and a 72px radius on the typographic-profile image.
   Squared off to match the terminal, exactly as on PeerTube.
   The avatar stays round-ish at 2px only if you want it to; if you'd
   rather keep the circular profile image, delete the two 50% rules. */
.gh-btn,
.gh-primary-btn,
.gh-outline-btn,
.gh-text-btn,
.gh-card,
.gh-card-image,
.gh-card-image img,
.gh-article-image,
.gh-subscribe-input,
.gh-cta,
.gh-about-image,
.gh-card-access,
input,
textarea,
select,
pre,
:not(pre) > code {
    border-radius: var(--jk-radius) !important;
}


/* ---------- 4. Masthead ----------
   The wordmark is the one place the glow runs at full strength. Both a
   text logo (site title) and an image logo are handled: the text rules
   glow via text-shadow, the image via a drop-shadow filter below. Both
   read from the same knobs and both widen on hover. */
:root {
    --jk-logo-glow: 18px;        /* resting halo */
    --jk-logo-glow-hover: 30px;  /* wider halo on hover */
}

.gh-head-logo {
    font-size: calc(2.4rem * var(--jk-display-scale));
    font-weight: 400; /* VT323 has one weight; 800 triggers faux-bold */
    color: var(--jk-amber);
    text-shadow:
        0 0 calc(2px * var(--jk-dark)) var(--jk-glow),
        0 0 calc(var(--jk-logo-glow) * var(--jk-dark)) var(--jk-glow);
    transition: text-shadow 0.2s ease;
}

/* Image logo: text-shadow can't touch an <img>, so its glow is a
   drop-shadow filter over the logo's non-transparent pixels — which is
   why a picture logo wasn't glowing before. Kept in step with the text
   version so the two are interchangeable. */
.gh-head-logo img {
    filter:
        drop-shadow(0 0 calc(2px * var(--jk-dark)) var(--jk-glow))
        drop-shadow(0 0 calc(var(--jk-logo-glow) * var(--jk-dark)) var(--jk-glow));
    transition: filter 0.2s ease;
}

/* Hover widens the halo — text and image handled separately since one
   glows through text-shadow and the other through filter. */
.gh-head-logo:hover {
    text-shadow:
        0 0 calc(2px * var(--jk-dark)) var(--jk-glow),
        0 0 calc(var(--jk-logo-glow-hover) * var(--jk-dark)) var(--jk-glow);
}

.gh-head-logo:hover img {
    filter:
        drop-shadow(0 0 calc(2px * var(--jk-dark)) var(--jk-glow))
        drop-shadow(0 0 calc(var(--jk-logo-glow-hover) * var(--jk-dark)) var(--jk-glow));
}

@media (prefers-reduced-motion: reduce) {
    .gh-head-logo,
    .gh-head-logo img {
        transition: none;
    }
}

/* Nav sizing knob. VT323's small x-height means it reads smaller than
   a sans at the same nominal value, so the header needed more than the
   1.6rem Solo sets. */
:root {
    --jk-nav-size: 2.1rem;
    --jk-head-btn-size: 1.7rem;
}

.gh-head-menu a,
.gh-head-link {
    font-size: calc(var(--jk-nav-size) * var(--jk-display-scale));
    /* Same accent bloom as the headers (§7c) and the wordmark, so the
       whole top chrome reads as lit rather than the wordmark glowing
       alone. Collapses to 0 on a light background via --jk-dark like
       every other glow. */
    filter: var(--jk-header-glow);
}

/* Terminal cursor on the active nav item — structural, not decorative:
   it marks where you are, which is the one thing nav needs to say. */
.gh-head-menu .nav-current a::before {
    content: '> ';
    color: var(--jk-amber);
}

/* The current item is already marked by the cursor above, so it doesn't
   need the glow too — killing it lets the active page read as settled
   while the other links stay lit. 0-2-1 beats the 0-1-1 glow rule, so
   this wins without !important. pointer-events:none also makes it
   inert — no point linking a page to itself — and cursor:default drops
   the hand so it doesn't invite a click. */
.gh-head-menu .nav-current a {
    filter: none;
    pointer-events: none;
    cursor: default;
}

.gh-head-menu a:hover,
.gh-head-link:hover {
    color: var(--jk-amber);
    opacity: 1;
}

/* On the homepage "Large background" layout the header is transparent,
   and Solo colours the nav links by setting their REST colour on a
   high-specificity selector built from its real markup —
   .is-head-transparent .gh-head .nav>li a (0-3-2), not .gh-head-menu a.
   That REST rule outranks a .gh-head-menu a:hover (0-3-1) because it
   carries an extra type selector, so the amber hover lost on the root
   page. Fix: mirror Solo's own selectors for each head state and append
   :hover, so each wins by exactly one added pseudo-class. (Transparent
   uses .nav>li a; the dark/brand states use .nav a — matched as Solo
   writes them.) */
.is-head-transparent .gh-head .nav > li a:hover,
.is-head-transparent .gh-head-link:hover,
.is-head-dark:not(.is-head-transparent) .gh-head .nav a:hover,
.is-head-dark:not(.is-head-transparent) .gh-head-link:hover,
.is-head-brand:not(.is-head-transparent) .gh-head .nav a:hover,
.is-head-brand:not(.is-head-transparent) .gh-head-link:hover {
    color: var(--jk-amber);
    opacity: 1;
}

/* Hairline under the header instead of Solo's transparent float, so
   the terminal has a top chrome edge. */
.gh-head {
    border-bottom: 1px solid var(--jk-hairline);
}

/* is-head-transparent is set on the homepage when you're using the
   "Large background" header layout — the rule above would draw a line
   across the cover image, so drop it there. */
.is-head-transparent .gh-head {
    border-bottom: 0;
}

/* Black fade behind the transparent masthead. On the "Large
   background" homepage layout the menu, wordmark and Subscribe button
   float directly over the cover photo — legible on a dark image, gone
   on a bright one. This lays a top-anchored gradient scrim between the
   cover and the header content: dark at the very top where the chrome
   sits, fully transparent by the bottom so the photo is untouched
   below the fold of the header.

   It's a ::before on .gh-head — which is full-viewport-width and
   z-indexed above the cover when transparent — with z-index:-1, so it
   sits ABOVE the photo but BEHIND the logo/menu/button. Scoped to
   .is-head-transparent so inner pages, which already have the
   near-black bar and hairline, never get it.

   Two knobs: darkness of the fade, and how far down it reaches. */
:root {
    --jk-masthead-scrim: rgba(0, 0, 0, 0.72);
    --jk-masthead-scrim-height: 220px;
}

.is-head-transparent .gh-head::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: var(--jk-masthead-scrim-height);
    /* Middle stop keeps the falloff from looking like a hard band —
       most of the darkness lives in the top third, then eases out. */
    background: linear-gradient(
        to bottom,
        var(--jk-masthead-scrim) 0%,
        color-mix(in srgb, var(--jk-masthead-scrim) 38%, transparent) 55%,
        transparent 100%
    );
    z-index: -1;
    pointer-events: none;
}

.gh-icon-btn:hover,
.gh-search:hover {
    color: var(--jk-amber);
}

/* Account / Subscribe button, styled to match PeerTube's primary
   buttons: filled accent, 2px corners, VT323 uppercase, no outline.

   Solo doesn't just leave this alone — the shared theme assets force
   it to white-on-near-black whenever the header sits over an image or
   a colored bar:
     .is-head-transparent .gh-head-btn { color:#15171a; background:#fff }
   plus identical rules for .is-head-dark and .is-head-brand. All are
   0-2-0, so a bare .gh-primary-btn (0-1-0) loses regardless of load
   order. That's why the button has been white on your cover photo.

   Each variant is matched here at equal specificity rather than
   reached for with !important, so the rules stay overridable later. */
.gh-head-btn.gh-btn,
.is-head-transparent .gh-head-btn,
.is-head-dark .gh-head-btn,
.is-head-brand .gh-head-btn {
    /* Solo sizes .gh-btn padding in em against a --multiplier knob:
       calc(.75em * var(--multiplier,1)) calc(1.15em * var(--multiplier,1)).
       The previous version here hardcoded padding at 0.3em, which
       stopped it tracking font-size — so as the text got bigger the
       button got proportionally squatter. That's the awkward sizing.
       Setting the multiplier instead keeps Solo's intended proportions
       and scales the whole button together. */
    --multiplier: 0.8;
    font-size: calc(var(--jk-head-btn-size) * var(--jk-display-scale));
    font-weight: 400;
    color: #fff; /* white on the accent fill — see note below */
    background-color: var(--jk-amber);
    border: 1px solid var(--jk-amber);
    border-radius: var(--jk-radius);
    box-shadow: 0 0 calc(14px * var(--jk-dark)) var(--jk-glow);
    transition: background-color 0.15s ease, box-shadow 0.15s ease,
                border-color 0.15s ease;
}

/* PeerTube's primary button behaviour: the fill darkens while the
   glow around it widens, so it reads as pressed-and-lit rather than
   just brighter. */
.gh-head-btn.gh-btn:hover,
.is-head-transparent .gh-head-btn:hover,
.is-head-dark .gh-head-btn:hover,
.is-head-brand .gh-head-btn:hover {
    opacity: 1;
    background-color: color-mix(in srgb, var(--jk-amber) 78%, #000);
    border-color: color-mix(in srgb, var(--jk-amber) 78%, #000);
    box-shadow: 0 0 calc(30px * var(--jk-dark)) var(--jk-glow);
}

@media (prefers-reduced-motion: reduce) {
    .gh-head-btn.gh-btn {
        transition: none;
    }
}
/* The #fff above is the one hardcoded color in this file. Ghost gives
   no "contrasting text for the accent" variable, and your red carries
   white fine. If you ever move the accent to a pale color, this is the
   line to revisit. */


/* ---------- 5. Homepage header ---------- */
.gh-about-primary {
    font-size: calc(clamp(3.2rem, 2rem + 4vw, 6rem) * var(--jk-display-scale));
    font-weight: 400;
    line-height: 1;
    letter-spacing: 0.02em;
    /* Two halos, and the order matters. Filters apply left to right,
       each one taking the previous result as its input, and each
       drop-shadow paints behind everything accumulated so far — so the
       LAST entry sits furthest back.

       Red first (tight core, then wide bloom), dark scrim last. The
       scrim is cast from a silhouette that already includes the red
       glow, so it spreads wider than the letters and lifts the whole
       headline off a bright cover photo without outlining the glyphs. */
    filter:
        drop-shadow(0 0 calc(2px * var(--jk-dark)) var(--jk-glow))
        drop-shadow(0 0 calc(28px * var(--jk-dark)) var(--jk-glow))
        drop-shadow(0 2px 22px var(--jk-scrim));
}

.gh-about-secondary {
    font-size: calc(1.8rem * var(--jk-display-scale));
    opacity: 0.72;
}

/* Subscribe field styled as a prompt line. Solo renders this as a
   fake input (a div with placeholder text) that opens Portal on
   click — so this is pure presentation, no form semantics to break. */
.gh-subscribe-input {
    font-size: calc(1.6rem * var(--jk-display-scale));
    border: 1px solid var(--jk-hairline);
    background: var(--jk-tint);
    box-shadow: 0 0 calc(20px * var(--jk-dark)) color-mix(in srgb, var(--jk-amber) 18%, transparent);
}

.gh-subscribe-input::before {
    content: '> ';
    color: var(--jk-amber);
}


/* ---------- 6. Buttons ---------- */
.gh-btn {
    font-size: calc(1.5rem * var(--jk-display-scale));
    font-weight: 400;
    border: 1px solid var(--jk-amber);
}

.gh-primary-btn {
    background-color: var(--jk-amber);
    box-shadow: 0 0 calc(16px * var(--jk-dark)) var(--jk-glow);
}

.gh-primary-btn:hover {
    opacity: 1;
    box-shadow: 0 0 calc(26px * var(--jk-dark)) var(--jk-glow);
}

.gh-outline-btn,
.gh-head-btn:not(.gh-btn) {
    color: var(--jk-amber);
    border-color: var(--jk-hairline);
}


/* ---------- 7. Post feed ---------- */

/* Post titles carry no glow — deliberately flat. A bloom behind every
   card turned the whole index into a haze, so the titles sit as plain
   light text and hover is the only accent they get. The steady-state
   phosphor bloom lives on the headers (§7c) instead. */
.gh-card-title {
    font-weight: 400;
    letter-spacing: 0.02em;
    transition: color 0.2s ease;
}
/* NOTE: the earlier version of this rule set font-size here and it
   never took effect. Solo sizes card titles per layout
   (.has-classic-feed .gh-card-title, 0-2-0) which outranks a bare
   .gh-card-title (0-1-0) regardless of file order. Sizing now lives
   in §7b at matching specificity. */

/* Hover shifts the title to the accent colour — no bloom, just the
   colour change, so the affordance stays but the feed stays clean. */
.gh-card-link:hover .gh-card-title {
    color: var(--jk-amber);
}

.gh-card-meta {
    font-size: calc(1.5rem * var(--jk-display-scale));
}

/* Solo separates meta items with a • pseudo-element. Swapped for a
   pipe, which is the divider a terminal would actually use. */
.gh-card-meta > * + *:not(script):not(.gh-card-access)::before {
    content: '|';
    background: none !important;
    width: auto !important;
    height: auto !important;
    border-radius: 0 !important;
    opacity: 0.4;
}

/* The members/paid lock badge. This is the Joel+ gate made visible,
   so it's the one element in the feed that gets amber — it's the
   difference between a post you can read and one you can't. */
.gh-card-access {
    color: var(--jk-amber);
    border: 1px solid var(--jk-hairline);
    background: var(--jk-tint);
    padding: 0.2em 0.6em;
}

.gh-card-access svg {
    fill: var(--jk-amber);
}

/* Feed card hover.
   Problem: setting background-color on .gh-card meant the tint box
   started exactly at the text's left edge and ended exactly at the
   image's right edge, so it read as a rectangle drawn tight around
   the content rather than a highlighted row.

   Padding the card would fix the tint but push the title out of
   alignment with the "> LATEST" marker above it. So the tint is a
   pseudo-element bled outward with negative horizontal inset instead:
   the card box, the text, and the §7b divider all stay exactly where
   they are, and only the tint extends. */
:root {
    --jk-card-bleed: clamp(1.6rem, 1.2rem + 1vw, 3.2rem);
    --jk-card-tint-halo: -2rem;
}

.has-classic-feed .gh-card,
.has-parallax-feed .gh-card {
    position: relative;
}

.has-classic-feed .gh-card::before {
    content: '';
    position: absolute;
    /* Vertical geometry, since this is what looked wrong before:
       the card carries padding-top of one gap (below the divider) and
       margin-top of one gap (above it). Insetting the tint by a third
       of the gap left it shorter than the image, so the image stuck
       out the bottom of the highlight.

       Now it sits half a gap below the divider and extends half a gap
       past the content, symmetric top and bottom, and taller than the
       image at any size. */
    /* Vertical extent, expressed as halo — how far the tint reaches
       past the content, top and bottom. Measuring outward from the
       content rather than inward from the card box means this number
       does what it looks like it does: smaller = tighter to the image.

       The top value has to subtract from the gap because the card's
       padding-top sits between its box edge and the content. */
    top: calc(var(--jk-feed-gap) - var(--jk-card-tint-halo));
    bottom: calc(var(--jk-card-tint-halo) * -1);
    left: calc(var(--jk-card-bleed) * -1);
    right: calc(var(--jk-card-bleed) * -1);
    z-index: 0;
    background-color: var(--jk-tint);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

.has-classic-feed .gh-card:hover::before {
    opacity: 1;
}

/* The first card is the exception: the §7b divider rule is
   .gh-card + .gh-card, so card one has no border and no padding-top.
   The `top` calc above assumes a full gap of padding to sit inside,
   so on card one it pushed the tint below the content and the image
   poked out the top. With no padding to subtract from, top is just
   the halo, same as bottom. */
.has-classic-feed .gh-feed .gh-card:first-child::before {
    top: calc(var(--jk-card-tint-halo) * -1);
}

/* No-image cards are short, and the image-tuned offsets above (top
   pushed a full gap-plus down, bottom inset) collapse the tint into a
   thin strip drifting across the middle — the title lands on it, the
   date falls outside it below. Re-anchor to the card box so the tint
   wraps the whole title+meta block with a small symmetric halo, and
   both lines sit on it. Non-first cards carry a gap of padding-top
   below the divider, so the tint starts half a gap in; the first card
   has no padding, so it starts half a gap above the content instead.
   Higher specificity than both rules above, so order-independent. */
.has-classic-feed .gh-feed .gh-card.no-image::before {
    top: calc(var(--jk-feed-gap) * 0.5);
    bottom: calc(var(--jk-feed-gap) * -0.5);
}

.has-classic-feed .gh-feed .gh-card.no-image:first-child::before {
    top: calc(var(--jk-feed-gap) * -0.5);
    bottom: calc(var(--jk-feed-gap) * -0.5);
}

/* Keeps the link content above the tint layer. */
.has-classic-feed .gh-card-link {
    position: relative;
    z-index: 1;
}

/* Parallax cards are already full-bleed, so they keep the simple
   background swap — there's no text edge to escape from. */
.has-parallax-feed .gh-card {
    transition: background-color 0.2s ease;
}

.has-parallax-feed .gh-card:hover {
    background-color: var(--jk-tint);
}


/* ---------- 7b. Feed dividers and density ----------
   Solo is built for a site with a handful of long posts and sizes the
   feed accordingly: Classic titles clamp up to 5.2rem, gaps up to 8rem,
   Typographic gaps up to 16rem. With a short feed that's a lot of
   scrolling for not much index.

   All three layouts are handled — the body class decides which block
   applies, so this works whichever post_feed_layout you're on.

   Two knobs. Retune here rather than editing each clamp. */
:root {
    --jk-feed-gap: clamp(1.6rem, 1.2rem + 1vw, 2.4rem);
    --jk-feed-title: clamp(2.4rem, 2rem + 1.1vw, 3.4rem);
}

/* The divider itself. border-top on the second and subsequent cards
   rather than a pseudo-element, so it can't be knocked out of place by
   the flex/grid each layout sets up on the card's children.
   :first-child never gets one, so no stray rule above the top post. */
.gh-feed .gh-card + .gh-card {
    border-top: 1px solid var(--jk-hairline);
    margin-top: var(--jk-feed-gap);
    padding-top: var(--jk-feed-gap);
}

/* --- Classic ---
   Two overrides needed beyond the shared rule above: Solo gives the
   no-image variants a much larger gap (up to 16rem), and the title
   clamp needs beating at 0-2-0. */
.has-classic-feed .gh-feed .gh-card + .gh-card.no-image,
.has-classic-feed .gh-feed .gh-card.no-image + .gh-card {
    margin-top: var(--jk-feed-gap);
    padding-top: var(--jk-feed-gap);
}

/* Text-only posts: Solo caps these cards at max-width 1200px to keep
   line length sane, but the §7b divider is a border-top on the card,
   so a narrow card draws a short line — which is why the rule above a
   text post stopped early while the one below it ran full width.

   Card goes full width so the divider does too; the 1200px cap moves
   to the text column, where it was actually doing the work. */
.has-classic-feed .gh-feed .gh-card.no-image {
    max-width: none;
}

.has-classic-feed .gh-feed .gh-card.no-image .gh-card-wrapper {
    max-width: 1200px;
}

/* ...which introduced the indent you just saw. Solo lays the card out
   with flex-direction: row-reverse so the image sits on the right.
   On a text post the image is display:none, leaving the wrapper as the
   only flex item — and under row-reverse, main-start is the RIGHT
   edge, so a wrapper narrower than the card packs right and leaves the
   gap on the left.

   Reversing has no purpose when there's only one item, so text posts
   get normal row direction and the title lines up with every other
   title in the feed. */
.has-classic-feed .gh-feed .gh-card.no-image .gh-card-link {
    flex-direction: row;
}

.has-classic-feed .gh-feed .gh-card-title,
.has-classic-feed .gh-feed .kg-width-full .gh-card-title {
    font-size: calc(var(--jk-feed-title) * var(--jk-display-scale));
}

/* Solo's row-reverse card puts the image at ~half width with a gap of
   up to 8rem. Tightened so the text column carries more of the row. */
.has-classic-feed .gh-feed .gh-card-link {
    gap: clamp(2.4rem, 2rem + 1.6vw, 4rem);
}

/* Image size is the real driver of card height here, not type.
   Solo makes .gh-card-image flex:1 against a flex:1 text column, so
   it takes half the row — on a 1440px container that's a ~700px-wide
   image at 16:9, i.e. ~394px of height per post on its own.

   Two knobs. Width is the one to reach for first: narrowing the
   column shrinks height proportionally at the same aspect ratio.
   Aspect is there if you'd rather crop letterbox than go narrower. */
:root {
    --jk-card-image-width: 38%;
    --jk-card-image-aspect: 56.25%; /* 16:9. 50% = 2:1, 42% ≈ 21:9 */
}

.has-classic-feed .gh-feed .gh-card-image {
    flex: 0 0 var(--jk-card-image-width);
}

.has-classic-feed .gh-feed .gh-card-image::before {
    padding-bottom: var(--jk-card-image-aspect);
}

/* Below 991px Solo stacks the card to a column and the image goes
   full width, where a narrow flex-basis would fight the stack.
   Handing it back is deliberate. */
@media (max-width: 991px) {
    .has-classic-feed .gh-feed .gh-card-image {
        flex: 1;
    }
}

/* --- Typographic --- */
.has-typographic-feed .gh-feed .gh-card {
    margin-top: var(--jk-feed-gap);
}

.has-typographic-feed .gh-feed .gh-card-title {
    font-size: calc(var(--jk-feed-title) * var(--jk-display-scale));
}

/* --- Parallax ---
   min-height is what makes this layout tall, not type size. 640px is
   roughly a full viewport per post; 420px gets two on screen. */
.has-parallax-feed .gh-feed .gh-card {
    min-height: 420px;
}

.has-parallax-feed .gh-feed .kg-width-full .gh-card {
    min-height: 520px;
}

.has-parallax-feed .gh-feed .gh-card-wrapper {
    padding: 4rem 3.2rem;
}

.has-parallax-feed .gh-feed .gh-card-title,
.has-parallax-feed .gh-feed .kg-width-full .gh-card-title {
    font-size: calc(var(--jk-feed-title) * var(--jk-display-scale));
}

/* Parallax cards are full-bleed images — a hairline between two of
   them lands on the image edge and reads as a seam, not a divider.
   Spacing alone separates them there. */
.has-parallax-feed .gh-feed .gh-card + .gh-card {
    border-top: 0;
    padding-top: 0;
    margin-top: var(--jk-feed-gap);
}


/* ---------- 7c. Header glow (site-wide) ----------
   "Glow behind every header." The masthead wordmark (§4) and the
   homepage hero (§5) already bloom; this extends the same amber halo
   to the headers that never had one — the post-page title, in-content
   headings, the membership CTA title and the comments header — via the
   shared --jk-header-glow token, so they all match and all retune from
   §1 alongside the post titles.

   .gh-card-title is deliberately NOT in this list: it carries its own
   filter plus a hover variant in §7, and folding it in here would drop
   the hover state. To glow anything else, add its selector below. */
.gh-article-title,
.gh-content h1,
.gh-content h2,
.gh-content h3,
.gh-content h4,
.gh-cta-title,
.gh-comments-title {
    filter: var(--jk-header-glow);
}


/* ---------- 8. Post page ----------
   Sizing knobs for the post/page title and its subtitle (Ghost calls
   the subtitle a "custom excerpt"; Solo renders it as
   .gh-article-excerpt directly under the title). */
:root {
    --jk-article-title-size: clamp(2.6rem, 1.4vw + 2rem, 3.6rem);
    --jk-article-excerpt-size: clamp(1.6rem, 0.2vw + 1.5rem, 1.8rem);
}

.gh-article-title {
    font-weight: 400;
    letter-spacing: 0.01em;
    font-size: calc(var(--jk-article-title-size) * var(--jk-display-scale));
}
/* Why the title was 19.52px: the previous rule here said
   `font-size: calc(1em * var(--jk-display-scale))`. I meant "scale
   whatever size is inherited", but font-size:1em resolves against the
   PARENT, not against the element's own cascaded value — so it threw
   away Solo's clamp(4rem, 2.86vw + 2.63rem, 7.2rem) and resolved to
   the header's 16px instead. 16 × 1.22 = 19.52.

   Specificity didn't save it either: both rules are 0-1-0 and mine
   loads later, so mine won. Same mistake as the card titles, opposite
   outcome — there the layout selector outranked me and my rule did
   nothing; here I outranked Solo and actively broke it. Any `1em`
   font-size in this file is a bug. */

.gh-article-excerpt {
    font-size: var(--jk-article-excerpt-size);
}
/* Left in Manrope deliberately. It's a sentence of prose sitting
   directly above body copy, and VT323 at 17px is a readability
   problem rather than a style choice — same rule as §2. */

/* Locked-post overlap fix. On a members-only post Solo pulls the CTA
   gate (.gh-cta — z-index:20, opaque background, its own stacking
   context from transform:translateZ(0)) upward via .gh-cta-gradient's
   -16rem margin to fade the preview into it. When there's little or no
   preview content, that pull reaches the article header and the gate
   paints over the tag chip. Lifting the whole header above the gate
   keeps the tag (and title, if the pull is deep enough) on top. */
.gh-article-header {
    position: relative;
    z-index: 21; /* one above Solo's .gh-cta at 20 */
}

.gh-article-tag {
    font-size: calc(1.4rem * var(--jk-display-scale));
    color: var(--jk-amber);
    /* Opaque page-coloured fill (Solo leaves the chip transparent) so
       where the chip sits over the gate it reads as a clean chip rather
       than showing the gate's tint through its middle. On the normal
       dark page this matches the background, so it's invisible there. */
    background-color: var(--background-color);
}

/* The '#' prefix marks the tag as a collection link. VT323's hash is
   a thin, low-contrast glyph, so at 0.6 opacity it read as a smudge.
   Near-full opacity, sized up a touch against the label, and the gap
   moved off the string onto margin so it can't collapse. */
.gh-article-tag::before {
    content: '#';
    font-size: 1.15em;
    opacity: 0.85;
    margin-right: 0.3em;
}

.gh-article-meta {
    font-size: calc(1.4rem * var(--jk-display-scale));
}

.gh-content h1,
.gh-content h2,
.gh-content h3,
.gh-content h4 {
    font-weight: 400;
    letter-spacing: 0.02em;
}

/* Content heading scale.
   Solo only sets h2 (2.25em) and h3 (1.6em) and leaves h1 to the
   shared assets — which is why an editor h1 was landing *smaller*
   than the h2 below it, and why it was still in the body sans while
   the h2 had already gone VT323.

   Sizes are in em against --content-font-size (~17–20px), so the whole
   scale tracks the reading size. The base ems set the hierarchy
   between levels; --jk-content-heading-scale multiplies all four at
   once, so bump that one knob to grow (or shrink) the in-content
   headings together without disturbing their relative sizing.
   Sits above 1 because VT323's small x-height reads lighter than a
   sans at the same em. */
:root {
    --jk-content-heading-scale: 1.25;
}

.gh-content h1 { font-size: calc(2.55em * var(--jk-content-heading-scale)); }
.gh-content h2 { font-size: calc(1.9em  * var(--jk-content-heading-scale)); }
.gh-content h3 { font-size: calc(1.48em * var(--jk-content-heading-scale)); }
.gh-content h4 { font-size: calc(1.2em  * var(--jk-content-heading-scale)); }

.gh-content a {
    color: var(--jk-amber);
    text-decoration-color: var(--jk-hairline);
    text-underline-offset: 0.2em;
}

.gh-content a:hover {
    text-decoration-color: var(--jk-amber);
}

blockquote:not([class]) {
    border-left: 2px solid var(--jk-amber);
    padding-left: 2rem;
}

:not(pre) > code {
    color: var(--jk-amber);
    background-color: var(--jk-tint);
    border: 1px solid var(--jk-hairline);
}

/* Deliberately NOT switched to VT323 — code needs to be legible
   character-by-character and JetBrains Mono already is. */
pre {
    border: 1px solid var(--jk-hairline);
}


/* ---------- 9. Membership CTA  [PORTAL-adjacent] ----------
   The gate itself, on a paid post. Everything outside the Portal
   iframe is yours; the modal that opens on click is not. */
:root {
    --jk-cta-max-width: 90rem;   /* narrower than the 1440px content */
}

.gh-cta {
    border: 1px solid var(--jk-hairline);
    background: var(--jk-tint);
    padding: 3.2rem;
    text-align: center;
    /* Narrow the full-width panel to a centred modal. Solo pins it to
       grid-column 1/span 12 !important; max-width caps the rendered
       width and margin-inline:auto centres it within that span (Solo's
       margin-top is untouched). This is what actually clears the tag:
       the tag is left-aligned, so a centred narrower gate can't sit
       under it no matter how the vertical placement lands. */
    max-width: var(--jk-cta-max-width);
    margin-inline: auto;
}

/* Gate placement. Solo dissolves the truncated preview into the gate
   with .gh-cta-gradient (margin-top:-16rem), pulling the whole assembly
   up over the last stretch of content. On a post with little or no
   preview that overshoots into the article header, so the gate climbed
   onto the tag. Zeroing the rise removes the pull entirely, so the
   CTA's own positive margin-top places it well below the header. The
   trade-off is only on posts with a long preview — the fade then sits
   below the content instead of dissolving over it; dial toward -16rem
   if you want that dissolve back and don't mind the climb. */
:root {
    --jk-cta-rise: 0;
}

.gh-cta-gradient {
    margin-top: var(--jk-cta-rise);
}

.gh-cta-title {
    font-size: calc(2rem * var(--jk-display-scale));
    font-weight: 400;
    color: var(--jk-amber);
}

.gh-cta-title::before {
    content: '⌁ ';
}

.gh-cta-link {
    font-size: calc(1.5rem * var(--jk-display-scale));
    opacity: 0.7;
    cursor: pointer;
}

.gh-cta-link:hover {
    opacity: 1;
    color: var(--jk-amber);
}

/* Solo fades the truncated post body into the CTA with a gradient
   keyed to --background-color; it already follows your background
   setting, so it needs no override. Left here as a note so future-you
   doesn't go looking for why the fade "just works". */


/* ---------- 10. Footer ---------- */
/* Solo ships padding-top: clamp(8rem, 4.55vw + 6.18rem, 12rem) here.
   An earlier version of this file flattened that to 4rem, which is
   what pinned the footer bar up against the end of the post.

   Space above the rule is margin (gap between content and the bar),
   space below it is padding (breathing room inside the bar). Both are
   needed — padding alone leaves the rule sitting right on the last
   line of text. */
.gh-foot {
    /* The hairline needs air on both sides or it reads as underlining
       the last paragraph rather than closing the page. margin-top is
       the gap above the rule, padding-top the gap below it. */
    margin-top: clamp(4rem, 3rem + 2vw, 6rem);
    border-top: 1px solid var(--jk-hairline);
    padding-top: clamp(4.8rem, 3.5rem + 3vw, 7.2rem);
    padding-bottom: clamp(4rem, 3rem + 2.4vw, 6.4rem);
}

.gh-foot-menu a {
    font-size: calc(1.4rem * var(--jk-display-scale));
}

.gh-foot-menu a:hover,
.gh-social-links a:hover {
    color: var(--jk-amber);
}

.gh-copyright {
    font-size: calc(1.4rem * var(--jk-display-scale));
    opacity: 0.6;
}


/* ---------- 11. Signature: the Joel+ pulse ----------
   Carried over from the PeerTube theme, where a pulsing element
   pointed AT Joel+ from the video side. On this site it belongs on
   the thing it was always pointing to: the subscribe button.

   Only ever one pulse on a page — restraint is the whole reason it
   reads as emphasis rather than noise. */
.gh-head-btn.gh-primary-btn,
.gh-cta .gh-primary-btn {
    animation: jk-pulse 3.4s ease-in-out infinite;
}

@keyframes jk-pulse {
    0%, 100% {
        box-shadow: 0 0 calc(10px * var(--jk-dark)) var(--jk-glow);
    }
    50% {
        box-shadow: 0 0 calc(28px * var(--jk-dark)) var(--jk-glow);
    }
}

@media (prefers-reduced-motion: reduce) {
    .gh-head-btn.gh-primary-btn,
    .gh-cta .gh-primary-btn {
        animation: none;
    }
}


/* ---------- 12. Accessibility floor ----------
   Solo's focus styles are the browser default over a dark surface,
   which is close to invisible. Amber ring instead. */
a:focus-visible,
button:focus-visible,
.gh-btn:focus-visible,
.gh-card-link:focus-visible,
.gh-subscribe-input:focus-visible {
    outline: 2px solid var(--jk-amber);
    outline-offset: 3px;
}