/* Spectromus — public site.
   The palette, type and panel treatment are lifted from the Cloud UI portal
   (svelte-app/src/app.css) so the site and the product read as one product.
   Tokens are duplicated rather than imported: the portal is a separate deploy
   on a separate host, and a shared file neither of them owns is a file that
   goes stale in one of them. */

/* Inter, self-hosted — same reasoning as the portal: no font-CDN request on
   every page load, and the site keeps its typography on networks that cannot
   reach one. One file per subset covers every weight (variable font). */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/inter-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
        U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
        U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/inter-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
        U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
        U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The fallback, with Inter's metrics forced onto it.
 *
 * `font-display: swap` means the page is first painted in whatever local face
 * the stack falls to, and then repainted in Inter. If the two disagree about
 * how tall a line is, every paragraph rewraps at that moment and everything
 * below it moves — a layout shift with no user input behind it, which is the
 * kind that counts against the page. It was measurable: 0.15 in the band
 * around 861px, where a card's text column reflowed and pushed the rest of the
 * page down.
 *
 * These four overrides make the stand-in occupy the same space as the real
 * thing, so the swap changes the shapes and not the layout. They are Inter's
 * own ratios against Arial: ascender 1984 and descender 494 over an em of
 * 2048, with the size adjusted for the difference in x-height.
 *
 * `local()` only — this never causes a download. If the machine has no Arial
 * the stack simply carries on to the next name.
 */
@font-face {
    font-family: 'Inter Fallback';
    src: local('Arial');
    size-adjust: 107.12%;
    ascent-override: 90.49%;
    descent-override: 22.56%;
    line-gap-override: 0%;
}

:root {
    --font-ui: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

    --brand: #05a4a4;
    --brand-dark: #048787;
    --brand-darker: #036b6b;

    /* White text on --brand is 3.06:1, which is under AA for anything below
       24px — and that is the primary button and every link in the topbar.
       These two are the same teal taken far enough down to carry white:
       5.52:1 and 8.04:1. --brand keeps its job as an accent, where nothing
       is read against it. */
    --brand-cta: #037575;
    --brand-deep: #025a5a;
    --brand-tint: #e0f5f5;
    --brand-tint-text: #037575;
    --brand-glow: rgba(5, 164, 164, 0.18);

    /* The two colours in the Spectromus logo. The deep one carries headings
       and the footer, which is what ties the mark to the page rather than
       leaving it a sticker in the corner. */
    --ink: #15525B;
    --mint: #44bdaa;

    /* The mark under the current nav item, and not --mint, which is the reason
       this is its own token. --mint measures 2.53:1 against the topbar
       gradient where the mobile nav sits — under the 3:1 a non-text indicator
       needs, so the one thing telling you where you are would be the one thing
       you could not see. This is the first step light enough to clear it on
       both bars: 3.91:1 desktop, 3.09:1 mobile. */
    --nav-mark: #5ecfbc;

    --text: #2c3e50;
    /* Was #6b7f86: 4.2:1 on white and 3.83:1 on the tinted sections, under AA
       on both, for most of the prose on the site. This clears 4.5 against the
       lightest and the darkest background it is used on. */
    --text-muted: #5c6f75;

    --success-bg: #e6f6ee;
    --success-border: #22a866;
    --success-text: #157a45;

    --danger-bg: #fbeae9;
    --danger-border: #d64d42;
    --danger-text: #a3352c;

    --panel-shadow: 0 2px 10px rgba(20, 40, 45, 0.06);
    --panel-border: 1px solid rgba(5, 164, 164, 0.06);

    /* The face of a feature card, and it has to be this exact value: it is
       FRAME_BG in capture-screenshots.js, the colour every screenshot is matted
       onto. The mat is baked into the file and fills the media half of the
       card, so if this token and that constant ever disagree, the card gets a
       visible seam down the middle and no amount of CSS will close it.

       Both halves being one tone is the whole point. The card was white with a
       tinted image beside it, which read as two panels pushed together rather
       than one card with a picture in it. */
    --card-face: #f1f7f7;

    /* The banner panel, and deliberately NOT --card-face.
     *
     * --card-face is pinned to the colour the screenshots are matted onto, so
     * it cannot move without re-cutting every image. This can: the banner shows
     * its picture as a bordered, rounded card rather than bleeding it to the
     * panel edge, so the mat sitting a shade lighter reads as the screenshot
     * being a thing on a surface instead of a seam.
     *
     * How far it can go is set by the text on it. --text-muted measures 4.45:1
     * here, under AA, which is why --banner-muted exists below. One more step
     * down and the kicker fails as well. */
    --banner-face: #e3eeee;

    /* The lead paragraph inside the banner. Darker than --text-muted, which is
     * tuned for white and the page gradient and gives 4.45:1 on the panel.
     * This is 5.24:1 there, and still 6.20:1 if it is ever used on white. */
    --banner-muted: #51646b;

    /* One inner gutter for every card on the page — solution cards, the
       feature cards, the blog list, the about and contact panels. They had 32px, 20px
       and 28px, which put the text inside them on three different left edges
       within 12px of each other: not obviously broken, but enough to make the
       page look untidy as you scroll past it. */
    --card-pad: 28px;

    /* How wide the page runs. Three things have to agree on this — the content
       wrap, the topbar and the footer — and for a while they did not: the wrap
       was widened to 1320 and the other two were left on the 1100 they had been
       copied from, so the menu and the copyright sat inside a narrower column
       than everything between them. One value, read in all three places. */
    --wrap-max: 1320px;

    /* Two users now: the blog's code blocks and Trogon's terminal panel.
       A token for the same reason --wrap-max is one — a constant repeated
       in two places is a constant that stops matching. */
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
    scrollbar-gutter: stable;
    /* Anchor nav under a sticky header: without this the heading lands behind
       the bar and the section looks like it starts at its second paragraph. */
    scroll-behavior: smooth;
    scroll-padding-top: 84px;
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

body {
    font-family: var(--font-ui);
    font-feature-settings: "tnum" 1, "zero" 1;
    background: linear-gradient(180deg, #f6fafa 0%, #eef6f6 100%);
    background-attachment: fixed;
    color: var(--text);
    -webkit-font-smoothing: antialiased;
    line-height: 1.6;
    min-height: 100vh;
}

img { max-width: 100%; display: block; }
/* <picture> is inline by default, which puts a baseline gap under every
   screenshot that is wrapped in one. */
picture { display: block; }

a { color: var(--brand-darker); }

/* Keyboard users get the same halo the portal's inputs use, everywhere.
   The ring is --brand-darker rather than --brand: --brand is 2.91:1 against
   the page background, under the 3:1 non-text contrast asks for, and on the
   topbar it is the gradient's own starting colour — an invisible ring on the
   first thing a keyboard reaches. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
    outline: 2px solid var(--brand-darker);
    outline-offset: 2px;
    border-radius: 4px;
}

/* On the teal bar the ring goes the other way, for the same reason. */
.topbar a:focus-visible {
    outline-color: #ffffff;
}

/* Was 1100, which left 90px of nothing each side of a 1280 laptop and 410px of
   it at 1920. The cap is border-box, so below 1320 the padding is the only
   gutter and the content fills the window.
 *
 * Not wider than this, and not a percentage: the prose in these sections has
 * no column of its own, so the wrap is what stops a paragraph running to 120
 * characters a line. Measured at 1320 the longest is about 96, which is the
 * top of what is comfortable and the reason it does not go further. Anything
 * that needs to be narrower says so itself — .banner-lead caps at 46ch. */
.wrap {
    max-width: var(--wrap-max);
    margin: 0 auto;
    padding: 0 24px;
}

/* Above the laptop sizes the cap steps up. 1320 was chosen against a 1920
   monitor and leaves a 4K one — 2560 CSS pixels at the scaling most people
   actually run, 3840 at none — with more empty margin than content.
   Two steps rather than a percentage, so the width lands on a value the
   layout has been looked at rather than whatever the window happens to be.

   The paragraphs are what stop this going further, and each of them now
   carries its own measure: 1320 was the point where the widest prose in a
   section head reached about 96 characters, and a wrapper wider than that is
   only safe because the text inside no longer fills it. */
@media (min-width: 1560px) {
    :root { --wrap-max: 1480px; }
}

@media (min-width: 1900px) {
    :root { --wrap-max: 1660px; }
}

/* And a third step for a 4K monitor being run at no scaling at all, where the
   two above still left more margin than content. Measured rather than
   guessed: the longest paragraph on the page is 73 characters at this width,
   because every block of prose carries its own measure and none of them fill
   the wrapper. */
@media (min-width: 2400px) {
    :root { --wrap-max: 2000px; }
}

/* Screen-reader-only, for the skip link and form labels that are visually
   carried by the placeholder. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    background: white;
    color: var(--brand-darker);
    padding: 10px 16px;
    z-index: 100;
    font-weight: 600;
}
.skip-link:focus { left: 12px; top: 12px; }

/* ---------------------------------------------------------------- Topbar */
/* The portal's topbar, unchanged: same gradient, same sticky behaviour, same
   shadow. Someone moving between the site and the product should not feel a
   seam at the top of the page. */
.topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    background: linear-gradient(135deg, var(--brand-cta) 0%, var(--brand-deep) 100%);
    color: white;
    box-shadow: 0 2px 10px rgba(3, 107, 107, 0.2);
}

.topbar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    padding: 14px 24px;
    max-width: var(--wrap-max);
    margin: 0 auto;
}

