/* luisdelatorre.de - shared stylesheet
 *
 * The single design source for every page. Colour tokens, base rules and .btn are
 * carried over from the hand-written mockup unchanged; the repeated inline style
 * bundles of that mockup are expressed here as one class each.
 *
 * Fonts are self-hosted (site/fonts/, IBM Plex latin subsets from @fontsource):
 * no page makes a third-party request, which is what the Datenschutzerklaerung says.
 */

/* ---------------------------------------------------------------- fonts --- */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-400.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: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-500.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: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400.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: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-500.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;
}

/* --------------------------------------------------------------- tokens --- */

/* Carried over from the mockup verbatim. --surface and --dash were defined there
 * but never referenced, so they are deliberately not carried across.
 *
 * --muted carries every secondary label, note and metadata value on the site,
 * so it is set by measurement rather than by eye. Against --bg it reaches
 * 6.38:1 light and 7.80:1 dark, clearing WCAG AA for normal text with room to
 * spare, while --text stays far ahead at 15.04:1 and 14.36:1 -- the hierarchy
 * is preserved, the floor is not. */
:root {
  color-scheme: light dark;
  --bg: #F5F3F0;
  --text: #1F1E1C;
  --muted: #5C5852;
  --rule: #D9D5CF;
  --accent: #638082;
  --accent-ink: #4A6466;
  --portrait-opacity: 1;
  --portrait-surface: #BFBBB8;

  --font-sans: 'IBM Plex Sans', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1B1A18;
    --text: #ECE9E4;
    --muted: #B2ADA5;
    --rule: #34322E;
    --accent: #8AA6A7;
    --accent-ink: #A9C0C1;
    --portrait-opacity: .84;
    --portrait-surface: #2A2927;
  }
}

/* ----------------------------------------------------------------- base --- */

html,
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Only bites when a single word is wider than its column, which on a German
   * page means Qualifizierungschancengesetz in a phone-width card. Without it
   * that one word pushes a horizontal scrollbar onto the whole document. */
  overflow-wrap: break-word;
}

/* Every text element in the mockup carried an explicit margin:0. */
h1,
h2,
h3,
p,
dl,
dd,
figure {
  margin: 0;
}

/* The underline was drawn in --rule, which sits at 1.32:1 against --bg and so
 * read as no underline at all: a link was distinguished by colour alone. It is
 * now a softened currentColor -- quiet beside the text it belongs to, but at
 * 70% it still clears 3:1 against --bg at the worst pairing on the site (an
 * --accent-ink link in the light theme, 3.06:1), so the non-colour cue is
 * actually perceivable. */
a {
  color: var(--accent-ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in oklch, currentColor 70%, transparent);
  text-underline-offset: 3px;
  transition: text-decoration-color .15s, color .15s;
}

a:hover {
  text-decoration-color: currentColor;
}

/* --accent-ink rather than --accent: 5.74:1 light and 9.11:1 dark against --bg,
 * where --accent only reaches 3.84:1 light. */
a:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
  border-radius: 2px;
}

::selection {
  background: color-mix(in oklch, var(--accent) 30%, transparent);
}

.btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  color: var(--text);
  text-decoration: none;
  font-size: 15px;
  font-weight: 500;
  transition: background .15s;
}

.btn:hover {
  background: color-mix(in oklch, var(--accent) 14%, transparent);
}

/* The hero's primary action, and the only filled element on the site. The fill
 * is --accent-ink rather than --accent because --accent only reaches 3.8:1
 * against --bg in the light theme, which fails as a label colour; --accent-ink
 * clears 5.7:1 light and 9.1:1 dark. Hover mixes the fill toward --text, which
 * moves it away from the --bg label in both themes rather than only one. */
.btn--primary {
  background: var(--accent-ink);
  border-color: transparent;
  color: var(--bg);
}

.btn--primary:hover {
  background: color-mix(in oklch, var(--accent-ink) 85%, var(--text));
}

/* The hero's two calls to action: side by side, wrapping once the intro column
 * is too narrow to hold both. */
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* --------------------------------------------------------------- layout --- */

/* The page container. .page--narrow is the single-column measure used by the
 * legal pages. */
