/* FONT */
@font-face {
  font-family: 'faction';
  src: url("font/faction.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'syne';
  src: url("font/Syne-Bold.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'syne-semi';
  src: url("font/Syne-SemiBold.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'syne-bold';
  src: url("font/Syne-ExtraBold.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

/* THEME
   Every color on the site routes through these variables so the "clr me"
   button (main.js) can restyle the whole page by overriding them. */
:root {
  --bg: #ffffff;       /* page background */
  --ink: #030303;      /* text, borders, strokes */
  --muted: #BBB8BF;    /* secondary text: labels, dates, counters */
  --soft: #c7c7c7;     /* decorative strokes (hero echo outlines) */
  --accent: #28a745;   /* flourishes (+1 animation) */
}

/* GENERAL */
::-moz-selection {
  color: var(--bg);
  background-color: rgba(0, 0, 0, 0);
}

::selection {
  color: var(--bg);
  background-color: rgba(0, 0, 0, 0);
}

/* Custom-element wrappers for the shared header/footer must not create
   their own box, so the inner header/footer lay out as before. */
site-header,
site-footer {
  display: contents;
}

/* GENERAL */
body {
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  background-color: var(--bg);
  color: var(--ink);
  font-size: 24px;
  font-family: Syne;
  font-style: normal;
  letter-spacing: 0.06em;
  /* Lining figures everywhere: all digits sit on the same cap/x height
     instead of Syne's default old-style numerals that dip below the line. */
  font-variant-numeric: lining-nums;
}

body p {
  font-size: 24px;
  line-height: 150%;
}

header {
  width: 100%;
    font-size: 16.66667px;
    position: fixed;
    height: 20px;
    max-width: 1200px;
    top: 60px;
    /* Keep the fixed nav above all page content (oversized works titles, the
       hero frame, etc.) on every page. */
    z-index: 10000;
}

header .logo {
  position: absolute;
    top: 0;
    left: 0;
  /* The logo is a <p>, not a heading: every page carries its own h1 now, and
     the site name repeated as one on all three was not it. Size, leading and
     margin are stated rather than inherited from the element, so swapping the
     tag again can never move it. */
  font-size: inherit;
  line-height: inherit;
  margin: 0;
}

header nav {
  position: absolute;
    top: 0;
    right: 0;
  z-index: 9999;
}

header nav .main_nav {
  width: auto;
}

header nav .main_nav li {
  float: left;
  margin: 0 20px;
}

.link-nav li a {
  text-transform: lowercase;
  position: relative;
  z-index: 10;
}

/* "clr" switch: a two-state toggle between the colored and b&w versions */
.color-me-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}

.color-me-label {
  text-transform: lowercase;
}

.color-me {
  position: relative;
  width: 44px;
  height: 20px;
  padding: 0;
  border: 2px solid var(--ink);
  border-radius: 12px;
  background: var(--bg);
  cursor: pointer;
  transition: background-color ease .2s;
}

.color-me-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--ink);
  transition: transform ease .2s, background-color ease .2s;
}

.color-me[aria-checked="true"] {
  background: var(--ink);
}

.color-me[aria-checked="true"] .color-me-knob {
  transform: translateX(24px);
  background: var(--bg);
}

.link-nav li a:before {
  content: '';
  width: 100%;
  height: 2px;
  z-index: -10;
  position: absolute;
  bottom: -2px;
  left: 0px;
  box-sizing: border-box;
  border: 1px solid var(--ink);
  transition: ease .2s;
}

/* Hover, and the current page's nav item (forced-active hover state). */
.link-nav li a:hover:before,
.link-nav li a[aria-current="page"]:before {

  border: 2px solid var(--ink);
  width: calc( 100% + 12px);
  left: -6px;
  height: calc( 100% + 12px);
  bottom: -6px;
  transition: ease .2s;
}




.big-title {

  font-family: faction;
  font-size: 96px;
  margin-bottom: 60px;
}

/* Tighter gap when a subtitle sits directly under the title. */
.big-title:has(+ .section-subtitle) {
  margin-bottom: 16px;
}

.main-section {
  margin: 180px 80px;
}



.squared {
  height: 80vh;
  width: 80vh;
  max-width: 500px;
  max-height: 500px;
  border: 2px solid var(--ink);
  position: fixed;
  margin: auto;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -99999;
}

/* Permanent project info under each title — text styles aligned with the
   about-page resume (.resume-list). */
.works-info {
  padding-top: 4px;
}


.works-meta {
  display: block;
  font-family: syne-semi;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin-bottom: 6px;
}

.works-info p {
  font-family: syne-semi;
  font-size: 14px;
  margin: 0;
  /* Cap the measure for comfortable line length (~50 characters). */
  max-width: 50ch;
}

.squared .img {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.squared .img img {
  width: 100%;
  height: 100%;
  /* The frame is only square on desktop; cover keeps the square photos
     undistorted (cropped, not stretched) when the frame is a rectangle. */
  object-fit: cover;
}

/* Idle state: no src until a project is hovered — hide the element so the
   browser's broken-image glyph can't show (visible on dark palettes). */
.squared .img img:not([src]) {
  visibility: hidden;
}

.main-section p {
  margin: 40px 0;
  max-width: 760px;
  position: relative;
}

a {
  position: relative;
}

.inline-link{
  display: inline-block;
  position: relative;
}

.inline-link:before {
  content: '';
  width: 100%;
  height: 2px;
  position: absolute;
  bottom: 1px;
  left: 0;
  background-color: var(--ink);
  transition: ease .2s;
}

/* The underline collapses to a dot while the link is live. Hover is fenced
   behind (hover: hover) because a touch screen leaves an element stuck in
   its hover state until something else is touched — there the same look is
   driven by .is-active, which the script puts on and takes off with the
   photos it shows. */
@media (hover: hover) {
  .inline-link:hover:before {
    width: 2px;
    left: calc(50% - 1px);
    transition: ease .2s;
  }
}

.inline-link.is-active:before {
  width: 2px;
  left: calc(50% - 1px);
  transition: ease .2s;
}

#works .works-list {
  font-family: syne-bold;
  font-size: 71.66362px;
}

/* Editorial index rows: oversized title with the project info stacked
   underneath at every screen size, generous whitespace between projects.
   Hovering one row dims the rest and slides an external-link arrow out of
   the title. The whole row is one click target (stretched-link overlay). */
#works .works-list .works-links {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
  align-items: start;
  margin: 96px 0 96px 0;
  padding: 0;
  transition: opacity .25s ease;
}

#works .works-list .works-links:last-child {
  margin-bottom: 0;
}