.brand {
    display: flex;
    align-items: center;
    gap: 10px;
    text-decoration: none;
    color: inherit;
    transition: opacity 0.15s;
}
.brand:hover { opacity: 0.85; }

/* The mark is two teals on transparent, and the darker of them disappears
   against the topbar gradient. Brightening it to white keeps the wordmark
   legible without shipping a second file. */
/* Sized by height, width follows. The mark is six to one, so 22px of height
   is about 134px of width — roughly the footprint the old logo-plus-wordmark
   pair had, and the letters are now 22px tall instead of 7px.

   The mark is two teals and the darker one disappears against the topbar
   gradient, so it is flattened to white. That is also why there is no coloured
   version here to keep in step. */
.brand img {
    height: 28px;
    width: auto;
    display: block;
    filter: brightness(0) invert(1);
}

/* --------------------------------------------------------------- Nav */
.nav {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
}

/* Plain labels; a rule under the one you are reading.

   No item carries a fill, resting or hovered. Two rounds of this bar went
   wrong by adding one: Contact Us alone at 16% read as "selected" rather than
   "this is the action", because every other link's *hover* was 15% — a heavier
   dose of the same material, which is what a state looks like. Giving every
   item that fill instead fixed the false selection and turned the bar into a
   row of chips.

   Dropping the fill altogether also settles an accessibility problem that
   predates both attempts. White on the 15% hover fill measured 4.32:1 against
   the mobile gradient, under AA for 13px text — the phone bar spans the full
   width and sits on a lighter part of the gradient than the desktop one. With
   no fill under it the label is pure white on the bar itself: 7.38:1 on
   desktop, 5.83:1 on a phone.

   Every item here is an anchor into one page, so what the bar can usefully say
   is which section is on screen. That is what the underline is for; hover
   previews it. */
.nav a {
    color: rgba(255, 255, 255, 0.92);
    text-decoration: none;
    font-size: 13.5px;
    font-weight: 500;
    padding: 7px 12px;
    border-radius: 6px;
    transition: color 0.15s;
    white-space: nowrap;
    position: relative;
}

/* Inset to the label rather than the padding box, so the rule is the width of
   the word and reads as belonging to it. */
.nav a::after {
    content: "";
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: 2px;
    height: 2px;
    border-radius: 2px;
    background: var(--nav-mark);
    transform: scaleX(0);
    transition: transform 0.18s ease;
}

.nav a:hover,
.nav a[aria-current] {
    color: #fff;
}

.nav a:hover::after,
.nav a[aria-current]::after {
    transform: scaleX(1);
}

/* ------------------------------------------------- Our Solutions, the group */
/* Two products behind one label. The default state below is not the closed
   dropdown — it is no dropdown at all: the group dissolves into the bar and
   its two links sit in it as ordinary items, which is what a visitor without
   JavaScript keeps, and what everybody gets below 860px.
   `.nav.js` is added by nav.js, and only then does any of this become a menu.

   Same shape as the banner: the no-script state is the one in the markup, and
   the script upgrades it. The difference is which way round the default runs —
   the banner defaults to the enhanced form because a layout shift on the
   largest element on the page is worse than one in a menu, and here the menu
   is the one that cannot be reached at all if the script never lands. */
.nav-group,
.nav-menu {
    display: contents;
}

.nav-toggle {
    display: none;
}

/* --- With script ------------------------------------------------------ */
/* The dropdown, at every width.
   It used to be fenced behind a min-width, because the bar below 860px was a
   horizontal scroller and a panel positioned out of a box with
   `overflow-x: auto` is clipped by it — the menu opened with its bottom half
   sliced off. The scroller is gone: it existed to carry seven items on a
   phone, and putting both products behind one label leaves two. Two items do
   not need to scroll, so the panel has nothing left to clip it.
   See the max-width block further down, which no longer flattens the group. */
/* --- With script ---------------------------------------------------- */
.nav.js .nav-group {
    display: block;
    position: relative;
}

/* Matched to `.nav a` rather than inheriting the button defaults, which is
   most of what is here: a <button> brings its own font, its own background and
   a border, and none of them are the bar's. */
.nav.js .nav-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font: inherit;
    font-size: 13.5px;
    font-weight: 500;
    font-family: inherit;
    color: rgba(255, 255, 255, 0.92);
    background: none;
    border: 0;
    padding: 7px 12px;
    border-radius: 6px;
    cursor: pointer;
    white-space: nowrap;
    position: relative;
    transition: color 0.15s;
}

/* The same rule the links get, so the group does not read as a different kind
   of thing from the items beside it. */
.nav.js .nav-toggle::after {
    content: "";
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: 2px;
    height: 2px;
    border-radius: 2px;
    background: var(--nav-mark);
    transform: scaleX(0);
    transition: transform 0.18s ease;
}

