/* =============================================================================
   Xandev V3 - page compositions
   Load after tokens.css, base.css, ui.css.

   No inline style attributes anywhere in the templates: the CSP has no
   'unsafe-inline' for styles and a nonce does not cover style attributes.
   Everything a page needs is a class in here.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   Section scaffolding
   -------------------------------------------------------------------------- */

.section--tight  { padding-block: clamp(2.5rem, 5vw, 4.5rem); }

.section--sunken {
    background: var(--surface-sunken);
    border-block: 1px solid var(--line-faint);
}

.section-head {
    display: grid;
    gap: var(--space-3);
    max-width: 46rem;
    margin-block-end: var(--space-7);
}

/* The gradient hairline that opens a section - one of only two places the
   brand gradient appears site-wide. */
.section-head .rule { margin-block-end: var(--space-1); }

.section-head__body {
    color: var(--text-secondary);
    font-size: var(--text-lg);
}

.section-head--between {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-5);
    max-width: none;
}

.section-head--between > div {
    display: grid;
    gap: var(--space-3);
    max-width: 42rem;
}

.section-head--spaced { margin-block-start: var(--space-11); }

.page-head {
    padding-block: clamp(3rem, 7vw, 6rem) clamp(1.5rem, 3vw, 2.5rem);
}

.page-head h1 { margin-block-start: var(--space-3); }

.page-head__body {
    margin-block-start: var(--space-4);
    max-width: 42rem;
    font-size: var(--text-lg);
    color: var(--text-secondary);
}

/* -----------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero {
    position: relative;
    padding-block: clamp(3.5rem, 9vw, 7rem) clamp(2rem, 4vw, 3.5rem);
    overflow: hidden;
}

/* A single soft brand wash behind the headline. One ambient effect, not the
   four V2 stacked (particles, mouse glow, two radial glows) - those competed
   with each other and cost a frame budget on every scroll. */
/* -----------------------------------------------------------------------------
   The measured figures under the hero

   Hidden until the pre-paint `js` class says a script can fill them in, so a
   reader without JavaScript gets no empty frame and nothing moves when the
   measurement lands. `hidden` alone is not enough, since a rule with a class
   selector beats the attribute's own display:none.
   -------------------------------------------------------------------------- */

.metrics[hidden] { display: none; }

.metrics {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-5) clamp(var(--space-5), 5vw, 3rem);

    margin-block: var(--space-6) 0;
    padding-block-start: var(--space-4);
    border-block-start: 1px solid var(--line);

    /* The figures arrive late by definition. Coming in from slightly low says
       "this resolved" rather than "this was always here", which is the honest
       reading and the only reason the movement is allowed. */
    opacity: 0;
    translate: 0 var(--space-2);
    transition: opacity 480ms var(--ease), translate 480ms var(--ease);
}

.metrics[data-ready] {
    opacity: 1;
    translate: 0 0;
}

.metrics__item { display: grid; gap: var(--space-1); }

