/* ═══════════════════════════════════════════════════════════════
   Self-hosted type. These files are served from this domain, so no
   visitor IP is sent to Google before they have agreed to anything —
   see README, and CLAUDE.md "third parties".

   Archivo is VARIABLE: the wdth axis is what the name in the masthead
   uses. Replacing it with static instances would silently break that.
   Both families are SIL Open Font License 1.1 — see fonts/LICENSE.
   ═══════════════════════════════════════════════════════════ */
@font-face{
  font-family:'Archivo';
  font-style:normal;
  font-weight:100 900;
  font-stretch:62% 125%;
  font-display:swap;
  src:url("fonts/archivo-var-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'Archivo';
  font-style:normal;
  font-weight:100 900;
  font-stretch:62% 125%;
  font-display:swap;
  src:url("fonts/archivo-var-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/jetbrains-mono-400-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/jetbrains-mono-400-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("fonts/jetbrains-mono-500-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("fonts/jetbrains-mono-500-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ═══════════════════════════════════════════════════════════════
   Design tokens. Change colour and spacing here, never inline.

   Warm neutral paper, warm near-black ink, deep petrol accent. The
   warmth is deliberate: it sits with the portrait's background and is
   easier to read for long stretches than a cool blue-grey. Dark mode
   is a genuine second palette, not an inversion.
   ═══════════════════════════════════════════════════════════ */
:root{
  --paper:#F4F2EF;
  --card:#FBFAF8;
  --ink:#1F1D1B;
  --ink-mid:#6C6660;
  --rule:#DCD7D0;
  --accent:#0F5F5C;
  --accent-wash:#E2EDEB;
  --on-accent:#FFFFFF;
  --shadow:16 14 12;

  --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
  --sans:"Archivo",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;

  --gut:clamp(20px,4vw,64px);
  --rail:150px;
  --max:1240px;
}

@media(prefers-color-scheme:dark){
  :root{
    --paper:#171614;
    --card:#201E1B;
    --ink:#EFEBE5;
    --ink-mid:#A39B92;
    --rule:#37342F;
    --accent:#57BDB0;
    --accent-wash:#1E3B37;
    --on-accent:#10201E;
    --shadow:0 0 0;
  }
  /* Photographs are lit for a white page; take the glare off. */
  .portrait img,.card-img img,.hero-img img,.plate img{filter:brightness(.92) contrast(1.02)}
}

*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
  background:var(--paper);color:var(--ink);
  font-family:var(--sans);font-size:17px;line-height:1.55;
  font-variation-settings:"wdth" 100,"wght" 400;
  -webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit}
button{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}

/* Text is not selectable — a deliberate choice, see CLAUDE.md. Form fields
   are exempt, or the contact form could not be edited or corrected. */
body{-webkit-user-select:none;user-select:none}
input,textarea,select,[contenteditable],.allow-select{-webkit-user-select:text;user-select:text}

.wrap{max-width:var(--max);margin:0 auto;padding:0 var(--gut)}

.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);padding:10px 16px;z-index:100}
.skip:focus{left:0}

/* Present to screen readers and to the PDF text layer, invisible on screen. */
.vh{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;
}

/* ═══ Icons ════════════════════════════════════════════════
   The silhouette PNGs are drawn as a CSS mask filled with currentColor, so
   they inherit the text colour and flip correctly in dark mode — an <img>
   would stay black on a dark background. The path arrives as --i from PHP.
   They are decorative: every one sits next to a real text label. */
.ico{
  display:inline-block;flex:none;
  width:1.45em;height:1.45em;
  vertical-align:-.32em;margin-right:.45em;
  background-color:currentColor;
  -webkit-mask:var(--i) no-repeat center/contain;
          mask:var(--i) no-repeat center/contain;
}

/* Brand logos keep their own colours, so they stay images rather than masks.
   They are supplied as transparent PNGs and sit directly on the page — no
   tile, no border. Each one already carries whatever backdrop it needs
   (ArchiCAD, for instance, is a self-contained blue square). */
.logo{
  display:inline-block;width:30px;height:30px;flex:none;
  object-fit:contain;
}

/* mono utility label, used everywhere */
.lbl{
  font-family:var(--mono);font-size:12px;font-weight:500;
  letter-spacing:.09em;text-transform:uppercase;color:var(--ink-mid);
}
.lbl a{text-decoration:none;border-bottom:1px solid var(--rule)}
.lbl a:hover{color:var(--accent);border-color:var(--accent)}
.lbl a + a{margin-left:14px}

/* ═══ Scroll-linked motion ═════════════════════════════════════
   Position, not playback. site.js writes opacity and transform straight from
   the scroll offset on every frame, so the page moves only while the scrollbar
   moves and runs backwards when you scroll up.

   Note the absence of a `transition` here, and it must stay absent: a
   transition would ease towards each new value and lag half a second behind
   the scrollbar, which is precisely the disconnected feeling this replaces.

   The moving state is applied by JS (it adds .rv), never by the stylesheet
   alone:
     - no JavaScript      -> the class is never added, everything is visible
     - reduced motion     -> the script returns early, nothing is hidden
     - print / PDF        -> forced visible again in the @media print block
   A stylesheet-only hidden state would mean any of those three cases renders
   a blank page, which is exactly the failure worth engineering around. */
/* site.js sets exactly one thing per element: --p, from 0 (a screen below) to
   1 (settled). Everything below is choreography written against that number,
   so the motion design lives here in CSS and the script stays a scroll reader.

   --p defaults to 1, so any element the script never reaches — or if the
   script never runs at all — is already in its finished position. */
/* ONE idea, applied everywhere: each block hinges into place against the rail,
   like a drawing sheet being laid onto a board. It arrives from the right and
   from further away, swung open on a vertical hinge at its left edge, and
   closes flat as it settles. The rail label is the spine it closes against, so
   that one travels along the rail instead.

   Earlier versions sent neighbours in from opposite sides, meaning to suggest
   meshing gears. It read as randomness. Everything now shares one origin, one
   axis and one direction; the only thing that differs between blocks is WHEN
   they arrive, which is what makes a sequence legible rather than chaotic.

   No `will-change`: promoting 29 blocks costs memory, rounds their geometry to
   device pixels (which broke exact reversibility), and made Chromium rasterise
   the portrait at full size into the PDFs. Transform and opacity are still
   GPU-accelerated while they change. */

/* The depth the hinge swings through. Without a perspective on the parent,
   rotateY is a flat squash and the whole effect collapses. */
.sec-body,.pdata,.cols,.grid,.plates,.phead{perspective:1500px}

.rv{--p:1;opacity:var(--p)}

/* Transforms apply ONLY while a block is actually moving. A settled element
   keeps no transform at all, so it holds no compositing layer and its text
   stays crisp — an identity transform would still blur it slightly. */
.sec > .lbl.rv.moving{transform:translate3d(calc((1 - var(--p)) * -34px),0,0)}
.sec > .lbl.rv::before{transform:scaleX(var(--p));transform-origin:left}

.role.rv.moving,
.entry.rv.moving,
.pd-row.rv.moving,
.cols > div.rv.moving,
.card.rv.moving,
.plate.rv.moving,
.prose section.rv.moving,
.phead h1.rv.moving,
.phead .intro.rv.moving,
.next.rv.moving{
  transform-origin:left center;
  transform:
    translate3d(calc((1 - var(--p)) * 44px), calc((1 - var(--p)) * 10px), calc((1 - var(--p)) * -70px))
    rotateY(calc((1 - var(--p)) * -8deg));
}

/* Depth of field: distant blocks are soft and pull into focus as they land.
   Kept light — a heavy blur frozen at the bottom of the screen looks like a
   rendering fault rather than an effect. */
.role.rv.moving,.entry.rv.moving,.card.rv.moving,.plate.rv.moving,.cols > div.rv.moving{
  filter:blur(calc((1 - var(--p)) * 1.5px));
}

/* Icons turn into place with the same rotational sense as the hinge. */
.pd-row.rv.moving .pd-ico,
.cols > div.rv.moving h3 .ico,
.sec > .lbl.rv.moving .ico{
  transform:rotate(calc((1 - var(--p)) * -32deg)) scale(calc(0.65 + var(--p) * 0.35));
}

/* Masthead parallax is written inline by the same loop. */
.wf-name,.portrait{will-change:transform}

/* The sticky filter bar only earns a shadow once it is actually stuck. */
.filter{transition:box-shadow .25s ease}
html.is-scrolled .filter{box-shadow:0 6px 18px -12px rgba(0,0,0,.35)}

/* ═══ DIC — staged entrance ════════════════════════════════════
   Owned entirely by this block, the DIC block in site.js, partials/dic.php,
   and the dic.enabled flag in data/content.json. Nothing outside those four
   refers to it, so setting the flag to false rolls the whole thing back.

   Two states, both driven by classes on <html>:
     .dic         the feature is on at all
     .dic-intro   the opening card is up (server-rendered, so no first-paint
                  flash; JS removes it on the first click, scroll or keypress)

   The splash carries copies of the name and portrait rather than moving the
   real header, so a failed script cannot strand the page — see the failsafe
   animation below, which dismisses it with no JavaScript at all. */

.dic-splash{
  position:fixed;inset:0;z-index:80;
  display:flex;align-items:center;justify-content:center;
  background:var(--paper);
  padding:var(--gut);
  overflow:hidden;          /* a long name can never push the page sideways */
  /* Failsafe: if the script never runs, this clears itself. The page beneath
     is the real, complete page, so nothing is lost by it going away. */
  animation:dic-failsafe 1ms linear 12s forwards;
}
@keyframes dic-failsafe{to{opacity:0;visibility:hidden;pointer-events:none}}

.dic-splash-in{
  display:flex;flex-direction:column;align-items:center;
  gap:clamp(20px,3.5vw,40px);
  animation:dic-arrive .9s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes dic-arrive{from{opacity:0;transform:translateY(18px) scale(.97)}}

/* .dic-portrait carries no styling of its own — it shares the .portrait-img
   rule further down, so the card and the header can never drift apart. */

/* Deliberately larger than the header it resolves into — that size difference
   is the whole point of the opening card, and the FLIP scales it down on the
   way out. Tuned to a 16-character name: at the top of the clamp it measures
   about 1290px, which still clears the gutters on a 1500px screen. Re-check
   both ends if the name in content.json changes length. */
.dic-name{
  font-size:clamp(32px,8.8vw,132px);line-height:.9;letter-spacing:-.022em;
  font-variation-settings:"wdth" 116,"wght" 800;
  white-space:nowrap;text-align:center;max-width:100%;
}

/* While the card is up the page behind must not scroll — the first scroll is
   the gesture that dismisses it, not a scroll of the document. */
html.dic-intro{overflow:hidden}
html.dic-intro body{overflow:hidden}

/* Dismissal. JS measures where the real portrait and name sit and moves these
   copies onto them, so the card resolves into the header rather than merely
   fading. The background clears a little faster than the pieces travel. */
.dic-splash.dic-out{background:transparent;transition:background .45s ease .1s}

/* The transition is declared here, on the resting state — NOT inside the
   .dic-out rule. A property whose transition only begins to exist in the same
   style recalculation that changes its value has no previous value to animate
   from, so it snaps. That is exactly what happened: the name eased across
   while the portrait jumped straight to its destination. */
.dic-splash .dic-portrait,
.dic-splash .dic-name{
  transition:transform .72s cubic-bezier(.4,0,.2,1),opacity .32s ease .34s;
}
.dic-splash.dic-out .dic-portrait,
.dic-splash.dic-out .dic-name{opacity:0}
.dic-splash.dic-gone{display:none}

/* Someone who has asked for less motion gets the page, not a card in front of
   it. The bar is motion-free once in place, so it stays. */
@media(prefers-reduced-motion:reduce){
  .dic-splash{display:none}
  html.dic-intro,html.dic-intro body{overflow:visible}
}

/* ═══ Click-to-pulse ═══════════════════════════════════════════
   Clicking a block flashes it, so a reader can mark where they are on
   a dense page. Drawn on a pseudo-element behind the content, so it
   never disturbs the text it highlights. JS adds and removes .pulse. */
.pulse{position:relative}
.pulse::after{
  content:"";position:absolute;inset:-10px -16px;z-index:-1;
  pointer-events:none;border-radius:4px;opacity:0;
  background:var(--accent-wash);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 34%,transparent);
  animation:segpulse 1.4s cubic-bezier(.4,0,.2,1) 2;
}
@keyframes segpulse{0%,100%{opacity:0}28%{opacity:1}}

/* ═══ Nav ══════════════════════════════════════════════════ */
.nav{border-bottom:1px solid var(--rule);background:var(--paper)}
.nav-in{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:16px var(--gut)}
.nav-home{
  font-family:var(--mono);font-size:13px;letter-spacing:.08em;text-transform:uppercase;
  text-decoration:none;
}
.nav-links{display:flex;align-items:center;gap:22px}
.nav-links a,.nav-contact{
  font-family:var(--mono);font-size:13px;letter-spacing:.06em;text-transform:uppercase;
  text-decoration:none;color:var(--ink-mid);background:none;border:0;padding:0;cursor:pointer;
  transition:color .15s;
}
.nav-links a:hover,.nav-contact:hover{color:var(--accent)}
.nav-links a[aria-current="page"]{color:var(--ink);text-decoration:underline;text-underline-offset:4px}
/* Language switch — quiet, but visibly a control rather than a nav item. */
.nav-lang{border:1px solid var(--rule);border-radius:100px;padding:5px 12px}
.nav-lang:hover{border-color:var(--accent)}
.nav-lang-short{display:none}

/* ═══ Masthead (home) ══════════════════════════════════════ */
.masthead{padding:clamp(30px,6vw,64px) var(--gut) clamp(26px,4vw,48px)}
.masthead > .lbl{display:block;margin-bottom:clamp(22px,5vw,48px)}

/* One line, one name. The size is tuned to a 16-character name — if the
   name in content.json changes length, re-check at 360px and at 1240px. */
.waterfall{margin-bottom:clamp(28px,5vw,46px)}
.wf-line{display:flex;align-items:baseline;gap:16px;border-bottom:2px solid var(--ink);padding-bottom:10px}
.wf-name{line-height:.9;letter-spacing:-.022em;white-space:nowrap;flex:0 1 auto;min-width:0}
.wf-1 .wf-name{font-size:clamp(30px,8.4vw,104px);font-variation-settings:"wdth" 116,"wght" 800}

.wf-line,.spec{opacity:0;transform:translateY(10px);animation:rise .55s cubic-bezier(.2,.7,.3,1) forwards}
.spec{animation-delay:.1s}
@keyframes rise{to{opacity:1;transform:none}}

.spec{display:grid;gap:24px 32px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));padding-top:4px}
.spec-k{display:block;margin-bottom:6px}
.spec-v{font-size:16px;font-variation-settings:"wght" 500;line-height:1.45}
/* Same treatment as the role line on the cover letter, so the two documents
   read as one set. */