.nav.js .nav-toggle:hover,
.nav.js .nav-group.is-open .nav-toggle { color: #fff; }

.nav.js .nav-toggle:hover::after,
.nav.js .nav-group.is-open .nav-toggle::after { transform: scaleX(1); }

/* Scroll-spy marks the link, and the link is inside a closed panel where
   nobody can see it. The group wears it instead, so the bar still answers
   "which section am I in" when the answer is one of the products. */
.nav.js .nav-group:has(a[aria-current]) .nav-toggle::after {
    transform: scaleX(1);
}

/* A chevron, drawn rather than fetched — two borders on a rotated square, the
   same trick the address pin uses. Nudged up by a quarter of its own height
   because a rotated square hangs below the text baseline it is centred on. */
.nav-caret {
    width: 6px;
    height: 6px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    transition: transform 0.18s ease;
}

.nav.js .nav-group.is-open .nav-caret {
    transform: translateY(1px) rotate(-135deg);
}

/* The panel. White on the page rather than teal on the bar: it hangs over the
   content, and a translucent teal layer over a screenshot is unreadable. */
.nav.js .nav-menu {
    display: none;
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    min-width: 172px;
    padding: 6px;
    background: #fff;
    border: 1px solid rgba(5, 164, 164, 0.16);
    border-radius: 10px;
    box-shadow: 0 12px 28px rgba(3, 60, 60, 0.18);
    z-index: 40;
}

.nav.js .nav-group.is-open .nav-menu { display: block; }

/* Dark on white, so none of the bar's own link rules apply — and the underline
   does not either. In a panel this size the rule would be a line under every
   item; a fill is the state that reads here. */
.nav.js .nav-menu a {
    display: block;
    color: var(--ink);
    font-size: 14px;
    font-weight: 600;
    padding: 9px 12px;
    border-radius: 6px;
    white-space: nowrap;
}

.nav.js .nav-menu a::after { content: none; }

.nav.js .nav-menu a:hover,
.nav.js .nav-menu a:focus-visible {
    background: var(--brand-tint);
    color: var(--brand-darker);
}

/* Which product's section is on screen, said inside the panel too — the group
   underline says one of them is, this says which. */
.nav.js .nav-menu a[aria-current] {
    color: var(--brand-darker);
    background: var(--brand-tint);
}

/* The bar is white on teal and this panel is not, so the outline the topbar
   sets for its own links is invisible in here. */
.nav.js .nav-menu a:focus-visible {
    outline: 2px solid var(--brand-cta);
    outline-offset: -2px;
}


/* Below the breakpoint the links move to their own row under the logo, and
   that row scrolls sideways rather than wrapping. There are seven of them now
   and on a phone wrapping stacked three rows of chrome above every page — a
   header taller than the content it sits on. A hamburger would be the other
   answer, but it hides the whole site behind a tap; one scrollable strip keeps
   the first few visible and the rest a swipe away. */
@media (max-width: 860px) {
    .topbar-inner {
        flex-direction: column;
        align-items: stretch;
        /* Was 8px. The mark and the strip under it read as one crowded block,
           and the bigger mark made it worse. */
        gap: 14px;
        padding: 12px 16px;
    }
    /* The mark centres over the menu below it. The brand link is a full-width
       flex row here — the column layout stretches it — so this is centring the
       image inside the link rather than the link inside the bar. */
    .brand { justify-content: center; }

    /* Not a scroller any more.
       It was one because seven items wrapped into three rows of chrome above
       every page. Both products now sit behind Our Solutions, which leaves two
       items — they fit at 320px with room to spare, and `overflow-x: auto`
       would clip the dropdown panel the moment it opened.

       flex-wrap stays permissive rather than nowrap: if a third item is ever
       added and the row runs out of width, wrapping is a worse outcome than a
       clipped menu but a better one than an item nobody can reach. */
    .nav {
        flex-wrap: wrap;
        overflow: visible;
        gap: 2px 4px;
        justify-content: space-between;
    }
    /* The group stays a dropdown here. What changes is the panel: on a phone
       it is anchored to the left edge of the bar rather than to the button, so
       a menu opening under a button near the middle of the row does not hang
       off the screen. */
    .nav.js .nav-group { position: static; }
    .nav.js .nav-menu {
        left: 0;
        right: auto;
        /* Wide enough to read as a menu. max-content alone sized it to the
           longest word — 86px, which looks like a tooltip somebody left open.
           Still capped to the viewport so it cannot run off a 320px screen. */
        min-width: 150px;
        width: max-content;
        max-width: calc(100vw - 32px);
        top: calc(100% + 10px);
    }
    /* The bar is the positioning context now, so it needs to be one. */
    .nav.js { position: relative; }
    /* The rule follows the label, so it follows the label's padding too. */
    .nav a::after { left: 10px; right: 10px; }
    .nav a {
        padding: 7px 10px;
        font-size: 13px;
        scroll-snap-align: start;
        /* flex-shrink would squeeze the labels into two lines each once the
           row overflows, which is the thing the scroller exists to avoid. */
        flex: 0 0 auto;
    }
    /* Logo row + nav row: the anchor offset has to clear both or every
       section lands with its heading behind the bar. */
    html { scroll-padding-top: 108px; }
    .solution, .panel { scroll-margin-top: 108px; }
}

/* The mobile bar runs all the way up to 860px, which is small tablets as well
   as phones, and a four-item menu spread across 800px is four items with a
   hand's width between them. Above phone size the strip is capped and centred
   under the mark instead, so the spacing stays deliberate rather than just
   wide.

   The cap is on the scroll container, so a menu that grows past it scrolls
   inside the cap — and because the spread is space-between, the first item
   still starts at the left edge and stays reachable. */
@media (min-width: 561px) and (max-width: 860px) {
    .nav {
        max-width: 460px;
        margin: 0 auto;
    }
}

/* ------------------------------------------------------------------ Hero */
/* Was 72/56, which put 90px of nothing between the bar and the first word and
   another 56 under the dots. The banner is a panel now and carries its own
   inner padding, so the section around it does not need to hold the page open
   as well. */
.hero {
    padding: 34px 0 40px;
}

/* Was clamp(30px, 5vw, 46px). At 46 the headline was the biggest thing on the
   page by a wide margin — every section heading below it is 22-28px — so the
   top of the page did not sit in the same type scale as the rest of it. */
.hero h1 {
    font-size: clamp(27px, 3.6vw, 38px);
    line-height: 1.15;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ink);
    text-wrap: balance;
    max-width: 16ch;
}

/* `.hero p` used to live here, styling the lead paragraph of the old
   single-column hero. That paragraph is gone — the hero is the product banner
   now, and its only direct <p> is the eyebrow, which has its own rule below.

   It had to go rather than just being left: as an element selector it scored
   (0,1,1), which beat every single-class rule inside the banner. The kicker and
   the lead were both being drawn as 19px grey hero paragraphs, and the darker
   panel is what made it obvious — the lead ignored its new colour and sat at
   4.45:1 instead of 5.24. A dead rule that still wins the cascade is worse than
   one that does nothing. */