.metrics__label {
    order: 2;
    font-size: var(--text-3xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Display face, because these are the second-largest thing in the hero and a
   figure set in the body face would read as a caption rather than a claim.
   Tabular so the digits do not shuffle sideways as they settle. */
.metrics__value {
    order: 1;
    margin: 0;

    font-family: var(--font-display);
    font-stretch: var(--width-display);
    font-weight: 800;
    font-size: clamp(1.5rem, 3.4vw, 2.1rem);
    letter-spacing: var(--tracking-display);
    line-height: 1;
    font-variant-numeric: tabular-nums;

    color: var(--text-primary);
}

/* The explanation behind the (i).

   Shown on hover and on keyboard focus with no script at all, which covers a
   mouse and a keyboard entirely. A finger gets neither, so `home.js` toggles
   aria-expanded on tap: that is the only part that needs JavaScript, and
   without it a phone simply sees the label as it was before.

   The bubble is a sibling rather than a child of the button, so it is not
   inside the thing being hovered: a tooltip that sits under the pointer flickers
   as the pointer enters it. */

.metrics__item--explained { position: relative; }

.hint {
    display: inline-grid;
    place-items: center;
    vertical-align: -0.15em;
    margin-inline-start: 0.35rem;

    width: 1rem;
    height: 1rem;
    padding: 0;

    border: 0;
    background: none;
    color: var(--text-muted);
    cursor: pointer;

    transition: color var(--duration) var(--ease);
}

.hint svg { width: 100%; height: 100%; }
.hint:hover { color: var(--brand-text); }
.hint:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 50%; }

.hint__bubble {
    position: absolute;
    inset-block-start: calc(100% + var(--space-3));
    inset-inline-start: 0;
    z-index: 3;

    width: max-content;
    max-width: min(26rem, calc(100vw - 3rem));
    margin: 0;
    padding: var(--space-3) var(--space-4);

    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-raised);
    box-shadow: 0 16px 34px -20px rgb(0 0 0 / 0.6);

    font-family: var(--font-body);
    font-size: var(--text-xs);
    line-height: 1.55;
    letter-spacing: normal;
    text-transform: none;
    color: var(--text-secondary);

    opacity: 0;
    visibility: hidden;
    translate: 0 -0.25rem;
    transition: opacity var(--duration) var(--ease),
                translate var(--duration) var(--ease),
                visibility var(--duration) var(--ease);
}

/* Matched on the container, not as a sibling of the button.

   The button sits inside the <dt> and the bubble is a sibling of that <dt>, so
   they were never siblings and `.hint ~ .hint__bubble` matched nothing: hover
   worked, because that rule was on the container, and the tap silently did
   not. aria-expanded was flipping correctly the whole time, which is exactly
   why reading the attribute rather than the computed style would have called
   this a pass.

   `is-open` is a class rather than :has(), so nothing here depends on how new
   the browser is. */
.metrics__item--explained:hover .hint__bubble,
.metrics__item--explained:focus-within .hint__bubble,
.metrics__item--explained.is-open .hint__bubble {
    opacity: 1;
    visibility: visible;
    translate: 0 0;
}

/* Opens upward on a phone.

   The metrics row wraps at that width, which pushes this item to the bottom of
   the hero, and a bubble hanging below it opened mostly under the fold: about
   half of it visible, on the one device where the tap is the only way to read
   it at all. Above the row it opens over the paragraph, which is a popup doing
   what a popup does. */
@media (max-width: 47.99rem) {
    .hint__bubble {
        inset-block-start: auto;
        inset-block-end: calc(100% + var(--space-3));
        translate: 0 0.25rem;
    }

    .metrics__item--explained:hover .hint__bubble,
    .metrics__item--explained:focus-within .hint__bubble,
    .metrics__item--explained.is-open .hint__bubble {
        translate: 0 0;
    }
}

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

.metrics__note {
    flex-basis: 100%;
    margin: 0;
    font-size: var(--text-3xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);
}

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

/* -----------------------------------------------------------------------------
   Section reveals

   Armed by script only. Without JavaScript, without IntersectionObserver, or
   for a reader who has asked for less motion, `reveal-armed` is never set and
   these rules never apply, so every target is simply visible from the start.
   -------------------------------------------------------------------------- */

.reveal-armed [data-reveal] {
    opacity: 0;
    translate: 0 var(--space-4);
    transition: opacity 560ms var(--ease), translate 560ms var(--ease);
}

.reveal-armed [data-reveal][data-shown] {
    opacity: 1;
    translate: 0 0;
}

/* A short stagger down a list of siblings, so three cards arrive as a group
   rather than three separate events. Capped at four: past that it stops
   reading as one gesture and starts reading as a queue. */