/* Focus the pointed row by dimming its siblings — but only while a row is
   genuinely under the pointer. Keyed off the list's own :hover, the gaps
   between the rows counted too: the fixed photo frame sits in the middle of
   that empty space, so crossing it dimmed every title at once with nothing
   lit in return, which read as the links greying out by themselves.
   Fenced behind (hover: hover) as well, because a tap leaves a touch screen
   stuck in :hover until something else is touched — there the same stretch
   of empty space latched the whole list grey. Browsers without :has() simply
   never dim; there is no way to ask them "is any row hovered". */
@media (hover: hover) {
  #works .works-list:has(.works-links:hover) .works-links:not(:hover) {
    opacity: .35;
  }
}

#works .works-links > a {
  line-height: .95;
  /* Override the global `a { position: relative }` so the stretched-link
     overlay below resolves to the .works-links row, not the title itself. */
  position: static;
}

/* Stretched link: the title anchor's hit area covers the whole row, so a
   click anywhere on the project (title, meta, or description) follows it.
   The row's own text must stay non-positioned or it would paint above the
   overlay and swallow the click (`.main-section p` is position:relative). */
#works .works-links > a::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

#works .works-info,
#works .works-info p,
#works .works-meta {
  position: static;
}

/* Arrow affordance — hidden until hover, then slides in. */
#works .works-links > a::after {
  content: '\2197';
  display: inline-block;
  margin-left: .12em;
  font-size: .5em;
  vertical-align: 0.32em;
  color: var(--muted);
  opacity: 0;
  transform: translateX(-.3em);
  transition: opacity .25s ease, transform .25s ease, color .25s ease;
}

#works .works-links:hover > a::after {
  opacity: 1;
  transform: translateX(0);
  color: var(--ink);
}

/* Cap the description measure for a comfortable line length. Scoped under
   #works to win over `.main-section p { max-width: 760px }`. */
#works .works-info p {
  max-width: 50ch;
}

@media (prefers-reduced-motion: reduce) {
  #works .works-links > a::after { transition: none; }
}

/* CURSORS */
.cursor-brain {
  display: inline-block;
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🧠</text></svg>") 16 0,auto; /*!emojicursor.app*/
}
.cursor-bear {
  display: inline-block;
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🧸</text></svg>") 16 0,auto; /*!emojicursor.app*/
}
.cursor-external {
  display: inline-block;
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>↗️</text></svg>") 16 0,auto; /*!emojicursor.app*/
}
.cursor-tiger {
  display: inline-block;
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🐅</text></svg>") 16 0,auto; /*!emojicursor.app*/
}
.cursor-lacrosse {
  display: inline-block;
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🥍</text></svg>") 16 0,auto; /*!emojicursor.app*/
}
.cursor-beer {

  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🍺</text></svg>") 16 0,auto; /*!emojicursor.app*/

}
.cursor-me {

  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>💜</text></svg>") 16 0,auto; /*!emojicursor.app*/

}
.cursor-electricity {

  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>⚡️</text></svg>") 16 0,auto; /*!emojicursor.app*/

}
.cursor-chat {

  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>💌</text></svg>") 16 0,auto; /*!emojicursor.app*/

}