.hero-actions {
    margin-top: 30px;
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

/* ------------------------------------------------------------- Banner */
/* The two products, as slides.

   Before banner.js runs, .banner has no `js` class and every slide is an
   ordinary block in the flow: the page is a stacked list of products, which is
   a page that works. Everything that turns it into a slider is behind .js, so
   a script that never arrives cannot leave a visitor with one product and no
   way to reach the other. */
.banner-slide {
    display: grid;
    grid-template-columns: 1fr 1.05fr;
    gap: 44px;
    /* stretch, not center. Both copy columns are then the full height of the
       track, which is what lets the buttons below line up across slides — see
       .banner-copy. The picture is re-centred on its own. */
    align-items: stretch;
}

.banner-media { align-self: center; }

/* The products do not have the same amount to say, so centring each slide's
   copy as a block put the buttons at different heights: 347px down on one
   slide and 362px on the other, which reads as the page twitching every time
   it turns over.
 *
 * Pushed to the bottom of a full-height column instead, they land in the same
 * place on every slide whatever the copy above them does — and they keep doing
 * so at widths where the text wraps differently. */
.banner-copy {
    display: flex;
    flex-direction: column;
}

.banner-copy .hero-actions {
    margin-top: auto;
    padding-top: 28px;
}

/* One width for the call to action, wide enough for the longest product name
   rather than fitted to each. "Learn about Flok" is 140px and "Learn about
   Trogon" is 158, and two buttons a slide apart that differ by 18px look like
   a mistake. Raise this if a product name outgrows it. */
.banner-copy .btn-primary {
    min-width: 170px;
    text-align: center;
}

.banner-controls[hidden] { display: none; }

/* Every slide sits in the same grid cell, and the ones that are not current
   are hidden with `visibility` rather than taken out of the layout.

   This is what keeps the box from resizing. The products do not have the same
   amount to say — Trogon's slide is 30px taller than Flok's — so a track that
   only ever contained the current slide changed height every seven seconds and
   moved the whole page underneath it. Stacked in one cell the track is as tall
   as the tallest slide, and CSS works that out on its own: no measuring pass,
   and nothing to redo when the webfont lands and the text rewraps.

   `visibility: hidden` and not `display: none` because it has to hold its
   space. It still does the other things this needs — the slide is not painted,
   it is out of the accessibility tree, and the links inside it cannot be
   tabbed to.

   This is the *default*, not something switched on once the script runs. It
   was behind a .js class at first, and the class arriving is a layout change:
   a page that had been two articles tall collapsed to one and everything below
   jumped. Making the slider the state the page is painted in first costs
   nothing to a visitor with JavaScript, and the ones without are handed
   banner-nojs.css by the <noscript> in inc/header.php, which puts the stack
   back before anything is drawn. */
/* A surface with an edge, so the thing that changes every few seconds is
   visibly one region rather than the top of the page rearranging itself. The
   dots sit inside it, which is what says they belong to it.
 *
 * --card-face and not white, and it has to be: the screenshots are matted onto
 * exactly that colour upstream by capture-screenshots.js. On a white panel the
 * mat would read as a grey rectangle around every picture — the same seam the
 * feature cards were built to avoid. */
.banner {
    background: var(--banner-face);
    border: 1px solid rgba(5, 164, 164, 0.16);
    border-radius: 14px;
    padding: 32px 34px 24px;
    box-shadow: var(--panel-shadow);
    /* The slides travel sideways past the panel's edge on their way in and
       out. Without this they are drawn over the section beside them and the
       page scrolls horizontally to fit. */
    overflow: hidden;
}

.banner-track { display: grid; }

.banner-slide {
    grid-area: 1 / 1;
}

/* Before the script runs nothing carries .is-live, so the first slide is the
   one on show. Afterwards .banner.js wins on specificity and .is-live decides. */
.banner-slide:not(:first-child) { visibility: hidden; }

/* The slides move across: the one leaving goes out to the left, the one
   arriving comes in from the right, both fading as they travel. They already
   share a grid cell, so this is only a matter of each one's transform.
 *
 * `is-leaving` is what makes the direction consistent. Without it the outgoing
 * slide would fall back to the resting +38px and exit to the right — the same
 * side the new one is entering from, so the two would cross over each other.
 * banner.js puts the class on for the length of the move and takes it off
 * again.
 *
 * `visibility` is transitioned as well, with a duration of zero and a delay
 * that runs out exactly when the move does. That is what makes it work in both
 * directions: a slide going away stays present for the length of the animation
 * and only then leaves the accessibility tree, while one arriving is present
 * from the first frame. Animating opacity alone would leave an invisible slide
 * still catching clicks and still being read out.
 */
.banner.js .banner-slide {
    opacity: 0;
    visibility: hidden;
    transform: translateX(38px);
    transition: opacity 0.4s ease,
                transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
                visibility 0s linear 0.5s;
}

.banner.js .banner-slide.is-live {
    opacity: 1;
    visibility: visible;
    transform: translateX(0);
    transition: opacity 0.4s ease,
                transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
                visibility 0s;
}

.banner.js .banner-slide.is-leaving { transform: translateX(-38px); }

/* Going backwards — swiped right, or ArrowLeft — the outgoing slide leaves to
   the right instead. A slide that exits the same side the new one enters from
   reads as the deck moving the wrong way. */
.banner.js .banner-slide.is-leaving-back { transform: translateX(38px); }

/* And the one arriving has to come from the other side to match. Slides at
   rest sit at +38px, ready to enter from the right; while the banner is moving
   backwards the waiting ones sit at -38px instead.

   The three :not()s are load-bearing. Without them this also matches the slide
   that is on its way out — it lost .is-live a moment ago — and it would fight
   the rule above at equal specificity, leaving the winner to source order. */
.banner.js.is-back .banner-slide:not(.is-live):not(.is-leaving):not(.is-leaving-back) {
    transform: translateX(-38px);
}

/* The global reduced-motion rule shortens durations but not delays, which
   would leave the outgoing slide in the accessibility tree for half a second
   after it had gone — and it does nothing at all about the transforms, so a
   slide would still be sitting 38px off to one side. Cut all of it. */
@media (prefers-reduced-motion: reduce) {
    .banner.js .banner-slide,
    .banner.js .banner-slide.is-live,
    .banner.js .banner-slide.is-leaving,
    .banner.js .banner-slide.is-leaving-back,
    .banner.js.is-back .banner-slide:not(.is-live) {
        transition: none;
        transform: none;
    }
}


/* A grid item's default min-width is auto — the width of its content — so a
   1600px-wide image would refuse to shrink and push the row past the wrap. */
.banner-media { min-width: 0; }

.banner-media img {
    width: 100%;
    /* Not optional. The tag carries width and height to reserve the box before
       the image lands, and without this the height attribute is taken as the
       rendered height: the shot comes out full width and a thousand pixels
       tall. `auto` lets the pair describe the ratio and nothing else. */
    height: auto;
    border-radius: 10px;
    border: 1px solid rgba(5, 164, 164, 0.16);
    /* Lighter than it was: the picture used to float on the page gradient and
       needed the lift. Inside a panel of its own colour a heavy shadow reads
       as a second card stacked on the first. */
    box-shadow: 0 6px 20px rgba(20, 40, 45, 0.09);
}

.banner-kicker {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--brand-tint-text);
    margin-bottom: 8px;
}