.reveal-armed [data-reveal]:nth-child(2) { transition-delay: 80ms; }
.reveal-armed [data-reveal]:nth-child(3) { transition-delay: 160ms; }
.reveal-armed [data-reveal]:nth-child(4) { transition-delay: 240ms; }

@media (prefers-reduced-motion: reduce) {
    .reveal-armed [data-reveal] {
        opacity: 1;
        translate: none;
        transition: none;
    }
}

/* -----------------------------------------------------------------------------
   Hero backdrop: ruled paper with a line plotted on it

   Replaced a single radial wash that was strong enough on ink and effectively
   invisible on paper. That was not a tuning problem: in light mode --brand is
   the deep bronze, and bronze at twenty percent over #F5F6F9 is almost nothing,
   where amber at twenty percent over near-black is a great deal. The same rule
   produced two very different results, so light mode now gets structure of its
   own rather than a stronger version of the same haze: the ruling carries the
   design and the warm light only tints it.

   One element animates. See the note in home.html.twig for why.
   -------------------------------------------------------------------------- */

.hero__bg {
    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
}

.hero__bg > * {
    position: absolute;
    display: block;
}

/* The ruling. Masked away from the light so it reads as a lit surface rather
   than as a pattern laid over the whole box. */
.hero__grid {
    inset: 0;
    color: var(--text-primary);
    opacity: 0.07;

    background-image:
        linear-gradient(to right, currentColor 1px, transparent 1px),
        linear-gradient(to bottom, currentColor 1px, transparent 1px);
    background-size: clamp(38px, 4.4vw, 58px) clamp(38px, 4.4vw, 58px);

    -webkit-mask-image: radial-gradient(135% 108% at 20% 0%, #000 0%, #000 44%, transparent 80%);
    mask-image: radial-gradient(135% 108% at 20% 0%, #000 0%, #000 44%, transparent 80%);
}

[data-theme="light"] .hero__grid { opacity: 0.11; }

.hero__lamp,
.hero__cool {
    border-radius: 50%;
    filter: blur(34px);
    aspect-ratio: 1;
}

.hero__lamp {
    inset-block-start: -23rem;
    inset-inline-start: -6rem;
    width: min(42rem, 110vw);
    background: radial-gradient(circle, var(--brand) 0%, transparent 60%);
    opacity: 0.40;
}

.hero__cool {
    inset-block-start: -13rem;
    inset-inline-end: -7rem;
    width: min(28rem, 80vw);
    background: radial-gradient(circle, var(--accent) 0%, transparent 62%);
    opacity: 0.16;
}

[data-theme="light"] .hero__lamp { opacity: 0.34; }
[data-theme="light"] .hero__cool { opacity: 0.15; }

/* The ruling runs the full width, but the plotted line does not.
   
   At the hero's real height the curve stretches tall enough to cross the
   headline, and a line through the middle of "bringing you work" is the
   backdrop competing with the sentence it is supposed to sit behind. So the
   plot fades in across the left third and is only fully present out to the
   right, where there is nothing to compete with. The grid still covers
   everything, which is what keeps this one surface rather than two zones. */
.hero__plot {
    inset: 0;

    -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 14%, #000 44%, #000 100%);
    mask-image: linear-gradient(to right, transparent 0%, transparent 14%, #000 44%, #000 100%);
}

.hero__plot svg { display: block; width: 100%; height: 100%; }

.hero__area { fill: var(--brand); opacity: 0.06; }
.hero__line { fill: none; stroke: var(--text-primary); stroke-width: 1.25; opacity: 0.18; }

/* Lower on paper than on ink, not higher. A hairline of near-black on #F5F6F9
   carries much further than white at ten percent on near-black, so matching
   the numbers would not match the result. */
[data-theme="light"] .hero__area { opacity: 0.07; }
[data-theme="light"] .hero__line { opacity: 0.20; }

.hero__runner {
    fill: none;
    stroke: var(--brand);
    stroke-width: 2.25;

    /* A short lit segment on a very long gap, so one piece of the line is
       travelling rather than the whole line drawing itself in. */
    stroke-dasharray: 150 3000;

    filter: drop-shadow(0 0 7px var(--brand));
    animation: hero-run 11s linear infinite;
}

@keyframes hero-run {
    from { stroke-dashoffset: 3150; }
    to   { stroke-dashoffset: 0; }
}

/* Asked for less motion: keep the whole composition, stop the one thing that
   moves. The backdrop still reads, it simply holds still. */
@media (prefers-reduced-motion: reduce) {
    .hero__runner { animation: none; opacity: 0.55; }
}

/* On a narrow screen the ruling is more noise than structure at that size, and
   the curve has no room to be a curve. */
@media (max-width: 34rem) {
    .hero__grid { opacity: 0.05; }
    .hero__plot { display: none; }
}


/* No max-width here: this element also carries .container, and narrowing
   it re-centres the whole hero so it no longer lines up with the sections
   below. The heading and body constrain their own measure instead. */
.hero__inner {
    display: grid;
    gap: var(--space-5);
    justify-items: start;
}

.hero__heading {
    font-size: var(--text-5xl);
    max-width: 18ch;
}

.hero__body {
    max-width: 46ch;
    font-size: var(--text-lg);
    color: var(--text-secondary);
    line-height: var(--leading-relaxed);
}

.hero__actions { margin-block-start: var(--space-2); }

/* -----------------------------------------------------------------------------
   Problems

   Numbered because the list is a ranked set of three, and the numerals give
   the eye something to anchor on in an otherwise text-only block.
   -------------------------------------------------------------------------- */

.problem-list {
    display: grid;
    gap: 0;
    list-style: none;
    border-block-start: 1px solid var(--line);
}

.problem {
    padding-block: var(--space-6);
    border-block-end: 1px solid var(--line);
}

.problem__body { display: grid; gap: var(--space-3); }

.problem__title {
    font-size: var(--text-xl);
    max-width: 38ch;
}

.problem__text {
    max-width: 58ch;
    color: var(--text-secondary);
}

.problem__link {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-3);

    font-size: var(--text-sm);
    font-weight: 600;
    text-decoration: none;
}

.problem__link:hover { text-decoration: underline; }
.problem__link:hover .problem__proof { border-color: currentColor; }

/* The marker that replaced the number, and unlike the number it is true: each
   of these three links to a project or an article demonstrating the claim above
   it. A hairline outline rather than a filled chip, because amber is an accent
   and three filled badges in a row would be a block of it. */
.problem__proof {
    flex: none;
    padding: 0.15rem var(--space-2);

    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);

    font-size: var(--text-3xs);
    font-weight: 400;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);

    transition: border-color var(--duration) var(--ease);
}

