/* ============================================================================
   3D PRODUCTIVITY — custom layer over Solo
   ----------------------------------------------------------------------------
   Kept as a separate stylesheet (not merged into built/screen.css) so the fork
   stays diffable against upstream Solo.

   Built entirely on Solo's own design tokens — --color-border,
   --color-secondary-text, --container-width, --ghost-accent-color — so it
   tracks Solo rather than fighting it.

   NOTE (2026-08-13): this header used to claim the theme "inherits Solo's DARK
   MODE automatically". It does not — there is not a single
   prefers-color-scheme rule in built/screen.css. The theme is light-only in
   practice, which is why the contrast values below are stated against white and
   nothing here carries a dark-mode counterpart. Restore that claim only after
   verifying it against the built stylesheet, not the token names.
   ========================================================================== */

:root {
    /* Vertical rhythm — one scale, used everywhere. No arbitrary vmin. */
    --dp-space-xs: 0.8rem;
    --dp-space-sm: 1.6rem;
    --dp-space-md: 2.4rem;
    --dp-space-lg: 4rem;
    --dp-space-xl: 6.4rem;
    --dp-space-2xl: clamp(6.4rem, 9vw, 11rem);

    /* Fluid type, matching Solo's clamp approach */
    --dp-title-size: clamp(2.4rem, 1vw + 2.1rem, 3.2rem);
    --dp-lead-size: clamp(1.8rem, 0.5vw + 1.6rem, 2.2rem);
    --dp-body-size: clamp(1.6rem, 0.3vw + 1.5rem, 1.8rem);
    --dp-small-size: 1.4rem;

    --dp-radius: 12px;
    --dp-radius-lg: 16px;

    /* ONE width system. Solo's container is 1440px; the stage now matches it
       instead of inventing a narrower 1320px. Measure caps belong only on
       long-form prose, never on display type — that was the "title doesn't
       span on a wide window" bug. */
    --dp-max: var(--container-width, 1440px);

    /* Prose measure is now INDEPENDENT of the container (see POST WIDTH below).
       Previously post text was 8/12ths of the container, so widening the
       container would have blown the reading measure out to ~1230px. */
    --dp-prose: 960px;
    --dp-measure: 68ch;   /* comfortable reading measure for body prose only */

    /* Tinted surface that works in both light and dark */
    --dp-surface: color-mix(in srgb, var(--color-primary-text) 4%, transparent);
    --dp-surface-strong: color-mix(in srgb, var(--color-primary-text) 7%, transparent);

    /* ---- Contrast (WCAG 2.2, measured against #fff) ----------------------
       Solo ships --color-secondary-text: #999, which is 2.85:1 on white. That
       is below the 4.5:1 minimum for body text and it was in use in ~14 places
       here — read-next excerpts, join reassurance, quote citations, archive
       intro, post meta — all at 12-14px, where it is hardest to read. #767676
       is the darkest-possible gray that still reads as "secondary" while
       clearing the bar at 4.54:1. Do not lighten it back toward #999. */
    --color-secondary-text: #767676;

    /* The focus ring was var(--ghost-accent-color) directly. The brand accent
       #19c0ff is 2.09:1 on white — under the 3:1 that WCAG 2.2 requires of
       non-text UI — so the one affordance a keyboard user depends on was the
       least visible thing on the page. Darkening the accent 30% keeps the ring
       tied to whatever accent is set in Ghost admin (rather than hardcoding a
       cyan that silently stops matching) and lands at 4.13:1 for #19c0ff. */
    --dp-focus: color-mix(in srgb, var(--ghost-accent-color, #19c0ff) 70%, #000);

    /* Button hover was a hardcoded #0db0ef in both tool stylesheets — a hand-picked
       blue with no relationship to the accent, so changing the accent in Ghost admin
       left the hover state behind. Derived like --dp-focus, just a lighter darkening. */
    --dp-accent-hover: color-mix(in srgb, var(--ghost-accent-color, #19c0ff) 90%, #000);
}

/* The container itself must grow, or nothing inside it can. Floor at Solo's
   1440 so we never shrink; ceiling at 1840 so a 5K display doesn't produce a
   nav that spans the whole desk. */
@media (min-width: 1400px) {
    :root { --container-width: clamp(1440px, 93vw, 1840px); }
}

@media (min-width: 1600px) {
    :root { --dp-prose: 1020px; }
}

/* Fallback for browsers without color-mix */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
    :root {
        --dp-surface: rgba(128, 128, 128, 0.06);
        --dp-surface-strong: rgba(128, 128, 128, 0.11);
        /* 3.81:1 on white — the accent darkened by hand, since the mix above
           cannot be computed here. Fixed cyan rather than the live accent. */
        --dp-focus: #0b8cbf;
    }
}

/* ============================================================================
   ACCESSIBILITY UTILITIES
   ========================================================================== */

/* Hidden from sight, still read aloud. Used by page.hbs to give a page an h1
   when its title is deliberately switched off in Ghost admin. The clip-path +
   1px box is the standard recipe; display:none and visibility:hidden would
   remove it from the accessibility tree too, which defeats the point. */
.dp-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Skip link — offscreen until focused, then pinned to the top-left. Sits first
   in default.hbs so it is the first tab stop on every page. */
.dp-skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1000;
    padding: var(--dp-space-xs) var(--dp-space-sm);
    background: var(--color-darker-gray);
    color: #fff;
    font-size: var(--dp-small-size);
    text-decoration: none;
    border-radius: 0 0 var(--dp-radius) 0;
    transform: translateY(-100%);
}

/* Only moves into view on keyboard focus, so pointer users never see it. */
.dp-skip-link:focus {
    transform: translateY(0);
    outline: 2px solid var(--dp-focus);
    outline-offset: 2px;
}

/* ============================================================================
   SECTION SCAFFOLDING
   ========================================================================== */

.dp-section {
    padding-inline: max(4vmin, var(--dp-space-md));
    margin-block-start: var(--dp-space-2xl);
}

.dp-section-inner {
    max-width: var(--dp-max);
    margin-inline: auto;
}

/* Section headers: label + optional trailing meta, separated by a hairline */
.dp-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--dp-space-md);
    flex-wrap: wrap;
    margin-block-end: var(--dp-space-lg);
    padding-block-end: var(--dp-space-sm);
    border-block-end: 1px solid var(--color-border);
}

.dp-section-title {
    font-size: var(--dp-title-size);
    font-weight: 800;
    letter-spacing: -0.022em;
    line-height: 1.1;
    margin: 0;
}

.dp-section-meta {
    font-family: var(--font-mono);
    font-size: var(--dp-small-size);
    letter-spacing: 0.02em;
    color: var(--color-secondary-text);
    margin: 0;
}

/* Sits below the title/link row but inside the head, so it stays above the
   hairline. flex-basis:100% forces it onto its own line at every width. */
.dp-section-desc {
    flex-basis: 100%;
    margin: 0.6rem 0 0;
    max-width: var(--dp-measure);
    font-size: 1.5rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
    text-wrap: pretty;
}

/* Every other "go there" affordance in the theme is mono — .dp-stage-link,
   .dp-band-link, .dp-stage-about-link, .dp-tool-go. This one was sans at
   weight 600, so the same gesture rendered two different ways on one page
   ("All writing →" under Read vs "All 96+ pieces →" under Start here). */
.dp-section-link {
    font-family: var(--font-mono);
    font-size: var(--dp-small-size);
    letter-spacing: 0.02em;
    text-decoration: none;
    color: var(--color-secondary-text);
    transition: color 0.18s ease;
}

.dp-section-link:hover { color: var(--color-primary-text); }

/* ============================================================================
   THE STAGE — above-the-fold composition
   ----------------------------------------------------------------------------
   Ryan's requirement: understand everything he makes in ONE glance, no scroll.
   So this is a grid, not a stack: identity across the top, then three columns —
   daily video / recent writing / subscribe + follow.

   Sized to fit a laptop viewport. The 9:16 Short is the tallest element and
   therefore sets the row height, so the video column is deliberately narrow.
   ========================================================================== */

.dp-stage {
    padding-inline: max(4vmin, var(--dp-space-md));
    padding-block: clamp(var(--dp-space-lg), 5vh, var(--dp-space-xl));
}

.dp-stage-inner {
    max-width: var(--dp-max);
    margin-inline: auto;
    container-type: inline-size;
    container-name: stage;
}

/* ---------- identity row ---------- */

.dp-stage-head { margin-block-end: clamp(var(--dp-space-md), 3.5vh, var(--dp-space-lg)); }

.dp-stage-title {
    /* Baseline: sized against the CONTAINER (cqi), not the viewport (vw), so
       it stays proportional to its box at every width. */
    font-size: clamp(2.6rem, 5.4cqi + 0.4rem, 9rem);
    font-weight: 800;
    line-height: 1.04;
    letter-spacing: -0.034em;
    text-wrap: pretty;
    margin: 0;
}