/* The product name is the biggest thing in the banner. It is an h2 rather than
   an h1 because there are two of them and they are peers — see index.php. */
.banner-name {
    font-size: clamp(30px, 4vw, 42px);
    line-height: 1.1;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ink);
}

.banner-lead {
    margin-top: 14px;
    font-size: clamp(15px, 1.6vw, 17px);
    color: var(--banner-muted);
    max-width: 46ch;
}

/* What the product does, as a row of chips rather than a bulleted list. Four
   short capabilities read as a specification at a glance; the same four as
   list items read as prose that has been chopped up. */
.banner-features {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 18px;
}

.banner-features li {
    font-size: 12.5px;
    font-weight: 500;
    color: var(--brand-tint-text);
    /* White, not --brand-tint. The tint measures 1.05 against the darker panel
       — near enough the same tone that the chips would be outlines with no
       fill. White is 1.18 and the label on it is 5.52. */
    background: #fff;
    border: 1px solid rgba(5, 164, 164, 0.18);
    border-radius: 999px;
    padding: 5px 11px;
}

/* --- The controls ---------------------------------------------------- */
/* Centred under the whole banner rather than tucked under the copy column.
   They belong to the pair of slides, not to the half of one that happens to
   sit above them. */
.banner-controls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: 34px;
}

.banner-tabs { display: flex; align-items: center; gap: 9px; }

/* One dot per slide. The label inside is visually hidden, so the button has an
   accessible name without a visible one. */
.banner-tab {
    position: relative;
    width: 9px;
    height: 9px;
    padding: 0;
    cursor: pointer;
    border-radius: 50%;
    background: transparent;
    border: 1.5px solid rgba(5, 164, 164, 0.42);
    transition: background-color 0.18s, border-color 0.18s, transform 0.18s;
}

/* A 9px dot is a 9px target, which is a quarter of what a thumb needs. The hit
   area is grown with a transparent pseudo-element rather than padding: padding
   would inflate the painted circle instead of the area around it. */
.banner-tab::after {
    content: "";
    position: absolute;
    inset: -9px;
    border-radius: 50%;
}

.banner-tab:hover { border-color: var(--brand-cta); }

/* Filled and a little larger, so which one is live survives a glance. Colour
   alone would not: these are two small circles a few pixels apart. */
.banner-tab[aria-selected="true"] {
    background: var(--brand-cta);
    border-color: var(--brand-cta);
    transform: scale(1.25);
}

/* Deliberately quiet. It has to be there — content that moves on its own needs
   a way to stop it — but it is not what the row is for, and at the size of the
   dots beside it a bordered circle reads as the louder of the two. */
.banner-pause {
    width: 24px;
    height: 24px;
    display: grid;
    place-items: center;
    cursor: pointer;
    border-radius: 50%;
    background: transparent;
    border: 0;
    color: rgba(92, 111, 117, 0.55);
    transition: color 0.15s;
}
.banner-pause:hover { color: var(--brand-darker); }

/* Two bars for pause; the pressed state swaps them for a triangle. Drawn in
   CSS rather than shipped as two more files for 12 pixels of icon. */
.banner-pause-icon {
    width: 8px;
    height: 10px;
    border-left: 3px solid currentColor;
    border-right: 3px solid currentColor;
}
.banner-pause[aria-pressed="true"] .banner-pause-icon {
    width: 0;
    height: 0;
    border-left: 9px solid currentColor;
    border-right: 0;
    border-top: 6px solid transparent;
    border-bottom: 6px solid transparent;
    margin-left: 3px;
}

/* One column, at the same width the nav collapses at.

   This block lives here rather than with the other 860px rules further up the
   file, and it has to: those come *before* this section, and at equal
   specificity the two-column rule below them would win at every width. The
   symptom, when the hero had this bug, was a 156px-wide screenshot on a phone.

   The image is second in source order, so collapsing puts it under the buttons
   rather than between the reader and them. */
@media (max-width: 860px) {
    /* 34px of gutter each side of a 358px screen leaves very little for the
       words, so the panel gives some of it back. */
    .banner { padding: 24px 20px 18px; border-radius: 12px; }
    /* One column, and the copy row takes whatever height the taller slide left
       over. Without the 1fr the row is sized to its own content, so the actions
       have nothing to push against and the buttons land at a different height
       on each slide — 12px apart, which shows as a twitch every time the banner
       turns over. */
    /* One column, picture first.
       .banner-media is second in the markup because that is the reading order
       on a wide screen: the product is named, described, then shown. Stacked on
       a phone that order buries the picture below a screenful of text, where
       most readers never reach it - so the picture leads and the copy follows.
       Reordered in CSS rather than the markup so the desktop order, which is
       also the order a screen reader follows, is untouched.

       auto for the picture and 1fr for the copy, the reverse of what this rule
       used to say, because the rows have swapped. The 1fr is what the buttons
       push against: without it the copy row is sized to its own content, the
       actions have nothing to push down from, and the two slides put their
       buttons 12px apart - a twitch on every turn. */
    .banner-slide {
        grid-template-columns: 1fr;
        grid-template-rows: auto 1fr;
        gap: 26px;
    }
    .banner-media { order: -1; }

    /* The feature chips come off on a phone. Six of them wrap to three rows
       and push the button below the fold, and they are the least of what the
       slide has to say: the name, the sentence and the button are the whole
       point, and every chip is repeated in the section the button leads to. */
    .banner-features { display: none; }

    .banner-lead { max-width: 60ch; }
}

/* --------------------------------------------------------------- Buttons */
/* Straight from the portal, including the glow — it is what makes the primary
   button read as the one to press. */
.btn {
    display: inline-block;
    padding: 11px 18px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    font-family: inherit;
    text-decoration: none;
    transition: all 0.2s;
}

.btn-primary {
    background: var(--brand-cta);
    color: white;
    box-shadow: 0 2px 8px var(--brand-glow);
}
.btn-primary:hover {
    background: var(--brand-darker);
    box-shadow: 0 4px 12px var(--brand-glow);
}
.btn-primary:active { transform: translateY(1px); }

.btn-secondary {
    background: #ecf0f1;
    color: var(--text);
}
.btn-secondary:hover { background: #d5dbdb; }

/* -------------------------------------------------------------- Sections */
.section {
    padding: 26px 0 44px;
}

.section-head {
    margin-bottom: 26px;
}

.section-head h2 {
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink);
    text-wrap: balance;
}

.section-head p {
    margin-top: 8px;
    color: var(--text-muted);
    max-width: 62ch;
}

/* ------------------------------------------------------------- Solutions */
/* One card per solution, alternating the image side. The alternation is not
   decoration — with five cards of similar shape it is what stops the eye
   sliding past the middle three. */