/* The two-column grid that used to live here held the counter. Both are gone,
   rather than leaving an indent with nothing in it. */

/* -----------------------------------------------------------------------------
   Services
   -------------------------------------------------------------------------- */

.service-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

.service-grid--detailed {
    grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr));
}

.service-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);

    padding: var(--space-5);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: var(--edge-highlight);

    color: inherit;
    text-decoration: none;

    transition: border-color var(--duration) var(--ease),
                transform var(--duration) var(--ease),
                box-shadow var(--duration) var(--ease);
}

.service-card:hover {
    border-color: color-mix(in oklab, var(--brand) 45%, var(--line));
    transform: translateY(-2px);
    box-shadow: var(--shadow-2), var(--edge-highlight);
}

.service-card--lg { gap: var(--space-4); padding: var(--space-6); }

.service-card__head { display: grid; gap: var(--space-2); }

.service-card__title {
    font-size: var(--text-lg);
    letter-spacing: var(--tracking-tight);
}

.service-card--lg .service-card__title { font-size: var(--text-xl); }

.service-card__tagline {
    font-size: var(--text-sm);
    color: var(--text-secondary);
}

.service-card__outcome {
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
}

.service-card__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-block-start: auto;
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--line-faint);
    font-size: var(--text-xs);
}

.service-card__price { color: var(--text-primary); }
.service-card__timeline { color: var(--text-muted); }

