/* ============================================================
   COMPONENTS
   The site's components — project cards, the case dialog, the
   GitHub panels, process, qualifications, contact — on top of the
   base sheet (styleRedesign.css). styleSwiss.css restyles both.

   This file began as a cartoon "film grade" layer; the grade
   (grain, vignette, iris, ink boil, rubber-hose springs, cursor
   experiments) is gone and what remains is component structure.
   ============================================================ */

/* html is where :root lives, so it reads the token directly. */
html { background-color: var(--color-bg) !important; }
body.redesign { background-color: var(--color-bg) !important; }

/* ------------------------------------------------------------
   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: color-mix(in srgb, var(--color-raised) 55%, transparent);
    border: 1px solid var(--color-line);
    border-radius: 24px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: transform .45s var(--ease-swiss),
                background-color .5s var(--ease-soft),
                border-color .5s var(--ease-soft);
}

.redesign .projectCard:hover {
    background-color: color-mix(in srgb, var(--color-raised) 85%, transparent);
    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;
    transition: opacity .6s 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. render.js sets this. */
.redesign .projectShot img.is-loaded { opacity: 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,
        color-mix(in srgb, var(--color-ink) 3.5%, transparent) 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 .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: color-mix(in srgb, var(--color-raised) 55%, transparent);
    border: 1px solid var(--color-line);
    border-radius: 24px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: transform .45s var(--ease-swiss), 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);
}

.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;
}

.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 .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);
}

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

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

@media (prefers-reduced-motion: reduce) {
    .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(--ease-swiss),
                background-color .3s var(--ease-soft),
                color .3s var(--ease-soft);
}

.redesign .langToggle:hover {
    background-color: var(--color-ink);
    color: var(--color-raised);
    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;
}

/* A wrapping flex row sized every tile to its own label, so a group
   came out as a ragged staircase — "Figma" a third the width of "Adobe
   Illustrator", and the last row of each group ending wherever it
   happened to end. These are specimens of one kind of thing; they
   should be one size. A grid that fills the row and drops to a single
   full-width column on a phone. */
.redesign .skillRow {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    align-items: stretch;
}

@media (max-width: 600px) {
    .redesign .skillRow {
        grid-template-columns: 1fr;
    }
}

/* 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 {
    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(--ease-swiss),
                background-color .3s var(--ease-soft),
                color .3s var(--ease-soft);
}

.redesign .projectOpen:hover {
    background-color: var(--color-ink);
    color: var(--color-raised);
    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);
}

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

/* The panel used to be pinned to the top of the viewport at full
   height, so opening a project replaced the page rather than opening
   something on top of it: no top edge, no bottom edge, and the backdrop
   only visible in two thin strips at the sides. It read as a navigation,
   which it is not — you are still on the home page, looking at one
   card's detail. Centred, with air around it, it reads as a dialog. */
.caseModal {
    position: fixed;
    inset: 0;
    z-index: 9800;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4vh 4vw;
    box-sizing: border-box;
}

/* 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: var(--color-scrim);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    animation: caseFade .25s ease both;
}

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

.redesign .casePanel {
    position: relative;
    /* 920 left the reading column stranded: .caseText is capped at 66ch,
       so a 792px inner width put ~190px of empty paper down the right of
       every case. The panel is sized to the measure it carries. */
    width: min(760px, 100%);
    max-height: 92vh;
    margin: 0 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 56px clamp(24px, 5vw, 64px) 72px;
    background-color: var(--color-bg);
    border: 1px solid var(--color-line-strong);
    border-radius: 20px;
    animation: caseRise .35s var(--ease-soft) both;
}

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

@keyframes caseSheetUp {
    from { transform: translateY(100%); }
}

