/* ---------------------------------------------------------------------------
   chrome.css — the shell both halves of the site wear.

   The field and the wall were built at different times against different
   palettes: home.css on near-black with the red field accent, style.css on a
   slate-blue template with a blue/purple gradient title and blue buttons. They
   were one site with two designs, and the seam showed the moment anyone
   followed the link between them.

   Everything shared now lives here — tokens, the reset, the bracket mark, the
   bottom nav, the phone menu, and the panel sheet. home.css keeps the field.
   style.css keeps the wall. Neither owns a colour any more, so the two cannot
   drift apart again by editing only one of them.

   Load order is `fonts.css`, `chrome.css`, then the page's own stylesheet.
   --------------------------------------------------------------------------- */

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

:root {
    --bg: #111111;
    --text: #ffffff;
    --muted: rgba(255, 255, 255, 0.72);
    --quiet: rgba(255, 255, 255, 0.52);
    --line: rgba(255, 255, 255, 0.18);
    --panel: rgba(17, 17, 17, 0.78);

    /* One accent, sourced from the field. The canvas draws every entry point,
       ripple and halo with rgb(255 64 64) (see home-atmospheres.js); the chrome
       used to use a blue that appeared nowhere else on screen.

       --accent-rgb is rewritten each frame by home-experience.js with the live
       atmosphere accent, so panel and nav chrome shifts colour with whichever
       project is under the cursor. The literal below is the neutral fallback,
       and is what renders if JS never runs — which is also what the wall uses
       for its whole life, since nothing there repaints it. */
    --accent-rgb: 255, 64, 64;
    --accent: rgb(var(--accent-rgb));
    --accent-soft: rgba(var(--accent-rgb), 0.32);
    --accent-glow: rgba(var(--accent-rgb), 0.15);
    --accent-dim: rgba(var(--accent-rgb), 0.10);

    /* Two accents, deliberately.
       The atmosphere accents are tuned to be marks on a dark canvas, not text.
       Once the chrome started following the live accent it began painting real
       glyphs, and the archive accent (190,48,48) reaches only 3.35:1 against
       its own background — below the 4.5:1 WCAG asks for text at the eyebrow's
       ~11px. So --accent-rgb stays true for decoration (rings, borders, glows,
       focus outlines) and --accent-text-rgb is lifted toward white only as far
       as the threshold requires. Both are rewritten each frame by
       publishAccentToCss(); the literals here are the neutral fallback, which
       already passes and so is identical. */
    --accent-text-rgb: 255, 64, 64;
    --accent-text: rgb(var(--accent-text-rgb));
}

html,
body {
    min-height: 100%;
}