/* Definition pairs used for price/timeline specs. */
.spec-list {
    display: grid;
    gap: var(--space-2);
    margin-block-start: auto;
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--line-faint);
}

.spec-list > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
}

.spec-list dt {
    font-family: var(--font-mono);
    font-size: var(--text-3xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);
}

.spec-list dd { font-size: var(--text-sm); color: var(--text-primary); }

.spec-list--row {
    margin-block: var(--space-5);
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    border-block-start: none;
    padding-block-start: 0;
}

.spec-list--row > div {
    display: grid;
    gap: var(--space-1);
    justify-items: start;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
}

/* -----------------------------------------------------------------------------
   Work cards

   The no-image card is the primary design here, not a degraded one - most of
   the work is internal software with nothing shareable to photograph.
   -------------------------------------------------------------------------- */

.work-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}

.work-grid--wide { grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); }

.work-card {
    display: flex;
    flex-direction: column;

    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: var(--edge-highlight);
    overflow: hidden;

    color: inherit;
    text-decoration: none;

    transition: border-color var(--duration) var(--ease),
                transform var(--duration) var(--ease),
                box-shadow var(--duration) var(--ease);
}

.work-card:hover {
    border-color: var(--line-strong);
    transform: translateY(-3px);
    box-shadow: var(--shadow-3), var(--edge-highlight);
}

.work-card__media {
    aspect-ratio: 16 / 9;
    max-height: 14rem;
    overflow: hidden;
    background: var(--surface-sunken);
    border-block-end: 1px solid var(--line-faint);
}

.work-card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
    transition: scale var(--duration-slow) var(--ease-out);
}

.work-card:hover .work-card__media img { scale: 1.03; }

.work-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-5);
    flex: 1;
}

.work-card__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-2xs);
    color: var(--text-muted);
}

.work-card__client { color: var(--text-secondary); }

.work-card__title {
    font-size: var(--text-xl);
    letter-spacing: var(--tracking-tight);
}

.work-card__summary {
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
}

.work-card__more {
    margin-block-start: auto;
    padding-block-start: var(--space-3);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--brand-text);
}

/* Tech-stack tags. Mono, because they are machine names, not prose. */
.tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
}

.tag {
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-xs);
    font-family: var(--font-mono);
    font-size: var(--text-3xs);
    color: var(--text-muted);
}

/* -----------------------------------------------------------------------------
   Portal band
   -------------------------------------------------------------------------- */

.portal-band {
    display: grid;
    gap: var(--space-8);
    align-items: center;
}

@media (min-width: 60rem) {
    .portal-band { grid-template-columns: 1fr 1fr; }
}

.portal-band__copy { display: grid; gap: var(--space-4); }

.check-list {
    display: grid;
    gap: var(--space-3);
    list-style: none;
}

.check-list li {
    display: grid;
    grid-template-columns: 1.25rem 1fr;
    gap: var(--space-3);
    align-items: start;
    color: var(--text-secondary);
    font-size: var(--text-sm);
}

.check-list--tight { gap: var(--space-2); }

.check-list svg {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--brand);
    margin-block-start: 0.15rem;
}

/* An illustration of the idea, deliberately abstract - it must not read as a
   screenshot of a product that does not exist yet. */
.portal-vignette {
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    background: var(--surface-raised);
    box-shadow: var(--shadow-3), var(--edge-highlight);
    overflow: hidden;
}

.portal-vignette__bar {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-block-end: 1px solid var(--line-faint);
    background: var(--surface-sunken);
}

.portal-vignette__bar .dot {
    width: 0.6rem;
    height: 0.6rem;
    background: var(--line-strong);
}

.portal-vignette__rows { display: grid; }