.spec-role{color:var(--accent)}
.spec-v a{text-decoration:none;border-bottom:1px solid var(--rule);padding-bottom:1px}
.spec-v a:hover{color:var(--accent);border-color:var(--accent)}
/* The showcase reference, an aside within the status line — quieter than the
   status text it hangs off, so it reads as a footnote rather than a second item. */
.spec-aside{display:inline;color:var(--ink-mid);font-size:14px;font-variation-settings:"wght" 400}
.spec-aside a{color:var(--accent);border-bottom-color:color-mix(in srgb,var(--accent) 40%,transparent)}
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--accent);margin-right:8px;vertical-align:middle}

/* Portrait leads, text follows. */
.hero-body{
  display:grid;grid-template-columns:auto minmax(0,1fr);
  gap:clamp(26px,4vw,52px);align-items:center;
  margin-top:clamp(30px,4vw,44px);
}
.intro{max-width:56ch;font-size:clamp(17px,2.1vw,21px);line-height:1.62}

/* Circular, ringed in the accent. object-position pulls the crop upwards:
   a square cut from a 714x1000 portrait would otherwise centre on the chest. */
.portrait{width:clamp(132px,16vw,196px)}

/* A background image, not an <img> — see index.php. 50% 16% pulls the crop up:
   a square cut from a 714x1000 portrait would otherwise centre on the chest.

   DIC's opening card shares this rule rather than restating it. That matters:
   the two were separate declarations once, the framing here was retuned, and
   the card silently kept the old crop and the old radius — so the portrait
   appeared to change shape halfway through the entrance. One rule, one look. */