.page {
  max-width: 1080px;
  margin: 0 auto;
  padding: clamp(28px, 5vw, 64px) clamp(20px, 5vw, 56px) 40px;
  display: flex;
  flex-direction: column;
  gap: clamp(64px, 9vw, 112px);
}

.page--narrow {
  max-width: 720px;
  gap: 40px;
  font-size: 16px;
  line-height: 1.6;
}

.section {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* A legal-page section is a label and one paragraph, so it sits much tighter
 * than the main pages' section stack. */
.section--tight {
  gap: 8px;
}

/* Hairline plus its uppercase label, the group that opens every section. */
.section-head {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.rule {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--rule) 48px, var(--rule) calc(100% - 48px), transparent);
}

/* The case entries under "Selected work": each is a .cols row, and the rows
 * need more air between them than the 32px a .section puts between its head
 * and its content. */
.case-list {
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 5vw, 56px);
}

/* Two columns that collapse to one below ~640px. */
.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: clamp(32px, 5vw, 72px);
}

.cols--intro {
  gap: clamp(32px, 5vw, 64px);
  align-items: end;
}

.cols--top {
  align-items: start;
}

.intro-copy {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0;
}

/* The row that opens both main pages: wordmark left, language switch right. It
 * is the height of the wordmark alone, so it adds no vertical space. */
.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 20px;
}

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

/* ------------------------------------------------------------ typography --- */

/* Mono 14px in --muted: the site name, and the legal pages' date stamp.
 * 14px is the floor for every mono label on the site; mono is kept for the
 * labels and short technical values only, never for running prose. */
.wordmark,
.legal-date {
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}

/* The main pages set the wordmark in caps above the headline. The legal pages
 * carry it lowercase as a link home, which is how the mockup has it. */
.wordmark--caps {
  letter-spacing: .02em;
  text-transform: uppercase;
}

.wordmark a {
  color: inherit;
}