body {
    background: var(--bg);
    color: var(--text);
    /* Fallback inline, not only in fonts.css: if that file fails to load the
       custom property is undefined and font-family would resolve to the
       browser default serif rather than to the intended stack. */
    font-family: var(--font-sans, "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
}

button,
a {
    font: inherit;
}

button {
    color: inherit;
}

[hidden] {
    display: none !important;
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
}

.visually-hidden {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/* The entrance.
   `from` is explicit, and every caller uses `animation-fill-mode: both`,
   because the obvious way to write this is a trap. A `to`-only keyframe takes
   its start state from the element's own rule, so each caller has to declare
   `opacity: 0` on itself — which means the text is invisible until the
   animation runs, and the animation lives in a *different stylesheet* from the
   copy it reveals. Miss chrome.css and the page renders with nothing on it.

   Declaring the start state here instead makes the animation responsible for
   animating and nothing else. If this file never arrives, the wall is
   unstyled, but it is *there*. `both` is what keeps the entrance flicker-free:
   it applies this `from` during animation-delay, which is the job the base
   `opacity: 0` used to do. */
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ---------------------------------------------------------------------------
   The bracket mark.

   The field draws thousands of [ ] glyphs; every link on the site borrows the
   same mark instead of a generic underline, so the chrome and the canvas share
   one vocabulary. Two variants:

   - Absolutely positioned, for standalone controls (nav, buttons). They swing
     outward on hover without touching layout.
   - In flow, for links inside running copy. Mid-sentence an absolute bracket
     swings out over the preceding word; in flow it reserves its own space and
     only the transform moves, so nothing reflows on hover.
   --------------------------------------------------------------------------- */

.nav-link,
.text-action {
    background: none;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    display: inline-block;
    padding: 0 2px;
    pointer-events: auto;
    position: relative;
    text-decoration: none;
    transition: color 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-link::before,
.nav-link::after,
.text-action::before,
.text-action::after {
    color: var(--accent-text);
    opacity: 0.25;
    pointer-events: none;
    position: absolute;
    top: 50%;
    transition:
        opacity 0.28s cubic-bezier(0.22, 1, 0.36, 1),
        transform 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.nav-link::before,
.text-action::before {
    content: "[";
    left: -2px;
    transform: translate(4px, -50%);
}

.nav-link::after,
.text-action::after {
    content: "]";
    right: -2px;
    transform: translate(-4px, -50%);
}

.nav-link {
    font-size: 0.8rem;
}

.nav-link:hover,
.nav-link:focus-visible,
.text-action:hover,
.text-action:focus-visible {
    color: var(--text);
}

.nav-link:hover::before,
.nav-link:focus-visible::before,
.text-action:hover::before,
.text-action:focus-visible::before {
    opacity: 1;
    transform: translate(-8px, -50%);
}

.nav-link:hover::after,
.nav-link:focus-visible::after,
.text-action:hover::after,
.text-action:focus-visible::after {
    opacity: 1;
    transform: translate(8px, -50%);
}

/* The brackets travel 8px outward on hover, so the outline needs room or it
   clips them. */
.nav-link:focus-visible,
.text-action:focus-visible {
    outline-offset: 8px;
}

/* In-flow brackets. `.bracket-link` is the name to reach for in new markup;
   the rest are the selectors the field already ships and are kept so index.php
   does not need a class on every prose link. */
.bracket-link,
.landing-content p a,
.project-panel p a,
.panel-summary a,
.canvas-fallback p a {
    color: var(--text);
    pointer-events: auto;
    text-decoration: none;
    transition: color 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}

.bracket-link::before,
.bracket-link::after,
.landing-content p a::before,
.landing-content p a::after,
.project-panel p a::before,
.project-panel p a::after,
.panel-summary a::before,
.panel-summary a::after,
.canvas-fallback p a::before,
.canvas-fallback p a::after {
    color: var(--accent-text);
    display: inline-block;
    opacity: 0.45;
    transition:
        opacity 0.28s cubic-bezier(0.22, 1, 0.36, 1),
        transform 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.bracket-link::before,
.landing-content p a::before,
.project-panel p a::before,
.panel-summary a::before,
.canvas-fallback p a::before {
    content: "[";
    transform: translateX(2px);
}

.bracket-link::after,
.landing-content p a::after,
.project-panel p a::after,
.panel-summary a::after,
.canvas-fallback p a::after {
    content: "]";
    transform: translateX(-2px);
}

.bracket-link:hover,
.bracket-link:focus-visible,
.landing-content p a:hover,
.landing-content p a:focus-visible,
.project-panel p a:hover,
.project-panel p a:focus-visible,
.panel-summary a:hover,
.panel-summary a:focus-visible,
.canvas-fallback p a:hover,
.canvas-fallback p a:focus-visible {
    color: var(--accent-text);
}

.bracket-link:hover::before,
.bracket-link:hover::after,
.bracket-link:focus-visible::before,
.bracket-link:focus-visible::after,
.landing-content p a:hover::before,
.landing-content p a:hover::after,
.landing-content p a:focus-visible::before,
.landing-content p a:focus-visible::after,
.project-panel p a:hover::before,
.project-panel p a:hover::after,
.project-panel p a:focus-visible::before,
.project-panel p a:focus-visible::after,
.panel-summary a:hover::before,
.panel-summary a:hover::after,
.panel-summary a:focus-visible::before,
.panel-summary a:focus-visible::after,
.canvas-fallback p a:hover::before,
.canvas-fallback p a:hover::after,
.canvas-fallback p a:focus-visible::before,
.canvas-fallback p a:focus-visible::after {
    opacity: 1;
    transform: translateX(0);
}

/* ---------------------------------------------------------------------------
   The bottom nav.
   --------------------------------------------------------------------------- */

.bottom-nav {
    align-items: center;
    bottom: 0;
    display: flex;
    justify-content: space-between;
    left: 0;
    padding: 12px calc(16px + env(safe-area-inset-right, 0px))
             calc(12px + env(safe-area-inset-bottom, 0px))
             calc(16px + env(safe-area-inset-left, 0px));
    pointer-events: none;
    position: absolute;
    right: 0;
    z-index: 20;
}

.bottom-nav-left,
.bottom-nav-right {
    align-items: center;
    display: flex;
    gap: 14px;
}

.bottom-nav-right {
    gap: 20px;
}

.nav-icon-box {
    align-items: center;
    border: 1.5px solid rgba(255, 255, 255, 0.5);
    border-radius: 3px;
    display: flex;
    font-size: 10px;
    font-weight: 700;
    height: 24px;
    justify-content: center;
    letter-spacing: 0;
    width: 24px;
}

/* These are hidden once `html.js` swaps in the menu, but without JS they are
   the only route to the nav and have to stand on their own. Bare text measured
   15-18px tall; WCAG 2.5.8 asks 24. inline-flex, because min-height does
   nothing to an inline-block. */
.bottom-nav .nav-link {
    align-items: center;
    display: inline-flex;
    min-height: 24px;
}

/* ---------------------------------------------------------------------------
   Panels. A bottom sheet on a phone, a card on the right at desktop width.
   The field uses these for project details, the index and About; the wall uses
   them for About alone.
   --------------------------------------------------------------------------- */

.panel-shell {
    align-items: end;
    display: grid;
    inset: 0;
    justify-content: stretch;
    padding: 0;
    pointer-events: none;
    position: fixed;
    z-index: 50;
}

.panel-backdrop {
    background: rgba(0, 0, 0, 0.16);
    inset: 0;
    pointer-events: auto;
    position: fixed;
}

.project-panel {
    --panel-close-space: 42px;
    /* end, not center: the sheet has to meet the bottom edge. The old base was
       `center`, which beat the phone query's `align-items: end` and left the
       sheet floating with equal gaps above and below - a centred card with a
       flat bottom. Mobile-first removes the conflict rather than overriding it. */
    align-self: end;
    background: rgba(17, 17, 17, 0.88);
    border: 1px solid var(--line);
    border-radius: 8px 8px 0 0;
    box-shadow: 0 20px 80px rgba(0, 0, 0, 0.45);
    max-height: 78dvh;
    overflow: auto;
    /* Scrolling to the end of the eight-project list used to chain to the
       document underneath and rubber-band the whole page. */
    overscroll-behavior: contain;
    padding: 24px;
    padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
    pointer-events: auto;
    position: relative;
    width: 100%;
}

.project-panel h2 {
    font-size: 1.3rem;
    font-weight: 650;
    letter-spacing: 0;
    line-height: 1.2;
    margin: 7px 0 10px;
}

.project-panel p {
    color: var(--muted);
    font-size: 0.92rem;
    line-height: 1.55;
}

/* Panel prose had no spacing between paragraphs, so the About copy ran
   together as one block — the last line of one paragraph and the first of the
   next were a single line apart, indistinguishable from a wrap. Only shows up
   where a panel has more than one paragraph, which is why it survived: About
   is the only one that does, on either page. `p + p` rather than a bottom
   margin on every `p`, so the last paragraph does not push the links away. */
.project-panel p + p {
    margin-top: 12px;
}

.panel-summary {
    color: var(--text);
    margin-bottom: 14px;
}

.eyebrow {
    color: var(--accent-text);
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* Sticky, so it stays reachable while the sheet scrolls. That also means the
   body text scrolls *under* it: a float only reserves space for the lines
   beside it, and once the element detaches it reserves nothing.

   The class order matters here and is a trap. `.panel-close` and `.icon-button`
   have identical specificity, the button carries both, and `.icon-button` is
   declared with `background: transparent`. So the fill below was being thrown
   away and the control was fully transparent — only `backdrop-filter`
   survived, which frosts what is behind without hiding it, so scrolled text
   read straight through the glyph. The descendant selector wins the cascade
   without depending on source order. */
.project-panel .panel-close {
    backdrop-filter: blur(12px);
    background: rgba(17, 17, 17, 0.97);
    float: right;
    margin: -8px -8px 8px 12px;
    position: sticky;
    top: 0;
    z-index: 2;
}

.project-panel > .eyebrow,
.project-panel > h2 {
    padding-right: var(--panel-close-space);
}

.icon-button {
    align-items: center;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 4px;
    cursor: pointer;
    display: inline-flex;
    font-size: 1.25rem;
    height: 32px;
    justify-content: center;
    line-height: 1;
    width: 32px;
}

/* Nothing inside a panel may push it sideways.
   On a phone the sheet is a fixed-width box with `overflow: auto`, so a single
   unbroken token - a URL used as a link label, a long compound tag, a pasted
   path - does not wrap, it turns the sheet into a horizontally scrolling one.
   Measured at 320px: a 44-character link label overflowed the content box by
   43px and gave the sheet 42px of horizontal scroll.

   `anywhere` rather than `break-word`: only `anywhere` also shrinks the
   intrinsic min-content width, which is what actually lets a flex item like a
   pill sit inside its row instead of forcing the row wider. The property is
   inherited, so one declaration per container covers every descendant. */
.project-panel,
.mobile-menu-items {
    overflow-wrap: anywhere;
}

.panel-links {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 18px;
}

.panel-links a {
    color: var(--text);
    font-size: 0.86rem;
    /* Link labels come from the project data. A URL used as a label is the
       single worst offender for sideways overflow, so cap it here too. */
    max-width: 100%;
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* ---------------------------------------------------------------------------
   The bracket menu — phone only.

   The field draws thousands of [ ] glyphs and every link on the page wears a
   pair, so the menu is that same mark at full scale rather than three grey
   lines borrowed from somewhere else. Closed, a [ ] around three rules. Open,
   the two brackets travel apart to the screen edges and the items rise between
   them.

   Only translateX is animated. A bracket built from borders distorts under
   scale - the arms thin out as the spine stretches - so the brackets are drawn
   at final size and only ever slide.
   --------------------------------------------------------------------------- */

/* Hidden until `html.js` is set, so the flat links stay the route to the nav
   when the script never runs. The inline script in the head sets it before
   first paint, so there is no flash of both. */
.menu-toggle {
    align-items: center;
    background: none;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    display: none;
    height: 48px;
    justify-content: center;
    pointer-events: auto;
    position: relative;
    width: 48px;
}

.js .menu-toggle {
    display: inline-flex;
}

/* With the toggle present the flat links are duplicated inside the menu, so
   they come out of the bar rather than sitting there at 15px tall. */
.js .bottom-nav-left .nav-link,
.js .bottom-nav-right {
    display: none;
}

.menu-toggle::before,
.menu-toggle::after {
    color: var(--accent-text);
    font-size: 1.35rem;
    line-height: 1;
    opacity: 0.55;
    position: absolute;
    top: 50%;
    transition:
        opacity 0.28s cubic-bezier(0.22, 1, 0.36, 1),
        transform 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.menu-toggle::before {
    content: "[";
    transform: translate(0, -50%);
    left: 6px;
}

.menu-toggle::after {
    content: "]";
    transform: translate(0, -50%);
    right: 6px;
}

/* The small brackets hand off to the full-height pair as the menu opens. */
.menu-toggle[aria-expanded="true"]::before {
    opacity: 0;
    transform: translate(-10px, -50%);
}

.menu-toggle[aria-expanded="true"]::after {
    opacity: 0;
    transform: translate(10px, -50%);
}

.menu-toggle-mark {
    display: flex;
    flex-direction: column;
    gap: 3px;
    height: 13px;
    justify-content: space-between;
    width: 16px;
}

.menu-toggle-mark i {
    background: currentColor;
    display: block;
    height: 1.5px;
    transition:
        opacity 0.2s ease,
        transform 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
    width: 100%;
}

/* Three rules fold into a cross. */
.menu-toggle[aria-expanded="true"] .menu-toggle-mark i:nth-child(1) {
    transform: translateY(5.75px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle-mark i:nth-child(2) {
    opacity: 0;
}

.menu-toggle[aria-expanded="true"] .menu-toggle-mark i:nth-child(3) {
    transform: translateY(-5.75px) rotate(-45deg);
}

.mobile-menu {
    inset: 0;
    position: fixed;
    /* Above the nav (20), below the panels (50) — opening a panel from here
       closes the menu and hands over to the sheet. */
    z-index: 40;
}

.mobile-menu-backdrop {
    background: rgba(17, 17, 17, 0.9);
    inset: 0;
    opacity: 0;
    position: absolute;
    transition: opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.mobile-menu.is-open .mobile-menu-backdrop {
    opacity: 1;
}

.menu-bracket {
    border: 2px solid var(--accent);
    bottom: calc(28px + env(safe-area-inset-bottom, 0px));
    opacity: 0;
    position: absolute;
    top: calc(28px + env(safe-area-inset-top, 0px));
    transition:
        opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1),
        transform 0.52s cubic-bezier(0.22, 1, 0.36, 1);
    width: 26px;
}

.menu-bracket-start {
    border-right: 0;
    left: calc(16px + env(safe-area-inset-left, 0px));
    /* Parked just left of centre, so opening reads as the pair separating. */
    transform: translateX(calc(50vw - 58px));
}

.menu-bracket-end {
    border-left: 0;
    right: calc(16px + env(safe-area-inset-right, 0px));
    transform: translateX(calc(-50vw + 58px));
}

.mobile-menu.is-open .menu-bracket {
    opacity: 1;
    transform: translateX(0);
}

/* Sits inside the bracket frame, aligned to the same 58px gutter the items
   use, so the frame reads as one composition rather than a box with a button
   dropped on top of it. */
.menu-close {
    color: var(--muted);
    height: 48px;
    opacity: 0;
    position: absolute;
    right: calc(58px + env(safe-area-inset-right, 0px));
    top: calc(44px + env(safe-area-inset-top, 0px));
    transition:
        color 0.28s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1);
    width: 48px;
}

/* Behind the brackets, which take 0.52s to reach the edges — the way out
   should not arrive before the thing it closes has finished opening. */
.mobile-menu.is-open .menu-close {
    opacity: 1;
    transition-delay: 0.34s;
}

.menu-close:active,
.menu-close:focus-visible {
    border-color: var(--accent-soft);
    color: var(--text);
}

.mobile-menu-items {
    align-items: flex-start;
    display: flex;
    flex-direction: column;
    gap: 4px;
    inset: 0;
    justify-content: center;
    padding: 0 calc(58px + env(safe-area-inset-left, 0px));
    position: absolute;
}

.mobile-menu-items a,
.mobile-menu-items button {
    background: none;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    font-size: clamp(1.35rem, 6.5vw, 2rem);
    font-weight: 600;
    letter-spacing: 0;
    line-height: 1.15;
    /* The whole row is the target. This is the point of the menu: it removes
       the 15px tap targets rather than padding around them. */
    min-height: 64px;
    opacity: 0;
    padding: 4px 0;
    text-align: left;
    text-decoration: none;
    transform: translateY(14px);
    transition:
        color 0.28s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1),
        transform 0.46s cubic-bezier(0.22, 1, 0.36, 1);
    width: 100%;
}

.mobile-menu.is-open .mobile-menu-items a,
.mobile-menu.is-open .mobile-menu-items button {
    opacity: 1;
    transform: translateY(0);
}

/* Staggered behind the brackets, which take 0.52s to reach the edges. */
.mobile-menu.is-open .mobile-menu-items > :nth-child(1) { transition-delay: 0.16s; }
.mobile-menu.is-open .mobile-menu-items > :nth-child(2) { transition-delay: 0.23s; }
.mobile-menu.is-open .mobile-menu-items > :nth-child(3) { transition-delay: 0.30s; }

.mobile-menu-items a:active,
.mobile-menu-items button:active,
.mobile-menu-items a:focus-visible,
.mobile-menu-items button:focus-visible {
    color: var(--text);
}

/* ---------------------------------------------------------------------------
   Desktop. The links have room to stand on their own, so the toggle goes away
   and the panels return to a card on the right.
   --------------------------------------------------------------------------- */

@media (min-width: 700px) {
    .bottom-nav {
        padding: 16px calc(24px + env(safe-area-inset-right, 0px))
                 calc(16px + env(safe-area-inset-bottom, 0px))
                 calc(24px + env(safe-area-inset-left, 0px));
    }

    .nav-link {
        font-size: 0.9rem;
    }

    .js .menu-toggle {
        display: none;
    }

    /* inline-flex, not inline-block: the base rule uses it to give the link a
       min-height, and inline-block would silently drop that back to 18px. */
    .js .bottom-nav-left .nav-link {
        display: inline-flex;
    }

    .js .bottom-nav-right {
        display: flex;
    }

    /* The menu is a phone affordance. Above this width it can never open, but
       belt and braces in case a resize lands mid-session. */
    .mobile-menu {
        display: none;
    }

    .panel-shell {
        align-items: stretch;
        justify-content: end;
        padding: 20px;
    }

    .project-panel {
        align-self: center;
        border-radius: 8px;
        max-height: calc(100dvh - 40px);
        max-width: min(420px, calc(100vw - 40px));
        padding-bottom: 24px;
        width: 420px;
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        /* Zeroing duration does not zero delay, and the entrances are
           staggered up to 1s. Without this a visitor who asked for less motion
           watches an empty page for a second before the copy appears — the
           entrance is gone but the wait for it is not. */
        animation-delay: 0.001ms !important;
        scroll-behavior: auto !important;
        transition-duration: 0.001ms !important;
    }

    /* Zeroing duration does not zero delay. Without this the menu items would
       still wait out their 0.16-0.30s stagger and then pop in, which is the
       one thing a visitor asking for less motion would notice most. The close
       control matters most of all: a way out that arrives 0.34s late is a way
       out that is not there when it is looked for. */
    .mobile-menu.is-open .mobile-menu-items > *,
    .mobile-menu.is-open .menu-close {
        transition-delay: 0s !important;
    }

    /* Without the hover transition the brackets would pop between 0.25 and 1;
       hold them at full strength so links read as links at a glance. */
    .nav-link::before,
    .nav-link::after,
    .text-action::before,
    .text-action::after {
        opacity: 1;
        transform: translateY(-50%);
    }

    .nav-link::before,
    .text-action::before {
        left: -8px;
    }

    .nav-link::after,
    .text-action::after {
        right: -8px;
    }

    /* In-flow brackets: nothing to reposition, just hold them visible. */
    .bracket-link::before,
    .bracket-link::after,
    .landing-content p a::before,
    .landing-content p a::after,
    .project-panel p a::before,
    .project-panel p a::after,
    .panel-summary a::before,
    .panel-summary a::after,
    .canvas-fallback p a::before,
    .canvas-fallback p a::after {
        opacity: 1;
        transform: none;
    }
}

/* Coarse pointers get no hover state at all, so the brackets have to be legible
   at rest rather than on approach. */
@media (hover: none) {
    .nav-link::before,
    .nav-link::after,
    .text-action::before,
    .text-action::after {
        opacity: 0.6;
    }

    .bracket-link::before,
    .bracket-link::after,
    .landing-content p a::before,
    .landing-content p a::after,
    .project-panel p a::before,
    .project-panel p a::after,
    .panel-summary a::before,
    .panel-summary a::after,
    .canvas-fallback p a::before,
    .canvas-fallback p a::after {
        opacity: 0.8;
        transform: none;
    }
}