.portrait-img,
.dic-portrait{
  width:100%;aspect-ratio:1;
  background:var(--portrait) 50% 16% / 85% no-repeat,var(--card);
  border-radius:70%;
  border:3px solid var(--accent);
  box-shadow:0 0 0 1px var(--paper) inset;
  -webkit-user-drag:none;-webkit-touch-callout:none;
}
/* Only the size differs: the card shows it larger, and the entrance scales it
   down onto the header's. Must come after the shared rule above. */
.dic-portrait{width:clamp(150px,23vw,260px)}

/* ═══ Page header (work, project) ══════════════════════════ */
.phead{padding:clamp(32px,6vw,66px) var(--gut) clamp(24px,3vw,36px)}
.phead > .lbl{display:block;margin-bottom:14px}
.phead h1{
  font-size:clamp(34px,7vw,78px);line-height:.95;letter-spacing:-.025em;
  font-variation-settings:"wdth" 112,"wght" 700;
}
.phead .intro{margin-top:20px}
.phead-meta{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:16px}

/* ═══ Filter bar ═══════════════════════════════════════════ */
.filter{
  position:sticky;top:0;z-index:20;
  background:color-mix(in srgb,var(--paper) 86%,transparent);
  backdrop-filter:blur(10px);
  border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
}
.filter-in{display:flex;align-items:center;gap:12px;padding:13px var(--gut);overflow-x:auto;scrollbar-width:none}
.filter-in::-webkit-scrollbar{display:none}
.filter-in > .lbl{flex:none;padding-right:4px}
.chip{
  flex:none;font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;
  background:none;color:var(--ink-mid);border:1px solid var(--rule);border-radius:100px;
  padding:7px 14px;cursor:pointer;transition:.15s;
}
.chip:hover{border-color:var(--ink);color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}