/* Fitted mode — set by 3dp.js on wide viewports. The script measures the
   headline's true single-line width and solves for the size that fills the
   container, so the line always resolves rather than wrapping into two short
   stacked lines. Works for ANY headline text, including copy changed later
   in Ghost admin. */
.dp-stage-title.is-fitted {
    font-size: var(--dp-title-fit);
    white-space: nowrap;
    line-height: 1.02;
}

.dp-stage-cred {
    margin: var(--dp-space-sm) 0 0;
    /* Tracks the container. The old 0.35vw ramp moved it ~3px across the whole
       desktop range, which is why it read as fixed. */
    font-size: clamp(1.5rem, 1.15cqi + 1.02rem, 2.6rem);
    line-height: 1.55;
    color: var(--color-secondary-text);
    /* 80ch is the upper edge of a comfortable measure. Because the font now
       grows, 80ch is a much wider box in pixels than it used to be. */
    /* Tracks the container. An 80ch cap froze the line at ~1266px while the
       container ran to 1711px at 1840 wide, which read as a bug rather than a
       measure. */
    max-width: 100%;
    text-wrap: pretty;
}

/* ---------- the three-column grid ---------- */

.dp-stage-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--dp-space-lg);
    align-items: start;
}

/* Tablet: video beside writing, subscribe spans below */
@media (min-width: 760px) {
    .dp-stage-grid {
        grid-template-columns: minmax(220px, 260px) 1fr;
        gap: var(--dp-space-lg);
    }
    .dp-stage-join { grid-column: 1 / -1; }
}

/* Desktop: all three visible at once — the actual goal */
@media (min-width: 1100px) {
    .dp-stage-grid {
        grid-template-columns: clamp(230px, 20vw, 320px) minmax(0, 1fr) clamp(300px, 24vw, 400px);
    }
    .dp-stage-join { grid-column: auto; }
}

/* Below 760px the three columns stack, so DOM order silently becomes reading
   order — and Watch (one clip) landed above Read (96 posts). Measured at
   375x812: the first post title sat at y=761 against an 812px fold, so writing
   was effectively off the first screen on a phone. Read leads here instead.
   The desktop triad (Watch | Read | Implement) is deliberately untouched.
   This is VISUAL order only — keyboard focus still follows DOM order. Delete
   this block to put Watch back on top when video earns the lead. */
@media (max-width: 759px) {
    .dp-stage-read      { order: 1; }
    .dp-stage-watch     { order: 2; }
    .dp-stage-implement { order: 3; }
}

/* ---------- column furniture ---------- */

.dp-stage-label {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--dp-space-sm);
    margin-block-end: var(--dp-space-sm);
    padding-block-end: 0.8rem;
    border-block-end: 1px solid var(--color-border);
}

.dp-stage-label h2 {
    font-size: 1.3rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    margin: 0;
}

.dp-stage-link {
    font-family: var(--font-mono);
    font-size: 1.2rem;
    letter-spacing: 0.02em;
    color: var(--color-secondary-text);
    text-decoration: none;
    white-space: nowrap;
}

.dp-stage-link { transition: color 0.18s ease; }
.dp-stage-link:hover { color: var(--color-primary-text); }

/* ---------- video ---------- */

.dp-video {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--dp-radius);
    overflow: hidden;
    background: var(--color-darker-gray);
    box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 14px 34px -14px rgba(0,0,0,0.3);
}

.dp-video.is-short { aspect-ratio: 9 / 16; }

.dp-video iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}

.dp-stage-blurb {
    margin: var(--dp-space-sm) 0 0;
    font-size: 1.4rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
}

/* ---------- writing list ---------- */

.dp-stage-posts {
    list-style: none;
    margin: 0;
    padding: 0;
}

.dp-stage-post + .dp-stage-post { border-block-start: 1px solid var(--color-border); }

.dp-stage-post-link {
    display: block;
    padding: var(--dp-space-sm) 0;
    text-decoration: none;
    color: var(--color-primary-text);
    transition: opacity 0.16s ease;
}

.dp-stage-post-link:hover { opacity: 0.62; }

.dp-stage-post-link:focus-visible {
    outline: 2px solid var(--dp-focus);
    outline-offset: 3px;
}

.dp-stage-post-title {
    display: block;
    font-size: clamp(1.6rem, 0.4vw + 1.5rem, 1.9rem);
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: -0.014em;
    text-wrap: pretty;
}

.dp-stage-post-meta {
    display: flex;
    gap: 0.6rem;
    margin-block-start: 0.4rem;
    font-family: var(--font-mono);
    font-size: 1.15rem;
    color: var(--color-secondary-text);
}

/* ---------- subscribe + follow ---------- */

.dp-stage-join {
    display: flex;
    flex-direction: column;
    gap: var(--dp-space-md);
}