.solution {
    background: var(--card-face);
    border-radius: 10px;
    /* Its own border and shadow rather than the shared panel tokens. Those are
       tuned for a white panel on a tinted page, where the fill does the
       separating; this card is now nearly the tone of the section behind it,
       so the edge has to do that work instead. */
    box-shadow: 0 2px 14px rgba(20, 40, 45, 0.07);
    border: 1px solid rgba(5, 164, 164, 0.16);
    overflow: hidden;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: stretch;
    margin-bottom: 22px;
    /* Anchor targets sit on the card, so a nav click lands on the whole thing
       rather than on its heading with the image already scrolled past. */
    scroll-margin-top: 84px;
}

.solution:nth-child(even) .solution-media { order: 2; }

/* Cards whose image is a screenshot of the product rather than an
   illustration. An illustration reads at any size; a screenshot of a table
   does not, and at half a card it is a picture of text nobody can make out.
   These give the image about two thirds and leave the body a column that still
   holds a comfortable line — the copy on both of them is three lines.

   The ratio has to be stated for both parities. The media is moved with
   `order`, not by being a different child, so on the reversed cards the body
   lands in column one — and column one is the wide one unless it is swapped
   here too. */
.solution-shot { grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr); }
.solution-shot:nth-child(even) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.75fr); }

/* The ratio is stated here rather than left to whichever image happens to be
   inside. `height: 100%` on the image never resolved — the row had no height
   of its own to be a percentage of — so each image laid out at its own
   intrinsic ratio and set the height of the card around it. Three of the four
   are 4:3 and the fourth is 16:9, which is why one card in the stack was 95px
   shorter than its neighbours. Fixing the ratio here makes every card the
   same height and gives object-fit: cover something to actually do. */
/* The media half carries the same face as the card. In practice the image
   covers all of it — the screenshots are matted to exactly this box — so this
   only shows on the illustration card, where the drawing is a different ratio.
   It is stated anyway: a screenshot recaptured at the wrong size should sit on
   the card's own colour, not on a default.

   One shape, repeated. The four screenshots are matted to the same frame
   upstream — 800x500 before the retina doubling, see CARD_FRAME in
   capture-screenshots.js — so they arrive here as four identical files and the
   stack of cards is one rectangle down the page rather than four that are
   nearly but not quite the same height.

   The ratio is restated here anyway, and it is not redundant. It is what the
   box is, rather than what happens to be inside it: a screenshot recaptured
   without the matting step run over it gets letterboxed into the right shape,
   which looks like padding, instead of setting its own height and pulling one
   card out of line, which looks broken. It also brings the illustration card
   into the same shape without needing its drawing redone.

   Dropped below 960px, where the cards are one column and each fills the
   width. Nothing sits beside anything there for its height to have to match,
   and a full-width box at this ratio is a very tall picture above two
   sentences — which is what the image caps in that breakpoint exist to stop. */
.solution-media {
    position: relative;
    aspect-ratio: 800 / 500;
    background: var(--card-face);
}

/* The image sets the height of the box, and the box sets the height of the
   row. `object-fit: contain` still matters for the case the copy beside it is
   the taller of the two: then the row grows past the picture and the picture
   stays centred in what is left rather than stretching. */
.solution-media img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: center;
}

.solution-body {
    padding: var(--card-pad);
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 12px;
}

.solution-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--brand-tint-text);
    background: var(--brand-tint);
    padding: 4px 10px;
    border-radius: 4px;
    /* align-self only shrinks this when the parent happens to be a flex
       container, which .solution-body is and .section-head is not — so in a
       section heading the pill stretched into a full-width tinted bar.
       inline-block makes it hold its own width wherever it is put; inside the
       flex parent it is blockified and align-self still does the work. */
    display: inline-block;
    align-self: flex-start;
}

.solution-body h3 {
    font-size: 21px;
    font-weight: 650;
    letter-spacing: -0.01em;
    color: var(--ink);
    text-wrap: balance;
}

.solution-body p {
    color: var(--text-muted);
    font-size: 15px;
}

/* A second line of copy that qualifies the first, kept visually subordinate
   so the card still has one main sentence. */
.solution-note {
    font-size: 13.5px;
    color: var(--text-muted);
    padding-left: 12px;
    border-left: 3px solid var(--mint);
}

/* These collapse earlier than the other two-column blocks on the page. Side by
   side below about this width the copy column is narrow enough that the text
   grows taller than the image beside it, which takes the card height back off
   the ratio above and leaves the stack ragged again. Stacked, the copy can be
   as long as it likes. */
@media (max-width: 960px) {
    .solution, .solution-shot, .solution-shot:nth-child(even) {
        grid-template-columns: 1fr;
    }
    /* The order swap has to be undone too, or on one column the image of every
       even card jumps below its own text. */
    .solution:nth-child(even) .solution-media { order: 0; }
    /* Capped, because on one column the media box is the full card width and
       an illustration at 1200x900 would be a 790px-tall picture above two
       sentences. The cap is on the image rather than the box: the box takes
       its height from what is inside it now, so a limit on the box would be a
       limit the image could overflow. */
    .solution-media { aspect-ratio: auto; }
    /* `auto`, not the 100% the base rule uses. With the box taking its height
       from the image and the image taking its height from the box, neither had
       a number until the file arrived and the card was zero pixels tall until
       then. `auto` plus the width and height on the tag lets the ratio reserve
       the space up front, and max-height still caps it. */
    .solution-media img { height: auto; max-height: 300px; }
    /* Screenshots get a taller cap. Held to 300 they come out around 450
       wide, which is where a screenshot of a table stops being readable —
       the whole reason these cards are treated differently at all. */
    .solution-shot .solution-media img { max-height: 460px; }
}

/* --------------------------------------------------------------- Contact */
.panel {
    background: white;
    border-radius: 10px;
    padding: var(--card-pad);
    box-shadow: var(--panel-shadow);
    border: var(--panel-border);
    scroll-margin-top: 84px;
}

.contact-grid {
    display: grid;
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    gap: 34px;
}

@media (max-width: 760px) {
    .contact-grid { grid-template-columns: 1fr; gap: 24px; }
}

.contact-aside h2 {
    font-size: 22px;
    font-weight: 700;
    color: var(--ink);
    letter-spacing: -0.01em;
}

.contact-aside p {
    margin-top: 10px;
    color: var(--text-muted);
    font-size: 15px;
    /* Its own measure, like every other block of prose on the page. This
       column grows with the wrapper, so on a 4K monitor it was the one
       paragraph still filling the width — 108 characters a line where the
       rest of the page stays near 70. */
    max-width: 64ch;
}

.contact-email {
    display: inline-block;
    margin-top: 16px;
    font-size: 16px;
    font-weight: 600;
    color: var(--brand-darker);
    text-decoration: none;
    padding: 8px 12px;
    margin-left: -12px;
    border-radius: 6px;
    transition: background-color 0.15s;
}
.contact-email:hover { background: var(--brand-tint); }

/* ----------------------------------------------------------------- Forms */
.form-group {
    display: flex;
    flex-direction: column;
    margin-bottom: 16px;
}

.form-label {
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 6px;
}