.portal-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-block-end: 1px solid var(--line-faint);
    font-size: var(--text-sm);
    color: var(--text-secondary);
}

.portal-row:last-child { border-block-end: none; }
.portal-row--active { background: color-mix(in oklab, var(--accent) 7%, transparent); }

/* -----------------------------------------------------------------------------
   Process steps - numbered because the order is real information
   -------------------------------------------------------------------------- */

.process-list {
    display: grid;
    gap: var(--space-4);
    list-style: none;
    counter-reset: step;
}

.process-step {
    display: grid;
    grid-template-columns: 2.5rem 1fr;
    gap: var(--space-4);
    align-items: baseline;
    padding-block-end: var(--space-4);
    border-block-end: 1px solid var(--line-faint);
    color: var(--text-secondary);
}

.process-list > .process-step:last-child {
    padding-block-end: 0;
    border-block-end: none;
}

.process-step__index {
    font-size: var(--text-sm);
    color: var(--brand-text);
}

.process-list--compact { gap: var(--space-3); }
.process-list--compact .process-step { font-size: var(--text-sm); }

/* -----------------------------------------------------------------------------
   FAQ
   -------------------------------------------------------------------------- */

.faq-list { display: grid; gap: var(--space-2); }

.faq {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
    overflow: hidden;
}

.faq summary {
    padding: var(--space-4) var(--space-5);
    cursor: pointer;
    font-weight: 600;
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
    content: "+";
    font-family: var(--font-mono);
    color: var(--text-muted);
    flex-shrink: 0;
}

.faq[open] summary::after { content: "\2212"; }

.faq p {
    padding: 0 var(--space-5) var(--space-5);
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
}

/* -----------------------------------------------------------------------------
   Articles
   -------------------------------------------------------------------------- */

.page-head--article { padding-block-end: var(--space-5); }

.article-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-block-end: var(--space-4);
}

.article-meta a { color: var(--text-secondary); text-decoration: none; }
.article-meta a:hover { color: var(--brand-text); }

.article-hero {
    margin-block: var(--space-6);
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    overflow: hidden;
    box-shadow: var(--shadow-3);
}

.article-hero img { width: 100%; height: auto; }

.article-body { margin-block: var(--space-7); }

/* The article body comes from markdown, so these style bare elements rather
   than classes - there is nowhere to hang a class on generated output. */
.article-body h2 {
    font-size: var(--text-2xl);
    margin-block-start: var(--space-8);
    margin-block-end: var(--space-3);
}

.article-body h3 {
    font-size: var(--text-lg);
    margin-block-start: var(--space-6);
    margin-block-end: var(--space-2);
}

.article-body ul,
.article-body ol {
    padding-inline-start: var(--space-5);
    display: grid;
    gap: var(--space-2);
}

.article-body li { color: var(--text-secondary); }
.article-body img { border-radius: var(--radius-md); margin-block: var(--space-5); }

.article-body table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
    margin-block: var(--space-5);
}

.article-body th,
.article-body td {
    padding: var(--space-3);
    border-block-end: 1px solid var(--line-faint);
    text-align: left;
}

.pull-quote {
    margin-block: var(--space-7);
    padding: var(--space-6);
    border: none;
    border-inline-start: 2px solid var(--brand);
    background: var(--surface-raised);
    border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
}

.pull-quote p {
    font-family: var(--font-display);
    font-stretch: 104%;
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-tight);
    color: var(--text-primary);
}