/* ═══ Rail section ═════════════════════════════════════════
   The accent tick above each rail label is the segmentation cue — it
   gives the eye a fixed left-hand rhythm to travel down. */
.sec{display:grid;grid-template-columns:var(--rail) 1fr;gap:0 36px;padding:clamp(40px,5.5vw,66px) 0;border-bottom:1px solid var(--rule)}
.sec:last-child{border-bottom:0}
/* The rail label is a fixed-width grid column, so an over-long label does not
   widen it — it silently overflows into the content. Let it wrap instead.
   German is the constraint here: "BERUFSERFAHRUNG" is half again as long as
   "EXPERIENCE", so check any new label in German, not English. */
.sec > .lbl{padding-top:4px;font-size:13px;color:var(--ink);letter-spacing:.1em;overflow-wrap:anywhere}
.sec > .lbl::before{content:"";display:block;width:28px;height:2px;background:var(--accent);margin-bottom:12px}
/* The icon takes over as the section's accent marker where there is one, so
   the tick and the icon never stack. It sits on its own line: inline, the
   icon plus "BERUFSERFAHRUNG" would exceed the fixed rail column. */
.sec > .lbl .ico{display:block;width:34px;height:34px;margin:0 0 11px;color:var(--accent)}
/* The credentials folder is a solid, wide shape, so at the shared size it reads
   heavier than the other section marks. One notch down evens them out. */