.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(--ease-swiss), 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) {
    /* A sheet, pinned to the bottom three edges, with the page still
       visible above it — so the phone shows you are on top of something
       rather than somewhere else, and the thumb reaches the content and
       the close button without a stretch. */
    .caseModal {
        align-items: flex-end;
        padding: 0;
    }
    .redesign .casePanel {
        width: 100%;
        max-height: 88dvh;
        padding: 34px 22px 48px;
        border: 1px solid var(--color-line-strong);
        border-bottom: none;
        border-radius: 20px 20px 0 0;
        animation: caseSheetUp .32s var(--ease-soft) both;
    }
    /* The grab bar. It is not draggable — it is the shape people read as
       "this came up from the bottom and goes back down". */
    .redesign .casePanel::before {
        content: '';
        position: sticky;
        top: 0;
        display: block;
        width: 42px;
        height: 4px;
        margin: -14px auto 18px;
        border-radius: 4px;
        background-color: var(--color-line-strong);
    }
    .redesign .caseText { margin-bottom: 32px; }
}

/* ------------------------------------------------------------
   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: var(--color-raised) !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 {
    /* Was a wrapping flex row of 12.5px captions. Three columns on a rule,
       so the figures read as a set and carry the weight the content
       deserves -- these are the only numbers on the site a shortlist
       cannot also claim. */
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0 20px;
    margin-top: 26px;
    padding-top: 18px;
    border-top: 1px solid var(--color-line);
}

.redesign .heroStat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    line-height: 1.3;
    letter-spacing: 0.04em;
    color: var(--color-ink-muted);
    /* Not uppercase: these are sentences ("messages a day, in production"),
       and tracked-out capitals on a nine-word label is chrome, not reading. */
    text-transform: none;
}

.redesign .heroStat b {
    font-family: var(--font-serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(2.8rem, 3.4vw, 4rem);
    line-height: 1;
    letter-spacing: -0.01em;
    text-transform: none;
    color: var(--color-ink);
}

@media (max-width: 900px) {
    /* Three columns of a figure-plus-sentence do not survive a phone. */
    .redesign .heroStats { grid-template-columns: 1fr 1fr; gap: 16px 20px; }
}

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

/* ------------------------------------------------------------
   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(--ease-swiss);
}

.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, #2A4562);
    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;
    /* The right edge lands on the shared measure, not on the viewport,
       so the intro column stays in relation to the wordmark instead of
       running away from it on a wide screen. */
    padding: 12vh var(--hero-gutter) 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-seal);
    display: flex;
    align-items: center;
    gap: 12px;
}

.redesign .heroFounder::before {
    content: '';
    width: 22px;
    height: 1px;
    background: var(--color-seal);
    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(--ease-swiss);
}

.redesign .roleCard:hover {
    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-seal);
}

.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-seal);
}

.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(--ease-swiss);
}

.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-seal);
    background: linear-gradient(
        180deg,
        color-mix(in srgb, var(--color-seal) 5%, transparent),
        transparent 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);
}

/* 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;
    /* The form does not need half the page; the reasons to use it do. */
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: 72px;
    /* Was align-items: center inside min-height: 100vh, which floated the
       whole page in the middle of a screen-tall box -- about 500px of empty
       paper above the heading and as much below the button. Top-aligned, and
       the box is only as tall as it needs to be. */
    align-items: start;
    width: 100%;
    min-height: 0;
    padding: 132px var(--hero-gutter) 96px;
    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::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;
    }
}

/* ------------------------------------------------------------
   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;
    /* Back to the middle. It was moved to the right gutter when the calendar
       was 710px wide and collided with it; at 526px the centre is clear, and
       the right gutter is where the buttons are -- which is the collision it
       traded into. */
    left: 50%;
    transform: translateX(-50%);
    bottom: 26px;
    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.hasMotion .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.hasMotion .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 wordmark
       would earn the page a horizontal scrollbar. */
    overflow-x: hidden;
    overflow-y: scroll;      /* always reserved: no reflow between short and long pages */
}

/* Only the page scrollbar was ever styled. Every other scroller on the
   site — the case panel, the contribution chart's horizontal track, the
   navigation overlay, the textarea on the contact form — handed the
   visitor the operating system's own grey bar, which on Windows is a
   16px channel with square arrows sitting inside a warm paper dialog.

   scrollbar-width and scrollbar-color inherit, so one declaration on
   html covers Firefox for the whole document. The WebKit
   pseudo-elements do not inherit, so they are set once against a
   descendant selector rather than repeated per container. */
html {
    scrollbar-width: thin;
    scrollbar-color: var(--color-line-strong) transparent;
}

html::-webkit-scrollbar,
.redesign ::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