.pull-quote cite {
    display: block;
    margin-block-start: var(--space-3);
    font-style: normal;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.article-foot {
    display: grid;
    gap: var(--space-6);
    padding-block-start: var(--space-6);
    border-block-start: 1px solid var(--line);
}

@media (min-width: 52rem) {
    .article-foot {
        grid-template-columns: 1fr auto;
        align-items: center;
    }
}

.next-link {
    display: grid;
    gap: var(--space-2);
    text-decoration: none;
    color: inherit;
}

.next-link__title {
    font-family: var(--font-display);
    font-stretch: var(--width-display);
    font-size: var(--text-xl);
    font-weight: 700;
    letter-spacing: var(--tracking-tight);
}

.next-link:hover .next-link__title { color: var(--brand-text); }

.article-foot__cta {
    display: grid;
    gap: var(--space-3);
    justify-items: start;
}

.article-foot__cta h2 { font-size: var(--text-lg); }

.related { display: grid; gap: var(--space-3); }
.related__list { display: grid; gap: var(--space-2); list-style: none; }
.related__list a { text-decoration: none; font-weight: 600; }
.related__list a:hover { text-decoration: underline; }

/* -----------------------------------------------------------------------------
   Post listings
   -------------------------------------------------------------------------- */

.post-list {
    display: grid;
    list-style: none;
    border-block-start: 1px solid var(--line);
}

.post-row {
    display: grid;
    gap: var(--space-3);
    padding-block: var(--space-6);
    border-block-end: 1px solid var(--line);
    text-decoration: none;
    color: inherit;
    transition: background-color var(--duration) var(--ease);
}

@media (min-width: 48rem) {
    .post-row {
        grid-template-columns: 10rem 1fr;
        gap: var(--space-6);
        padding-inline: var(--space-4);
        margin-inline: calc(var(--space-4) * -1);
        border-radius: var(--radius-md);
    }

    .post-row:hover { background: var(--surface-hover); }
}

.post-row__meta {
    display: flex;
    gap: var(--space-3);
    font-size: var(--text-2xs);
    color: var(--text-muted);
    padding-block-start: 0.3rem;
}

@media (min-width: 48rem) {
    .post-row__meta { flex-direction: column; gap: var(--space-1); }
}

.post-row__body { display: grid; gap: var(--space-3); }

.post-row__title {
    font-size: var(--text-xl);
    letter-spacing: var(--tracking-tight);
    max-width: 32ch;
}

.post-row:hover .post-row__title { color: var(--brand-text); }

.post-row__excerpt {
    max-width: 62ch;
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
}

/* -----------------------------------------------------------------------------
   Pricing
   -------------------------------------------------------------------------- */

.pricing-table__name a {
    font-family: var(--font-display);
    font-stretch: 104%;
    font-size: var(--text-base);
    font-weight: 700;
    text-decoration: none;
    color: var(--text-primary);
}

.pricing-table__name a:hover { color: var(--brand-text); }

.pricing-table__tagline {
    display: block;
    margin-block-start: var(--space-1);
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--text-muted);
}

.pricing-notes {
    display: grid;
    gap: var(--space-5);
    margin-block-start: var(--space-7);
}

/* -----------------------------------------------------------------------------
   Demos
   -------------------------------------------------------------------------- */

.demo-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
    margin-block-end: var(--space-8);
}

.demo-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-5);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: var(--edge-highlight);
}

.demo-card__body { display: grid; gap: var(--space-2); flex: 1; }
.demo-card__title { font-size: var(--text-lg); letter-spacing: var(--tracking-tight); }

.demo-card__text {
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
}

.demo-card .btn { align-self: start; }

/* -----------------------------------------------------------------------------
   Contact
   -------------------------------------------------------------------------- */

.contact-layout {
    display: grid;
    gap: var(--space-7);
    align-items: start;
}

@media (min-width: 60rem) {
    .contact-layout { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: var(--space-8); }
}

.contact-form { display: grid; gap: var(--space-5); }

.field-row {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}

.req { color: var(--brand); }

.contact-form__foot {
    display: grid;
    gap: var(--space-3);
    justify-items: start;
}

.contact-aside { display: grid; gap: var(--space-4); }
.contact-aside .panel { display: grid; gap: var(--space-3); }
.contact-aside a { font-weight: 600; }