.sec > .lbl .ico-sm{width:28px;height:28px;margin-bottom:13px}
.sec > .lbl:has(.ico)::before{display:none}
.sec-body{min-width:0}

/* ═══ Project cards ════════════════════════════════════════ */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:28px}
.grid-lg{grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:clamp(28px,3.5vw,44px)}
.card{display:block;text-decoration:none}
.card-img{aspect-ratio:4/3;background:var(--card);border:1px solid var(--rule);border-radius:3px;overflow:hidden;margin-bottom:12px}
.card-img img{width:100%;height:100%;object-fit:cover;transition:transform .35s}
.card:hover .card-img img{transform:scale(1.03)}
.card:hover .card-t{color:var(--accent)}
.card-t{font-size:17px;font-variation-settings:"wght" 600;transition:color .18s}
.grid-lg .card-t{font-size:20px;letter-spacing:-.01em}
.card-m{font-family:var(--mono);font-size:12px;color:var(--ink-mid);margin-top:4px}
.card-b{font-size:15px;color:var(--ink-mid);margin-top:8px;line-height:1.5}

/* filter states, shared by cards and highlights */
.on{--_on:1}
.off{opacity:.28}
.card.on .card-t{color:var(--accent)}

/* ═══ Experience ═══════════════════════════════════════════ */
.role + .role{margin-top:32px;padding-top:32px;border-top:1px solid var(--rule)}
.role-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:5px 12px;margin-bottom:13px}
.role-t{font-size:20px;font-variation-settings:"wght" 600;letter-spacing:-.01em}
.role-o{font-size:20px;color:var(--ink-mid)}
.role-d{font-family:var(--mono);font-size:12px;color:var(--ink-mid);margin-left:auto;white-space:nowrap}
.hl{list-style:none}
.hl li{position:relative;padding:5px 0 5px 18px;margin-bottom:3px;font-size:16px;line-height:1.6;border-left:2px solid transparent;transition:opacity .2s,background .2s,border-color .2s}
.hl li::before{content:"";position:absolute;left:4px;top:15px;width:5px;height:1px;background:var(--ink-mid)}
.hl li.on{background:var(--accent-wash);border-left-color:var(--accent);padding-left:16px}
.hl li.on::before{display:none}

/* ═══ Simple lists ═════════════════════════════════════════ */
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:28px}
.cols h3{display:flex;align-items:center;font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-mid);margin-bottom:10px}
.cols ul{list-style:none;font-size:16px}
.cols li{padding:3px 0}
.cols li.has-logo{display:flex;align-items:center;gap:11px;padding:5px 0}

/* ═══ Personal details ═════════════════════════════════════
   Icon, label and value on one row. The label column is fixed so the values
   line up; German labels ("Staatsangehörigkeit") set that width. */
/* Icon and value only — the labels are visually hidden (see index.php), so
   this is two columns, not three. */
.pdata{display:grid;gap:13px}
.pd-row{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:0 15px}
.pd-ico{width:26px;height:26px;align-self:center;margin-right:0;color:var(--ink-mid)}
.pdata dd{font-size:16px;font-variation-settings:"wght" 500}
.pd-redacted{color:var(--ink-mid);font-variation-settings:"wght" 400;font-style:italic;font-size:15px}
.entry + .entry{margin-top:20px}
.entry-t{font-size:17px;font-variation-settings:"wght" 600}
.entry-m{font-family:var(--mono);font-size:12px;color:var(--ink-mid);margin-top:3px;line-height:1.5}

