/* Kroissant — one stylesheet. Tokens first (docs/03-design-spec.md §8), then ~250 lines of rules. */

:root {
  /* space */
  --chrome-x: 16px; /* side margin of the wordmark and menu glyph; the gallery used to share it */
  --gutter: round(down, calc(var(--chrome-x) / 3), 1px); /* outer margins and tile gap: a third of the chrome margin (client feedback 2026-09-03: "marges plus fines, 1/3 de l'existant"), whole pixels */
  --chrome-top: clamp(16px, var(--chrome-x) / 2, 40px); /* half the chrome side margin: optical balance, neither cramped (1:3) nor a void (1:1) */
  --mark-h: 18px;
  --hit: 44px;
  --stagger-ratio: 0.2;
  --first-top: calc(2 * var(--chrome-top) + var(--mark-h)); /* same space under the wordmark as above it (client feedback #1) */
  /* colour */
  --bg: #fff;
  --fg: #000;
  --overlay: #000;
  --overlay-fg: #fff;
  --placeholder: #f2f2f2;
  /* motion */
  --t-ui: 250ms;
  --t-image: 500ms;
  /* type */
  --font-label: "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-prose: "Newsreader", Georgia, "Times New Roman", serif;
  --track-upper: 0.09em;
  --track-text: 0.02em;
  --z-chrome: 100;
  --z-overlay: 200;
}
@media (min-width: 760px) {
  :root { --chrome-x: 32px; --mark-h: 20px; }
}
@media (min-width: 1240px) {
  :root { --chrome-x: clamp(48px, 5vw, 96px); --mark-h: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  :root { --t-ui: 0ms; --t-image: 0ms; }
}

@font-face {
  font-family: "Newsreader";
  src: url("../fonts/newsreader-latin.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---------- base */
*, *::before, *::after { box-sizing: border-box; }
html { background: var(--bg); color: var(--fg); -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }
body {
  margin: 0;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-label);
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: var(--track-text);
  -webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
b, strong { font-weight: inherit; } /* one weight on the whole site */
:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---------- fixed chrome: wordmark top-left, menu glyph top-right, on their own margin (--chrome-x); the gallery sits a third of it from the edge.
   Plain black, always (docs/07 D4): over the few dark photographs it simply gets quieter, as on the reference.
   A blended/inverting variant was tried and rejected as distracting. */
.chrome {
  position: fixed; inset: 0 0 auto 0;
  z-index: var(--z-chrome);
  height: calc(var(--chrome-top) + var(--mark-h));
  pointer-events: none;
  color: var(--fg);
}
.mark {
  position: absolute; left: var(--chrome-x); top: var(--chrome-top);
  height: var(--mark-h); pointer-events: auto;
}
.mark-img { height: 100%; width: auto; transition: filter var(--t-ui) ease; }
.toggle {
  --w: round(nearest, calc(var(--mark-h) * 3 / 4), 1px); /* whole CSS pixels keep the tiny traced curves crisp; nominal 3:4 */
  position: absolute;
  right: 0; top: 0;
  width: 56px; height: 56px; /* mobile: a real, fully in-viewport touch target; the drawing stays on the chrome lines */
  padding: 0; border: 0; background: none; cursor: pointer; pointer-events: auto;
  color: inherit; transition: color var(--t-ui) ease;
}
.toggle-glyph {
  position: absolute; right: var(--chrome-x); top: var(--chrome-top);
  width: var(--w); height: var(--mark-h); fill: currentColor;
  shape-rendering: geometricPrecision;
}
@media (min-width: 760px) {
  .toggle {
    right: calc(var(--chrome-x) - (var(--hit) - var(--w)) / 2); /* glyph's right edge on the chrome margin line */
    top: calc(var(--chrome-top) + var(--mark-h) / 2 - var(--hit) / 2);
    width: var(--hit); height: var(--hit);
    display: grid; place-items: center;
  }
  .toggle-glyph { position: static; }
}
.toggle:focus-visible { outline-offset: -2px; }

/* over a dark tile (JS sets .on-dark from data-lum) or over the open menu, the piece turns white */
.toggle.on-dark, body.menu-open .toggle { color: var(--overlay-fg); }
.mark.on-dark .mark-img, body.menu-open .mark-img { filter: invert(1); }
body.menu-open .chrome { z-index: calc(var(--z-overlay) + 1); }

/* ---------- menu overlay */
.menu {
  position: fixed; inset: 0; z-index: var(--z-overlay);
  display: grid; place-items: center;
  background: var(--overlay); color: var(--overlay-fg);
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-ui) ease, visibility 0s linear var(--t-ui);
}
.menu[hidden] { display: grid; } /* keep the transition; visibility does the hiding */
body.menu-open .menu { opacity: 1; visibility: visible; transition: opacity var(--t-ui) ease; }
.menu { --menu-size: 20px; }
@media (min-width: 760px) { .menu { --menu-size: 24px; } }
.menu-list { list-style: none; margin: 0; padding: 0; text-align: center; }
.menu-list > li + li { margin-top: 1.35em; } /* body em (19 px): the rhythm approved on the preview, independent of the link size */
.menu-link {
  display: inline-block; padding: 0.15em 0.2em;
  font-size: var(--menu-size); line-height: 1.2; text-transform: uppercase; letter-spacing: var(--track-upper);
}
/* last row (client, 2026-09-03): Instagram | one word to the other language — a 1 px rule between (an element, not a glyph),
   same row pitch as the rest. The icon is 24 px inside its 44 px hit box; the word gets the same invisible padding so the
   rule sits centred between the two visible shapes. */
.menu-meta { --insta: 24px; display: flex; justify-content: center; align-items: center; gap: calc(var(--menu-size) / 2); }
.menu-rule { width: 1px; height: var(--insta); background: currentColor; opacity: 0.4; }
.menu-lang { font-size: calc(var(--menu-size) / 2); display: inline-grid; place-items: center; min-height: var(--hit); padding: 0 calc((var(--hit) - var(--insta)) / 2); }
.menu-lang.is-off, .menu-insta.is-off { opacity: 0.3; }
.menu-insta { display: inline-grid; place-items: center; width: var(--hit); height: var(--hit); }
.menu-insta + .menu-linkedin { margin-left: calc(-1 * (var(--hit) - var(--insta))); } /* glyphs 12 px apart, not 32 (client, 2026-09-06); the hit boxes overlap by their padding only */
.menu a.menu-link:hover, .menu a.menu-insta:hover { opacity: 0.7; transition: opacity var(--t-ui) ease; }
body.menu-open { overflow: hidden; }

/* ---------- main frame. It is the size container every photo box is computed from (cqw = its content width). */
.main { display: block; flex: 1 0 auto; width: 100%; padding: var(--first-top) var(--gutter) var(--gutter); container-type: inline-size; }

/* ---------- home gallery: two staggered columns (one on phones), one gutter everywhere.
   The photo IS the box, the way every ordinary page does it: an <img> at the column width with an explicit whole-pixel
   height (from its own --w/--h), no wrapper sizing, no object-fit, no transform. Earlier versions stretched the photo into
   a sized box with object-fit: cover and shifted the second column with a translate; Firefox's compositor snaps the clip
   and the scaled texture separately during async smooth scrolling, and the photos' edges flickered by a pixel (2026-09-03).
   Column width, height and stagger are rounded once, in layout, against the main frame (cqw). */
.gallery {
  --col: 100cqw;
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: var(--col); gap: var(--gutter);
}
.tile {
  --tile-h: round(down, calc(var(--col) * var(--h, 3) / var(--w, 2)), 1px);
  position: relative; margin: 0;
  -webkit-user-select: none; user-select: none;
}
.tile .k-img {
  display: block; width: 100%; height: var(--tile-h);
  background: var(--placeholder); /* the wait; the photo's own pixels replace it — nothing is ever drawn behind a photo */
  opacity: 0; transition: opacity var(--t-image) ease;
  -webkit-user-drag: none;
}
.tile .k-img.is-loaded { opacity: 1; background: none; }
.k-img.is-instant { transition-duration: calc(var(--t-image) / 2); } /* already there when the script ran (cache): a quicker fade, half the loading one (250 ms) */
/* without JavaScript nothing adds .is-loaded: show images immediately */
@media (scripting: none) { .tile .k-img, .sheet-figure .k-img { opacity: 1; background: none; } }
@media (min-width: 760px) {
  .gallery {
    --col: round(down, calc((100cqw - var(--gutter)) / 2), 1px);
    grid-template-columns: repeat(2, var(--col));
    justify-content: space-between; /* the rounding remainder (< 1 px) goes to the gap, never to a box */
    grid-auto-flow: row dense;
    align-items: start;
    /* room for the shifted second column at the bottom: stagger = tile height × ratio, tile height = 1.5 × tile width */
    padding-bottom: round(calc(var(--col) * 1.5 * var(--stagger-ratio)), 1px);
  }
  /* deal tiles alternately: odd → column 1, even → column 2 (DOM order = CMS order = reading order) */
  .tile:nth-child(odd) { grid-column: 1; }
  .tile:nth-child(even) { grid-column: 2; }
  /* column 2 sits lower by --stagger-ratio of a tile's own height: a layout offset in whole pixels, not a transform */
  .tile:nth-child(even) { top: round(calc(var(--tile-h) * var(--stagger-ratio)), 1px); }
}
/* Studio photos under the text: same grid and gutter, own aspect ratios, no stagger; one chrome margin above (= the text's top inset) */
.gallery--studio { margin-top: var(--first-top); }
@media (min-width: 760px) {
  .gallery--studio { padding-bottom: 0; }
  .gallery--studio .tile:nth-child(odd), .gallery--studio .tile:nth-child(even) { grid-column: auto; top: auto; }
}

/* ---------- text pages: same two columns; portrait left, text right. Unlike the gallery, these columns stop on the
   chrome's margin line — the portrait aligns with the wordmark (client, 2026-09-06: "il n'y a que ça sur la page,
   le décalage n'a pas de sens"); --inset is the difference between the chrome margin and the gallery gutter. */
.sheet { --inset: calc(var(--chrome-x) - var(--gutter)); --col: calc(100cqw - 2 * var(--inset)); display: grid; grid-template-columns: var(--col); gap: var(--gutter); padding: 0 var(--inset); }
.sheet-figure { margin: 0; }
.sheet-figure .k-img { display: block; width: 100%; height: round(down, calc(var(--col) * var(--h, 3) / var(--w, 2)), 1px); background: var(--placeholder); opacity: 0; transition: opacity var(--t-image) ease; }
.sheet-figure .k-img.is-loaded { opacity: 1; background: none; }
@media (max-width: 759px) { .sheet-figure + .sheet-text { margin-top: 28px; } } /* single column: air between the portrait and the label (client, 2026-09-06: "un peu d'espace ici") */
.sheet-text { max-width: 34em; }
@media (min-width: 760px) {
  .sheet { --col: round(down, calc((100cqw - 2 * var(--inset) - var(--gutter)) / 2), 1px); grid-template-columns: repeat(2, var(--col)); justify-content: space-between; align-items: start; }
  .sheet-figure { position: sticky; top: var(--first-top); }
  .sheet--text .sheet-text { grid-column: 2; }
  .sheet--contact .contact-links { grid-column: 1; }
  .sheet--contact .contact-form { grid-column: 2; }
}

/* Studio desktop: portrait and copy form one viewport composition. The portrait is shown whole, at its own ratio
   (--w/--h from the file), as wide as the column unless the available height caps it; nothing is ever cropped. */
@media (min-width: 1024px) {
  .sheet--studio { --studio-h: 100cqh; } /* the main frame's content height: what is left between chrome and footer (fixed page below) */
  .sheet--studio .sheet-figure {
    --fig-w: round(down, min(var(--col), calc(var(--studio-h) * var(--w, 2) / var(--h, 3))), 1px);
    width: var(--fig-w);
    position: relative; top: auto; /* cancel the generic sticky offset: this figure shares the text's top line */
  }
  .sheet--studio .sheet-figure .k-img { height: round(down, calc(var(--fig-w) * var(--h, 3) / var(--w, 2)), 1px); }
  /* Copy sits on the photo's bottom line, the label stays on its top line; the space opens between them
     (client, 2026-09-06: "aligne le texte à la photo en bas, un espace entre le titre et le début du texte"). */
  .sheet--studio .sheet-text { align-self: stretch; display: flex; flex-direction: column; }
  .sheet--studio .sheet-text .prose { margin-top: auto; }
}

/* Fixed pages — Studio and Contact (client, 2026-09-06: "une page fixe"): chrome, content and footer share one viewport,
   nothing scrolls. The footer loses the tall gap it keeps under the gallery; the main frame becomes a size container so
   the Studio figure can read the room left (100cqh) whatever the footer's height. */
@media (min-width: 1024px) {
  body.page-fixed { height: 100svh; }
  body.page-fixed .main { flex: 1 1 auto; min-height: 0; overflow-y: auto; container-type: size; } /* size container: 100cqh = the room left; overflow scrolls inside the frame rather than under the footer */
  body.page-fixed .site-footer { padding-top: 32px; padding-bottom: 32px; }
}

.label {
  margin: 0 0 1.6em;
  font-size: 12px; font-weight: inherit; line-height: 1;
  text-transform: uppercase; letter-spacing: var(--track-upper);
}
.prose {
  font-family: var(--font-prose); font-weight: 300;
  font-size: 17px; line-height: 1.5; letter-spacing: var(--track-text);
}
.prose p { margin: 0 0 1em; }
.sheet--studio .prose p:last-of-type { display: inline; margin-bottom: 0; }
.studio-endmark { display: inline-block; width: 9px; height: 9px; margin-left: 0.18em; fill: currentColor; vertical-align: -0.5px; } /* optical position: half a pixel below the text baseline */
.prose a { text-decoration: underline; text-underline-offset: 0.15em; text-decoration-thickness: 1px; }
.prose em { font-style: italic; }
.prose--small { font-family: var(--font-label); font-size: 14px; }
@media (min-width: 760px) {
  .prose { font-size: 20px; line-height: 1.45; letter-spacing: 0.03em; }
  .prose--small { font-size: 15px; line-height: 1.5; letter-spacing: var(--track-text); }
}
@media (min-width: 1024px) and (max-height: 960px) {
  .sheet--studio .label { margin-bottom: 1.2em; }
  .sheet--studio .prose { font-size: 17px; line-height: 1.4; }
}
@media (min-width: 1240px) { .prose { font-size: 21px; } }

.details p { margin: 0 0 0.6em; font-size: 15px; }
.details a { text-decoration: underline; text-underline-offset: 0.15em; text-decoration-thickness: 1px; transition: opacity var(--t-ui) ease; }
.details a:hover, .details a:focus-visible { opacity: 0.6; }
@media (min-width: 760px) { .details p { font-size: 16px; } }

/* ---------- contact: three outward links (email, Instagram, LinkedIn), each with the arrow that says "this opens
   something" (client, 2026-09-03), then the enquiry form. Fields follow her reference: label above a flat grey field
   (the site's only grey, the image placeholder), budget and surface side by side, no borders, no placeholders. */
.links { list-style: none; margin: 0 0 2em; padding: 0; }
.links li + li { margin-top: 0.5em; }
.link-out { display: inline-flex; align-items: baseline; gap: 0.45em; font-size: 17px; line-height: 1.4; transition: opacity var(--t-ui) ease; }
.link-out:hover, .link-out:focus-visible { opacity: 0.6; }
.arrow { width: 0.55em; height: 0.55em; flex: none; transform: translateY(-0.05em); transition: transform var(--t-ui) ease; }
.link-out:hover .arrow, .button:hover .arrow { transform: translate(0.1em, -0.15em); }
@media (min-width: 760px) { .link-out { font-size: 19px; } }

.form-lead { margin: 0 0 1.6em; font-family: var(--font-prose); font-weight: 300; font-size: 19px; line-height: 1.45; }
@media (min-width: 760px) { .form-lead { font-size: 21px; } }
.contact-form { margin-top: 2.5em; } /* phones: below the links */
@media (min-width: 760px) { .contact-form { margin-top: 0; } } /* desktop: the lead shares the label's top line */
.form { display: block; }
.form-hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.field { display: block; margin: 0 0 1.1em; }
.field label { display: block; margin-bottom: 0.45em; font-size: 11px; line-height: 1; text-transform: uppercase; letter-spacing: var(--track-upper); }
.field input, .field textarea {
  display: block; width: 100%; margin: 0; padding: 0.55em 0.75em;
  font: inherit; font-size: 15px; letter-spacing: var(--track-text); color: var(--fg);
  background: var(--placeholder); border: 0; border-radius: 0; -webkit-appearance: none; appearance: none;
}
.field textarea { resize: vertical; height: 6em; min-height: 3em; }
/* short laptops: the fixed Contact page keeps its footer in view — tighter rows, shorter textarea */
@media (min-width: 1024px) and (max-height: 800px) {
  .form-lead { margin-bottom: 1em; }
  .field { margin-bottom: 0.7em; }
  .field label { margin-bottom: 0.3em; }
  .field input, .field textarea { padding: 0.45em 0.75em; }
  .field textarea { height: 3em; }
}
.field input:focus-visible, .field textarea:focus-visible { outline: 1px solid currentColor; outline-offset: 0; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gutter); } /* the gallery gutter, not the chrome margin (client, 2026-09-06: "moins d'espace entre project type et location") */
.form-submit { margin: 0.4em 0 0; text-align: right; }
.button {
  display: inline-flex; align-items: baseline; gap: 0.45em;
  margin: 0; padding: 0.4em 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 12px; line-height: 1; letter-spacing: var(--track-upper); text-transform: uppercase; color: inherit;
  transition: opacity var(--t-ui) ease;
}
.button:hover, .button:focus-visible { opacity: 0.6; }
.form-notice { margin: 0 0 1.4em; font-size: 15px; line-height: 1.5; }
.form-notice--error { text-decoration: underline; text-underline-offset: 0.15em; text-decoration-thickness: 1px; }

/* ---------- footer: one quiet baseline after the staggered gallery, with the credit secondary. Set in a lighter grey,
   not black (client, 2026-09-06: "plus discret", then "encore plus clair" twice) — #b3b3b3 is below the 4.5:1 AA threshold, her call;
   links return to black on hover/focus. */
.site-footer {
  --fg-quiet: #b3b3b3;
  display: grid;
  grid-template: "copy" auto "social" auto "credit" auto / 1fr; /* phones: one left column, one voice (client, 2026-09-06: "chaotique") */
  align-items: baseline;
  gap: 1.4em;
  padding: 48px var(--chrome-x) max(32px, env(safe-area-inset-bottom));
  color: var(--fg-quiet);
  font-size: 9px;
  line-height: 1.2;
  letter-spacing: var(--track-upper);
  text-transform: uppercase;
}
.site-footer p { margin: 0; }
.footer-copy { grid-area: copy; display: flex; flex-wrap: wrap; gap: 0.4em 1.6em; }
.footer-legal { display: inline-flex; align-items: baseline; gap: 0.45em; } /* legal notice beside the year, with the arrow (client, 2026-09-06) */
.footer-social { grid-area: social; justify-self: start; display: inline-flex; align-items: center; gap: 1em; }
.footer-social .footer-rule { width: 1px; height: 1em; background: currentColor; opacity: 0.6; } /* Instagram | LinkedIn, a 1 px bar between (client, 2026-09-06) */
.footer-link { transition: color var(--t-ui) ease; }
a.footer-link:hover, a.footer-link:focus-visible { color: var(--fg); }
.footer-link.is-off { opacity: 0.4; }
.footer-credit { grid-area: credit; justify-self: start; } /* same voice as the rest — uppercase, same size (client, 2026-09-06: "le même discours") */
@media (min-width: 760px) {
  .site-footer {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas: "copy social credit";
    gap: 20px;
    padding-top: clamp(88px, 8vw, 120px);
    padding-bottom: max(48px, env(safe-area-inset-bottom));
    font-size: 10px;
  }
  .footer-social { justify-self: center; }
  .footer-credit { justify-self: end; }
}

.sheet--404 { min-height: calc(100svh - var(--first-top) - var(--gutter)); display: grid; place-content: center; text-align: center; }
.sheet--404 a { text-decoration: underline; text-underline-offset: 0.15em; }