.form-input,
.form-textarea {
    padding: 10px 12px;
    border: 1px solid #b6dedd;
    box-shadow: 0 0 0 3px rgba(5, 164, 164, 0.07);
    border-radius: 6px;
    font-size: 14px;
    font-family: inherit;
    color: var(--text);
    background: white;
    transition: border-color 0.15s, box-shadow 0.15s;
}

.form-textarea {
    min-height: 120px;
    resize: vertical;
}

.form-input:focus,
.form-textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-glow);
}

/* Marks the field the server rejected, so a message naming "email" has
   something on screen to point at. */
.form-input.is-invalid,
.form-textarea.is-invalid {
    border-color: var(--danger-border);
    box-shadow: 0 0 0 3px rgba(214, 77, 66, 0.12);
}

.field-error {
    font-size: 12px;
    color: var(--danger-text);
    margin-top: 6px;
}

/* The honeypot. Off-screen rather than display:none — some bots skip hidden
   inputs but fill positioned ones, and a real user never reaches it because
   it is out of the tab order and told not to autofill. */
.hp-field {
    position: absolute;
    left: -9999px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.msg {
    padding: 14px 16px;
    border-radius: 6px;
    margin-bottom: 20px;
    font-size: 14px;
}

.msg-success {
    background: var(--success-bg);
    border-left: 4px solid var(--success-border);
    color: var(--success-text);
}

.msg-error {
    background: var(--danger-bg);
    border-left: 4px solid var(--danger-border);
    color: var(--danger-text);
}

/* ------------------------------------------------------------------ Blog */
/* Narrower than the rest of the site. Everything else on the page is panels
   and screenshots, which want the room; running text does not — past about 75
   characters the eye loses the start of the next line. */
.wrap-narrow { max-width: 740px; }

.post-index {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* The whole card is the link, not the title inside it. A title-only target in
   a list of three-line entries is a small thing to hit and an odd thing to
   miss. */
.post-index a {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 20px var(--card-pad);
    background: white;
    border-radius: 10px;
    box-shadow: var(--panel-shadow);
    border: var(--panel-border);
    text-decoration: none;
    color: inherit;
    transition: box-shadow 0.15s, transform 0.15s;
}
.post-index a:hover {
    box-shadow: 0 6px 20px rgba(20, 40, 45, 0.10);
    transform: translateY(-1px);
}

.post-index time,
.post-meta {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brand-tint-text);
}

.post-index strong {
    font-size: 19px;
    font-weight: 650;
    line-height: 1.35;
    color: var(--ink);
    letter-spacing: -0.01em;
}
.post-index span { color: var(--text-muted); }

.post h1 {
    font-size: 32px;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    color: var(--ink);
    text-wrap: balance;
}

.post-meta { margin: 10px 0 26px; }

.post h2 {
    font-size: 21px;
    font-weight: 650;
    line-height: 1.3;
    letter-spacing: -0.01em;
    color: var(--ink);
    margin: 34px 0 12px;
}

.post p { margin-bottom: 16px; }

.post-list {
    margin: 0 0 18px 20px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}

/* Log lines and regulatory output, which are read one line at a time and must
   not be re-wrapped — a folded frequency range is a different range. The block
   scrolls inside itself so a long line never takes the page sideways. */
.post-code {
    background: var(--ink);
    color: #d8ecef;
    border-radius: 8px;
    padding: 16px 18px;
    margin-bottom: 20px;
    overflow-x: auto;
    font-size: 13px;
    line-height: 1.65;
    -webkit-overflow-scrolling: touch;
}
.post-code code {
    font-family: var(--mono);
    white-space: pre;
}

.post-back {
    display: inline-block;
    margin-bottom: 22px;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
}
.post-back:hover { text-decoration: underline; }

.post-foot {
    margin-top: 34px;
    padding-top: 20px;
    border-top: 1px solid rgba(5, 164, 164, 0.14);
    color: var(--text-muted);
}

@media (max-width: 560px) {
    .post h1 { font-size: 26px; }
    .post h2 { font-size: 19px; }
    .post-index a { padding: 16px 18px; }
    .post-index strong { font-size: 17px; }
}

/* --------------------------------------------------------- Mailing list */
/* A band above the footer rather than a panel in the page. It is on every
   page, including the blog, and a card would make it look like one more thing
   to read; a band reads as the end of the page. */
.mailing {
    padding: 34px 0;
    border-top: 1px solid rgba(5, 164, 164, 0.14);
    background: linear-gradient(180deg, rgba(224, 245, 245, 0) 0%, rgba(224, 245, 245, 0.5) 100%);
}

.mailing-inner {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: 28px;
    align-items: center;
}

.mailing-copy h2 {
    font-size: 21px;
    font-weight: 650;
    letter-spacing: -0.01em;
    color: var(--ink);
    margin-bottom: 6px;
}
.mailing-copy p {
    font-size: 14.5px;
    color: var(--text-muted);
    max-width: 46ch;
}

/* The field and its button on one line, the field taking whatever is left.
   `min-width: 0` because a flex item with an intrinsic minimum — which an
   input has — otherwise refuses to shrink and pushes the button out. */
.mailing-row {
    display: flex;
    gap: 10px;
}
.mailing-row .form-input {
    flex: 1 1 auto;
    min-width: 0;
}
.mailing-row .btn { flex: 0 0 auto; }

.mailing-note,
.mailing-msg {
    margin-top: 9px;
    font-size: 13px;
}
.mailing-note { color: var(--text-muted); }
/* Stated in words as well as colour: role="status" carries it to a screen
   reader, and colour alone carries it to nobody who cannot see it. */
.mailing-msg-success { color: var(--success-text); font-weight: 600; }
.mailing-msg-error   { color: var(--danger-text);  font-weight: 600; }

@media (max-width: 760px) {
    .mailing-inner { grid-template-columns: 1fr; gap: 16px; }
    /* Stacked, because a 44px button beside a field in a 320px column leaves
       the field about six characters wide. */
    .mailing-row { flex-direction: column; }
    .mailing-row .btn { width: 100%; }
}

/* ---------------------------------------------------------------- Footer */
.footer {
    margin-top: 50px;
    background: var(--ink);
    color: rgba(255, 255, 255, 0.75);
    padding: 30px 0;
    font-size: 13.5px;
}

/* One line now, centred. It was space-between with the copyright on the left
   and the city and email on the right; with the second half gone,
   space-between would leave the survivor hard against the left edge, which
   reads as a layout that lost something rather than one line on purpose. */
.footer-inner {
    max-width: var(--wrap-max);
    margin: 0 auto;
    padding: 0 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    flex-wrap: wrap;
    text-align: center;
}

.footer a {
    color: rgba(255, 255, 255, 0.9);
    text-decoration: none;
}
.footer a:hover { text-decoration: underline; }

/* --------------------------------------------------- Hero additions */
/* The audience line sits above the headline rather than below it: "who this
   is for" is the filter a visitor applies before they read anything else, and
   below the headline it would be read after the pitch instead of before. */
.hero p.hero-eyebrow {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--brand-tint-text);
    margin-bottom: 14px;
}