/* Per-project hover cursor for the works list. Keyed on each link's own
   class so it overrides the shared .cursor-external (declared above). */
.link-proximus {
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>📡</text></svg>") 16 0,auto;
}
.link-aaa {
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>⚡️</text></svg>") 16 0,auto;
}
.link-bsit {
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🪻</text></svg>") 16 0,auto;
}
.link-lax {
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>👶</text></svg>") 16 0,auto;
}
.link-culturius {
  cursor:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'  width='52' height='62' viewport='0 0 100 100' style='fill:black;font-size:31px;'><text y='50%'>🎶</text></svg>") 16 0,auto;
}




.contact-section p {
  margin: 40px 0;
  max-width: 760px;
}


footer {
  width: 100%;
  height: 20px;
  border: 2px solid var(--ink);
  box-sizing: border-box;
  font-size: 16.66667px;
  position: relative;
  margin: 60px 0;
}

footer .social-list {
  line-height: 20px;
  position: absolute;
  left: 0;
  top: -30px;
}

footer .social-list li {

  float: left;
  margin-right: 40px;
}

footer .geo {
  line-height: 20px;
  position: absolute;
  right: 0;
  bottom: -30px;
  font-variant-numeric: lining-nums;
}


/* ABOUT */

.click-game{

  display: block;
  border: 2px solid var(--ink);
  height: 600px;
  width: 100%;

  position: relative;
}

.click-game img {

  object-fit: cover;
  width: 100%;
  height: 100%;
  opacity: 0;
  z-index: -999;
}

/* Click-game flash: the photo pops in instantly, then dissolves straight
   away over a 200ms ease-out fade — no hold. */
.click-game img.is-flashing {
  animation: game-flash 200ms ease-out forwards;
}

@keyframes game-flash {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

.click-game:after {

  content:'click to see me';
  font-size: 12px;
  color: var(--bg);
  text-align: center;

  display: block;
  padding: 20px;
  max-width: 60px;
  border-radius: 30px;
  background-color: var(--ink);

  /* True centering regardless of the badge's own size; the spin uses the
     separate `rotate` property so it can't override this translate. */
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  animation: rotating 42s infinite linear;
}

@keyframes rotating {
  from {
    rotate: 0deg;
  }
  to {
    rotate: 360deg;
  }
}




/* ABOUT */

.skill-title {

  font-family: syne-semi;
  font-size: 16px;
  font-weight: 500;
  color: var(--muted);
  line-height: 50px;
  width: 100%;
  height: auto;
  position: relative;
}

.skill-title:after {

  content:'';
  display: block;
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 1px;
  background-color: var(--ink);

}

/* Scoped to beat the .main-section p margin so the top margin is removed. */
.main-section .section-subtitle {
  font-family: syne-semi;
  font-size: 16px;
  color: var(--muted);
  margin-top: 0;
}

.skills-wrapper {
  margin-top: 40px;
}

.skills-row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  width: 100%;
  gap: 160px;
}

.skills-row:first-child {

  margin-bottom: 60px;

}

.skills-col {
  display: flex;
  flex-direction: column;
  flex-basis: 100%;
  flex: 1;
}

.skill-list .skill-item {

  font-size: 24px;
  letter-spacing: 5%;
  line-height: 30px;
  margin: 20px 0;
  font-family: syne-semi;

  /* Clickable counter, same concept as the Wireframe one */
  position: relative;
  cursor: pointer;
  user-select: none;
}

/* Keyboard users need to see which word they are on — the items are now
   real controls (role=button, tabindex) rather than decorative list items. */
.skill-list .skill-item:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* "+1" flourish thrown by every skill-word counter (main.js). Lives here
   rather than in a page <style> block so every page shares one definition. */
.plus-one-animation {
  position: absolute;
  top: -10px;
  left: 50px;
  transform: translateX(-50%);
  font-size: 1.2rem;
  color: var(--accent); /* themed by the "clr me" palette */
  font-weight: bold;
  animation: fade-up 800ms ease-out forwards;
  pointer-events: none; /* never swallow the next click */
}

