/* ============================================================
   CARTOON FILTER LAYER
   A 1930s film grade + rubber-hose motion applied ON TOP of the
   editorial redesign. It deliberately does NOT replace the
   typography, the palette or the card language — it only warms
   the picture and makes things move like a hand-drawn cel.

   Remove this one <link> and the site is exactly as it was.
   ============================================================ */

.redesign {
    /* A nudge toward aged film stock, not a new palette. */
    --color-bg: #EFE8DA;
    --color-bg-alt: #E7DFCD;

    --cup-ink: #141414;
    --spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* html sits outside .redesign, so it can't read the override above —
   it needs the literal value or the two disagree during overscroll. */
html { background-color: #EFE8DA !important; }
body.redesign { background-color: var(--color-bg) !important; }

/* ------------------------------------------------------------
   1. The grade — grain, vignette, gate hair
   ------------------------------------------------------------ */

/* The redesign ships its own static grain; ours moves, so drop it. */
body.redesign::before { display: none !important; }

/* Film grain, repositioned 12x/sec by cuphead.js. Kept low —
   it should read as texture, never as noise. */
.cupGrain {
    position: fixed;
    inset: -50%;
    width: 200%;
    height: 200%;
    pointer-events: none;
    z-index: 9000;
    opacity: 0.13;
    mix-blend-mode: multiply;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
    will-change: transform;
}

/* Warm projector bulb: bright in the middle, falling off at the
   corners. Subtle enough that you feel it rather than see it. */
.cupVignette {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 9001;
    background:
        radial-gradient(ellipse 88% 78% at 50% 45%,
            rgba(176, 122, 56, 0.00) 45%,
            rgba(120, 80, 34, 0.10) 82%,
            rgba(70, 44, 16, 0.20) 100%);
}

/* A hair in the gate every few seconds. */
.cupScratch {
    position: fixed;
    top: 0;
    width: 1px;
    height: 100vh;
    pointer-events: none;
    z-index: 9002;
    background: linear-gradient(to bottom, transparent, rgba(255, 250, 238, 0.6) 35%, transparent 70%);
    opacity: 0;
}

/* ------------------------------------------------------------
   2. Motion — rubber hose, applied to what is already there
   ------------------------------------------------------------ */

/* The name already ships letterEnter + letterWave in styleRedesign.css.
   NEVER set `animation` on .ch here: the shorthand drops letterEnter,
   whose `forwards` fill is the only thing lifting the letters off
   opacity: 0 — the whole title vanishes. Redefining the keyframes by
   name is the safe way to give the idle a little more character. */
/* Rubber hose: each letter arrives tall-and-narrow, overshoots
   wide-and-short, then settles. That squash-and-stretch beat is what
   the plain fade was missing. */
@keyframes letterEnter {
    0% {
        opacity: 0;
        transform: translateY(0.55em) rotate(-12deg) scale(0.62, 1.42);
    }
    50% {
        opacity: 1;
        transform: translateY(-0.10em) rotate(4deg) scale(1.16, 0.86);
    }
    76% {
        transform: translateY(0.03em) rotate(-2deg) scale(0.95, 1.05);
    }
    100% {
        opacity: 1;
        transform: translateY(0) rotate(0deg) scale(1, 1);
    }
}

/* And the idle is a travelling wave rather than every letter bobbing
   in unison — the stagger already in the delays does the rest. */
@keyframes letterWave {
    0%, 100% { transform: translateY(0)       rotate(0deg)    scale(1, 1); }
    22%      { transform: translateY(-0.07em) rotate(-2deg)   scale(0.985, 1.025); }
    48%      { transform: translateY(0.015em) rotate(0.9deg)  scale(1.025, 0.98); }
    74%      { transform: translateY(-0.035em) rotate(-0.8deg) scale(1, 1); }
}

/* Pivot at the baseline, the way a drawn letter would sit on it. */
.redesign .diagonalTitle .ch {
    display: inline-block;
    transform-origin: 50% 88%;
    will-change: transform;
}

/* Squash and stretch. The whole rubber-hose vocabulary in two rules:
   wide-and-short on approach, tall-and-narrow on impact. */
.redesign .btnPrimary,
.redesign .btnSecondary,
.redesign .projectVisit {
    transition: transform .28s var(--spring),
                background-color .4s var(--ease-soft),
                border-color .4s var(--ease-soft) !important;
}

.redesign .btnPrimary:hover,
.redesign .btnSecondary:hover,
.redesign .projectVisit:hover {
    transform: translateY(-3px) scale(1.05, 0.95);
}

.redesign .btnPrimary:active,
.redesign .btnSecondary:active,
.redesign .projectVisit:active {
    transform: translateY(2px) scale(0.96, 1.04);
}

/* Cards settle with a little overshoot instead of a linear glide. */
.redesign .bggb {
    transition: transform .45s var(--spring),
                background-color .5s var(--ease-soft),
                border-color .5s var(--ease-soft) !important;
}

.redesign .bggb:hover {
    transform: translateY(-5px) rotate(-0.35deg);
}

/* The accordion icon already tilts on hover; give it a wobble in. */
@keyframes cupWiggle {
    0%   { transform: rotate(0deg); }
    35%  { transform: rotate(-12deg); }
    65%  { transform: rotate(5deg); }
    100% { transform: rotate(-8deg); }
}

html.cupMotion .redesign .accordion:hover img,
html.cupMotion .redesign .accordion.active img {
    animation: cupWiggle .5s var(--spring) both;
}

/* Tags pop rather than fade. */
.redesign .item {
    transition: transform .3s var(--spring),
                border-color .3s var(--ease-soft),
                color .3s var(--ease-soft) !important;
}

.redesign .item:hover {
    transform: translateY(-2px) rotate(-1.5deg);
}

/* ------------------------------------------------------------
   2b. The landing — where the cartoon actually shows up
   ------------------------------------------------------------ */

/* The name keeps its face and its colours — it just picks up the hard
   offset shadow that every inked cartoon title has. Two tones so the
   two lines read against each other. */
.redesign .diagonalTitle .firstName {
    text-shadow: 6px 6px 0 var(--color-accent);
}

.redesign .diagonalTitle .lastName {
    text-shadow: 6px 6px 0 var(--color-ink);
}

/* Iris-in on load — the shot opens from a dot, the way a cartoon
   starts. Purely additive: remove this layer and the original loader
   behaves exactly as before. */
@property --iris {
    syntax: '<percentage>';
    initial-value: 0%;
    inherits: false;
}

.cupIris {
    position: fixed;
    inset: 0;
    z-index: 9600;
    pointer-events: none;
    background-color: var(--cup-ink, #141414);
    --iris: 0%;
    -webkit-mask-image: radial-gradient(circle at 50% 48%,
        transparent 0 var(--iris), #000 calc(var(--iris) + 1.2%));
            mask-image: radial-gradient(circle at 50% 48%,
        transparent 0 var(--iris), #000 calc(var(--iris) + 1.2%));
    animation: cupIrisOpen 1.15s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

@keyframes cupIrisOpen {
    0%   { --iris: 0%;   opacity: 1; }
    85%  { --iris: 85%;  opacity: 1; }
    100% { --iris: 105%; opacity: 0; }
}

/* If @property is unsupported the mask never animates, so fade the
   whole thing out on its own track and never trap the page behind it. */
@supports not (background: paint(x)) {
    .cupIris { animation: cupIrisFade .8s ease forwards; }
}

@keyframes cupIrisFade {
    to { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .cupIris { display: none !important; }
}

/* ------------------------------------------------------------
   3. Line boil — only on the two biggest display elements
   Chrome drops any element whose filter: url() points at a missing
   id, so these are gated on the class cuphead.js adds once the
   <defs> are actually in the document.
   ------------------------------------------------------------ */

html.cupInk .redesign .diagonalTitle { filter: url(#cupBoil); }
html.cupInk .redesign .sectionHeading { filter: url(#cupBoilSoft); }

/* ------------------------------------------------------------
   4. Project cards — the existing card language, plus a film grade
      on the screenshot that clears when you hover.
   ------------------------------------------------------------ */

.redesign .projectsGrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 48px 40px;
    width: 100%;
    margin-top: 20px;
}

.redesign .projectCard {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 18px;
    padding: 26px;
    background-color: rgba(255, 253, 247, 0.55);
    border: 1px solid var(--color-line);
    border-radius: 24px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: transform .45s var(--spring),
                background-color .5s var(--ease-soft),
                border-color .5s var(--ease-soft);
}

.redesign .projectCard:hover {
    background-color: rgba(255, 253, 247, 0.85);
    border-color: var(--color-line-strong);
    transform: translateY(-6px) rotate(-0.3deg);
}

.redesign .projectShot {
    position: relative;
    aspect-ratio: 16 / 10;
    border-radius: 16px;
    overflow: hidden;
    background-color: var(--color-bg-alt);
    border: 1px solid var(--color-line);
}

/* Old stock while at rest; full colour on hover. */
.redesign .projectShot img {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
    display: block;
    opacity: 0;
    filter: sepia(0.45) saturate(0.75) contrast(1.06);
    transition: opacity .6s var(--ease-soft), filter .5s var(--ease-soft);
}

/* Only revealed once the file has actually decoded, so a failed or
   still-pending screenshot leaves the plate below visible instead of
   a broken-image icon. cuphead.js sets this. */
.redesign .projectShot img.is-loaded { opacity: 1; }

.redesign .projectCard:hover .projectShot img {
    filter: sepia(0.05) saturate(1) contrast(1);
}

/* The plate underneath: big initial on a soft diagonal weave. This is
   what shows while the screenshot service responds, and it stays put
   permanently if the service never does. */
.redesign .projectShot::after {
    content: attr(data-initial);
    position: absolute;
    inset: 0;
    z-index: 0;
    display: grid;
    place-items: center;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: clamp(56px, 8vw, 96px);
    color: var(--color-ink-muted);
    opacity: 0.5;
    background-image: repeating-linear-gradient(
        45deg,
        rgba(20, 20, 20, 0.035) 0 10px,
        transparent 10px 20px);
}

.redesign .projectHead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
}

.redesign .projectName {
    margin: 0;
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(2.4rem, 2.6vw, 3.2rem);
    letter-spacing: -0.025em;
    line-height: 1.05;
    color: var(--color-ink);
}

.redesign .projectHost {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .projectCopy {
    margin: 0;
    flex: 1;
    font-family: var(--font-sans);
    font-size: var(--t-md);
    line-height: 1.6;
    color: var(--color-ink-soft);
}

.redesign .projectTags {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.redesign .projectVisit {
    align-self: flex-start;
    font-family: var(--font-sans);
    font-weight: 500;
    font-size: var(--t-sm);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-ink);
    background: transparent;
    border: 1px solid var(--color-ink);
    border-radius: 100px;
    padding: 12px 22px;
    cursor: pointer;
}

.redesign .projectVisit:hover {
    background-color: var(--color-ink);
    color: #FFF;
}

.redesign .projectNo {
    position: absolute;
    top: 18px;
    right: 26px;
    z-index: 2;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.14em;
    color: var(--color-ink-muted);
}

/* ------------------------------------------------------------
   5. GitHub panels — same language again
   ------------------------------------------------------------ */

/* One panel per row. repeat(auto-fit, minmax(340px, 1fr)) laid out
   THREE tracks at 1440px — 3x340 + 2x40 fits inside the 1268px content
   box — so the measured grid was "613.6px 613.6px 0px": two panels and
   a collapsed phantom column. More importantly, two side-by-side cards
   left the 825px-wide year calendar 560px to live in. */
.redesign .ghGrid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 28px;
    width: 100%;
    margin-top: 20px;
}

/* Identity left, activity right. */
.redesign .ghPanel {
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr);
    gap: 28px 32px;
    align-items: start;
}

.redesign .ghIdentity {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
}

.redesign .ghBody {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

@media (max-width: 820px) {
    .redesign .ghPanel {
        grid-template-columns: 1fr;
        gap: 18px;
    }
}

.redesign .ghPanel {
    min-width: 0;
    padding: 28px 26px;
    background-color: rgba(255, 253, 247, 0.55);
    border: 1px solid var(--color-line);
    border-radius: 24px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: transform .45s var(--spring), border-color .5s var(--ease-soft);
}

.redesign .ghPanel:hover {
    border-color: var(--color-line-strong);
    transform: translateY(-4px);
}

.redesign .ghHead {
    display: flex;
    align-items: center;
    gap: 16px;
}

.redesign .ghAvatar {
    width: 52px;
    height: 52px;
    flex: none;
    border-radius: 50%;
    object-fit: cover;
    background-color: var(--color-bg-alt);
    border: 1px solid var(--color-line);
    filter: sepia(0.4) saturate(0.8);
}

.redesign .ghWho { min-width: 0; }

.redesign .ghKind {
    display: inline-block;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .ghHandle {
    margin-top: 2px;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: 2.4rem;
    line-height: 1.1;
    color: var(--color-ink);
    word-break: break-all;
}

.redesign .ghStats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.redesign .ghStat {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.02em;
    color: var(--color-ink-soft);
    border: 1px solid var(--color-line);
    border-radius: 100px;
    padding: 5px 14px;
}

.redesign .ghStat b { font-weight: 500; color: var(--color-ink); }

/* For the work account this is the only number that can reflect the
   private commits, so it gets more weight than the plain stats. */
.redesign .ghContrib {
    align-self: flex-start;
    margin-top: -8px;
    padding: 7px 16px;
    font-size: var(--t-xs);
    color: var(--color-ink);
    border-color: var(--color-line-strong);
}

.redesign .ghContrib b {
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: var(--t-xl);
    margin-right: 2px;
}

/* Declared once; the identical rule that used to sit here as well is
   in section 15 with the rest of the calendar. */
.redesign .ghChartWrap img {
    display: block;
    width: 100%;
    min-width: 460px;
    height: auto;
    filter: sepia(0.5) saturate(1.1) contrast(1.05);
}

.redesign .ghFeed {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-line);
    border-radius: 14px;
    overflow: hidden;
}

.redesign .ghEvent {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--color-line);
    transition: background-color .3s var(--ease-soft);
}

.redesign .ghEvent:last-child { border-bottom: none; }
.redesign .ghEvent:hover { background-color: rgba(255, 253, 247, 0.6); }

.redesign .ghDot {
    width: 7px;
    height: 7px;
    flex: none;
    margin-top: 7px;
    border-radius: 50%;
    background-color: var(--color-ink-muted);
}

.redesign .ghEvent[data-type="PushEvent"] .ghDot,
.redesign .ghEvent[data-type="PullRequestEvent"] .ghDot { background-color: var(--color-accent); }

.redesign .ghEventText {
    min-width: 0;
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    line-height: 1.45;
    color: var(--color-ink-soft);
    word-break: break-word;
}

.redesign .ghEventText b { font-weight: 500; color: var(--color-ink); }

.redesign .ghEventWhen {
    flex: none;
    margin-left: auto;
    padding-left: 10px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .ghNote {
    padding: 18px 16px;
    text-align: center;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    line-height: 1.5;
    color: var(--color-ink-muted);
}

/* ------------------------------------------------------------
   6. Cursor — four variants, picked with ?cursor= in the URL
      ring | blob | iris | ball        (see cuphead.js)
   ------------------------------------------------------------ */

/* Only while auditioning: hide the site's own elastic dot and take
   over the native pointer. With no ?cursor= in the URL none of this
   applies and cursorRedesign.js runs exactly as it always did. */
html.cupCursorLab .redesign .circle { display: none !important; }

html.cupCursorLab .redesign,
html.cupCursorLab .redesign * { cursor: none !important; }

.cupCur {
    position: fixed;
    top: 0;
    left: 0;
    pointer-events: none;
    z-index: 9500;
    transform: translate(-200px, -200px);
    will-change: transform;
}

.cupCur svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* --- 1. ring: a hand-inked outline, nothing filled --------- */
.cupCur[data-v="ring"] { width: 44px; height: 44px; }
.cupCur[data-v="ring"] .ringLine {
    fill: none;
    stroke: var(--cup-ink, #141414);
    stroke-width: 3;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: stroke-width .2s var(--spring);
}
.cupCur[data-v="ring"] .ringDot { fill: var(--cup-ink, #141414); }
.cupCur[data-v="ring"].is-hover .ringLine { stroke-width: 6; }

/* --- 2. blob: a drop of ink, heavy squash and stretch ------ */
.cupCur[data-v="blob"] {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background-color: var(--cup-ink, #141414);
    transition: background-color .2s linear;
}
.cupCur[data-v="blob"].is-hover { background-color: var(--color-accent, #B84A1F); }

.cupSmear {
    position: fixed;
    top: 0;
    left: 0;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background-color: var(--cup-ink, #141414);
    pointer-events: none;
    z-index: 9499;
    opacity: 0;
    transform: translate(-200px, -200px);
    will-change: transform, opacity;
}

/* --- 3. iris: two arcs closing like a camera shutter ------- */
.cupCur[data-v="iris"] { width: 52px; height: 52px; }
.cupCur[data-v="iris"] .irisArc {
    fill: none;
    stroke: var(--cup-ink, #141414);
    stroke-width: 4;
    stroke-linecap: round;
    transform-origin: 26px 26px;
    animation: cupIrisSpin 6s linear infinite;
    transition: stroke-width .2s var(--spring);
}
.cupCur[data-v="iris"] .irisArc:nth-of-type(2) { animation-direction: reverse; }
.cupCur[data-v="iris"] .irisDot { fill: var(--cup-ink, #141414); }
.cupCur[data-v="iris"].is-hover .irisArc { stroke-width: 7; }

@keyframes cupIrisSpin { to { transform: rotate(360deg); } }

/* --- 4. ball: the classic bouncing-ball exercise ----------- */
.cupCur[data-v="ball"] {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: var(--color-accent, #B84A1F);
    box-shadow: 0 0 0 3px var(--cup-ink, #141414);
    transition: background-color .2s linear;
}
.cupCur[data-v="ball"].is-hover { background-color: var(--cup-ink, #141414); }

.cupTrail {
    position: fixed;
    top: 0;
    left: 0;
    border-radius: 50%;
    background-color: var(--cup-ink, #141414);
    pointer-events: none;
    z-index: 9498;
    transform: translate(-200px, -200px);
    will-change: transform;
}

/* The little label that only appears when you are auditioning them. */
.cupCurLabel {
    position: fixed;
    left: 18px;
    bottom: 18px;
    z-index: 9700;
    padding: 9px 15px;
    border-radius: 100px;
    background-color: var(--cup-ink, #141414);
    color: #FFF6E2;
    font-family: var(--font-sans);
    font-size: 12px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    pointer-events: none;
}

@media (hover: none), (pointer: coarse) {
    .redesign, .redesign * { cursor: auto !important; }
    .cupCur, .cupSmear, .cupTrail, .cupCurLabel { display: none !important; }
}

/* ------------------------------------------------------------
   7. Responsive & motion safety
   ------------------------------------------------------------ */

@media (max-width: 820px) {
    .redesign .ghGrid { grid-template-columns: 1fr; }
    .cupGrain { opacity: 0.09; }
}

@media (prefers-reduced-motion: reduce) {
    .cupGrain { opacity: 0.06; }
    .cupScratch { display: none !important; }
    .redesign * { animation-duration: .01ms !important; }
}

/* ------------------------------------------------------------
   8. Real headings, language toggle, condensed skills
   These arrived with the accessibility pass: the page used to be
   built entirely from <div>s.
   ------------------------------------------------------------ */

/* h1/h2 now carry the classes the divs used to. Kill the UA
   margins and weight so nothing moves visually. */
/* .diagonalTitle is a decorative <div aria-hidden> now, not the page
   heading — the wordmark repeats the name in the <title> and gives a
   screen reader thirteen separate letter spans. The <h1> is the role
   line (.heroRole) on the home page, and .sectionHeading elsewhere. */
.redesign h1.heroRole,
.redesign h2.sectionHeading,
.redesign h1.sectionHeading,
.redesign h2.sectionHeadingSmall,
.redesign h3.roleName,
.redesign h3.projectName {
    margin: 0;
    font-weight: inherit;
}

.redesign h2.sectionHeading,
.redesign h1.sectionHeading {
    font-weight: 400;
}

/* Language toggle, parked in the menu bar before the hamburger. */
.redesign .langToggle {
    margin-left: auto;
    margin-right: 18px;
    padding: 7px 15px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.12em;
    color: var(--color-ink);
    background: transparent;
    border: 1px solid var(--color-line-strong);
    border-radius: 100px;
    cursor: pointer;
    transition: transform .28s var(--spring),
                background-color .3s var(--ease-soft),
                color .3s var(--ease-soft);
}

.redesign .langToggle:hover {
    background-color: var(--color-ink);
    color: #FFF;
    transform: scale(1.06, 0.94);
}

/* Skills, rendered from content.js into one container per layout. */
.redesign .skillsStack {
    display: flex;
    flex-direction: column;
    width: 100%;
    margin-top: 8px;
}

.redesign .skillRow {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
}

/* Role and year, above the case copy. */
.redesign .projectRole {
    margin: -6px 0 0;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

/* ------------------------------------------------------------
   9. Accessibility & semantics pass
   The nav used to be <button onclick>: not crawlable, no
   middle-click, no "copy link". These rules let the real anchors
   inherit the styling the buttons had.
   ------------------------------------------------------------ */

.redesign a.option,
.redesign a.btnPrimary,
.redesign a.contactLink {
    text-decoration: none;
}

/* NOT a.btnPrimary: that one already gets `color: white` from
   styleForms.css, and `color: inherit` here (0,1,1) outranks it
   (0,1,0), which painted the label in ink on an ink-filled pill —
   invisible. It only needs the underline removed. */
.redesign a.option,
.redesign a.contactLink {
    color: inherit;
}

/* .option was a block-level button; an inline anchor would collapse. */
.redesign a.option {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
}

.redesign a.btnPrimary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.redesign a.contactLink {
    background: none;
    border: none;
    outline: none;
}

/* Visible to screen readers, invisible on screen. Every form field
   now has one: a placeholder is not a label, it disappears the moment
   you start typing and it fails WCAG 3.3.2. */
.srOnly {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Netlify's honeypot: a real field that only a bot would fill in. */
.hpField {
    position: absolute !important;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ------------------------------------------------------------
   10. How I work — the process section
   ------------------------------------------------------------ */

.redesign .methodBlock {
    display: flex;
    flex-direction: column;
    gap: 44px;
    width: 100%;
    margin-top: 12px;
}

.redesign .methodIntro {
    margin: 0;
    max-width: 62ch;
    font-family: var(--font-sans);
    font-size: var(--t-form);
    line-height: 1.65;
    color: var(--color-ink-soft);
}

/* Two columns of three on a wide screen; the numbers carry the
   sequence, so wrapping never loses the order. */
.redesign .stepList {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 34px 44px;
    list-style: none;
    margin: 0;
    padding: 0;
    counter-reset: none;
}

.redesign .stepItem {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    padding-top: 20px;
    border-top: 1px solid var(--color-line);
}

.redesign .stepNo {
    flex: none;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: 2.6rem;
    line-height: 1;
    color: var(--color-accent);
}

.redesign .stepBody { min-width: 0; }

.redesign .stepPhase {
    display: block;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign h3.stepTitle {
    margin: 4px 0 8px;
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: 2.4rem;
    line-height: 1.1;
    color: var(--color-ink);
}

.redesign .stepCopy {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--t-md);
    line-height: 1.6;
    color: var(--color-ink-soft);
}

/* The three published commitments. Numbers, not adjectives — so the
   number is the loudest thing in each one. */
.redesign .pledgeRow {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 28px;
    padding: 32px 0 8px;
    border-top: 1px solid var(--color-line-strong);
}

.redesign .pledge {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.redesign .pledgeValue {
    font-family: var(--font-serif);
    font-style: italic;
    font-size: clamp(4.2rem, 5vw, 6rem);
    line-height: 0.9;
    color: var(--color-ink);
}

.redesign .pledgeUnit {
    font-family: var(--font-sans);
    font-style: normal;
    font-size: var(--t-sm);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-accent);
    margin-left: 8px;
}

.redesign .pledgeCopy {
    max-width: 30ch;
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    line-height: 1.5;
    color: var(--color-ink-muted);
}

@media (max-width: 820px) {
    .redesign .methodBlock { gap: 32px; }
    .redesign .stepList { gap: 26px; }
    .redesign .pledgeRow { padding-top: 24px; }
}

/* ------------------------------------------------------------
   11. The case view
   One dialog reused for every project. The card carries a short
   line; the full case, and the extra frames, live in here.
   ------------------------------------------------------------ */

.redesign .projectCard { cursor: pointer; }

.redesign .projectActions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 20px;
}

.redesign .projectOpen {
    font-family: var(--font-sans);
    font-weight: 500;
    font-size: var(--t-sm);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-ink);
    background: transparent;
    border: 1px solid var(--color-ink);
    border-radius: 100px;
    padding: 12px 22px;
    cursor: pointer;
    transition: transform .28s var(--spring),
                background-color .3s var(--ease-soft),
                color .3s var(--ease-soft);
}

.redesign .projectOpen:hover {
    background-color: var(--color-ink);
    color: #FFF;
    transform: translateY(-3px) scale(1.05, 0.95);
}

/* The secondary action once "Read the case" is the primary one. */
.redesign .projectVisit {
    border-color: var(--color-line-strong);
    color: var(--color-ink-muted);
}

.redesign .projectVisit:hover {
    background-color: transparent;
    color: var(--color-ink);
    border-color: var(--color-ink);
}

/* Lock the page behind the dialog. */
html.caseOpen, html.caseOpen body { overflow: hidden !important; }

.caseModal {
    position: fixed;
    inset: 0;
    z-index: 9800;
    display: flex;
    align-items: flex-start;
    justify-content: center;
}

/* Without this the dialog never actually goes away: a class selector
   outranks the browser's own [hidden] { display: none }, so the
   backdrop stayed on top of the page as an invisible click-blocker
   after the first close and nothing on the site could be clicked
   again. Any `display` set on an element that toggles `hidden` needs
   this counterpart. */
.caseModal[hidden] { display: none !important; }

.caseBackdrop {
    position: absolute;
    inset: 0;
    background-color: rgba(24, 20, 14, 0.55);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    animation: caseFade .25s ease both;
}

@keyframes caseFade { from { opacity: 0; } }

.redesign .casePanel {
    position: relative;
    width: min(920px, 100%);
    max-height: 100vh;
    margin: 0 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 56px clamp(24px, 5vw, 64px) 72px;
    background-color: var(--color-bg);
    border-left: 1px solid var(--color-line);
    border-right: 1px solid var(--color-line);
    animation: caseRise .35s var(--ease-soft) both;
}

@keyframes caseRise {
    from { opacity: 0; transform: translateY(24px); }
}

.redesign .caseClose {
    position: sticky;
    top: 0;
    float: right;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--color-line-strong);
    background-color: var(--color-bg);
    cursor: pointer;
    z-index: 2;
    transition: transform .28s var(--spring), background-color .3s var(--ease-soft);
}

/* Drawn rather than typed, so it stays crisp and needs no glyph. */
.redesign .caseClose::before,
.redesign .caseClose::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 17px;
    height: 1.5px;
    margin: -0.75px 0 0 -8.5px;
    background-color: var(--color-ink);
}

.redesign .caseClose::before { transform: rotate(45deg); }
.redesign .caseClose::after  { transform: rotate(-45deg); }

.redesign .caseClose:hover {
    background-color: var(--color-ink);
    transform: rotate(90deg);
}

.redesign .caseClose:hover::before,
.redesign .caseClose:hover::after { background-color: var(--color-bg); }

.redesign .caseHead { margin-bottom: 18px; }

.redesign .caseRole {
    display: block;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign h2.caseTitle {
    margin: 8px 0 6px;
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(4rem, 6vw, 6.4rem);
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--color-ink);
}

.redesign .caseHost {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-accent);
    text-decoration: none;
}

.redesign .caseHost:hover { text-decoration: underline; }

.redesign .caseTags {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 32px;
}

.redesign .caseText {
    max-width: 66ch;
    margin-bottom: 44px;
}

.redesign .caseCopy {
    margin: 0 0 18px;
    font-family: var(--font-sans);
    font-size: var(--t-form);
    line-height: 1.7;
    color: var(--color-ink-soft);
}

.redesign .caseCopy:last-child { margin-bottom: 0; }

.redesign .caseShots {
    display: flex;
    flex-direction: column;
    gap: 20px;
    margin-bottom: 44px;
}

.redesign .caseShotsLabel {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .caseShots img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--color-line);
    border-radius: 14px;
    background-color: var(--color-bg-alt);
}

.redesign a.caseVisit { align-self: flex-start; }

@media (max-width: 820px) {
    .redesign .casePanel {
        padding: 44px 22px 56px;
        border-left: none;
        border-right: none;
    }
    .redesign .caseText { margin-bottom: 32px; }
}

/* ------------------------------------------------------------
   12. The parked cursor
   cursorRedesign.js only positions .circle on the first mousemove,
   so until then it sits at 0,0 — a stray ring overlapping the
   wordmark on every fresh load. Hold it back until it has a real
   position to be at.
   ------------------------------------------------------------ */
.redesign .circle {
    opacity: 0;
    transition: opacity .25s var(--ease-soft);
}

html.cursorLive .redesign .circle { opacity: 1; }


/* ------------------------------------------------------------
   13. Primary button hover
   styleForms.css turns the label black on hover and sweeps a white
   blob (::before) across the button — both written for a version
   of this button that went light on hover. The redesign pins the
   fill to ink, so the label vanished and the blob sat on top of it.
   ------------------------------------------------------------ */

.redesign .btnPrimary::before,
.redesign .btnPrimary:hover::before {
    display: none !important;
}

.redesign .btnPrimary:hover span,
.redesign .btnPrimary:focus span,
.redesign .btnPrimary:focus-visible span {
    color: #FFF !important;
    transform: none !important;
}

/* ------------------------------------------------------------
   14. Live numbers and the activity strip in the hero
   The bottom third of the hero used to be empty paper. These are
   real figures off the same two requests the activity panels
   already make, so they cost nothing extra.
   ------------------------------------------------------------ */

.redesign .heroStats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 24px;
    margin-top: 22px;
}

.redesign .heroStat {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .heroStat b {
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: var(--t-2xl);
    letter-spacing: 0;
    text-transform: none;
    color: var(--color-ink);
    margin-right: 4px;
}

/* The strip under the signature. It used to share the wordmark's -9deg
   tilt so the two read as one mark; the cost was that a 7-row weekday
   grid rendered on a slant, which is unreadable as a grid, and the
   caption came out rotated too. Horizontal now: JS measures the title
   and sets left/top, because the title is absolutely positioned and its
   size is a clamp() that moves with the viewport. */
.redesign .heroActivity {
    position: absolute;
    z-index: 4;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    pointer-events: auto;
}

.redesign .heroActivity:empty { display: none; }

/* One GitHub-style contribution block per identity, stacked. */
.redesign .sparkRows {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.redesign .sparkRow {
    display: flex;
    align-items: center;
    gap: 12px;
}

/* Fixed width so both blocks start on the same x — otherwise "Company"
   and "Personal" push their grids out of alignment and the weeks stop
   lining up between blocks, which is the whole point of stacking them. */
.redesign .sparkWho {
    flex: none;
    width: 72px;
    text-align: right;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .heroCells {
    display: block;
    overflow: visible;
}

/* The whole block fades and lifts once, per identity. The versions
   before this animated every cell: at 7 rows that is ~180 elements per
   block and ~360 on the page, all animating to say nothing the fills do
   not already say. Two animated elements now. */
html.cupMotion .redesign .sparkRow {
    animation: blockIn .55s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes blockIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    html.cupMotion .redesign .sparkRow { animation: none !important; }
}

.redesign .heroActLink {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: color .3s var(--ease-soft), transform .28s var(--spring);
}

.redesign .heroActLink:hover {
    color: var(--color-accent);
    transform: translateX(3px);
}

/* No room for it beside a phone-width hero, and that hero is already
   a full column. */
@media (max-width: 900px) {
    .redesign .heroActivity { display: none !important; }
}

/* ------------------------------------------------------------
   15. The contribution calendar
   Drawn as SVG from the per-day counts rather than pulled as a
   tinted third-party image, so the ramp is the site's own accent
   and every square carries a native tooltip.
   ------------------------------------------------------------ */

.redesign .ghChartWrap {
    border: 1px solid var(--color-line);
    border-radius: 14px;
    padding: 14px 12px;
    overflow-x: auto;
}

/* The calendar keeps its natural size and the wrapper scrolls when it
   does not fit, which is what .ghChartWrap's overflow-x:auto was always
   for. max-width:100% overrode it and scaled the whole SVG instead —
   cells, month labels and weekday labels together — down to 65% on a
   desktop and 32% on a phone. A 9px label at 32% is 3px. */
.redesign .ghHeatmap {
    display: block;
    max-width: none;
    flex: none;
    height: auto;
}

.redesign .ghHeatmap .ghMonth,
.redesign .ghHeatmap .ghWeekday {
    font-family: var(--font-sans);
    font-size: var(--t-chart);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    fill: var(--color-ink-muted);
}

.redesign .ghLegend {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 10px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .ghSwatch {
    width: 11px;
    height: 11px;
    border-radius: 2.5px;
    display: inline-block;
}

.redesign .ghLegend span:first-child { margin-right: 3px; }
.redesign .ghLegend span:last-child  { margin-left: 3px; }

/* ------------------------------------------------------------
   16. The logo
   Back to the real monogram; the text wordmark that replaced it
   carried no meaning the hero was not already carrying.
   ------------------------------------------------------------ */
.redesign .menuLogo {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    transition: transform .3s var(--spring);
}

.redesign .menuLogo:hover { transform: rotate(-6deg) scale(1.05); }

.redesign .menu img {
    width: 46px;
    height: auto;
}

/* ============================================================
   17. Components introduced when the duplicated desktop/mobile
       markup was collapsed into one block per page.

   Everything below renders from Scripts/content.js through
   Scripts/render.js, so About and Qualifications now share the
   card language the home page already used instead of each
   page inventing its own.
   ============================================================ */

/* The menu trigger was a <div onclick> — unreachable by keyboard and
   invisible to assistive tech. It is a real <button> now, which brings
   the UA's own chrome with it, so that gets reset here rather than in
   styleMenu.css where the shape is defined. */
.hamb {
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    display: block;
}

.hamb:focus-visible {
    outline: 2px solid var(--color-accent, #B84A1F);
    outline-offset: 6px;
    border-radius: 4px;
}

/* ------------------------------------------------------------
   17a. The hero, now that the role line carries a subtitle
   ------------------------------------------------------------ */

/* Wraps .heroIntro on the home page. It exists so the mobile
   breakpoint has a single thing to neutralise, instead of the
   inline padding that used to be written twice. */
.redesign .heroIntroWrap {
    display: flex;
    width: 100%;
    justify-content: flex-end;
    padding: 12vh 40px 40px;
    box-sizing: border-box;
}

/* The inner-page hero (About, Qualifications).

   These inherited the home page's full-height stage — min-height:100vh
   with the content bottom-aligned — but they have no wordmark to fill
   it. The measured result was that the first real content started at
   y=593 of a 900px viewport on About: roughly two thirds of the
   opening screen was blank paper, and on a phone it was worse (y=634
   of 844). A recruiter's first scroll was spent on nothing.

   Shorter, and the content sits in the optical centre. */
.redesign .pageHero {
    min-height: 56vh;
    justify-content: center;
    padding: 124px 4vw 64px;
    box-sizing: border-box;
}

.redesign .pageHero .heroIntro {
    max-width: 620px;
}

@media (max-width: 768px) {
    .redesign .pageHero {
        min-height: 0;
        padding: 108px 6vw 48px;
    }
}

/* "Full-stack · QA automation · AI agents" — the disciplines line
   under the role. Small caps, wide tracking: it reads as a
   qualifier, not as a second headline. */
.redesign .heroDisciplines {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-ink-soft);
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: -8px;
}

/* The consultancy is a separate credential from the job title, so
   it gets its own line with a rule beside it. */
.redesign .heroFounder {
    font-family: var(--font-serif);
    font-style: italic;
    font-size: var(--t-lg);
    color: var(--color-accent);
    display: flex;
    align-items: center;
    gap: 12px;
}

.redesign .heroFounder::before {
    content: '';
    width: 22px;
    height: 1px;
    background: var(--color-accent);
    flex: none;
}

/* ------------------------------------------------------------
   17b. About
   ------------------------------------------------------------ */

/* Body paragraphs after the display-size opening line. */
.redesign .aboutPara {
    font-family: var(--font-sans);
    font-size: clamp(1.5rem, 1.25vw, 1.7rem);
    font-weight: 400;
    line-height: 1.7;
    color: var(--color-ink-soft);
    max-width: 68ch;
    margin: 0;
}

/* Three scannable facts between the bio and the hobbies, so the
   page answers "what is he doing now" without being read. */
.redesign .nowGrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 1px;
    background: var(--color-line);
    border: 1px solid var(--color-line);
    border-radius: 16px;
    overflow: hidden;
    width: 100%;
}

.redesign .nowItem {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 28px 26px;
    background: var(--color-bg);
}

.redesign .nowLabel {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.redesign .nowCopy {
    font-family: var(--font-serif);
    font-style: italic;
    font-size: var(--t-2xl);
    line-height: 1.25;
    color: var(--color-ink);
}

/* Inside a hobby accordion. */
.redesign .hobbyBody {
    display: flex;
    flex-direction: column;
    gap: 18px;
    width: 100%;
    box-sizing: border-box;
}

.redesign .hobbyBody .multiContent {
    flex-wrap: wrap;
    gap: 20px;
}

.redesign .hobbyBody .multiContent img {
    border-radius: 12px;
    height: auto;
}

/* ------------------------------------------------------------
   17c. Qualifications — roles, education, certificates
   ------------------------------------------------------------ */

/* The generic vertical stack inside an accordion panel. Replaces
   the .VerticalLayout.leftHorizontal.gap30 pile-up that the
   hand-written pages used. */
.redesign .stackRow {
    display: flex;
    flex-direction: column;
    gap: 18px;
    width: 100%;
    box-sizing: border-box;
}

.redesign .roleCard {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 26px 28px;
    border: 1px solid var(--color-line);
    border-radius: 16px;
    background: var(--color-bg);
    transition: border-color .35s var(--ease-soft), transform .35s var(--spring);
}

.redesign .roleCard:hover {
    border-color: var(--color-line-strong);
    transform: translateY(-2px);
}

/* A left rule marks the roles that are still running, so "present"
   is visible before anyone reads a date. */
.redesign .roleCard.is-current {
    border-left: 2px solid var(--color-accent);
}

.redesign .roleHead {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px 20px;
}

.redesign .roleName {
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 500;
    font-size: clamp(2.1rem, 2.4vw, 2.8rem);
    line-height: 1.15;
    letter-spacing: -0.01em;
    color: var(--color-ink);
    margin: 0;
}

.redesign .rolePeriod {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
    white-space: nowrap;
}

.redesign .roleCard.is-current .rolePeriod {
    color: var(--color-accent);
}

.redesign .roleOrg {
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    font-weight: 500;
    letter-spacing: 0.04em;
    color: var(--color-ink);
}

.redesign .roleCopy {
    font-family: var(--font-sans);
    font-size: var(--t-md);
    line-height: 1.65;
    color: var(--color-ink-soft);
    max-width: 72ch;
    margin: 0;
}

/* Certificates. The title is a <button> rather than a link because
   the target is a PDF download, not a page. */
.redesign .certCard {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    padding: 22px 26px;
    border: 1px solid var(--color-line);
    border-radius: 16px;
    background: var(--color-bg);
    transition: border-color .35s var(--ease-soft), transform .35s var(--spring);
}

.redesign .certCard:hover {
    border-color: var(--color-line-strong);
    transform: translateY(-2px);
}

/* ISTQB is the credential the whole positioning rests on, so it
   renders first and reads as the headline of the group. */
.redesign .certCard.is-key {
    border-color: var(--color-accent);
    background: linear-gradient(
        180deg,
        rgba(184, 74, 31, 0.05),
        rgba(184, 74, 31, 0) 70%
    );
}

.redesign .certTitle {
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 500;
    font-size: clamp(1.9rem, 2.2vw, 2.5rem);
    line-height: 1.2;
    text-align: left;
    color: var(--color-ink);
    background: none;
    border: 0;
    /* Measured 23px tall on a phone, against a 44px minimum. The
       padding is vertical only so the text stays flush left with the
       rest of the card. */
    padding: 10px 0;
    min-height: 44px;
    display: flex;
    align-items: center;
    cursor: pointer;
    transition: color .3s var(--ease-soft);
}

.redesign .certTitle:hover {
    color: var(--color-accent);
}

/* The download affordance, revealed on hover so the resting state
   stays typographic. */
.redesign .certTitle::after {
    content: ' \2193';
    opacity: 0;
    transition: opacity .3s var(--ease-soft);
}

.redesign .certCard:hover .certTitle::after,
.redesign .certTitle:focus-visible::after {
    opacity: 1;
}

.redesign .certOrg {
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    color: var(--color-ink);
}

.redesign .certYear {
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

/* ------------------------------------------------------------
   17d. Contact — one grid instead of two hand-written layouts
   ------------------------------------------------------------ */

/* align-items:end with no top padding pushed the taller right column
   up past the top of the viewport, so the GitHub link slid underneath
   the fixed menu bar and got clipped, while the bottom third of the
   screen sat empty. Centred, with enough top padding to clear the
   bar, and the height comes from the content. */
.redesign .contactGrid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 60px;
    align-items: center;
    width: 100%;
    min-height: 100vh;
    padding: 132px 4vw 80px;
    box-sizing: border-box;
}

.redesign .contactIntro {
    display: flex;
    flex-direction: column;
    gap: 20px;
    position: relative;
    z-index: 4;
}

.redesign .contactPanel {
    display: flex;
    flex-direction: column;
    gap: 34px;
    position: relative;
    z-index: 4;
}

/* Feedback on the submit button while the request is in flight —
   previously the form gave no sign it had been received. */
.redesign .btnPrimary[aria-busy="true"] {
    opacity: 0.6;
    pointer-events: none;
}

/* ------------------------------------------------------------
   17e. Responsive
   ------------------------------------------------------------ */

@media (max-width: 900px) {
    .redesign .contactGrid {
        grid-template-columns: 1fr;
        gap: 48px;
        align-items: start;
        min-height: 0;
        padding: 120px 6vw 80px;
    }
}

@media (max-width: 768px) {
    .redesign .heroIntroWrap {
        padding: 0;
        display: block;
    }
    .redesign .nowCopy {
        font-size: var(--t-xl);
    }
    .redesign .roleCard,
    .redesign .certCard {
        padding: 22px 20px;
    }
    .redesign .roleHead {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
    }
    .redesign .rolePeriod {
        white-space: normal;
    }
}

@media (max-width: 530px) {
    .redesign .heroFounder {
        font-size: var(--t-md);
    }
    .redesign .heroDisciplines {
        font-size: var(--t-xs);
        letter-spacing: 0.1em;
    }
    .redesign .nowItem {
        padding: 22px 20px;
    }
}

/* ============================================================
   18. Desktop navigation

   The four destinations used to be reachable only through the
   hamburger, at every width. Measured on a 1440px viewport, no
   nav link was visible until the overlay was opened — so three
   of the four pages were invisible to anyone who did not click
   an unlabelled icon.

   The links are cloned from the overlay by functions.js, so the
   labels, the current-page state and the i18n keys all still
   have exactly one source.
   ============================================================ */

.redesign .navInline {
    display: none;
}

.redesign .navInlineLink {
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-ink-soft);
    text-decoration: none;
    padding: 8px 2px;
    position: relative;
    white-space: nowrap;
    transition: color .3s var(--ease-soft);
}

/* The rule grows from the centre on hover, and stays put for the
   page you are on — so "where am I" and "where can I go" use the
   same visual language. */
.redesign .navInlineLink::after {
    content: '';
    position: absolute;
    left: 2px;
    right: 2px;
    bottom: 2px;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: center;
    transition: transform .32s var(--ease-soft);
}

.redesign .navInlineLink:hover {
    color: var(--color-ink);
}

.redesign .navInlineLink:hover::after {
    transform: scaleX(1);
}

.redesign .navInlineLink.is-current {
    color: var(--color-accent);
}

.redesign .navInlineLink.is-current::after {
    transform: scaleX(1);
}

@media (min-width: 900px) {
    .redesign .navInline {
        display: flex;
        align-items: center;
        gap: 26px;
        margin-left: 40px;
    }
    /* The overlay is unreachable and redundant once the links are in
       the bar, so its trigger goes too rather than sitting there as a
       second way to the same four places. */
    .redesign .hamb {
        display: none;
    }
}

/* ------------------------------------------------------------
   19. The custom cursor, made progressive

   cursor:none was unconditional in CSS while the replacement dot
   was drawn by cursorRedesign.js using mix-blend-mode. If that
   script did not run, or the blend mode was not applied, the
   visitor was left with no pointer at all and no way to get one
   back. Now the CSS only hides the real cursor once the script
   has confirmed it has a substitute on screen.
   ------------------------------------------------------------ */
html.hasCustomCursor .redesign,
html.hasCustomCursor .redesign * {
    cursor: none !important;
}

html.hasCustomCursor .redesign input,
html.hasCustomCursor .redesign textarea,
html.hasCustomCursor .redesign [contenteditable] {
    cursor: text !important;
}

/* ------------------------------------------------------------
   20. Scroll cue

   The home page is six screens on a desktop and ten on a phone,
   and the only hint that anything followed the hero was a small
   rotated link in the bottom-left corner. This is a plain
   affordance at the foot of the first screen; it fades out for
   good as soon as the page moves.
   ------------------------------------------------------------ */
.redesign .scrollCue {
    position: absolute;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%);
    z-index: 4;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.28em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
    text-decoration: none;
    opacity: 1;
    transition: opacity .5s var(--ease-soft);
}

.redesign .scrollCue svg {
    width: 14px;
    height: 20px;
    display: block;
    stroke: currentColor;
    stroke-width: 1.4;
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

html.cupMotion .redesign .scrollCue svg {
    animation: cueNudge 2.1s ease-in-out infinite;
}

@keyframes cueNudge {
    0%, 100% { transform: translateY(0); opacity: 0.55; }
    50%      { transform: translateY(4px); opacity: 1; }
}

/* Once you have scrolled, you know. */
body.hasScrolled .redesign .scrollCue,
.redesign.hasScrolled .scrollCue {
    opacity: 0;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    html.cupMotion .redesign .scrollCue svg { animation: none !important; }
}

/* No room for it under a hero that is already a full column. */
@media (max-width: 768px) {
    .redesign .scrollCue {
        display: none;
    }
}

/* Between 900px and 1100px the bar has the logo, four labels and the
   language toggle to fit; tighten the tracking and the gaps rather than
   let anything wrap or collide. */
@media (min-width: 900px) and (max-width: 1100px) {
    .redesign .navInline {
        gap: 16px;
        margin-left: 24px;
    }
    .redesign .navInlineLink {
        font-size: var(--t-xs);
        letter-spacing: 0.1em;
    }
}

/* ============================================================
   21. The scrollbar

   It used to be styled on <body>, which is not the scroll
   container — the viewport is — so the rules were inert and the
   visitor got the operating system's own scrollbar over a
   5,500px aged-paper page.

   html is the scroll container now, declared rather than
   inherited through the body-overflow propagation quirk, so
   there is exactly one place that owns it. Both the standard
   properties (Firefox, and Chrome 121+) and the WebKit
   pseudo-elements are set, because neither covers everything.
   ============================================================ */

html {
    /* overflow-x cannot stay `visible` next to a non-visible
       overflow-y — the used value would become `auto` and the tilted
       wordmark would earn the page a horizontal scrollbar. */
    overflow-x: hidden;
    overflow-y: scroll;      /* always reserved: no reflow between short and long pages */
    scrollbar-width: thin;
    scrollbar-color: rgba(20, 20, 20, 0.28) transparent;
}

html::-webkit-scrollbar {
    width: 10px;
}

html::-webkit-scrollbar-track {
    background: transparent;
}

html::-webkit-scrollbar-thumb {
    background: rgba(20, 20, 20, 0.22);
    border-radius: 10px;
    /* Inset with a transparent border so the thumb reads as a slim
       marker floating on the paper rather than a bar in a channel. */
    border: 3px solid transparent;
    background-clip: content-box;
}

html::-webkit-scrollbar-thumb:hover {
    background: var(--color-accent);
    background-clip: content-box;
}

/* The menu used to lock scrolling with document.body.style.overflowY,
   which stops working the moment html owns the scrolling. A class, so
   the lock lives in the stylesheet next to the rule it overrides. */
html.menuOpen {
    overflow: hidden;
}

/* ============================================================
   22. Non-text contrast for controls — WCAG 1.4.11

   Measured across the four pages: all 364 text runs pass 1.4.3,
   but twelve non-text boundaries came in under the 3:1 the
   success criterion asks for. They are not all violations, and
   the distinction matters:

   NOT a violation, left alone. Card borders, section dividers,
   the chart wrapper and the accordion rule all use
   --color-line (1.22:1). 1.4.11 covers boundaries "needed to
   identify" a component — these enclose content that is already
   legible on its own, and the accordion announces itself with a
   "+" in --color-ink-muted at 5.37:1. Darkening them would cost
   the paper its lightness and buy no accessibility.

   A violation, fixed below. Four controls whose border is the
   ONLY thing saying where the control is, all at 1.52:1 or
   1.22:1. The text field is the clearest case: its background is
   transparent, so a 1px underline at 1.52:1 was the entire
   affordance.

   --color-line-control measures 3.20:1 on the page ground and
   3.14:1 on the card ground, so it holds on both surfaces.
   ============================================================ */

:root {
    --color-line-control: rgba(20, 20, 20, 0.5);
}

/* The field boundary. Background is transparent, so this rule is it. */
.redesign .inputNormal {
    border-bottom-color: var(--color-line-control) !important;
}

/* Pill controls whose outline is their whole shape. */
.redesign .langToggle,
.redesign .confirmModalClose {
    border-color: var(--color-line-control);
}

/* This one had it right — border: 1px solid var(--color-ink) — and then
   a later rule overrode the colour to --color-line-strong to make it
   read as the secondary action. The control token keeps it visually
   quieter than the primary button while staying perceivable. */
.redesign .projectVisit {
    border-color: var(--color-line-control);
}

/* The legend exists to explain the ramp, so its own chips have to be
   discernible — the lightest swatch was rgba(20,20,20,0.07) on paper,
   1.15:1, i.e. an invisible square in a row explaining a scale. */
.redesign .ghSwatch {
    box-shadow: inset 0 0 0 1px var(--color-line-control);
}