.hero-lead { margin-top: 18px; }

/* ------------------------------------------------------- Platform */
/* The flagship section is set on a tinted band so it reads as the centre of
   the page rather than the first of six equal blocks. */
.section-platform {
    background: linear-gradient(180deg, rgba(224, 245, 245, 0.55) 0%, rgba(224, 245, 245, 0) 100%);
    border-top: 1px solid rgba(5, 164, 164, 0.10);
}

.section-platform .section-head .solution-eyebrow { margin-bottom: 12px; }

/* ------------------------------------------------- Who we are, in the aside */
/* This was a section of its own — panel, heading, three paragraphs — and it is
   now two sentences under "Have a query?". The rules it needs are smaller than
   the ones it had: a note beside a form is set below the invitation above it,
   not at the size of body copy in a section that has the page to itself. */
.contact-who {
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid #eef3f4;
}

/* Beats `.contact-aside p` (0,1,1) on specificity, which is what sets the
   size for the two paragraphs above this one. Same colour, a step down in
   size: this is background about the sender, not the offer. */
.contact-aside .contact-who p {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 14px;
    line-height: 1.65;
}
.contact-aside .contact-who p:last-child { margin-bottom: 0; }

/* ====================================================================
   Small screens
   ==================================================================== */

/* Tablets and large phones. The two-column grids have already collapsed at
   760px above; this is about the space around them. */
@media (max-width: 620px) {
    /* Narrower gutter inside every card, in one place — see --card-pad. */
    :root { --card-pad: 18px; }

    .wrap { padding: 0 16px; }

    .hero { padding: 26px 0 30px; }
    .hero h1 { max-width: none; }
    .hero-actions { gap: 10px; }
    /* Full-width buttons: at this size two side by side leave each too narrow
       to read comfortably, and a stacked pair is the usual phone pattern. */
    .hero-actions .btn { flex: 1 1 100%; text-align: center; }

    .section { padding: 18px 0 32px; }
    .section-head { margin-bottom: 18px; }
    .section-head h2 { font-size: 22px; }

    .solution-body h3 { font-size: 19px; }

    .footer { padding: 24px 0; }
    .footer-inner { flex-direction: column; align-items: flex-start; gap: 8px; }
}

/* Small phones. 360px is the narrowest width worth designing for; below it
   the layout still holds, it just gets tight. */
@media (max-width: 400px) {
    :root { --card-pad: 14px; }

    .wrap { padding: 0 13px; }
    .topbar-inner { padding: 10px 13px; }
    /* A little smaller here, where the topbar is already two rows. Was two
       rules deep — a 26px immediately overridden by a 19px in the same
       block. */
    .brand img { height: 24px; }
    .hero { padding: 20px 0 26px; }
}

/* Touch pointers get bigger targets than a mouse needs. The hit area is grown
   with a transparent pseudo-element rather than padding: box-sizing is
   border-box here, so padding eats the painted dot instead of surrounding it
   (9px of padding on a 9px dot left 6px of colour), and the `background`
   shorthand in the rules above resets background-clip anyway — which left the
   selected dot painting its whole 24px box while its siblings nearly
   vanished. A pseudo-element extends what a thumb can land on and touches
   none of that. */
@media (pointer: coarse) {
    .nav a { padding-top: 9px; padding-bottom: 9px; }
}

/* Wide displays used to widen this one section's column to 1320px, on the
   grounds that the screenshots benefit from room. They do not: the files are
   2080px wide and the column shows them at 1050, which is exactly 2x. Going
   to 1272 displays the same pixels at 1.63x — bigger, not sharper — and it
   cost the section its alignment, putting "Cloud Network Management" 110px
   left of every other heading on the page above 1500px. One column width. */

@media (max-height: 460px) and (orientation: landscape) {
    .hero { padding: 28px 0 24px; }
}
/* ----------------------------------------------------------- The terminal */
/* Trogon's specifications, dressed as the login banner on the slide above.
   The colours are lifted from trogon-login.svg deliberately — the picture and
   this panel are meant to read as the same machine.

   Everything here is presentation. The content inside is a pair of definition
   lists, so the specifications survive a screen reader, a search engine and a
   copy-paste, none of which can see text drawn into an SVG. That is the whole
   reason this is not simply a second image. */
.term {
    background: linear-gradient(160deg, #123036 0%, #0d252a 100%);
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 18px 40px rgba(3, 60, 60, 0.18);
}

.term-bar {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 13px 18px;
    background: #1a3f47;
}

.term-dot {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: #3d6d75;
    flex: 0 0 auto;
}

.term-name {
    flex: 1;
    text-align: center;
    font-family: var(--mono);
    font-size: 13px;
    color: #7fa8ae;
    /* The three dots are on the left, so centring the name in the row puts it
       off-centre in the window. This pushes it back by their width. */
    margin-right: 60px;
}

.term-body {
    padding: 26px 22px 22px;
    font-family: var(--mono);
    font-size: 14px;
    line-height: 1.5;
}

.term-cmd {
    color: #e6f2f2;
    font-weight: 600;
    margin: 0 0 14px;
}
.term-cmd span { color: #5fb0d6; }

/* Not the first: the second and later command lines get air above them, so the
   panel reads as two commands rather than one long block. */
.term-rows + .term-cmd { margin-top: 26px; }

.term-rows {
    display: grid;
    gap: 7px 18px;
    margin: 0 0 4px;
}
.term-rows div {
    display: grid;
    grid-template-columns: 9.5rem 1fr;
    gap: 4px 14px;
    align-items: baseline;
}
/* The feature names are longer than the spec labels, so their column is too —
   rather than sizing both to the widest and leaving the specs adrift. */
.term-rows-wide div { grid-template-columns: 11.5rem 1fr; }

.term-rows dt {
    color: #44bdaa;
    white-space: nowrap;
}

/* The feature names are shared with the chips on the slide, where they are
   sentence case. A terminal wants them lowercase, so it is done here rather
   than in the data — the markup keeps EasyMesh and Spectral Scanner, which is
   what a screen reader announces and what a search engine indexes. */
.term-rows-lower dt { text-transform: lowercase; }
.term-rows dd {
    margin: 0;
    color: #a9c4c7;
}

.term-prompt { margin: 24px 0 0; }

.term-caret {
    display: inline-block;
    width: 8px;
    height: 1.05em;
    background: #e6f2f2;
    vertical-align: text-bottom;
    animation: term-blink 1.15s steps(2, start) infinite;
}

@keyframes term-blink {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0; }
}

/* The one piece of motion in the section, and it is decoration. */
@media (prefers-reduced-motion: reduce) {
    .term-caret { animation: none; }
}

/* On a phone a 9.5rem label column leaves nothing for the value, so the pair
   stacks: name, then what it does underneath it. */
@media (max-width: 620px) {
    .term-body { padding: 20px 16px 18px; font-size: 13px; }
    .term-rows div,
    .term-rows-wide div { grid-template-columns: 1fr; gap: 1px; }
    .term-rows dd { padding-bottom: 6px; }
    .term-name { margin-right: 40px; font-size: 12px; }
}