@keyframes fade-up {
  from {
    opacity: 1;
    transform: translate(-50%, 0);
  }
  to {
    opacity: 0;
    transform: translate(-50%, -50px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .plus-one-animation {
    animation: none;
    opacity: 0;
  }
}

.skill-count {
  color: var(--muted);
  font-variant-numeric: lining-nums tabular-nums;
  /* Syne only ships in bold weights, so use a light system numeral here. */
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 300;
  /* Raised exponent, like the ² in m². */
  font-size: 0.6em;
  vertical-align: super;
  line-height: 0;
}

.resume-list .resume-company {

  font-family: syne-bold;
  font-size: 34px;
}
.resume-list .resume-date {

  font-family: syne-semi;
  font-size: 14px;
  color: var(--muted);
  margin-bottom: 12px;
}


/* Job title, sitting between the company and the description. Smaller than
   the company but still in --ink, so it reads as part of the heading rather
   than as another muted meta line like the date. */
.resume-role {
  font-family: syne-semi;
  font-size: 16px;
  line-height: 1.3;
  margin: 8px 0;
  color: var(--ink);
}

.resume-list p {

  font-family: syne-semi;
  font-size: 14px;
  width: 600px;
  margin-bottom: 0;
}

.resume-item {

  /* The rail's inset. The dot is positioned from it, so changing the padding
     at a breakpoint keeps the dot centred on the line automatically — a
     hard-coded offset silently drifts off the rail instead. */
  --rail-pad: 20px;

  position: relative;
  border-left: 2px solid var(--ink);
  /* Spacing lives in the padding, not a margin: a margin breaks the left
     border between entries and the rail comes out dashed. */
  padding: 40px var(--rail-pad) 60px;
  margin-bottom: 0;
}

/* Each entry is a dot on a continuous rail, so the career reads as one
   thread, with its skills listed as tags underneath. */
/* The dot hangs off the date rather than the entry, so it stays centred on
   the date line instead of drifting whenever the padding changes. */
.resume-item .resume-date {
  position: relative;
}

.resume-item .resume-date::before {
  content: '';
  position: absolute;
  /* Back out through the padding and the 2px border, then half the dot's
     own width, so its centre lands on the centre of the rail. */
  --dot: 16px;
  left: calc(-1 * (var(--rail-pad) + 1px + var(--dot) / 2));
  top: 50%;
  translate: 0 -50%;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--ink);
}

.resume-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 18px 0 0;
  padding: 0;
  list-style: none;
}