/* -----------------------------------------------------------------------------
   Closing CTA
   -------------------------------------------------------------------------- */

.cta-band {
    display: grid;
    gap: var(--space-5);
    align-items: center;

    padding: clamp(var(--space-6), 5vw, var(--space-8));
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    background: var(--surface-raised);
    box-shadow: var(--edge-highlight);
}

@media (min-width: 52rem) {
    .cta-band { grid-template-columns: 1fr auto; gap: var(--space-8); }
}

.cta-band__copy { display: grid; gap: var(--space-3); max-width: 46ch; }
.cta-band__copy h2 { font-size: var(--text-2xl); }
.cta-band__copy p { color: var(--text-secondary); }

/* -----------------------------------------------------------------------------
   Header refinements
   -------------------------------------------------------------------------- */

/* The compact menu.

   The row of links and the header CTA step aside below the breakpoint, because
   they crowd the logo and the CTA duplicates the one in the hero. They used to
   step aside and leave nothing behind, so a phone got a logo, a theme toggle,
   and no way to reach any other page on the site. This is what replaces them.

   Built on a details element, so it works with no JavaScript at all. See the
   note in partials/header.html.twig. */

/* Deliberately NOT position: relative. The panel is meant to span the header,
   and a positioned .nav-menu makes itself the containing block, which pins the
   panel to the width of the button: 48px of menu instead of the full width. */
.nav-menu { display: none; }

.nav-menu__button {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;

    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    cursor: pointer;

    /* The default disclosure triangle, removed in both spellings: WebKit still
       wants the pseudo-element and everything else wants list-style. */
    list-style: none;
    transition: border-color var(--duration) var(--ease), color var(--duration) var(--ease);
}

.nav-menu__button::-webkit-details-marker { display: none; }
.nav-menu__button::marker { content: ""; }
.nav-menu__button:hover { color: var(--text-primary); border-color: var(--line-strong); }
.nav-menu__button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.nav-menu__icon { width: 1.15rem; height: 1.15rem; grid-area: 1 / 1; }
.nav-menu__icon--close { display: none; }
.nav-menu[open] .nav-menu__icon--open { display: none; }
.nav-menu[open] .nav-menu__icon--close { display: block; }

/* Anchored to the header rather than to the button, so it spans the full width
   and its edge lines up with the header's own border. The header is sticky,
   which already makes it the containing block for this. */
.nav-menu__panel {
    position: absolute;
    inset-inline: 0;
    inset-block-start: calc(100% + 1px);

    display: grid;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-5) var(--space-5);

    border-block-end: 1px solid var(--line);
    background: var(--surface-page);

    /* Opaque on purpose. The header is transparent over the hero, and a menu
       you can read the page through is a menu you cannot read. */
    box-shadow: 0 18px 40px -24px rgb(0 0 0 / 0.55);
}

.nav-menu__links { display: grid; }

.nav-menu__links a {
    padding-block: var(--space-3);
    border-block-end: 1px solid var(--line-faint);

    font-size: var(--text-base);
    font-weight: 500;
    color: var(--text-secondary);
    text-decoration: none;
}

.nav-menu__links a:hover { color: var(--text-primary); }
.nav-menu__links a[aria-current="page"] { color: var(--brand-text); }
.nav-menu__cta { margin-block-start: var(--space-3); text-align: center; }

@media (max-width: 47.99rem) {
    .site-nav { display: none; }
    .header-cta { display: none; }
    .nav-menu { display: block; }
}

/* The panel is positioned against the header, so it must never be left open
   when the row of links comes back. */
@media (min-width: 48rem) {
    .nav-menu[open] .nav-menu__panel { display: none; }
}

/* An article hero. Fixed aspect so a portrait photo cannot push the whole
   article below the fold, and object-fit so it crops rather than distorts. */
.article-hero {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    margin-block-end: var(--space-7);
    border-radius: var(--radius-lg);
    background: var(--surface-sunken);
}