/* ═══ Project page ═════════════════════════════════════════ */
.hero-img{border:1px solid var(--rule);border-radius:3px;overflow:hidden;background:var(--card);margin-bottom:clamp(30px,5vw,54px)}
.prose{max-width:62ch}
/* Legal text: quieter and denser than a case study, and it must stay readable
   in print — it is the one page someone may keep a copy of. */
.prose-legal{padding-bottom:clamp(30px,5vw,60px)}
.prose-legal p{font-size:16px;line-height:1.65;color:var(--ink-mid)}
.prose-legal section + section{margin-top:clamp(22px,3vw,32px)}
.prose h2{font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-mid);margin-bottom:11px}
.prose p{font-size:clamp(17px,1.9vw,19px);line-height:1.65}
.prose section + section{margin-top:clamp(28px,3.5vw,42px)}
.plates{display:grid;gap:clamp(22px,3vw,38px);margin-top:clamp(34px,5vw,58px)}
.plate figcaption{margin-top:10px;font-family:var(--mono);font-size:12px;color:var(--ink-mid)}
.plate img{border:1px solid var(--rule);border-radius:3px;background:var(--card);width:100%}
.next{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:10px;padding:clamp(30px,4vw,48px) 0;text-decoration:none;border-top:1px solid var(--rule)}
.next-t{font-size:clamp(24px,4vw,42px);letter-spacing:-.02em;font-variation-settings:"wdth" 108,"wght" 700;transition:color .18s}
.next:hover .next-t{color:var(--accent)}

/* ═══ Buttons ══════════════════════════════════════════════ */
.btn{
  display:inline-block;font-family:var(--mono);font-size:13px;letter-spacing:.05em;
  text-transform:uppercase;text-decoration:none;cursor:pointer;border-radius:2px;
  background:var(--ink);color:var(--paper);border:1px solid var(--ink);
  padding:14px 24px;transition:.18s;
}
.btn:hover{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.btn-ghost{background:none;color:var(--ink)}
.btn-ghost:hover{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}

/* ═══ Footer ═══════════════════════════════════════════════ */
.foot{border-top:1px solid var(--rule);margin-top:clamp(20px,4vw,40px)}
.foot-in{display:flex;flex-wrap:wrap;gap:22px;justify-content:space-between;align-items:center;padding:44px var(--gut) 60px}
.foot-l{display:flex;flex-wrap:wrap;gap:10px}
.foot-r{display:flex;flex-direction:column;gap:8px;text-align:right}
/* The quietest thing on the page — nobody needs to read the year. */
.foot-year{color:var(--rule);font-size:9px;letter-spacing:.07em}

/* ═══ Contact modal ════════════════════════════════════════ */
.modal{
  border:0;border-radius:5px;padding:clamp(26px,4vw,44px);
  max-width:520px;width:calc(100% - 2*var(--gut));
  max-height:calc(100dvh - 2*var(--gut));
  background:var(--paper);color:var(--ink);
  /* The `* { margin:0 }` reset above kills the browser's own centring for
     <dialog>, which is what pinned this to the top-left. Restore it. */
  position:fixed;inset:0;margin:auto;
  box-shadow:0 28px 70px -14px rgba(var(--shadow) / .34);
}
.modal::backdrop{background:rgba(20,18,16,.46);backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px)}
.modal-close-form{position:absolute;top:10px;right:14px}
.modal-close{background:none;border:0;font-size:26px;line-height:1;cursor:pointer;color:var(--ink-mid);padding:4px 8px}
.modal-close:hover{color:var(--accent)}
.modal-h{font-size:clamp(26px,4vw,36px);letter-spacing:-.02em;font-variation-settings:"wdth" 108,"wght" 700}
.modal-sub{font-size:15px;color:var(--ink-mid);margin:10px 0 26px;line-height:1.55}

/* ═══ Contact modal: parked state ══════════════════════════
   Shown while contact.parked is true in content.json. Same shape as the
   sent state, so the modal does not change size when the form returns. */
.modal-parked{text-align:center;padding:16px 4px 6px}
.modal-parked h2{outline:none}
.modal-parked .modal-sub{margin:12px auto 22px;max-width:36ch}
.parked-mail{margin:0 0 28px}
.parked-addr{
  display:inline-block;font-family:var(--mono);font-size:16px;
  color:var(--accent);text-decoration:none;
  border-bottom:1px solid color-mix(in srgb,var(--accent) 40%,transparent);
  padding-bottom:2px;word-break:break-all;
}
.parked-addr:hover{border-bottom-color:var(--accent)}
.parked-mail noscript span{font-family:var(--mono);font-size:15px;color:var(--ink-mid)}