.resume-tags li {
  padding: 5px 12px;
  border: 2px solid var(--ink);
  border-radius: 30px;
  font-family: syne-semi;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.test {

  position: relative;
}
.resume-decoration-parent {

  position: absolute;
  height: 100%;
  right: 0;
  top: 0;
}

.resume-decoration {

  position: sticky;
  height: 480px;
  width: 480px;
  right: 0;
  top: 200px;
  border: 2px solid var(--ink);
}


/* PROJECT PAGES */

/* Cross-document view transitions: the clicked works-list title morphs into
   the project hero title. Unsupported browsers just navigate normally. */
@view-transition {
  navigation: auto;
}

/* Shared-element names pairing each works-list link with its hero title
   (the hero side carries the same name via an inline style). */
.link-proximus  { view-transition-name: vt-proximus; }
.link-culturius { view-transition-name: vt-culturius; }
.link-bsit      { view-transition-name: vt-bsit; }
.link-aaa       { view-transition-name: vt-aaa; }
.link-lax       { view-transition-name: vt-lax; }

::view-transition-group(*) {
  animation-duration: 0.45s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hero: bordered frame with the title stacked five times — outlined echoes
   above and below the solid centre line. The frame and echoes are separate
   from the title so the view-transition morph target stays static. */
.project-hero {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  max-width: 960px;
  height: 600px;
  margin: 120px auto 0;
  overflow: hidden;
}

/* The rectangle, growing from the title's position outward. */
.project-hero-frame {
  position: absolute;
  inset: 0;
  border: 2px solid var(--ink);
  animation: frame-grow 0.5s cubic-bezier(0.3, 0.7, 0.3, 1) 0.1s backwards;
}

@keyframes frame-grow {
  from {
    transform: scale(0.15, 0.08);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

.project-title,
.project-echo {
  position: absolute;
  left: 50%;
  top: 50%;
  font-size: 60px;
  font-family: syne-bold;
  letter-spacing: 0.05em;
  line-height: normal;
  white-space: nowrap;
  margin: 0;
}

/* Solid title, centred and on top. */
.project-title {
  transform: translate(-50%, -50%);
  z-index: 1;
}

/* Secondary "echo" copies: same size as the title, two above and two below,
   each clipped to a single half so only the outer part of the letters shows —
   top halves up top, bottom halves below. Centred with the `translate`
   property so `transform` is free for the entrance slide, and pushed
   off-centre via `top`. */
.project-echo {
  translate: -50% -50%;
  color: var(--bg);
  -webkit-text-stroke: 2px var(--ink);
  paint-order: stroke fill;
  animation: echo-emerge 0.5s ease-out 0.35s backwards;
}

/* Every echo starts collapsed on the title (top:50%) and expands out to its
   resting `top`, so they all emerge from the same place — the centre. The
   resting value is the element's own base `top`; keeping it out of the
   keyframe avoids the var()-in-@keyframes pitfall. */
@keyframes echo-emerge {
  from { top: 50%; opacity: 0; }
  to   { opacity: 1; }
}

/* The frame div is the first child, so the echoes are children 2,3,5,6.
   Offsets in em (so they shrink with the font on mobile) and evenly stepped
   so the spacing between every copy is the same. inset(0 0 50% 0) hides the
   bottom half; inset(50% 0 0 0) hides the top. */
.project-echo:nth-child(2) { top: calc(50% - 1.7em);  clip-path: inset(0 0 50% 0); }
.project-echo:nth-child(3) { top: calc(50% - 0.85em); clip-path: inset(0 0 50% 0); }
.project-echo:nth-child(5) { top: calc(50% + 0.85em); clip-path: inset(50% 0 0 0); }
.project-echo:nth-child(6) { top: calc(50% + 1.7em);  clip-path: inset(50% 0 0 0); }

.project-meta {
  display: flex;
  justify-content: space-around;
  max-width: 960px;
  margin: 24px auto 0;
  font-family: syne-semi;
  font-size: 16px;
  letter-spacing: 0.05em;
  text-align: center;
}

.project-meta li {
  width: 190px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--ink);
}

/* Placeholder media frames from the design (to be filled with real work). */
.media-placeholder {
  border: 2px solid var(--ink);
  height: 424px;
  width: 100%;
}

.media-row {
  display: flex;
  gap: 40px;
}

.media-row .media-placeholder {
  flex: 1;
  min-width: 0;
}

@media (max-width: 768px) {
  .project-hero {
    height: 60vh;
    margin-top: 60px;
  }

  /* Smaller title on mobile; the em-based echo offsets shrink with it. */
  .project-title,
  .project-echo {
    font-size: 36px;
  }

  .project-meta {
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }

  .media-placeholder {
    height: 240px;
  }

  .media-row {
    flex-direction: column;
    gap: 20px;
  }

  /* In column direction flex:1 would collapse the boxes to their borders */
  .media-row .media-placeholder {
    flex: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .project-hero-frame,
  .project-echo {
    animation: none;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}


/* CONTACT FORM */

.contact-form {
  max-width: 600px;
  margin-top: 60px;
}

.form-field {
  margin-bottom: 40px;
}

.form-field label {
  display: block;
  font-family: syne-semi;
  font-size: 16px;
  text-transform: lowercase;
  margin-bottom: 12px;
}

.form-field input,
.form-field textarea {
  width: 100%;
  box-sizing: border-box;
  border: 2px solid var(--ink);
  border-radius: 0;
  background-color: var(--bg);
  font-family: syne-semi;
  font-size: 18px;
  letter-spacing: 0.06em;
  padding: 14px 16px;
  transition: box-shadow ease .2s;
}

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

.form-field input:focus,
.form-field textarea:focus {
  outline: none;
  box-shadow: 6px 6px 0 var(--ink);
}

/* Honeypot — parked far off-screen. Deliberately NOT display:none, which
   many spam bots detect and skip; aria-hidden + tabindex=-1 keep it out of
   screen readers and tab order so real humans never touch it. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Mirrors the .text-baseline appear transition */
.form-feedback {
  font-size: 12px;
  text-transform: uppercase;
  margin: 0;
  margin-top: 16px;
  min-height: 15px;
  opacity: 0;
  transform: translateY(-10px);
  transition: .5s;
  display: none;
}

.form-feedback.appear {
  display: block;
  opacity: 100%;
  transform: translateY(0px);
}

/* Inline field errors, in place of the browser's own validation bubbles.
   Same appear transition as the feedback above. */
.field-error {
  display: none;
  font-family: syne-semi;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-top: 10px;
  opacity: 0;
  transform: translateY(-10px);
  transition: .5s;
}

.field-error.appear {
  display: block;
  opacity: 100%;
  transform: translateY(0px);
}

/* Flag the offending field without introducing a colour the palette
   generator doesn't control — a dashed border reads as "look here". */
.form-field input[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-style: dashed;
}

/* Slowly-spinning pill badge — the same visual as the about page's click
   game (.click-game:after), shared by the contact form's submit button and
   the home page's "let's discuss" link so all three read as one component.
   Works on both <button> and <a>. */
.pill-badge {
  /* Same type and measure as the about page's click-game badge
     (.click-game:after): inherited Syne at 12px, a 60px measure and 20px of
     padding, so every badge on the site is the same object. Syne rather than
     syne-bold matters for size as well as texture — in the bold cut
     "discuss" and "another" are wider than the 60px measure and would force
     the badge to grow; in Syne they fit, so the badge stays round. */
  font-size: 12px;
  /* A <button> does not inherit letter-spacing through the UA stylesheet, so
     it came out "normal" while the click-game badge inherited the body's
     tracking. Inherit explicitly so all the badges are tracked alike. */
  letter-spacing: inherit;
  text-align: center;
  color: var(--bg);
  background-color: var(--ink);
  border: none;
  display: block;
  box-sizing: content-box;
  padding: 20px;
  /* An explicit width, not max-width: a <button> shrinks to fit its label
     ("Send" gave a 37px box) while a block <a> fills to the cap, so the two
     came out different sizes. Fixing the measure makes every badge the same
     100px across, like the click-game one. */
  width: 60px;
  border-radius: 30px;
  margin: 40px 0;
  animation: rotating 42s infinite linear;
}

/* Hover parks the badge back at its default, unrotated angle. */
.pill-badge:hover:not(:disabled) {
  animation: none;
  rotate: 0deg;
  transition: rotate .4s ease;
}

/* Form-specific states layered on top of the shared badge. */
.submit-btn:disabled {
  opacity: 30%;
  cursor: not-allowed;
}

.submit-btn:disabled:before {
  display: none;
}

/* Send: the badge collapses into a small dot and vanishes. */
.submit-btn.is-sending {
  animation: none;
  rotate: 0deg;
  transform: scale(0.04);
  opacity: 0;
  pointer-events: none;
  transition: transform .45s cubic-bezier(.55, 0, .68, .2),
              opacity .18s ease .34s,
              rotate .45s ease;
}

/* Send confirmation. It replaces the form rather than sitting under it, and
   it stays on screen: this is the receipt for a message that is already gone,
   so it must not fade away to nothing. */

/* The filled-in fields fold up as the message leaves. Collapsing the space
   matters as much as the fade: left in flow, the receipt ends up marooned at
   the bottom of a tall empty gap where the form used to be. */
.contact-form.is-sent .form-field {
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  max-height: 0;
  margin: 0;
  overflow: hidden;
  transition: opacity .22s ease, transform .22s ease,
              max-height .32s ease .06s, margin .32s ease .06s;
}

/* The badge animates itself (.is-sending collapses it to a dot); this rule
   only reclaims the 134px of space it leaves behind, and waits for that
   animation to finish first. Setting `transform` here would override the
   dot collapse, since this selector is the more specific one. */
.contact-form.is-sent .submit-btn {
  max-height: 0;
  margin: 0;
  overflow: hidden;
  pointer-events: none;
  transition: max-height .3s ease .28s, margin .3s ease .28s;
}

.send-celebration {
  position: relative;
  display: none;
  margin: 40px 0;
}

.send-celebration.is-active {
  display: block;
}

/* Scoped under .send-celebration so these beat `.main-section p`, which is
   the more specific selector and was forcing a 40px gap on both. */
.send-celebration .send-thanks {
  margin: 0 0 6px;
  font-family: syne-bold;
  font-size: 40px;
  line-height: 1.1;
  letter-spacing: 0.06em;
  color: var(--ink);
  opacity: 0;
  animation: thanks-in .5s cubic-bezier(.2, .8, .3, 1) forwards;
}

@keyframes thanks-in {
  from { opacity: 0; translate: 0 12px; }
  to   { opacity: 1; translate: 0 0; }
}

.send-celebration .send-note {
  max-width: 50ch;
  margin: 0 0 24px;
  font-family: syne-semi;
  font-size: 14px;
  color: var(--muted);
  opacity: 0;
  animation: thanks-in .5s ease .14s forwards;
}

/* The two glasses sit on their own row above the heading, in a box the
   hearts can burst out of. */
.send-beers {
  position: relative;
  display: inline-flex;
  margin-bottom: 14px;
  font-size: 44px;
  line-height: 1;
}

.send-beer {
  display: inline-block;
}

/* The mug glyph is drawn handle-on-the-right, so mirroring the left one
   turns the pair inwards and they clink face to face. */
.send-beer-left {
  transform: scaleX(-1);
}

.send-beer-left  { animation: cheers-left  .6s cubic-bezier(.3, 1.4, .5, 1) .1s both; }
.send-beer-right { animation: cheers-right .6s cubic-bezier(.3, 1.4, .5, 1) .1s both; }

/* `translate`/`rotate` are separate properties here, so they stack with the
   `transform: scaleX(-1)` mirror instead of cancelling it. */
@keyframes cheers-left {
  0%   { opacity: 0; translate: -22px 0; rotate: 26deg; }
  60%  { opacity: 1; translate: 3px 0;   rotate: -10deg; }
  100% { opacity: 1; translate: 0 0;     rotate: 0deg; }
}

@keyframes cheers-right {
  0%   { opacity: 0; translate: 22px 0;  rotate: 26deg; }
  60%  { opacity: 1; translate: -3px 0;  rotate: -10deg; }
  100% { opacity: 1; translate: 0 0;     rotate: 0deg; }
}

/* Confetti is created by main.js inside .send-beers, so the burst comes out
   of the glasses at the moment they meet. --dx/--dy/--rot randomise each. */
.confetti {
  position: absolute;
  top: 50%;
  left: 50%;
  font-size: var(--size, 14px);
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  animation: confetti-pop .9s cubic-bezier(.15, .7, .3, 1) .34s forwards;
}

@keyframes confetti-pop {
  0%   { opacity: 1; translate: 0 0; rotate: 0deg; scale: .4; }
  70%  { opacity: 1; }
  100% {
    opacity: 0;
    translate: var(--dx) var(--dy);
    rotate: var(--rot);
    scale: 1;
  }
}

/* "send another" is a .pill-badge like the submit button and the home page's
   CTA, so it inherits their shape, type and fill. The fade-in lives on the
   wrapper: put it on the button and it would replace the badge's spin, and
   the badge's hover rule (animation: none) would drop the button back to
   opacity 0 and make it vanish under the cursor. */
.send-again-wrap {
  margin: 0;
  opacity: 0;
  animation: thanks-in .5s ease .28s forwards;
}

.send-again {
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .pill-badge,
  .send-thanks,
  .send-note,
  .send-again,
  .send-beer,
  .confetti {
    animation: none;
    opacity: 1;
  }

  .submit-btn.is-sending {
    transition: opacity .2s ease;
  }
}


/* -------------------------------------------------------------------------- */
/* MEDIA QUERIES START                             */
/* -------------------------------------------------------------------------- */
@media (max-width: 1200px) {
  body {
    position: relative;
    margin: 0 60px;
    font-size: 20px;
  }
   header {
    width: 80%;
    left: 10%;
  }

  #works .works-list .works-links {
    margin-bottom: 72px;
  }

}


/* Tablet Portrait & Small Laptops (max-width: 992px) */
@media (max-width: 992px) {
  body {
    position: relative;
    margin: 0 60px;
    font-size: 20px;
  }

  body p {
    font-size: 20px;
  }

  header {
    width: 80%;
    left: 10%;
  }

  header .logo {
    left: 0;
  }

  header nav {
    right: 0;
  }

  header nav .main_nav {
    gap: 15px;
  }

  .big-title {
    font-size: 72px;
  }

  .main-section {
    margin: 120px 40px;
  }

  .squared {
    max-width: 400px;
    max-height: 400px;
  }

  #works .works-list {
    font-size: 50px;
  }

  .resume-list p {
    width: auto; /* Allow text to wrap naturally */
    max-width: 500px; /* Cap width for readability */
  }

  .skills-row {
    gap: 80px; /* Reduce gap */
  }

  .resume-decoration {
    height: 350px;
    width: 350px;
    top: 150px;
  }

  footer .social-list li {
    margin-right: 20px;
  }
}

/* Mobile Devices (max-width: 768px) */
@media (max-width: 768px) {
  body {
    margin: 0 20px; /* Smaller margins for mobile */
    font-size: 18px;
  }

  body p {
    font-size: 18px;
  }

  header {
    width: 100%;
    padding-top: 40px; /* Reduce header padding */
    position: static; /* Remove fixed position for header on mobile */
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  header .logo {
    position: static; /* Remove absolute positioning */
    top: auto;
    left: auto;
    margin-bottom: 20px; /* Space between logo and nav */
  }

  header nav {
    position: static; /* Remove absolute positioning */
    right: auto;
    top: auto;
  }

  header nav .main_nav {
    display: flex;
    justify-content: center;
    gap: 10px;
  }

  header nav .main_nav li {
    float: none;
    margin: 0 10px;
  }

  .big-title {
    /* Scaled down from the 96px desktop size, but no further: at the sizes
       this used to use, faction's outline thinned to a hairline and the
       titles stopped carrying the page. The stroke puts that weight back. */
    font-size: 80px;
    -webkit-text-stroke: 1px var(--ink);
    paint-order: stroke fill;
    margin-bottom: 30px;
  }

  .main-section {
    margin: 80px 0; /* Adjust margins */
    padding: 0 10px; /* Add some internal padding */
  }

  .squared {
    /* Stays a square, like every other screen size. Stretched to the full
       height of the phone it stopped reading as the frame it is on desktop,
       and the photo inside it filled the screen behind the text. The border
       thickens with the titles so the whole page keeps one weight. */
    width: calc(100vw - 48px);
    height: calc(100vw - 48px);
    max-width: none;
    max-height: none;
    border-width: 3px;
  }

  .main-section p {
    margin: 20px 0;
    max-width: 100%; /* Allow paragraphs to take full width */
  }

  #works .works-list {
    font-size: 44px; /* Smaller work titles */
  }

  /* Far enough apart that only one project is ever in the middle of the
     screen — that is what drives the image cycle on a touch screen, where
     there is no hover to trigger it. */
  #works .works-list .works-links {
    margin-bottom: 140px;
  }

  /* Mobile has no hover, so pin the external arrow to the right edge of
     every row instead of revealing it. The title reserves space so it wraps
     before reaching the arrow. */
  #works .works-links > a {
    padding-right: 1.1em;
  }

  #works .works-links > a::after {
    position: absolute;
    top: 0.08em;
    right: 0;
    margin-left: 0;
    font-size: 0.7em;
    line-height: 1;
    opacity: 1;
    transform: none;
    color: var(--ink);
  }

  .contact-section p {
    margin: 20px 0;
    max-width: 100%;
  }

  /* Desktop hangs the links above the rectangle and the coordinates below
     it, both absolutely positioned. On mobile the rows come back into flow
     and the rectangle is drawn between them (see .geo::before) — keeping the
     border on the footer itself turned the whole thing into a panel. */
  footer {
    height: auto;
    border: none;
    padding: 0;
    text-align: center;
    margin-top: 40px;
    margin-bottom: 40px;
  }

  /* Both rows are absolutely positioned on desktop, which pushed them
     outside the frame once the footer stacked. Back in flow on mobile. */
  footer .social-list,
  footer .geo {
    position: static;
    margin: 0;
  }

  footer .social-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 24px;
  }

  footer .social-list li {
    float: none;
    margin-right: 0;
  }

  footer .geo {
    display: block;
  }

  /* The divider is the hollow rectangle the desktop footer draws, not a
     line: a single rule has none of its weight. It hangs off .geo because
     both rows are in flow here, and that is the one spot between them a
     pseudo-element can sit. */
  footer .geo::before {
    content: '';
    display: block;
    height: 20px;
    border: 2px solid var(--ink);
    box-sizing: border-box;
    margin: 24px 0;
  }

  .click-game {
    /* Tall enough for the photos to register as photos. The desktop box is
       600px; this keeps most of that presence without pushing the badge
       below the fold. */
    height: 500px;
  }

  /* No size override here: this badge is the one every button on the site
     is matched to, so shrinking it on mobile broke that pairing. */

  .skills-row {
    flex-direction: column; /* Stack skills columns */
    gap: 40px;
  }

  .skills-row:first-child {
    margin-bottom: 40px;
  }

  .skill-list .skill-item {
    font-size: 20px;
    margin: 10px 0;
  }

  .resume-list .resume-company {
    font-size: 28px;
  }

  .resume-list .resume-date {
    font-size: 12px;
  }

  .resume-list p {
    font-size: 14px;
    width: auto;
  }

  .resume-item {
    --rail-pad: 14px;
    padding: 28px var(--rail-pad) 44px;
  }

  .resume-decoration-parent {
    position: static; /* Remove absolute positioning */
    height: auto;
  }

  .resume-decoration {
    position: static; /* Remove sticky positioning */
    height: 200px;
    width: 100%;
  }
}

/* Small Mobile Devices (max-width: 480px) */
@media (max-width: 480px) {
  .big-title {
    font-size: 64px;
  }

  /* As large as the longest title ("Proximus +") fits on one line beside
     the arrow. Any bigger and it overflowed the column, breaking onto a
     second line with the "+" stranded on its own. */
  #works .works-list {
    font-size: 30px;
  }

  /* A little shorter than at 768px, in step with the narrower screen —
     it used to be taller here, which made no sense. */
  .click-game {
    height: 440px;
  }

  .resume-list .resume-company {
    font-size: 24px;
  }
}

/* -------------------------------------------------------------------------- */
/* MEDIA QUERIES END                             */
/* -------------------------------------------------------------------------- */