html::-webkit-scrollbar-track,
.redesign ::-webkit-scrollbar-track {
    background: transparent;
}

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

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

/* Where the two scrollbars meet. Left unstyled it is a small grey
   square with a raised bevel — the one piece of native chrome that
   survives everything above. */
html::-webkit-scrollbar-corner,
.redesign ::-webkit-scrollbar-corner {
    background: transparent;
}

/* 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: color-mix(in srgb, var(--color-ink) 50%, transparent);
}

/* 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);
}

/* ------------------------------------------------------------
   20. Hover for the static sections
   See the note in the commit: the method steps, the commitments,
   the hero figures, the calendar cells and the About facts had no
   response at all. Three primitives, one language.
   ------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) {

    /* ---- THE MARK -------------------------------------------
       Figures: the number takes the seal, and a hairline strokes
       in underneath from the left. */

    .redesign .stepItem,
    .redesign .pledge,
    .redesign .heroStat {
        position: relative;
    }

    .redesign .stepNo,
    .redesign .pledgeValue,
    .redesign .heroStat b {
        transition:
            color .26s var(--ease-soft),
            transform .26s var(--ease-soft);
    }

    /* The stroke. Absolutely positioned on purpose: these parents are
       flex containers, so a static pseudo-element would become a flex
       item and take part in the layout. */
    .redesign .stepItem::after,
    .redesign .pledge::after,
    .redesign .heroStat::after {
        content: '';
        position: absolute;
        left: 0;
        bottom: -7px;
        width: 100%;
        height: 1px;
        background: var(--color-seal);
        transform: scaleX(0);
        transform-origin: left center;
        transition: transform .4s var(--ease-soft);
    }

    .redesign .stepItem:hover::after,
    .redesign .pledge:hover::after,
    .redesign .heroStat:hover::after {
        transform: scaleX(1);
    }

    /* ---- THE LIFT -------------------------------------------
       The three "Right now" facts on About: the ground firms up and
       the block rises a hair. */

    .redesign .nowItem {
        transition:
            background-color .3s var(--ease-soft),
            transform .3s var(--ease-soft);
    }

    .redesign .nowItem:hover .nowLabel {
        transition: color .26s var(--ease-soft);
    }

    /* ---- THE PRESS ------------------------------------------
       A calendar cell swells under the pointer. Both calendars carry a
       native <title>, so a hover response here is answering something
       the element already offers rather than implying a new one.
       transform-box: fill-box is what makes transform-origin: center
       mean the cell's own centre in SVG instead of the viewport's. */

    .redesign .ghHeatmap rect {
        transform-box: fill-box;
        transform-origin: center;
        transition: transform .14s ease-out;
    }

    .redesign .ghHeatmap rect:hover {
        transform: scale(2.1);
    }
}

/* Everything that MOVES hangs off hasMotion, so prefers-reduced-motion keeps
   the colour changes above and loses the travel. */
@media (hover: hover) and (pointer: fine) {

    html.hasMotion .redesign .stepItem:hover .stepNo,
    html.hasMotion .redesign .pledge:hover .pledgeValue,
    html.hasMotion .redesign .heroStat:hover b {
        transform: translateY(-3px);
    }

    html.hasMotion .redesign .nowItem:hover {
        transform: translateY(-3px);
    }

}

@media (prefers-reduced-motion: reduce) {
    /* Belt and braces: hasMotion is absent here anyway, but the stroke is a
       transition on a base rule rather than something hasMotion gates, so it
       gets switched off explicitly. */
    .redesign .stepItem::after,
.redesign .pledge::after,
.redesign .heroStat::after,
.redesign .ghHeatmap rect,
.redesign .nowItem {
        transition: none !important;
    }
    .redesign .ghHeatmap rect:hover {
        transform: none !important;
    }
}

/* ------------------------------------------------------------
   21. Contact
   The channels were inside the form panel at up to 50px of italic
   serif -- the biggest thing on the page, under no label, reading
   as part of the form they are the alternative to. They belong on
   the left, with the reasons to use them.
   ------------------------------------------------------------ */

.redesign .contactDirect,
.redesign .contactFacts {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin-top: 44px;
}

/* Down from clamp(2.8rem, 3.8vw, 5rem). Still the serif, still the
   emphasis of a direct line, but no longer shouting over the heading. */