/* ═══ Contact modal: sent state ════════════════════════════
   The panel is replaced, not appended to — a line of green text under the
   button is easy to miss, and leaves a filled-in form looking unsent. */
.modal-done{text-align:center;padding:14px 4px 6px}
.modal-done h2{outline:none}
.modal-done .modal-sub{margin:10px auto 26px;max-width:34ch}
.done-mark{color:var(--accent);display:flex;justify-content:center;margin-bottom:18px}
.done-tick{stroke-dasharray:40;stroke-dashoffset:40;animation:tick .45s .12s cubic-bezier(.2,.7,.3,1) forwards}
.done-ring{stroke-dasharray:145;stroke-dashoffset:145;animation:tick .55s cubic-bezier(.2,.7,.3,1) forwards}
@keyframes tick{to{stroke-dashoffset:0}}
@media(prefers-reduced-motion:reduce){
  .done-tick,.done-ring{animation:none;stroke-dashoffset:0}
}

.trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.f{display:block;margin-bottom:17px}
.f .lbl{display:block;margin-bottom:7px}
.f input,.f textarea{
  width:100%;font:inherit;font-size:16px;color:var(--ink);
  background:var(--card);border:1px solid var(--rule);border-radius:2px;
  padding:12px 13px;transition:border-color .15s;
}
.f input:focus,.f textarea:focus{border-color:var(--accent);outline:none}
.f textarea{resize:vertical;min-height:110px}
.f.invalid input,.f.invalid textarea{border-color:#B3261E}
.cform-foot{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:22px}
.cform-status{font-family:var(--mono);font-size:12px;letter-spacing:.04em;line-height:1.4}
.cform-status.ok{color:var(--accent)}
.cform-status.err{color:#B3261E}
@media(prefers-color-scheme:dark){
  .f.invalid input,.f.invalid textarea,.cform-status.err{border-color:#F2857A;color:#F2857A}
  .f.invalid input,.f.invalid textarea{color:var(--ink)}
}

/* ═══ Standalone message page (no-JS form fallback) ════════ */
.msg{padding:clamp(60px,12vw,140px) 0;max-width:56ch}
.msg h1{font-size:clamp(30px,6vw,56px);letter-spacing:-.02em;font-variation-settings:"wdth" 110,"wght" 700;margin-bottom:16px}
.msg p{font-size:19px;color:var(--ink-mid);margin-bottom:28px;line-height:1.6}

/* ═══ Responsive ═══════════════════════════════════════════ */
@media(max-width:860px){
  .hero-body{grid-template-columns:1fr;gap:26px;justify-items:start}
  .portrait{width:clamp(120px,30vw,164px)}
}
@media(max-width:760px){
  .sec{grid-template-columns:1fr;gap:18px}
  .sec > .lbl{padding-top:0}
  .pd-row{gap:0 12px}
  .role-d{margin-left:0;width:100%}
  .foot-in{flex-direction:column;align-items:flex-start}
  .foot-r{text-align:left}
  .nav-links{gap:15px}
  .nav-lang{padding:5px 10px}
}
@media(max-width:520px){
  /* Four nav items plus the name will not fit a phone at full size. */
  .nav-home,.nav-links a,.nav-contact{font-size:12px;letter-spacing:.04em}
  .nav-in{gap:12px}
  .nav-links{gap:12px}
  .nav-lang-long{display:none}
  .nav-lang-short{display:inline}
}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *{animation:none!important;transition:none!important}
  .wf-line,.spec{opacity:1;transform:none}
  /* No flashing, but the click still marks the block. JS clears it. */
  .pulse::after{opacity:.9}
}

/* ═══ Print — collapses the CV page to a clean A4 sheet ════ */
@media print{
  @page{margin:14mm}
  /* Paper turns white and the highlight wash goes away, but the palette is
     otherwise the screen's — warm ink, warm grey, and the petrol accent kept
     rather than flattened to black.

     The accent used to be forced to #000 here, on the assumption this would be
     run off on a mono office printer. It is not: these files are attached to
     emails and read on a screen, and stripping the colour left the CV looking
     like a fax next to the cover letter, which never had the override. Keep
     the two in step — if you change one palette, change both. */
  :root{
    --paper:#fff;
    --card:#fff;
    --ink:#1F1D1B;
    --ink-mid:#6C6660;
    --rule:#DCD7D0;
    --accent:#0F5F5C;
    --accent-wash:transparent;
    --on-accent:#fff;
  }
  body{font-size:10.5pt;line-height:1.34;-webkit-user-select:text;user-select:text}
  .wrap{max-width:none;padding:0}
  /* .foot-legal: the privacy link belongs to the website, not to the CV that
     gets printed or emailed to an employer. */
  .nav,.filter,.card-img,.foot-l,.foot-legal,.modal,.skip,.masthead > .lbl,.pulse::after{display:none!important}

  /* SAFEGUARD 1 of 2 for the PDFs. Scroll-linked motion is meaningless on
     paper — nothing scrolls — and any element left partway through its range
     would print faded or displaced, or vanish entirely. Force everything to
     its settled state.

     These need !important because site.js writes opacity and transform as
     INLINE styles, and an inline declaration beats a normal stylesheet rule.
     An author !important beats an inline normal declaration, which is what
     makes this hold. (generate-pdf.mjs also strips the inline values before
     printing; either alone is sufficient, and the redundancy is deliberate.) */
  /* DIC has no business on paper: no opening card, no left bar, and the
     scroll lock must not survive into the print layout. */
  .dic-splash{display:none!important}
  html.dic-intro,html.dic-intro body{overflow:visible!important}

  .rv,.rv.moving{--p:1!important;opacity:1!important;transform:none!important;filter:none!important;transition:none!important;will-change:auto!important}
  .rv *,.rv.moving *{transform:none!important;filter:none!important}
  .sec-body,.pdata,.cols,.grid,.plates,.phead{perspective:none!important}
  .sec > .lbl.rv::before{transform:scaleX(1)!important;transition:none!important}
  .wf-name,.portrait{transform:none!important;will-change:auto!important}
  .wf-1 .wf-name{font-size:26pt;font-variation-settings:"wdth" 100,"wght" 800}
  .wf-line{border-bottom:2px solid #000;margin-bottom:12px;padding-bottom:6px}
  .masthead{padding:0 0 14px}
  .phead{padding:0 0 12px}
  .hero-body{grid-template-columns:1fr auto;gap:16px;margin-top:12px}
  .intro{font-size:10pt;max-width:none;line-height:1.4}
  .role-h{margin-bottom:6px}
  .cols li{padding:1px 0}
  .cols li.has-logo{gap:8px;padding:2px 0}
  .cols h3{margin-bottom:6px}
  .pdata{gap:5px}
  .entry-m{line-height:1.35}
  .portrait{width:26mm}
  .spec{gap:8px 20px;padding-top:10px}
  /* Sections may break across pages; individual jobs and entries may not.
     Keeping a whole section together sounds tidier but pushes the entire
     Experience block onto its own sheet, turning a 2-page CV into a 3-page
     one with half of page 1 blank. */
  .sec{padding:9px 0;grid-template-columns:110px 1fr;break-inside:auto}
  /* At A4 the rail is only 110px. "BERUFSERFAHRUNG" at the screen size needs
     ~136px and would run into the job title beside it. 10px/.06em fits with
     room to spare, and a quiet rail label wants to be small on paper anyway. */
  .sec > .lbl{font-size:10px;letter-spacing:.06em;line-height:1.35}
  .sec > .lbl::before{display:none}
  .sec > .lbl .ico{width:24px;height:24px;margin-bottom:7px}
  .portrait{width:30mm}
  .portrait img{border-width:2px}
  .pdata{gap:8px}
  .pd-row{gap:0 11px}
  .pd-ico{width:19px;height:19px}
  .pdata dd{font-size:10.5pt}
  .pd-redacted{font-size:10pt}
  .logo{width:22px;height:22px}
  .cols li.has-logo{gap:8px;padding:3px 0}
  .grid{grid-template-columns:repeat(2,1fr);gap:10px}
  .card-b{display:none}
  .role,.entry,.cols > div{page-break-inside:avoid;break-inside:avoid}
  /* Tightened so the CV lands on two sheets. Without these the last two
     credentials and the footer tip onto a near-empty third page. If you
     add a section, re-check the page count — see CLAUDE.md. */
  .role + .role{margin-top:10px;padding-top:10px}
  .entry + .entry{margin-top:7px}
  .hl li{padding:1px 0 1px 14px;margin-bottom:0;line-height:1.36}
  .waterfall{margin-bottom:9px}
  .hero-body{gap:14px;margin-top:9px}
  /* Three columns on paper, not the two that auto-fit gives at this width —
     six skill groups otherwise need three rows and cost a whole page. */
  .cols{grid-template-columns:repeat(3,1fr);gap:11px 14px}
  .spec{gap:6px 20px}
  .hl li{opacity:1!important;background:none!important;border-left:none!important}
  .foot{border-top:1px solid #000;margin-top:16px}
  .foot-in{padding:10px 0 0}
  a{text-decoration:none}
}