.section-label {
  /* Keeps the section's hairline in view when the hero's secondary action jumps
   * to #cases-h, instead of landing the label flush against the viewport edge. */
  scroll-margin-top: clamp(24px, 4vw, 48px);
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.headline {
  font-size: clamp(30px, 4vw, 44px);
  line-height: 1.1;
  font-weight: 500;
  letter-spacing: -.015em;
  text-wrap: balance;
}

/* The legal pages' document title: the same treatment one step down the scale. */
.headline--legal {
  font-size: clamp(28px, 4vw, 36px);
  line-height: 1.15;
}

.subtitle {
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.35;
  font-weight: 400;
  color: var(--muted);
}

.lead {
  font-size: clamp(18px, 2.2vw, 24px);
  line-height: 1.4;
  max-width: 34ch;
  text-wrap: pretty;
}

.card-title {
  font-size: clamp(22px, 2.4vw, 28px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -.01em;
}

/* Same size as a card title, but a sentence rather than a heading. */
.statement {
  font-size: clamp(22px, 2.4vw, 28px);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -.01em;
  text-wrap: balance;
}

.prose {
  font-size: 17px;
  line-height: 1.55;
  max-width: 46ch;
  text-wrap: pretty;
}

.note {
  font-size: 16px;
  line-height: 1.6;
  max-width: 46ch;
  color: var(--muted);
  text-wrap: pretty;
}

/* Secondary copy that keeps the surrounding size, only the colour drops back. */
.muted {
  color: var(--muted);
}

/* A machine string set inside sans copy -- a URL, a handle -- where mono says
 * "type this exactly". Relative sizing keeps it from towering over its line,
 * since Plex Mono runs wider and taller than Plex Sans at equal px. */
.mono {
  font-family: var(--font-mono);
  font-size: .94em;
}

/* ----------------------------------------------------------- components --- */

/* Key/value grid. The term is a short label, so it keeps the mono idiom; the
 * value is read as a sentence -- "18 years in software engineering and
 * architecture" -- so it is set in the body face, which reads better at small
 * sizes and wraps better in a narrow column. Baseline alignment holds the two
 * faces on one line despite the size difference. */
.meta-list {
  margin: 8px 0 0;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: 20px;
  row-gap: var(--meta-row-gap, 10px);
  line-height: 1.55;
}

.meta-list dt {
  font-family: var(--font-mono);
  font-size: var(--meta-label-size, 14px);
  color: var(--muted);
}

.meta-list dd {
  font-size: var(--meta-size, 15px);
  text-wrap: pretty;
}

.meta-list--tight {
  --meta-row-gap: 8px;
}

.meta-list--wide {
  --meta-row-gap: 12px;
  margin: 0;
  align-content: start;
}

/* Below the two-column breakpoint the label column steals width the value
 * needs, so the pair stacks and the label sits over its value. */
@media (max-width: 480px) {
  .meta-list {
    grid-template-columns: 1fr;
    row-gap: 4px;
  }

  .meta-list dd:not(:last-child) {
    margin-bottom: var(--meta-row-gap, 10px);
  }
}

/* The workshop card's parts. Same mono label idiom as .section-label and the
 * .meta-list terms, but stacked over its copy rather than beside it, so the
 * card can be scanned by its labels instead of read straight through. */
.steps {
  display: grid;
  gap: 4px;
}

.steps dt,
.result dt {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.steps dt:not(:first-child) {
  margin-top: 18px;
}

.steps dd {
  font-size: 16px;
  line-height: 1.6;
  text-wrap: pretty;
}

/* Divides what the days contain from what they hand back. */
.steps--summary {
  border-top: 1px solid var(--rule);
  padding-top: 18px;
}

/* The evidence line of a case, set between its title and its story: the one
 * measured result, delivery or status a visitor should take away. The label
 * shares the .steps rule and names which of the three it is, so a measured
 * figure is never read as a delivery claim or the other way round. The claim
 * sits one step below the card title, and the accent rule at the left marks
 * it as evidence rather than as a second heading. */
.result {
  display: grid;
  gap: 6px;
  padding-left: 16px;
  border-left: 2px solid var(--accent);
}

.result-claim {
  font-size: clamp(18px, 1.75vw, 20px);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -.005em;
  text-wrap: balance;
}

/* The qualifier under the claim: the scope of an evaluation, or the status
 * that keeps a delivery honest. */
.result-note {
  font-size: 16px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

/* Capability tile: an accent rule over a label and its description. The note
 * was mono 12.5px, which made a plain sentence -- "Model, data and hosting
 * under European control" -- the hardest line in the card to read. It is body
 * copy now, and the label above it gains a step to stay ahead of it. */
.tile-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 16px 24px;
  margin-top: 4px;
}

.tile {
  border-top: 2px solid var(--accent);
  padding-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.tile-title {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  text-wrap: balance;
}

.tile-note {
  font-size: 15px;
  line-height: 1.55;
  color: var(--muted);
  text-wrap: pretty;
}

.portrait {
  justify-self: end;
  width: 100%;
  max-width: 400px;
  aspect-ratio: 3 / 4;
  background: var(--portrait-surface);
  overflow: hidden;
  border-radius: 2px;
}

/* The master is a chest-up portrait, and at 400px wide beside a headline that
 * leaves nearly half the frame as t-shirt. The scale, anchored at the top edge,
 * tightens it to a headshot: air above the hair, the head about 58% of the
 * frame, bottom edge at mid-chest. 1.4 was tried first and read like a passport
 * photo. The 1600 variant still has enough pixels for 2x screens at this zoom. */
.portrait img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 20%;
  opacity: var(--portrait-opacity);
  scale: 1.2;
  transform-origin: 50% 0;
}

/* EN / DE. Both languages are always links so the two pages stay structurally
 * identical; the current one is marked with aria-current and set in --text. */
.lang-switch {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: .02em;
}

.lang-switch a {
  color: var(--muted);
  text-decoration: none;
  padding: 2px 0;
}

.lang-switch a:hover,
.lang-switch a[aria-current] {
  color: var(--text);
}

.lang-switch span {
  color: var(--rule);
}

.site-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 28px;
  align-items: center;
  justify-content: space-between;
  padding-top: 24px;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}

/* A footer link carries the same --muted as the name beside it, so the
 * inherited underline is the only thing that marks it as a link. */
.site-footer a {
  color: var(--muted);
}

.site-footer a:hover {
  color: var(--text);
}

/* The legal pages carry their two links as one plain row rather than the main
 * pages' split bar of name and nav. */
.site-footer--legal {
  align-items: stretch;
  justify-content: flex-start;
  gap: 12px 24px;
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 24px;
}