.dp-stage-joinbox {
    padding: var(--dp-space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
    background: var(--dp-surface);
}

.dp-stage-jointitle {
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1.4;
    letter-spacing: -0.012em;
    margin: 0 0 var(--dp-space-sm);
    text-wrap: pretty;
}

.dp-stage-input { margin: 0; }

.dp-stage-reassure {
    margin: 1rem 0 0;
    font-size: 1.2rem;
    color: var(--color-secondary-text);
}

.dp-cta-placeholder { opacity: 0.55; }

/* ---------- platform pills ---------- */

.dp-platforms {
    display: flex;
    flex-wrap: wrap;
    gap: 0.7rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.dp-platform {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 1.3rem;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-size: 1.3rem;
    font-weight: 600;
    text-decoration: none;
    color: var(--color-primary-text);
    transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

.dp-platform:hover {
    background: var(--dp-surface);
    border-color: color-mix(in srgb, var(--color-primary-text) 22%, transparent);
    transform: translateY(-1px);
}

.dp-platform:focus-visible {
    outline: 2px solid var(--dp-focus);
    outline-offset: 2px;
}

.dp-platform svg { width: 16px; height: 16px; fill: currentColor; flex: none; }

/* On the stage home, the site header is redundant with the identity row */
body.is-stage-home .gh-head { border-block-end: 1px solid var(--color-border); }

/* ============================================================================
   BELOW-FOLD SECTIONS / SUPPORT
   ========================================================================== */

.dp-btn-ghost {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    padding: 1.1rem 2.4rem;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-size: var(--dp-small-size);
    font-weight: 600;
    text-decoration: none;
    color: var(--color-primary-text);
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.dp-btn-ghost:hover {
    background: var(--dp-surface);
    border-color: color-mix(in srgb, var(--color-primary-text) 22%, transparent);
}

.dp-support-inner {
    max-width: 60ch;
    margin-inline: auto;
    text-align: center;
    padding: clamp(3.2rem, 4vw, 5.6rem) clamp(2.4rem, 3vw, 4rem);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius-lg);
    background: var(--dp-surface);
}

.dp-support-title {
    font-size: var(--dp-title-size);
    font-weight: 800;
    letter-spacing: -0.022em;
    margin: 0 0 var(--dp-space-sm);
}

.dp-support-text {
    font-size: var(--dp-body-size);
    line-height: 1.6;
    color: var(--color-secondary-text);
    margin: 0 0 var(--dp-space-md);
}

/* ============================================================================
   /JOIN
   ========================================================================== */

.dp-join { display: flex; align-items: center; min-height: min(78vh, 900px); padding-block: var(--dp-space-xl); }

.dp-join-card { max-width: 64rem; margin-inline: auto; text-align: center; }

.dp-join-title {
    font-size: clamp(3rem, 4.4vw, 4.6rem);
    font-weight: 800;
    line-height: 1.1;
    letter-spacing: -0.032em;
    text-wrap: balance;
    margin: 0 0 var(--dp-space-md);
}

.dp-join-sub {
    font-size: var(--dp-lead-size);
    line-height: 1.5;
    color: var(--color-secondary-text);
    text-wrap: pretty;
    margin: 0 auto var(--dp-space-lg);
    max-width: 48ch;
}

.dp-join-input { margin: 0 auto var(--dp-space-sm); max-width: 46rem; }

.dp-join-reassure { font-size: var(--dp-small-size); color: var(--color-secondary-text); margin: 0 0 var(--dp-space-xl); }

.dp-join-credibility {
    font-size: var(--dp-small-size);
    line-height: 1.65;
    color: var(--color-secondary-text);
    max-width: 52ch;
    margin-inline: auto;
    padding-block-start: var(--dp-space-md);
    border-block-start: 1px solid var(--color-border);
}

.dp-join-content { margin-block-start: var(--dp-space-lg); text-align: left; font-size: var(--dp-body-size); line-height: 1.65; }

body.is-join-page .gh-head-menu,
body.is-join-page .gh-foot-menu,
body.is-join-page .dp-sticky { display: none; }

/* ============================================================================
   ARCHIVE
   ========================================================================== */

.dp-archive { padding-block: var(--dp-space-xl); }
.dp-archive-head { margin-block-end: var(--dp-space-lg); }

.dp-archive-intro {
    font-size: var(--dp-body-size);
    line-height: 1.65;
    color: var(--color-secondary-text);
    max-width: 62ch;
    margin-block-end: var(--dp-space-lg);
}

/* Card grid, replacing the date | title | reading-time list. Checked against the
   data first: all 96 published posts carry a feature image and tags, and
   {{excerpt}} generates text for the 74 posts with no custom excerpt — so no
   card renders with a hole where a field should be. */
.dp-archive-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
}

/* Row divider instead of card gutters — a list reads as one column, so the
   rule between entries does the separating that whitespace did in the grid. */
.dp-archive-grid > li + li { border-top: 1px solid var(--color-border); }

.dp-archive-card {
    position: relative;   /* anchor for the stretched title link */
    display: flex;
    flex-direction: column;
    gap: var(--dp-space-sm);
    padding-block: var(--dp-space-md);
}

.dp-archive-media {
    overflow: hidden;
    border-radius: var(--dp-radius);
    background: var(--dp-surface);
    flex: none;
}

.dp-archive-img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    transition: transform 0.28s ease;
}

.dp-archive-card:hover .dp-archive-img { transform: scale(1.03); }

/* Side-by-side from tablet up. Below that every variant stacks, because a
   200px thumbnail beside text is unreadable on a phone. */
@media (min-width: 700px) {
    .dp-archive-card {
        flex-direction: row;
        align-items: flex-start;
        gap: var(--dp-space-md);
    }
    .dp-archive-media { width: 220px; }
    .dp-archive-body  { flex: 1 1 auto; min-width: 0; }
    .dp-archive-title { font-size: 1.8rem; }
}

.dp-archive-body { display: flex; flex-direction: column; gap: 0.8rem; }

.dp-archive-tags { list-style: none; display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0; padding: 0; }

.dp-archive-tag {
    position: relative;   /* lifts the tag above the stretched link */
    z-index: 1;
    display: inline-block;
    padding: 0.2rem 0.9rem;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-family: var(--font-mono);
    font-size: 1.2rem;
    color: var(--color-secondary-text);
    text-decoration: none;
    transition: border-color 0.18s ease, color 0.18s ease;
}

.dp-archive-tag:hover { border-color: var(--color-primary-text); color: var(--color-primary-text); }

.dp-archive-title { margin: 0; font-size: 2rem; font-weight: 700; letter-spacing: -0.016em; line-height: 1.3; }

.dp-archive-link { color: var(--color-primary-text); text-decoration: none; }

/* Stretches the title's hit area over the whole card without nesting <a> tags,
   which is why the card isn't wrapped in one. */
.dp-archive-link::after { content: ""; position: absolute; inset: 0; }
.dp-archive-link:focus-visible { outline: 2px solid var(--dp-focus); outline-offset: 3px; }

.dp-archive-excerpt {
    margin: 0;
    font-size: 1.5rem;
    line-height: 1.55;
    color: var(--color-secondary-text);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.dp-archive-meta {
    display: flex;
    gap: 0.6rem;
    font-family: var(--font-mono);
    font-size: 1.3rem;
    color: var(--color-secondary-text);
}

/* ---- MOBILE: condensed row. 99 posts stacked full-width is several screens
   of scrolling per handful of entries, so below 700px the card becomes a
   compact row: square thumbnail, two-line title, two-line excerpt, no tags.
   Card height drops from roughly 430px to about 120px. ---- */
@media (max-width: 699px) {
    .dp-archive-card {
        flex-direction: row;
        align-items: flex-start;
        gap: var(--dp-space-sm);
        padding-block: var(--dp-space-sm);
    }
    .dp-archive-media { width: 96px; }
    .dp-archive-img   { aspect-ratio: 1 / 1; }
    .dp-archive-body  { flex: 1 1 auto; min-width: 0; gap: 0.4rem; }

    /* Tags wrap onto their own lines and cost more height than they earn at
       this width; the tag pages remain reachable from the post itself. */
    .dp-archive-tags { display: none; }

    .dp-archive-title {
        font-size: 1.6rem;
        line-height: 1.25;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }
    .dp-archive-excerpt { font-size: 1.35rem; line-height: 1.45; -webkit-line-clamp: 2; }
    .dp-archive-meta    { font-size: 1.15rem; }
}

@media (prefers-reduced-motion: reduce) {
    .dp-archive-img { transition: none; }
    .dp-archive-card:hover .dp-archive-img { transform: none; }
}

/* ============================================================================
   STICKY SUBSCRIBE BAR
   ========================================================================== */

.dp-sticky {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 90;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--dp-space-sm);
    flex-wrap: wrap;
    padding: 1.4rem max(5.2rem, 4vw) 1.4rem max(2rem, 4vw);
    background: color-mix(in srgb, var(--color-darker-gray) 96%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(12px);
    backdrop-filter: saturate(180%) blur(12px);
    border-block-start: 1px solid rgba(255,255,255,0.1);
    color: #fff;
    transform: translateY(110%);
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.dp-sticky.is-visible { transform: translateY(0); }
.dp-sticky-text { font-size: var(--dp-small-size); font-weight: 700; letter-spacing: -0.01em; margin: 0; }

.dp-sticky .gh-btn {
    background: #fff;
    color: var(--color-darker-gray);
    padding: 0.85rem 2rem;
    font-size: 1.35rem;
    font-weight: 700;
    border-radius: 999px;
}

.dp-sticky .gh-btn:hover { opacity: 0.88; }

.dp-sticky-close {
    position: absolute;
    inset-inline-end: 1.2rem;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: 0;
    color: inherit;
    opacity: 0.55;
    font-size: 2.2rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.4rem 0.8rem;
    border-radius: 6px;
    transition: opacity 0.18s ease;
}

.dp-sticky-close:hover { opacity: 1; }
.dp-sticky-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

@media (max-width: 600px) {
    .dp-sticky { padding-block: 1.1rem; gap: var(--dp-space-xs); }
    .dp-sticky-text { font-size: 1.3rem; flex-basis: 100%; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
    .dp-sticky, .dp-platform, .dp-btn-ghost, .dp-stage-link, .dp-stage-post-link { transition: none; }
    .dp-platform:hover { transform: none; }
}

/* ============================================================================
   RESPONSIVE — mobile is the primary case
   ----------------------------------------------------------------------------
   Most 3DP traffic arrives from TikTok / IG / Shorts, i.e. phones. The naive
   stack breaks badly there: a 9:16 Short rendered full-width is 636px tall on
   an iPhone 15 and 1308px on a tablet in portrait — it would consume the entire
   first screen on its own and defeat the "see everything at a glance" goal.

   So below the desktop breakpoint the Watch column goes HORIZONTAL: a
   constrained video beside its label and blurb. That keeps the video legible
   and tappable while leaving room for the writing list and the subscribe box.
   ========================================================================== */

/* ---------- phone + tablet: compact horizontal Watch ---------- */
@media (max-width: 1099px) {
    .dp-stage-watch {
        display: grid;
        grid-template-columns: minmax(0, 52%) 1fr;
        column-gap: var(--dp-space-md);
        row-gap: var(--dp-space-xs);
        align-items: start;
    }

    .dp-stage-watch .dp-stage-label { grid-column: 1 / -1; }

    /* Cap height so the video can never dominate the fold, incl. landscape */
    .dp-stage-watch .dp-video.is-short {
        width: 100%;
        max-width: 200px;
        aspect-ratio: 9 / 16;
        margin-inline-start: 0;
    }

    .dp-stage-watch-copy { align-self: start; }
    .dp-stage-blurb { margin-block-start: 0; }
}

/* At the tablet 2-col stage, Watch reverts to vertical (it has its own column) */
@media (min-width: 760px) and (max-width: 1099px) {
    .dp-stage-watch { grid-template-columns: 1fr; }
    .dp-stage-watch .dp-video.is-short { width: 100%; max-width: 240px; margin-inline: auto; }
    .dp-stage-blurb { margin-block-start: var(--dp-space-sm); }
}

/* ---------- small phones ---------- */
@media (max-width: 560px) {
    .dp-stage { padding-block: var(--dp-space-md); }

    .dp-stage-title { font-size: clamp(2.2rem, 7.5vw, 3rem); max-width: none; }

    /* Credibility is valuable but long; clamp it rather than lose it */
    /* No line-clamp. It was set to 3 lines when the credibility line was
       shorter; at 188 characters and 375px wide the text needs 93px inside a
       70px box, so the last line was cut mid-sentence. This is the one sentence
       that distinguishes 3DP from every other productivity blog — it wraps. */
    .dp-stage-cred { font-size: 1.45rem; line-height: 1.5; }

    .dp-stage-grid { gap: var(--dp-space-md); }

    .dp-stage-post-link { padding-block: 1.3rem; }
    .dp-stage-post-title { font-size: 1.55rem; }

    .dp-stage-joinbox { padding: var(--dp-space-sm); }
    .dp-stage-jointitle { font-size: 1.5rem; }
}

/* ---------- touch targets: 44px minimum ---------- */
@media (hover: none) and (pointer: coarse) {
    .dp-platform {
        min-height: 44px;
        padding-block: 0;
        align-items: center;
    }

    .dp-stage-post-link { min-height: 44px; }

    .dp-btn-ghost { min-height: 44px; padding-block: 0; align-items: center; }

    .dp-sticky-close { min-width: 44px; min-height: 44px; display: grid; place-items: center; }

    /* No hover lift on touch — it sticks after tap */
    .dp-platform:hover { transform: none; background: transparent; }
}

/* ---------- sticky bar clearance ----------
   The fixed bar would otherwise cover the last of the page content. */
body:has(.dp-sticky) { padding-block-end: 7rem; }

@supports not selector(:has(*)) {
    body { padding-block-end: 7rem; }
    body.is-join-page { padding-block-end: 0; }
}

body.is-join-page { padding-block-end: 0; }

/* ---------- overflow guards ----------
   Long unbroken post titles and the video must never cause sideways scroll. */
.dp-stage,
.dp-section { overflow-x: clip; }

.dp-stage-post-title,
.dp-archive-title { overflow-wrap: anywhere; }

.dp-video { max-width: 100%; }

/* ---------- short viewports (landscape phones) ---------- */
@media (max-height: 520px) and (orientation: landscape) {
    .dp-stage-watch .dp-video.is-short { max-width: 150px; }
    .dp-stage-cred { display: none; }
}

/* ============================================================================
   STAGE POST EXCERPTS
   ----------------------------------------------------------------------------
   Titles alone don't tell a visitor what a piece is about. Each row carries a
   short excerpt; the lead post gets more room since it does the most work.
   ========================================================================== */

.dp-stage-post-excerpt {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-block-start: 0.5rem;
    font-size: 1.45rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
    text-wrap: pretty;
}

.dp-stage-post.is-lead .dp-stage-post-excerpt { -webkit-line-clamp: 4; }

/* A hand-written summary shows in full — Ryan keeps them succinct on purpose,
   and cutting them mid-sentence made the list harder to scan. */
.dp-stage-post-excerpt.is-full { display: block; overflow: visible; -webkit-line-clamp: none; }

.dp-stage-post.is-lead .dp-stage-post-title {
    font-size: clamp(1.8rem, 0.6vw + 1.65rem, 2.2rem);
}

.dp-stage-post-meta { margin-block-start: 0.7rem; }

/* Platform pills now sit under the video inside the Watch column */
.dp-stage-watch .dp-platforms { margin-block-start: var(--dp-space-sm); }

/* Below 760px the Watch column runs horizontally: video in column 1, copy in
   column 2. The vertical short is ~320px tall while the blurb is only two
   lines, so measured at 375px the copy column ran 277px empty while the pills
   wrapped onto a separate 95px full-width row beneath the video. Putting the
   pills in the copy column fills the dead space and drops that extra row. */
@media (max-width: 759px) {
    .dp-stage-watch-copy {
        grid-column: 2;
        display: flex;
        flex-direction: column;
        gap: var(--dp-space-sm);
        min-width: 0;
    }

    .dp-stage-watch-copy .dp-stage-blurb { margin: 0; }
    .dp-stage-watch-copy .dp-platforms { margin-block-start: 0; }

    /* Stack rather than wrap: in a ~142px column the pills fit one per line
       anyway, but content-sizing left their right edges ragged. Column +
       stretch makes them uniform and gives each a full-width tap target. */
    .dp-stage-watch-copy .dp-platforms {
        flex-direction: column;
        align-items: stretch;
        flex-wrap: nowrap;
    }

    .dp-stage-watch-copy .dp-platform { justify-content: flex-start; }
}

@media (max-width: 560px) {
    .dp-stage-post-excerpt { font-size: 1.35rem; -webkit-line-clamp: 2; }
    .dp-stage-post.is-lead .dp-stage-post-excerpt { -webkit-line-clamp: 4; }

/* A hand-written summary shows in full — Ryan keeps them succinct on purpose,
   and cutting them mid-sentence made the list harder to scan. */
.dp-stage-post-excerpt.is-full { display: block; overflow: visible; -webkit-line-clamp: none; }
}

/* ============================================================================
   BELOW THE FOLD — Start here + explainer
   ----------------------------------------------------------------------------
   Deliberately in the STAGE's visual language (hairlines, mono meta, tinted
   surfaces) rather than Solo's `loop` cards, which read as a second design
   system on the same page — the thing Ryan flagged as "doesn't make sense
   from a font or layout standpoint."
   ========================================================================== */

.dp-pick-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--dp-space-md);
}

@media (min-width: 700px)  { .dp-pick-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .dp-pick-grid { grid-template-columns: repeat(3, 1fr); } }

.dp-pick { display: flex; }

.dp-pick-link {
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
    width: 100%;
    padding: var(--dp-space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
    text-decoration: none;
    color: var(--color-primary-text);
    background: transparent;
    transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

.dp-pick-link:hover {
    background: var(--dp-surface);
    border-color: color-mix(in srgb, var(--color-primary-text) 20%, transparent);
    transform: translateY(-2px);
}

.dp-pick-link:focus-visible { outline: 2px solid var(--dp-focus); outline-offset: 2px; }

/* Feature image, bled to the card edges. The negative margins cancel the
   card's own padding; the radius is 1px tighter than the card's so the image
   corner sits inside the border rather than fighting it.
   4/3 rather than 16/9 on purpose: every current pick is portrait or square
   (2000x2667, 2000x2000, and a portrait Unsplash frame), and a 16/9 crop took
   too thin a slice out of them. */
/* aspect-ratio deliberately absent here — it is declared once, further down
   under "START HERE — image height", with the reasoning for 16:9. This rule
   used to set 4/3 and was silently overridden by that later declaration, so
   editing it had no effect and looked like it should. */
.dp-pick-media {
    margin: calc(var(--dp-space-md) * -1) calc(var(--dp-space-md) * -1) 0;
    border-radius: calc(var(--dp-radius) - 1px) calc(var(--dp-radius) - 1px) 0 0;
    overflow: hidden;
    background: var(--dp-surface);
}

.dp-pick-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.dp-pick-tag {
    font-family: var(--font-mono);
    font-size: 1.1rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-secondary-text);
}

.dp-pick-title {
    font-size: clamp(1.8rem, 0.6vw + 1.65rem, 2.1rem);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.018em;
    margin: 0;
    text-wrap: pretty;
}

.dp-pick-excerpt {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0;
    font-size: 1.45rem;
    line-height: 1.55;
    color: var(--color-secondary-text);
}

.dp-pick-meta {
    margin-block-start: auto;
    padding-block-start: 0.8rem;
    font-family: var(--font-mono);
    font-size: 1.15rem;
    color: var(--color-secondary-text);
}

/* ---------- explainer ---------- */

.dp-about-inner {
    max-width: 62ch;
    padding: clamp(2.4rem, 3vw, 4rem);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius-lg);
    background: var(--dp-surface);
}

.dp-about-title {
    font-size: var(--dp-title-size);
    font-weight: 800;
    letter-spacing: -0.022em;
    margin: 0 0 var(--dp-space-sm);
}

.dp-about-text {
    margin: 0;
    font-size: var(--dp-body-size);
    line-height: 1.65;
    color: var(--color-secondary-text);
    text-wrap: pretty;
}

@media (hover: none) and (pointer: coarse) {
    .dp-pick-link:hover { transform: none; background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .dp-pick-link { transition: none; }
    .dp-pick-link:hover { transform: none; }
}

/* ============================================================================
   TOOLS STRIP (stage)
   ----------------------------------------------------------------------------
   Tools hold visitors 220-490 seconds vs ~45s for the homepage — by far the
   stickiest content — but had no path from the homepage at all. Full-width
   strip under the three columns so they're on the fold without crowding it.
   ========================================================================== */

.dp-stage-tools { margin-block-start: var(--dp-space-lg); }

.dp-tool-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--dp-space-sm);
}

@media (min-width: 700px) { .dp-tool-grid { grid-template-columns: repeat(2, 1fr); } }

.dp-tool { display: flex; }

.dp-tool-link {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
    padding: var(--dp-space-sm) var(--dp-space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
    text-decoration: none;
    color: var(--color-primary-text);
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.dp-tool-link:hover {
    background: var(--dp-surface);
    border-color: color-mix(in srgb, var(--color-primary-text) 22%, transparent);
}

.dp-tool-link:focus-visible { outline: 2px solid var(--dp-focus); outline-offset: 2px; }

.dp-tool-name {
    font-size: 1.7rem;
    font-weight: 700;
    letter-spacing: -0.016em;
    line-height: 1.25;
}

.dp-tool-desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 1.35rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
}

.dp-tool-go {
    margin-block-start: 0.2rem;
    font-family: var(--font-mono);
    font-size: 1.15rem;
    color: var(--color-secondary-text);
}

@media (hover: none) and (pointer: coarse) {
    .dp-tool-link { min-height: 44px; }
    .dp-tool-link:hover { background: transparent; }
}

/* ============================================================================
   STREAK CARD
   ----------------------------------------------------------------------------
   The subscribe box is short (~200px) but sits in a ~500px row beside the
   vertical video, leaving dead space. This fills it with the most persuasive
   proof on the page: an unbroken daily streak kept by someone with a full-time
   job demonstrates the practice instead of describing it.
   ========================================================================== */

.dp-streak-card {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: var(--dp-space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
}

.dp-streak-kicker {
    font-family: var(--font-mono);
    font-size: 1.1rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-secondary-text);
}

.dp-streak-big {
    font-size: clamp(2.6rem, 2.4vw, 3.6rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
}

.dp-streak-sub {
    font-size: 1.35rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
    text-wrap: pretty;
}

/* The join column now has two stacked blocks — let them breathe evenly */
.dp-stage-join { gap: var(--dp-space-sm); }

/* Below the desktop breakpoint the columns stack, so the streak sits inline
   and doesn't need to fill anything — keep it compact there. */
@media (max-width: 1099px) {
    .dp-streak-card { flex-direction: row; align-items: baseline; flex-wrap: wrap; gap: var(--dp-space-xs) var(--dp-space-sm); }
    .dp-streak-big { font-size: 2.2rem; }
    .dp-streak-sub { flex-basis: 100%; }
}

/* ============================================================================
   "WHAT IS 3DP" CARD (subscribe column)
   ----------------------------------------------------------------------------
   The subscribe box alone is ~200px in a ~500px row, leaving dead space. This
   fills it with the question a cold visitor actually has at the point of
   decision. Text comes from the real About-3DP page so the two can't drift.
   ========================================================================== */

.dp-stage-about {
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
    padding: var(--dp-space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
}

.dp-stage-about-kicker {
    font-family: var(--font-mono);
    font-size: 1.1rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--color-secondary-text);
}

.dp-stage-about-text {
    margin: 0;
    font-size: 1.45rem;
    line-height: 1.6;
    text-wrap: pretty;
}

.dp-stage-about-link {
    font-family: var(--font-mono);
    font-size: 1.2rem;
    text-decoration: none;
    color: var(--color-secondary-text);
    transition: color 0.18s ease;
}

.dp-stage-about-link:hover { color: var(--color-primary-text); }

@media (hover: none) and (pointer: coarse) {
    .dp-stage-about-link { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ============================================================================
   POST PAGE — text first
   ----------------------------------------------------------------------------
   Solo positions the author meta as a sticky rail absolutely placed across grid
   columns 1-3, which reserves a third of the canvas for a photo and byline. On a
   single-author site that's width spent on information the reader already has.
   Replaced with an inline date + tags line directly under the title.
   ========================================================================== */

.dp-post-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1rem;
    margin: 0 0 var(--dp-space-lg);
    padding-block-end: var(--dp-space-sm);
    border-block-end: 1px solid var(--color-border);
    font-family: var(--font-mono);
    font-size: 1.3rem;
    color: var(--color-secondary-text);
}

.dp-post-tags { display: inline-flex; flex-wrap: wrap; gap: 0.6rem; margin-inline-start: auto; }

.dp-post-tag {
    padding: 0.3rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.18s ease, color 0.18s ease;
}

.dp-post-tag:hover { color: var(--color-primary-text); border-color: color-mix(in srgb, var(--color-primary-text) 30%, transparent); }

@media (max-width: 600px) {
    .dp-post-tags { margin-inline-start: 0; flex-basis: 100%; }
}

/* Reclaim the width Solo reserved for the author rail, at every breakpoint */
.gh-article-meta { display: none !important; }

/* `1 / -1` rather than `1 / span 12`: it spans the explicit grid whatever its
   column count, so it stays correct if Solo's canvas changes shape at a
   breakpoint. Measured identical to `span 12` at 500/768/1440 — this is
   robustness, not a bug fix. (I briefly believed `span 12` was overflowing on
   mobile; it wasn't. Instrumenting the page showed scrollWidth == clientWidth
   at every width. The apparent clipping was headless Chrome flooring its
   viewport at 500px while writing a 390px-wide screenshot.) */
.post-template .gh-content > .gh-article-header,
.post-template .gh-article-title,
.post-template .gh-article-excerpt { grid-column: 1 / -1 !important; }

/* ============================================================================
   POST WIDTH — recentre after removing the author rail
   ----------------------------------------------------------------------------
   Solo places post content in grid columns 4-9 of 12: columns 1-3 held the
   sticky author rail and 10-12 were always empty. With the rail hidden, the
   text was left off-centre with a dead gutter on the left — the "white space
   for no apparent reason".

   Content now sits centred (cols 3-10) with a slightly wider measure. Kept
   deliberately short of full width: prose past ~85 characters per line is
   measurably harder to read, so this widens without wrecking readability.
   ========================================================================== */

@media (min-width: 992px) {
    /* Span the full canvas, then cap the LINE, not the column.
       WIDTH, not max-width. These are GRID ITEMS, and on a grid item an `auto`
       inline margin overrides `justify-self: stretch` — the item then sizes to
       fit-content and centres. With max-width alone that silently centred every
       SHORT paragraph, heading and caption on every post and page (caught on
       /think/: "Above all, he taught me to think." sat dead centre). An explicit
       width keeps the box full-measure so the auto margins only centre the box,
       never the text inside it. */
    .post-template .gh-canvas > *,
    .page-template .gh-canvas > * {
        grid-column: 1 / -1;
        width: min(100%, var(--dp-prose));
        max-width: none;
        margin-inline: auto;
    }

    /* Wide and full-bleed cards keep their intended treatment. */
    .post-template .gh-canvas > .kg-width-wide,
    .page-template .gh-canvas > .kg-width-wide {
        width: min(100%, calc(var(--dp-prose) + 260px));
    }

    .post-template .gh-canvas > .kg-width-full,
    .page-template .gh-canvas > .kg-width-full { width: 100%; }
}

/* ============================================================================
   READ NEXT
   ----------------------------------------------------------------------------
   Every post used to end at the subscribe CTA and a pair of prev/next arrows.
   Prev/next is chronological, which is almost never what a search visitor
   wants — they arrived on one idea and got offered "the thing published the
   day before". This block offers topically related work instead.
   ========================================================================== */

.dp-readnext {
    margin-block-start: var(--dp-space-xl);
    padding-block-start: var(--dp-space-lg);
    border-block-start: 1px solid var(--color-border);
}

.dp-readnext-list {
    list-style: none;
    margin: var(--dp-space-md) 0 0;
    padding: 0;
    display: grid;
    gap: var(--dp-space-sm);
}

/* Three across once there's room; stacked below that. */
@media (min-width: 800px) {
    .dp-readnext-list {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: var(--dp-space-md);
    }
}

.dp-readnext-link {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    height: 100%;
    padding: var(--dp-space-md);
    border-radius: var(--dp-radius);
    background: var(--dp-surface);
    text-decoration: none;
    color: inherit;
    transition: background 0.18s ease, transform 0.18s ease;
}

.dp-readnext-link:hover,
.dp-readnext-link:focus-visible {
    background: var(--dp-surface-strong);
    transform: translateY(-2px);
}

.dp-readnext-title {
    font-size: 1.8rem;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.015em;
    text-wrap: pretty;
}

.dp-readnext-excerpt {
    font-size: 1.45rem;
    line-height: 1.5;
    color: var(--color-secondary-text);
    /* Keep the three cards close to equal height regardless of excerpt length. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.dp-readnext-meta {
    margin-block-start: auto;
    font-size: var(--dp-small-size);
    color: var(--color-secondary-text);
    opacity: 0.8;
}

@media (prefers-reduced-motion: reduce) {
    .dp-readnext-link { transition: none; }
    .dp-readnext-link:hover { transform: none; }
}

/* ============================================================================
   WATCH / READ / IMPLEMENT  +  THE ASK BAND
   ----------------------------------------------------------------------------
   The stage used to be Watch | Read | Subscribe, with the tools in a strip
   underneath. Two problems with that: the tools were the last thing on the fold
   despite being the highest-engagement content on the site, and the subscribe
   card was narrow enough that its promise had to shrink to "weekly writing" —
   which describes one of the three things Ryan makes.

   Now the fold is the triad (which is also 3DP's own thesis), and the ask is a
   full-width band directly beneath it. A band is a BIGGER target than the column
   it replaces, and it has room to state the actual offer.
   ========================================================================== */

/* ---------- tools as ROWS, not cards ----------
   As bordered cards the tools shared the subscribe ask's border, radius and box
   shape, so the ask scanned as a third tool however it was tinted. Mirroring the
   Read column's row treatment leaves the ask as the only bordered box in the
   Implement column — the separation is structural, not just colour. */
.dp-tool-grid--stacked {
    display: block;
    grid-template-columns: none;
    gap: 0;
}

.dp-tool-grid--stacked .dp-tool { display: block; }

.dp-tool-grid--stacked .dp-tool + .dp-tool {
    border-block-start: 1px solid var(--color-border);
}

.dp-tool-grid--stacked .dp-tool-link {
    flex-direction: row;
    align-items: center;
    gap: var(--dp-space-sm);
    padding: var(--dp-space-sm) 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    transition: opacity 0.16s ease;
}

/* Match the Read rows' hover rather than the old card fill. */
.dp-tool-grid--stacked .dp-tool-link:hover {
    opacity: 0.62;
    background: transparent;
    border-color: transparent;
}

.dp-tool-grid--stacked .dp-tool-link:focus-visible { outline-offset: 3px; }

/* 3/2 keeps the whole hero graphic: these images carry a title bar across the
   middle, and a square crop cut it in half. */
.dp-tool-thumb {
    flex: none;
    width: 96px;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    border-radius: calc(var(--dp-radius) - 2px);
    background: var(--dp-surface);
}

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

.dp-tool-text {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

/* ---------- the ask ---------- */

.dp-stage-band {
    margin-block-start: clamp(var(--dp-space-md), 3.5vh, var(--dp-space-lg));
    padding: clamp(var(--dp-space-md), 2.4vw, var(--dp-space-lg));
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius-lg);
    background: var(--dp-surface);
    display: grid;
    gap: var(--dp-space-md);
    align-items: center;
}

/* Offer gets the larger share; the explainer sits beside it rather than under
   the subscribe field, where it used to compete with the button. */
@media (min-width: 900px) {
    .dp-stage-band {
        grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
        gap: var(--dp-space-lg);
    }
}

.dp-band-title {
    margin: 0 0 var(--dp-space-sm);
    /* Container-relative so it tracks the band, not the raw viewport. */
    font-size: clamp(1.9rem, 1.05cqi + 1.5rem, 2.9rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    text-wrap: pretty;
}

.dp-band-input { max-width: 48rem; }

.dp-band-reassure {
    margin: var(--dp-space-sm) 0 0;
    font-size: var(--dp-small-size);
    color: var(--color-secondary-text);
}

.dp-band-about {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    justify-content: center;
}

@media (min-width: 900px) {
    .dp-band-about {
        border-inline-start: 1px solid var(--color-border);
        padding-inline-start: var(--dp-space-lg);
    }
}

.dp-band-kicker {
    font-family: var(--font-mono);
    font-size: 1.2rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-secondary-text);
}

.dp-band-text {
    margin: 0;
    font-size: 1.5rem;
    line-height: 1.55;
    color: var(--color-secondary-text);
    text-wrap: pretty;
}

.dp-band-link {
    font-family: var(--font-mono);
    font-size: var(--dp-small-size);
    text-decoration: none;
    color: var(--color-primary-text);
    opacity: 0.75;
}

.dp-band-link:hover { opacity: 1; }

/* The ask, living under the tools in the Implement column. Sized to fit the
   space the tools leave in the row (~380px at 1440x813) so the triad and the
   subscribe field are both above the fold — a separate full-width band measured
   275px below it. */
.dp-stage-ask {
    /* Every value here is doing fold work. Measured at 1440x813: the Implement
       column has 445px above the fold, and the tools + ask wanted 562px. These
       trims recover the ~55px that put the subscribe field back on screen —
       don't loosen them without re-measuring. Baseline headroom at 1440x813 and
       1280x800 is only 35px, so any change here must be height-neutral. That
       is why the ask is distinguished by COLOUR, not by extra padding: sharing
       the tool cards' width, radius, border and neutral fill made it read as a
       third tool rather than the page's primary ask. */
    margin-block-start: var(--dp-space-sm);
    padding: var(--dp-space-sm) var(--dp-space-md) var(--dp-space-md);
    border: 1px solid color-mix(in srgb, var(--ghost-accent-color, #19c0ff) 42%, transparent);
    border-radius: var(--dp-radius);
    background: color-mix(in srgb, var(--ghost-accent-color, #19c0ff) 8%, transparent);
}

/* Browsers without color-mix fall back to the accent at fixed alpha. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
    .dp-stage-ask {
        border-color: rgba(25, 192, 255, 0.42);
        background: rgba(25, 192, 255, 0.08);
    }
}

.dp-ask-title {
    margin: 0 0 0.7rem;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.22;
    letter-spacing: -0.015em;
    text-wrap: pretty;
}

/* (The old in-column card padding/gap overrides lived here. They were removed
   when the tools became rows — being later in the file, they silently beat the
   row rules above.) */

.dp-ask-reassure {
    margin: 0.6rem 0 0;
    font-size: var(--dp-small-size);
    color: var(--color-secondary-text);
}

/* Solo's .gh-subscribe-input carries margin-top:40px, sized for a full-width
   band. Inside this box it stacked on the title's own 7px to open a 47px gap —
   larger than the box's 16px/24px padding, which is what made the ask read as
   broken rather than merely airy. */
.dp-stage-ask .gh-subscribe-input { margin-top: var(--dp-space-sm); }

/* OVERFLOW FLOOR (measured at 320px). The subscribe input is a flex row whose
   children keep min-width:auto, giving it a 345px min-content width. As a grid
   item in the single-column stage it could not shrink below that, so it set the
   track width for ALL THREE stage columns: the track computed to 345px inside a
   272px grid and .dp-stage{overflow-x:clip} silently cropped the excess — the
   headline, excerpts and pills were all cut off at 320px with no scrollbar to
   reveal it. Let the columns and the field shrink, and ellipsize the
   placeholder, which is decorative (Portal replaces it on click). */
.dp-stage-col { min-width: 0; }

.dp-stage-ask .gh-subscribe-input { min-width: 0; }
.dp-stage-ask .gh-btn { flex: none; }

/* Solo sizes this field's text at 1.7rem for a full-width band. In-column at
   375px that left the placeholder 148px against a 148px string, so it
   ellipsized on the most common phone width. 1.5rem clears it; the ellipsis
   above stays as the fallback below ~360px. Height is fixed at 52px, so this
   does not move the fold. */
.dp-stage-ask .gh-subscribe-input { font-size: 1.5rem; }

/* ---------- tablet (760–1099px): Implement spans the full row ----------
   In this range the stage is two columns, and auto-placement put Watch and Read
   in row 1 with Implement dropping to row 2 column 1 — the 260px rail. Measured
   at 900px that left a 528x486px void beside Implement, and squeezed its
   subscribe field until the placeholder rendered as "you@e…". Row 1's height is
   set by Read, so Implement also started ~100px below where Watch ended.
   Spanning the full row uses that width instead: tools two-up, ask beside them.
   Desktop (≥1100px, all three side by side) and phone (≤759px, stacked) are
   both unaffected — this is only the in-between. */
@media (min-width: 760px) and (max-width: 1099px) {
    .dp-stage-implement {
        grid-column: 1 / -1;
        display: grid;
        /* 330px floor is measured, not chosen: the field needs 147px for the
           button plus its padding and 130px for the placeholder, and the box
           adds 48px of its own. Below that the placeholder ellipsizes. */
        grid-template-columns: minmax(0, 1fr) minmax(330px, 380px);
        column-gap: var(--dp-space-lg);
        align-items: start;
    }

    /* The tools section is the grid item now — the label lives inside it, so
       the "Implement" hairline spans the tools only and no longer reads as a
       heading over the subscribe ask beside them. */
    .dp-implement-tools { grid-column: 1; }

    /* (The auto-fit two-up rule that used to live here is gone: the tools are
       full-width rows now, so there is no card width left to fit. It had solved
       a real problem — a hard repeat(2) crushed the cards to 100px at 760px —
       which the row layout makes moot.) */

    /* The ask no longer sits under the tools, so it drops its top margin. */
    .dp-stage-implement .dp-stage-ask {
        grid-column: 2;
        margin-block-start: 0;
    }

    /* Two-up halves the card width, so the 2-line clamp cut both blurbs
       mid-phrase ("A dynamic 3DProductivity tool to…"). */
    .dp-stage-implement .dp-tool-desc { -webkit-line-clamp: 3; }
}

.dp-stage-ask .dp-cta-placeholder {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ============================================================================
   QUOTE OF THE WEEK
   ----------------------------------------------------------------------------
   Two voices in one block: the quoted line, and Ryan's reading of it. They must
   not look like the same kind of text, or the reader cannot tell where the
   source stops and 3DP starts — which is both a credibility problem and a
   copyright one.

   The quote gets display weight and a hanging mark; the reflection sits in a
   tinted panel with a mono label, borrowing the visual grammar already used by
   the streak card. Everything is built on Solo's tokens, so dark mode needs no
   separate rules.
   ========================================================================== */

/* ALIGNMENT (2026-07-28): this used to cap .dp-section-inner at --dp-prose
   (960/1020px) while every other section on the page uses --dp-max
   (1440/1840px). Both are centred, so "Quote of the week" started several
   hundred pixels to the right of "Start here" and "Watch" — the misalignment
   Ryan spotted.

   The section now shares the common container and the reading measure is
   applied where it actually belongs: the quote and reflection TEXT. Capping the
   container was capping the heading too, which is what broke the grid. */
.dp-quote-text,
.dp-quote-take-text {
    max-width: var(--dp-measure);
}

.dp-quote-fig {
    margin: 0 0 var(--dp-space-md);
    padding: 0;
}

.dp-quote-text {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: clamp(2.1rem, 1.4vw + 1.7rem, 3rem);
    font-weight: 500;
    line-height: 1.28;
    letter-spacing: -0.02em;
    text-wrap: pretty;
}

/* Hanging quotation mark: sits in the left margin at wide sizes so the first
   letter of the quote stays flush with everything above it, and tucks back
   inline below the prose width where there is no margin to hang into. */
.dp-quote-text::before {
    content: "\201C";
    position: absolute;
    margin-left: -0.55em;
    color: var(--color-secondary-text);
    opacity: 0.45;
}

@media (max-width: 700px) {
    .dp-quote-text::before { position: static; margin-left: 0; }
}

.dp-quote-cite {
    margin-top: var(--dp-space-sm);
    font-size: var(--dp-small-size);
    color: var(--color-secondary-text);
}

.dp-quote-cite cite { font-style: italic; }

.dp-quote-link {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 0.2em;
    text-decoration-color: var(--color-border);
}

.dp-quote-link:hover { text-decoration-color: currentColor; }

.dp-quote-take {
    padding: var(--dp-space-md);
    background: var(--dp-surface);
    border-radius: var(--dp-radius-lg);
}

.dp-quote-take-label {
    margin: 0 0 var(--dp-space-xs);
    font-family: var(--font-mono);
    font-size: 1.1rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-secondary-text);
}

.dp-quote-take-text {
    margin: 0;
    max-width: var(--dp-measure);
    font-size: var(--dp-body-size);
    line-height: 1.6;
    text-wrap: pretty;
}

/* ============================================================================
   QUOTE OF THE WEEK — in-post variant
   ----------------------------------------------------------------------------
   Injected into the post body as a Ghost HTML card (see render_quote.py). It is
   deliberately quieter than the homepage block: inside an article it is a
   closing aside, not a headline act, and it must not out-shout Ryan's own prose
   two paragraphs above it.
   ========================================================================== */

.dp-post-quote {
    margin: var(--dp-space-lg) 0;
    padding: var(--dp-space-md);
    border-left: 3px solid var(--color-border);
    background: var(--dp-surface);
    border-radius: 0 var(--dp-radius) var(--dp-radius) 0;
}

.dp-post-quote blockquote {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 1.05em;
    line-height: 1.4;
}

.dp-post-quote-cite {
    margin: var(--dp-space-xs) 0 0;
    font-size: var(--dp-small-size);
    color: var(--color-secondary-text);
}

.dp-post-quote-take {
    margin: var(--dp-space-sm) 0 0;
    padding-top: var(--dp-space-sm);
    border-top: 1px solid var(--color-border);
    font-size: 0.95em;
    line-height: 1.6;
}

/* ============================================================================
   START HERE — image height
   ----------------------------------------------------------------------------
   The cards were 4:3 at every breakpoint. In the single-column phone layout a
   card is ~92vw, so a 4:3 image is ~69vw tall and three stacked picks pushed
   the support ask far below the fold. The image is supporting evidence for the
   title, not the content itself, so it gives up height first.

   Desktop was 4:3 on the reasoning that at three columns each card is already
   narrow. Changed to 16:9 everywhere on 2026-08-14, because that rule was
   quietly dictating how Ryan had to author every feature image on the site.

   Feature images are 1200x630 (1.905:1, the Open Graph shape). object-fit:cover
   against a 4:3 box shows only 1.333/1.905 = 70% of the width — 180px trimmed
   off each side. So the tightest crop on the site was deciding the safe area
   for artwork whose main job is the post header, where nothing is cropped at
   all. The result was images authored into their own middle: correct in the
   pick card, empty either side of the subject in the header and the social
   card.

   16:9 shows 1.778/1.905 = 93%. Same ratio as mobile and the archive, so there
   is now ONE safe area across the whole site instead of three.
   ========================================================================== */

.dp-pick-media { aspect-ratio: 16 / 9; }

/* ============================================================================
   HEADER NAV — wrap instead of overflowing (2026-07-27)
   ----------------------------------------------------------------------------
   Solo's `.gh-head-menu` is a nowrap flex row. With four items it measured
   24→517px inside a 327px grid cell at 375px, so "About Rye" sat entirely off
   the right edge of the phone with nothing to reveal it. Wrapping to two lines
   costs ~24px of header height and keeps every nav item reachable.
   ========================================================================== */
@media (max-width: 600px) {
    /* The overflowing box is the inner `ul.nav`, not `.gh-head-menu` — wrapping
       the outer flex container alone changed nothing, because the list itself
       carries `flex-wrap: nowrap` and measured 492px. */
    .gh-head-menu,
    .gh-head-menu .nav {
        flex-wrap: wrap;
        row-gap: 0.4rem;
        --head-nav-gap: 1.6rem;
    }

    .gh-head-menu .nav { justify-content: flex-start; column-gap: 1.6rem; }
}

/* ============================================================================
   QUOTE OF THE WEEK — one block, not three
   ----------------------------------------------------------------------------
   The heading, the quote and the reflection were three separate elements with
   plain gaps between them, so nothing signalled that they belonged together —
   it read as a stray quote followed by an unrelated paragraph. They now sit
   inside one bordered card, with the reflection divided from the quote rather
   than detached from it.
   ========================================================================== */

.dp-quoteweek .dp-section-head { margin-bottom: var(--dp-space-sm); }

.dp-quote-card {
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius-lg);
    overflow: hidden;
}

.dp-quote-card .dp-quote-fig {
    margin: 0;
    padding: var(--dp-space-md);
}

.dp-quote-card .dp-quote-take {
    margin: 0;
    padding: var(--dp-space-md);
    border-top: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--dp-surface);
}

/* Related post — a third band in the same card, not a floating link below it.
   Inside the border it reads as part of the same thought (someone else's line,
   Ryan's take, Ryan's post); outside, it would read as site furniture and be
   ignored. Tinted one step stronger than the reflection so the eye can tell it
   is a different KIND of thing — a destination rather than more prose. */
.dp-quote-card .dp-quote-related {
    margin: 0;
    padding: var(--dp-space-sm) var(--dp-space-md);
    border-top: 1px solid var(--color-border);
    background: var(--dp-surface-strong);
}

/* Hidden when the week's quote has no related post. Needs to beat the flex
   display below, hence the explicit rule rather than relying on [hidden]. */
.dp-quote-related[hidden] {
    display: none;
}

.dp-quote-related-link {
    display: flex;
    align-items: baseline;
    gap: var(--dp-space-xs);
    flex-wrap: wrap;
    text-decoration: none;
    color: var(--color-primary-text);
}

/* Label is the quiet half — it names the affordance once. The post title is
   what the reader is actually choosing, so it carries the weight. */
.dp-quote-related-label {
    flex: none;
    font-size: var(--dp-small-size);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-secondary-text);
}

.dp-quote-related-title {
    font-weight: 600;
}

.dp-quote-related-link:hover .dp-quote-related-title {
    text-decoration: underline;
    text-underline-offset: 3px;
}

.dp-quote-related-link:focus-visible {
    outline: 2px solid var(--dp-focus);
    outline-offset: 3px;
    border-radius: 4px;
}

/* ============================================================================
   POST CTA — the subscribe ask has to look like an ask (2026-07-28)
   ----------------------------------------------------------------------------
   subscribe-cta.hbs shipped a full class API — .dp-cta, .dp-cta-inner,
   .dp-cta-text, plus .dp-cta-end / .dp-cta-mid variants — and not one of them
   was ever styled. The only matching rule in either stylesheet was
   .dp-cta-placeholder, and code injection carries nothing but GA4. So both
   CTAs rendered as a bare <aside> in the content flow: no surface, no padding,
   no boundary of any kind.

   Mid-post that is actively harmful. 3dp.js clones the CTA between two body
   paragraphs, so with no styling the ask reads as Ryan's own prose interrupting
   itself — a subscribe pitch in his voice, mid-argument.

   It also explains Read next feeling crowded. .dp-readnext already reserves
   6.4rem and a rule above it, but that gap was being measured from a naked
   input field, so there was nothing for the space to separate FROM.
   ========================================================================== */

.dp-cta {
    margin-block: var(--dp-space-lg);
    padding: var(--dp-space-md);
    background: var(--dp-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius-lg);
}

.dp-cta-text {
    margin: 0 0 var(--dp-space-sm);
    font-size: var(--dp-body-size);
    line-height: 1.5;
}

/* A real field, submitted by Ghost's members API rather than opening Portal.
   Wraps to stacked below 460px so the input never drops under ~44px of touch
   target trying to share a row with the button. */
.dp-cta-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--dp-space-xs);
}

.dp-cta-field {
    flex: 1 1 22rem;
    min-width: 0;              /* flex children default to min-content, which an
                                  input sets from its `size` attribute (~20 chars) */
    padding: 1.1rem 1.4rem;
    font-size: var(--dp-body-size);
    color: var(--color-primary-text);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border);
    border-radius: var(--dp-radius);
}

.dp-cta-field:focus-visible {
    outline: 2px solid var(--color-primary-text);
    outline-offset: 1px;
}

.dp-cta-btn {
    flex: 0 0 auto;
    min-height: 4.4rem;        /* iOS minimum touch target */
    cursor: pointer;
}

/* Ghost toggles .loading / .success / .error on the FORM element itself. */
.dp-cta-msg {
    display: none;
    margin: var(--dp-space-xs) 0 0;
    font-size: var(--dp-small-size);
}

.dp-cta-form.error .dp-cta-msg-error { display: block; color: #c0392b; }
.dp-cta-form.success .dp-cta-msg-ok  { display: block; }
.dp-cta-form.success .dp-cta-row     { display: none; }

.dp-cta-form.loading .dp-cta-btn {
    opacity: 0.6;
    pointer-events: none;
}

/* Mid-post carries less weight than the end-of-post ask: it interrupts an
   argument the reader hasn't finished, and it pairs with a one-line pitch
   instead of the full paragraph. It KEEPS the surface and border — an unstyled
   ask mid-body reads as Ryan's own prose, which is the bug this all started
   from. Lighter, not invisible. */
.dp-cta-mid {
    padding: var(--dp-space-sm);
    margin-block: var(--dp-space-md);
}

.dp-cta-mid .dp-cta-text { margin-bottom: var(--dp-space-xs); }

/* ============================================================================
   COMMENTS SPACING (2026-07-28)
   ----------------------------------------------------------------------------
   Solo sets .gh-comments margin-top to clamp(4.8rem, 4.64vw + 2.57rem, 10rem),
   which resolves to ~92px at desktop and caps at 100px, on top of the 40px
   bottom margin on .dp-post-meta. The heading then reserves another 48px below
   itself. Nothing in this file had ever overridden it, so "Start the
   conversation" floated in ~140px of dead space.

   Pinned to the theme's own scale instead of Solo's vw-fluid value.

   SECOND PASS (same day): 64px was still too much, because the gap was never
   one margin. The article's last element is .dp-post-meta, which carries a 40px
   bottom margin of its own, and it does not collapse into .gh-comments — they
   are separated by the section boundary and .gh-comments is a grid container.
   So the real gap was ~104px, not 64px.

   Fixed structurally rather than by shrinking numbers again: post.hbs now puts
   the end-of-post CTA between the article and the comments, so that space holds
   the subscribe ask instead of nothing. What remains to tune is CTA -> comments,
   where .dp-cta's own 40px bottom margin stacks with the comments' top margin
   inside the grid container. Zero the first and let one value own the gap.
   ========================================================================== */
.gh-comments { margin-top: var(--dp-space-lg); }        /* 64px -> 40px */
.gh-comments-title { margin-bottom: var(--dp-space-md); } /* 48px -> 24px */

/* The end CTA sits directly above the comments block (post.hbs). Its bottom
   margin would otherwise stack with .gh-comments' top margin for an 80px gap. */
.dp-cta-end { margin-block-end: 0; }

/* read-next.hbs renders the recent-posts fallback as an unconditional sibling
   (a nested {{#get}} renders nothing in Ghost). 3dp.js removes it when the
   primary_tag block rendered; this is the no-JS safety net. */
.dp-readnext + .dp-readnext--fallback { display: none; }

/* ============================================================================
   ABOUT PAGE — let the prose wrap the portrait (2026-07-28)
   ----------------------------------------------------------------------------
   The photo is a 300x516 portrait sitting as a full-width block card between
   paragraphs, so it interrupted the read instead of sitting beside it.

   float has NO effect on a grid item, and .gh-content is Solo's 12-column
   canvas — so this has to switch the About page to block flow first, which in
   turn means moving the prose cap off the children (POST WIDTH above gives
   every canvas child an explicit width + auto margins) and onto the container.

   Scoped to .page-about deliberately: a block image is the right call
   everywhere else, and this page carries no kg-width-wide/full cards that would
   still need the grid. Verified: exactly one kg-image-card on the page.
   ========================================================================== */
@media (min-width: 992px) {
    .page-about .gh-content {
        display: block;
        width: min(100%, var(--dp-prose));
        margin-inline: auto;
    }

    .page-about .gh-content > * {
        width: auto;
        margin-inline: 0;
    }

    .page-about .gh-content .kg-image-card {
        float: right;
        width: 30rem;   /* the image's own 300px, at html{font-size:62.5%} */
        margin: 0.8rem 0 var(--dp-space-md) var(--dp-space-lg);
    }

    .page-about .gh-content .kg-image-card img {
        width: 100%;
        height: auto;
        border-radius: var(--dp-radius);
    }

    /* Contain the float so it cannot spill past the article into Read next. */
    .page-about .gh-content::after {
        content: "";
        display: block;
        clear: both;
    }
}

/* The mid-post clone sits inside .gh-content, where Solo's `> * + *` rule would
   otherwise set its top margin. Equal specificity (0,1,0), so this wins only
   because 3dp.css loads after screen.css — keep that order in default.hbs. */
.gh-content > .dp-cta-mid {
    margin-block: var(--dp-space-lg);
}

/* ============================================================================
   POST RHYTHM — Solo's em spacing compounds on headings (2026-07-28)
   ----------------------------------------------------------------------------
   Solo spaces content with `.gh-content > * + * { margin-top: 1.6em }`. On a
   paragraph that resolves against the body size (1.8rem) for ~29px — already
   loose. On a heading it resolves against the HEADING's own size, and Solo sets
   content h2 to 2.25em: 1.6 x 2.25 x 18px = ~65px above every h2, and ~46px
   above every h3. That is why the space before a header reads as a section
   break rather than as a header.

   Two problems, two fixes. Dial Solo's own multiplier down for the paragraph
   rhythm, then pin heading top margins in rem so they stop scaling with their
   own font-size. Nothing set --content-spacing-multiplier before, so it had
   been sitting at the default 1.
   ========================================================================== */

.gh-content {
    --content-spacing-multiplier: 0.8;   /* paragraph gap ~29px -> ~23px */
}

/* Ghost gives content headings an id, which is how Solo targets them. Adding
   the element selector takes specificity to (0,3,1) over Solo's (0,3,0). */
.gh-content > h2[id]:not(:first-child) { margin-top: 4rem; }
.gh-content > h3[id]:not(:first-child) { margin-top: 3.2rem; }

/* A heading following a card gets 2em !important from Solo, which lands at
   ~81px on an h2. Same compounding, so same fix — and it has to match the
   !important to land. */
.gh-content > .kg-card + h2[id] { margin-top: 4rem !important; }
.gh-content > .kg-card + h3[id] { margin-top: 3.2rem !important; }