.redesign .contactDirect .contactLink {
    font-size: clamp(1.7rem, 1.9vw, 2.2rem) !important;
    gap: 12px;
    position: relative;
    align-self: flex-start;
    transition: color .26s var(--ease-soft);
}

.redesign .contactDirect .contactLink::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 2px;
    height: 1px;
    background: var(--color-seal);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform .38s var(--ease-soft);
}

@media (hover: hover) and (pointer: fine) {
    .redesign .contactDirect .contactLink:hover::after { transform: scaleX(1); }
}

.redesign .contactDirect .contactLink img,
.redesign .contactDirect .contactLink svg { flex: none; }

/* The three answers. A definition list because that is what it is. */
.redesign .factList {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0;
    border-top: 1px solid var(--color-line);
}

.redesign .fact {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 14px 0;
    border-bottom: 1px solid var(--color-line);
}

.redesign .fact dt {
    font-family: var(--font-serif);
    font-style: italic;
    font-size: var(--t-lg);
    line-height: 1.15;
    color: var(--color-ink);
}

.redesign .fact dd {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--t-sm);
    color: var(--color-ink-muted);
}

/* Field labels,
so the only naming was the
   placeholder -- which disappears the moment someone types,
and which is
   why the form read as three bare rules. */
.redesign .fieldLabel {
    position: static;
    width: auto;
    height: auto;
    clip: auto;
    clip-path: none;
    overflow: visible;
    display: block;
    margin-bottom: 4px;
    font-family: var(--font-sans);
    font-size: var(--t-xs);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

/* Sized to its label rather than to the column. A full-width black pill
   under three hairlines was out of scale with everything near it. */
.redesign .contactPanel .btnPrimary {
    align-self: flex-start;
    width: auto !important;
    padding-left: 34px !important;
    padding-right: 34px !important;
}

/* ------------------------------------------------------------
   22. The work grid
   The cards were not short of polish -- they lift, tilt, and bring
   their screenshot from sepia to colour. They read basic because
   all six were identical. One irregular cell fixes more than any
   amount of detail inside a uniform one.
   ------------------------------------------------------------ */

/* The cards are uniform. A feature cell was here to break the template
   read of six identical boxes; he does not want one larger than the
   others, so the curiosity has to come from what they DO instead --
   see the pan and the hop below. */

/* The seal on hover, so the cards speak the same language as the steps,
   the figures and the current-role stamp. */
.redesign .projectCard::before {
    content: '';
    position: absolute;
    top: 0;
    left: 26px;
    right: 26px;
    height: 1px;
    background: var(--color-seal);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform .5s var(--ease-soft);
    z-index: 3;
}

.redesign .projectName {
    transition: color .3s var(--ease-soft);
}

@media (hover: hover) and (pointer: fine) {
    .redesign .projectCard:hover::before { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
    .redesign .projectCard::before { transition: none !important; }
}

/* ------------------------------------------------------------
   23. The calendar
   Every filled day used to cast a hard, unblurred seal-coloured
   offset — the same duotone device the wordmark carried, applied to
   the grid so the two would rhyme. With the wordmark set in one ink
   there is nothing left to rhyme with, and at a 10px cell the offset
   was not reading as a printing effect at all: it turned a year of
   work into a field of chunky two-tone pixels, which is the one thing
   a contribution chart must not look like. The colour ramp already
   carries the density.
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   24. Project cards: the overlays come off
   Three things sat between the viewer and the screenshots -- a
   sepia grade at rest, a backdrop blur, and a translucent ground.
   These are shots of real interfaces; graded down to sepia they
   read as archive photographs of someone else's work.
   ------------------------------------------------------------ */

.redesign .projectCard {
    background-color: var(--color-raised);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.redesign .projectShot img {
    filter: none !important;
}

/* The plate behind an image that has not decoded yet keeps the initial and
   loses the woven diagonal, which was the last of the overlays. */
.redesign .projectShot::after {
    background-image: none;
}

/* ------------------------------------------------------------
   25. Experience and certificates: rows, not boxes
   Eight bordered, rounded, separately-backgrounded rectangles
   stacked inside an accordion is a great deal of container for a
   list of roles. Rules instead -- the same device as the contact
   facts -- and a terracotta rule above the roles still running,
   which says "current" more quietly than a border on a box.
   ------------------------------------------------------------ */

/* Cards, with presence. The first pass made these hairline boxes and the
   second flattened them to rules; neither reads as a card. A raised surface
   off the page ground, real padding, and a thick seal edge on the roles still
   running -- a card that says something rather than a bordered rectangle. */
.redesign .roleCard,
.redesign .certCard {
    padding: 26px 28px !important;
    border: 1px solid var(--color-line);
    border-radius: 14px;
    background: var(--color-raised);
}

/* 28px a side is a desk measure. On a 390px phone it is the last of
   four nested paddings and it costs more than it gives — see the
   600px block in styleRedesign.css, which clears the two structural
   wrappers above this card. This sheet loads last and the rule above
   is !important, so the phone value has to be set here. */
@media screen and (max-width: 600px) {
    .redesign .roleCard,
    .redesign .certCard {
        padding: 22px 18px !important;
    }
}

.redesign .roleCard.is-current,
.redesign .certCard.is-key {
    border-left: 3px solid var(--color-seal);
    background: var(--color-raised);
}

@media (hover: hover) and (pointer: fine) {
    /* The card steps forward. No colour: it moves. */
    .redesign .roleCard,
    .redesign .certCard {
        transition: transform .3s var(--ease-swiss);
    }
    html.hasMotion .redesign .roleCard:hover,
    html.hasMotion .redesign .certCard:hover {
        transform: translateY(-3px);
    }
}

/* ------------------------------------------------------------
   26. Letters that hop
   The objection to the previous pass was fair: a hover that only
   recolours a thing is not an effect. The signature already moves
   one character at a time, so that is the move the whole site
   borrows. swissMotion.js splits the text; this staggers it.
   ------------------------------------------------------------ */

.hopSp { white-space: pre; }

.hopCh {
    display: inline-block;
    will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
    html.hasMotion .hopHost:hover .hopCh,
    html.hasMotion .projectCard:hover .projectName .hopCh,
    html.hasMotion .roleCard:hover .roleName .hopCh,
    html.hasMotion .certCard:hover .certTitle .hopCh {
        animation: letterHop .52s cubic-bezier(0.34, 1.56, 0.64, 1) both;
        /* 26ms apart: enough to read as a wave travelling along the word,
           short enough that a six-letter name finishes before the eye
           moves on. */
        animation-delay: calc(var(--i) * 26ms);
    }
}

/* Was a 0.26em hop with a 5deg tilt and a scale bounce per letter. The
   response to a hover is still per-letter, so the word still reads as one
   thing responding — it is just a lift now, not a jump. */
@keyframes letterHop {
    0%   { transform: translateY(0); }
    40%  { transform: translateY(-0.09em); }
    100% { transform: translateY(0); }
}

/* The figures and the step numbers hop too, so one language covers the
   whole site rather than colour here and motion there. */
@media (hover: hover) and (pointer: fine) {
    html.hasMotion .stepItem:hover .stepNo,
    html.hasMotion .pledge:hover .pledgeValue,
    html.hasMotion .heroStat:hover b {
        animation: letterHop .52s cubic-bezier(0.34, 1.56, 0.64, 1) both;
        transform: none;
    }
}

/* ------------------------------------------------------------
   27. Something to find in the cards
   Nothing was hidden, so there was nothing to discover -- every
   card showed everything at rest. The shot is cropped to the top
   of the page it captured; on hover it pans down and you see the
   rest of the site. A reveal rather than a decoration.
   ------------------------------------------------------------ */

.redesign .projectShot img {
    object-position: top center;
    transition: object-position 2.2s cubic-bezier(0.33, 0, 0.2, 1);
}

@media (hover: hover) and (pointer: fine) {
    html.hasMotion .redesign .projectCard:hover .projectShot img {
        object-position: bottom center;
    }
}

@media (prefers-reduced-motion: reduce) {
    .redesign .projectShot img { transition: none !important; }
    .hopCh { animation: none !important; }
}

/* ------------------------------------------------------------
   28. No colour-only hovers
   Twenty-two rules answered a pointer by recolouring something and
   nothing else. The colour is stripped out above; what replaces it
   is movement, because that is what reads as a response.
   ------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) {

    /* Tag chips: they lift and tighten rather than tinting. */
    .redesign .item {
        transition: transform .24s var(--ease-swiss), border-color .24s var(--ease-soft);
    }
    html.hasMotion .redesign .item:hover {
        transform: translateY(-2px) scale(1.04);
    }

    /* Accordion rows: the whole row steps in from the left, the way a
       drawer does when you pull it. */
    .redesign .accordion {
        transition: transform .3s var(--ease-soft);
    }
    html.hasMotion .redesign .accordion:hover {
        transform: translateX(6px);
    }

    /* Secondary buttons and the visit link: a small press-forward. */
    .redesign .btnSecondary,
    .redesign .projectVisit,
    .redesign .navInlineLink {
        transition: transform .26s var(--ease-swiss);
    }
    html.hasMotion .redesign .btnSecondary:hover,
    html.hasMotion .redesign .projectVisit:hover,
    html.hasMotion .redesign .navInlineLink:hover {
        transform: translateY(-2px);
    }

    .redesign .ghHeatmap rect:hover {
        transform: scale(2.6);
    }
}

@media (prefers-reduced-motion: reduce) {
    .redesign .item,
.redesign .accordion { transition: none !important; }
}

/* ------------------------------------------------------------
   29. Surfaces — a frame is not a fill

   Across the site a lot of things were drawn as an outline over
   the page: every tag, the GitHub stat chips, the secondary
   buttons, the language toggle, the chart well and the event
   feed. A border with `background: rgba(0,0,0,0)` behind it does
   not read as an object — it reads as an object that failed to
   load. The paper shows straight through, so the shape has an
   edge but no substance, and twenty of them in a row (the tags
   on Qualifications run to twenty-six) turn a page into wireframe.

   Two values do all of it, and both are already in the palette:

     raised  #FFFFFF   a container that sits ON the paper
     sunken  #F0EDE6   a chip or control cut INTO whatever is
                       behind it

   Chips and controls take the sunken tone rather than white for
   one specific reason: they appear on both grounds. A tag sits on
   the page (#FAF8F4) in the case dialog and on a white card in
   the role list — white would make it disappear on the second.
   The sunken tone is darker than both, so one value works
   everywhere and the component does not need to know where it is.

   .nowGrid is deliberately left alone: its background is the
   divider colour showing through 1px grid gaps, which is a rule,
   not a fill.
   ------------------------------------------------------------ */

/* Chips: tags, and the profile stats under each GitHub handle. */
.redesign .item,
.redesign .ghStat {
    background-color: var(--color-sunken) !important;
    border-color: var(--color-line-strong) !important;
}

/* Outline controls. The border already says "secondary"; the fill
   only has to stop them being holes in the page. Their hover still
   floods to ink, and the label with it. */
.redesign .btnSecondary,
.redesign .projectVisit,
.redesign .langToggle {
    background-color: var(--color-sunken) !important;
}

/* Containers. The panel is the thing on the paper; the chart well
   and the feed are cut into the panel, so they take the recess and
   the two levels stay legible inside one card. */
.redesign .ghPanel {
    background-color: var(--color-raised) !important;
    border-color: var(--color-line-strong) !important;
}

.redesign .ghChartWrap,
.redesign .ghFeed {
    background-color: var(--color-sunken) !important;
    border-color: var(--color-line-strong) !important;
}

/* The empty days of the calendar are drawn at 7% ink, which was
   tuned against the page ground. On the recessed well they lose the
   little separation they had, so the grid gets its own step. */
.redesign .ghChartWrap .offCells rect {
    fill: var(--color-raised);
}

/* ------------------------------------------------------------
   30. Edge weight — now that everything has a fill

   Section 29 gave every frame a surface, and that changed what
   the border is for. While a tag was a transparent outline the
   edge was the only thing drawing it, so it had to be strong. Now
   the fill separates the object from the paper and the border is
   only finishing it — at the weight it was carrying, it stopped
   reading as a finish and started reading as a box, which on
   Qualifications means twenty-six boxed pills in a stack.

   There were five resting weights across the site: 0.12, 0.22,
   0.5, solid ink, and one seal. Two survive:

     0.12  content — chips, tiles, panels, wells, cards
     0.22  controls — the things you click

   The step between them is the whole hierarchy: a tag and a
   button no longer have the same edge, which they did when both
   were near-black.

   Deliberately untouched: .btnPrimary, whose border matches its
   own ink fill, and the seal edge on the current role and the key
   certificate — a stamp, and the one place the accent is allowed
   to be loud.
   ------------------------------------------------------------ */

/* Content. */
.redesign .item,
.redesign .ghStat,
.redesign .panel .bggb,
.redesign .bggb,
.redesign .ghPanel,
.redesign .ghChartWrap,
.redesign .ghFeed,
.redesign .pledgeRow {
    border-color: var(--color-line) !important;
}

/* Controls: one step up, and nowhere near the solid ink and the
   50% they were carrying. */
.redesign .btnSecondary,
.redesign .projectVisit,
.redesign .langToggle {
    border-color: var(--color-line-strong) !important;
}

/* The hovers moved with them: a hover that lands on a value the
   resting state has already passed says nothing. Each is one step
   above its own resting weight, not an absolute value. */
@media (hover: hover) and (pointer: fine) {
    .redesign .panel .bggb:hover,
    .redesign .bggb:hover {
        border-color: var(--color-line-strong) !important;
    }

    .redesign .projectVisit:hover,
    .redesign .langToggle:hover {
        border-color: var(--color-ink-faint) !important;
    }
}

/* The open drawer's rule sits on the page, not on a filled surface,
   so it keeps the heavier of the two — it is the only thing marking
   where the panel begins. */
.redesign .accordion.active {
    border-bottom-color: var(--color-line-strong) !important;
}

/* The form fields were the last thing at 50%, and they are underlines
   rather than boxes, so the weight showed. They are controls: 0.22 at
   rest like the buttons. Focus already takes the rule to solid ink,
   and dropping the resting weight is what makes that jump legible —
   at 50% the focused and unfocused states were nearly the same line. */
.redesign .inputNormal {
    border-bottom-color: var(--color-line-strong) !important;
}

/* ------------------------------------------------------------
   31. The project card: three levels, not one

   Section 29 filled every frame and section 30 quietened every
   edge, and between them the bottom of a project card flattened
   out: the tags, "Read the case" and "Visit site" all ended up
   the same sunken pill with the same hairline, differing only by
   a point of type size. Three different kinds of thing wearing
   one costume — and the primary action of the card was the least
   findable thing on it.

   It also left a real bug behind. .projectOpen:hover fills with
   ink and turns its label to paper, but section 29's fill was
   !important and the hover's was not, so the fill stopped
   arriving while the label still went white: white on #F0EDE6,
   about 1.06:1. The same failure as the résumé buttons, and this
   one I introduced.

   So the card reads in three steps now:

     tag           sunken chip, no edge at all — a label
     Visit site    sunken chip with a 0.22 edge — a control
     Read the case solid ink, paper label — the action

   Dropping the tag's border is also the honest end of section 30:
   a chip that already has a fill does not need an outline as
   well, and the card carries four or five of them at once.
   ------------------------------------------------------------ */

.redesign .item {
    border: none !important;
}

.redesign .projectOpen {
    background-color: var(--color-ink) !important;
    border-color: var(--color-ink) !important;
    color: var(--color-bg) !important;
}

@media (hover: hover) and (pointer: fine) {
    /* It is already the darkest thing on the card, so the hover
       cannot go darker. It lifts, and the ink lightens by a step. */
    .redesign .projectOpen:hover {
        background-color: var(--color-ink-soft) !important;
        border-color: var(--color-ink-soft) !important;
        color: var(--color-bg) !important;
    }
}

/* "VISIT_SITE". The hop splitter wraps each letter in an
   inline-block span and leaves the spaces as bare text nodes, and
   text-decoration does not paint through an inline-block box — so
   the underline this link inherited from the user agent survived
   on exactly one character: the space between the two words. It
   drew what looked like an underscore in the middle of the label.
   .projectVisit is the only split element on the site that still
   had an underline; every other hop host already declares none. */
.redesign .projectVisit {
    text-decoration: none;
}
