/* ============================================================
   Géraldine Recker
   1 settings · 2 font · 3 base · 4 grid · 5 nav · 6 media
   7 text · 8 cursor · 9 scroll · 10 states · 11 mobile

   One 8-column grid holds the page. Every distance worth tuning is
   a setting in section 1 — change the number there, never in the
   rules below. Everything is black; weight carries all emphasis.
   ============================================================ */

/* ---------- 1. settings ---------- */

:root {

  /* colour */
  --bg: #ffffff;
  --ink: #111111;
  --select: #ffc9e2;        /* text selection */

  /* page */
  --cols: 8;
  --margin: 16px;           /* outer edge of the page, all four sides */
  --gutter: 16px;           /* space between columns */

  /* which columns each part uses */
  --col-nav: 1 / 2;         /* project list */
  --col-text: 1 / 3;        /* description, on the bottom margin */
  --col-media: 5 / -1;      /* images */

  /* images */
  --image-gap: 33vh;        /* between stacked images — a third of the screen height (test) */
  --portrait-max: calc(100vh - var(--margin) * 2);   /* tallest a vertical image gets */
  --caption-gap: 5px;       /* between an image and its caption */

  /* type */
  --fs-body: 14px;
  --lh-body: 1.3;
  --text-case: lowercase;   /* everything in lowercase; "none" keeps the sheet's capitals */

  /* project list */
  --lh-nav: 1.35;           /* line height — fixed, see section 5 */
  --nav-gap: 0px;           /* between project titles */
  --brand-gap: 0px;         /* between the name and the first title */
  --nav-meta-gap: 0px;      /* between the last title and "info" */

  /* description */
  --link-weight: 600;       /* a linked word in the text: semibold */
  --text-nav-gap: 16px;     /* least space between the list and a long text */
  --info-block-gap: 1em;    /* between blocks on the info page (bio, cv…) */

  /* phone */
  --margin-mobile: 16px;    /* outer edge of the page */
  --image-gap-mobile: 30px; /* between stacked images */
  --menu-gap-mobile: 14px;  /* below the name bar: to the list, and to the page */

  /* cursor */
  --cursor-size: 10px;      /* the dot that follows the mouse */
  --cursor-link-scale: 1.6; /* how much it grows over something clickable */
  --cursor-lag: 0;          /* 0 = sticks to the mouse; 0.8 = trails behind */
  --cursor-opacity: 1;

  /* scroll indicators */
}

/* ---------- 2. font ---------- */

@font-face {
  font-family: "Suisse Intl";
  src: url("fonts/SuisseIntl-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* In Suisse Int'l, Book is lighter than Regular. */
@font-face {
  font-family: "Suisse Intl";
  src: url("fonts/SuisseIntl-Book.woff2") format("woff2");
  font-weight: 350;
  font-style: normal;
  font-display: swap;
}

/* Name, info, contact, hover. Needs fonts/SuisseIntl-Semibold.woff2. */
@font-face {
  font-family: "Suisse Intl";
  src: url("fonts/SuisseIntl-Semibold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* The open project. Needs fonts/SuisseIntl-Bold.woff2 — without a
   real cut the browser smears the Regular, which looks like a
   printing fault on a grotesk. */
@font-face {
  font-family: "Suisse Intl";
  src: url("fonts/SuisseIntl-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* *Italic* in the sheet. Needs fonts/SuisseIntl-RegularItalic.woff2
   — without it the browser slants the Regular itself, which looks
   wrong. */
@font-face {
  font-family: "Suisse Intl";
  src: url("fonts/SuisseIntl-RegularItalic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* ---------- 3. base ---------- */

* { box-sizing: border-box; }

[hidden] { display: none !important; }

html { height: 100%; }

body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--ink);
  font-family: "Suisse Intl", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  text-transform: var(--text-case);
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--select); color: var(--ink); }
::-moz-selection { background: var(--select); color: var(--ink); }

a { color: inherit; text-decoration: none; }

a:focus-visible,
button:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

img, video { display: block; max-width: 100%; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- 4. grid ---------- */

/* The single row is told its size outright: exactly the window,
   minus the margins. Left to size itself ("auto"), a row may grow to
   fit a long description or a tall stack of images — and then the
   text runs past the bottom of the window instead of scrolling. */
body {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: var(--gutter);
  padding: var(--margin);
}

#bar { display: none; }        /* mobile only */

#sidebar {
  grid-column: var(--col-nav);
  grid-row: 1;
  align-self: start;
  z-index: 20;
}

#main {
  grid-column: 1 / -1;
  grid-row: 1;
  min-height: 0;
  height: 100%;
}

/* The same 8 columns again, so children land on the page's lines. */
.view {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: var(--gutter);
  height: 100%;
  min-height: 0;
}

.view > * { min-width: 0; }

/* ---------- 5. nav ---------- */

#sidebar ul { list-style: none; margin: 0; padding: 0; }
#sidebar li { margin: 0 0 var(--nav-gap); }

#sidebar a {
  display: block;
  line-height: var(--lh-nav);
}

/* Name, info and contact: always semibold, whatever page is open. */
#sidebar .brand,
#sidebar .meta a { font-weight: 600; }

#sidebar .brand { margin-bottom: var(--brand-gap); }
#sidebar .meta { margin-top: var(--nav-meta-gap); }

/* Projects, as in "Hovering graphics": the title alone at rest; on
   hover its type, medium and year follow on the same line,
   comma-separated; the open project shows the same line in bold.
   Weight alone — no grey, no underline.

   A line never wraps, so nothing below it can jump when it grows or
   turns bold. A long one simply runs on into the empty column beside
   the list. */
/* Only the words react to the mouse, not the empty width of the
   column beside them. */
#project-index a {
  display: inline;
  font-weight: 400;
  white-space: nowrap;
}

#project-index a.active { font-weight: 600; }

#project-index .line-meta { display: none; }

#project-index a:hover .line-meta,
#project-index a.active .line-meta { display: inline; }

#menu-toggle {
  border: 0;
  padding: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

/* ---------- 6. media ---------- */

.col-content {
  grid-column: var(--col-media);
  grid-row: 1;
  height: 100%;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Continuous scroll, no snapping. */
.frame {
  margin: 0 0 var(--image-gap);
  display: block;
}

.frame:last-child { margin-bottom: 0; }

/* Landscape fills the four columns. */
.frame img,
.frame video {
  width: 100%;
  height: auto;
}

/* Portrait is capped by height and centred across the span. JS tags
   the frame the moment the file reports its dimensions. */
.frame.is-portrait {
  display: flex;
  justify-content: center;
}

.frame.is-portrait img,
.frame.is-portrait video {
  width: auto;
  max-width: 100%;
  max-height: var(--portrait-max);
}

/* A Vimeo film. --ratio is width ÷ height, set from the film itself.
   Landscape fills the four columns; a vertical film is capped by
   --portrait-max and centred, like a vertical photo. */
.vimeo {
  position: relative;
  width: min(100%, calc(var(--portrait-max) * var(--ratio, 1.7778)));
  aspect-ratio: var(--ratio, 1.7778);
  margin: 0 auto;
  overflow: hidden;
}

/* Scaled up by a hair and clipped: where the film's proportions and
   the box round to different pixels, the player would otherwise show
   a one-pixel line of its own background along an edge. */
.vimeo iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  transform: scale(1.012);
}

#project-images { display: contents; }

#project-images figcaption {
  margin-top: var(--caption-gap);
}

/* Previewing another project from inside one: the media column shows
   only the hovered project's cover, never the open project's images
   beneath it. The text stays. */
body.is-previewing #project-images,
body.is-previewing #project-media { display: none; }

/* ---------- 7. text ---------- */

/* Description on the bottom margin, columns 1–2 — the same cell the
   nav occupies.

   The box is exactly as tall as its text and sits on the bottom
   line, so text that fits never scrolls — not even by a pixel. It
   can never grow taller than the space under the nav (--text-top is
   the nav's measured height), so a long description scrolls inside
   a box that starts below the list. Nothing in the box can travel
   up into the titles, because the box itself never reaches them. */
.col-text {
  grid-column: var(--col-text);
  grid-row: 1;
  align-self: end;
  max-height: calc(100% - var(--text-top, 0px));
  min-height: 0;
  min-width: 0;
  overflow-y: hidden;
  overflow-wrap: break-word;
}

/* Scrollable only when the text really is longer than its space —
   JS decides and adds the class. By default the box can't be
   scrolled at all, so a short text can't be nudged by a pixel or two
   of rounding, whatever the browser or font. */
.col-text.is-scrolling { overflow-y: auto; }

#project-text p { margin: 0; }

/* Links and italics typed in the sheet: [word](https://…) and *word*. */
.col-text a.text-link { font-weight: var(--link-weight); }
.col-text em { font-style: italic; }

/* Info and contact: blocks stacked, same text, one gap between. */
.info-block { margin: 0 0 var(--info-block-gap); }
.info-block p { margin: 0; }
/* Contact: name and address together, no empty line between */
#contact-content .info-block:first-child { margin-bottom: 0; }

.colophon { margin: 0; }
.colophon a { font-weight: var(--link-weight); }   /* clickable = 600, like every link */

#error {
  grid-column: var(--col-text);
  align-self: end;
}

/* ---------- 8. cursor ---------- */

@media (hover: hover) and (pointer: fine) {
  body.has-cursor,
  body.has-cursor a,
  body.has-cursor img,
  body.has-cursor video,
  body.has-cursor button { cursor: none; }
}

/* The box is the dot's height, offset by half a dot, so the pointer
   sits at the dot's centre and the label centres on the same line. */
#cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 200;
  display: none;
  align-items: center;
  height: var(--cursor-size);
  margin: calc(var(--cursor-size) / -2) 0 0 calc(var(--cursor-size) / -2);
  gap: 0.8em;
  opacity: var(--cursor-opacity);
  pointer-events: none;
  will-change: transform;
  transition: opacity 200ms ease;
}

body.has-cursor #cursor { display: flex; }
#cursor.is-away { opacity: 0; }

#cursor::before {
  content: '';
  flex: 0 0 auto;
  width: var(--cursor-size);
  height: var(--cursor-size);
  border-radius: 50%;
  background: var(--ink);
  transition: transform 150ms ease;
}

/* Over anything clickable — a title, a linked word, info, contact. */
#cursor.is-link::before { transform: scale(var(--cursor-link-scale)); }

.cursor-label {
  white-space: nowrap;
  line-height: 1;
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity 200ms ease, transform 200ms ease;
}

#cursor.has-label .cursor-label {
  opacity: 1;
  transform: translateX(0);
}

/* ---------- 9. scrollbars ---------- */

/* No scrollbars anywhere — page, columns, phone menu. Everything
   still scrolls; nothing shows it. */
html,
body,
* {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}

/* ---------- 10. states ---------- */

/* Landing — name and photo only, until the first click. */
#project-index,
#sidebar .meta { transition: opacity 500ms ease; }

.is-landing #project-index,
.is-landing #sidebar .meta {
  opacity: 0;
  pointer-events: none;
}

/* Focus — clicking an image clears everything around it. */
#sidebar,
#bar,
.col-text { transition: opacity 400ms ease; }

body.is-focus #sidebar,
body.is-focus #bar,
body.is-focus .col-text {
  opacity: 0;
  pointer-events: none;
}

/* ---------- 11. mobile ---------- */

@media (max-width: 800px) {

  /* The phone uses its own settings (section 1). */
  :root {
    --margin: var(--margin-mobile);
    --image-gap: var(--image-gap-mobile);
  }

  html, body {
    height: auto;
    overflow: visible;
  }

  body {
    display: block;
    padding: var(--margin);
  }

  /* Name left, "index" right — both stay put while you scroll. */
  #bar {
    display: flex;
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 40;
    align-items: baseline;
    justify-content: space-between;
    padding: var(--margin) var(--margin) var(--menu-gap-mobile);
    line-height: var(--lh-nav);
    background: var(--bg);
  }

  #bar .brand { font-weight: 600; }

  /* The desktop nav becomes the drop-down list. */
  #sidebar {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 30;
    max-height: 100vh;
    overflow-y: auto;
    padding: calc(var(--margin) + var(--lh-nav) * 1em + var(--menu-gap-mobile)) var(--margin) var(--margin);
    background: var(--bg);
    transform: translateY(-101%);
    transition: transform 240ms ease;
  }

  #sidebar.open { transform: translateY(0); }

  /* The name is already in the bar. */
  #sidebar .brand { display: none; }

  #sidebar .meta { margin-top: 1.4em; }

  /* Landing is a desktop idea; the list is always available here. */
  .is-landing #project-index,
  .is-landing #sidebar .meta {
    opacity: 1;
    pointer-events: auto;
  }

  #main {
    height: auto;
    padding-top: calc(var(--lh-nav) * 1em + var(--menu-gap-mobile));
  }

  .view {
    display: block;
    height: auto;
  }

  /* The desktop text column is pinned to the bottom and padded for
     the nav. On a phone it is just text, in the flow. */
  .col-text {
    align-self: auto;
    max-height: none;
    overflow: visible;
    margin: 0 0 var(--margin);
  }

  .col-content {
    height: auto;
    overflow: visible;
  }

  /* Full width on a phone, portrait included. */
  .vimeo { width: 100%; }
  .frame.is-portrait { display: block; }

  .frame.is-portrait img,
  .frame.is-portrait video {
    width: 100%;
    max-height: none;
  }

  /* No custom cursor on a phone. */
  #cursor { display: none !important; }

  /* The project's title line sits above its text, like the open line
     in the desktop index. Undoes .sr-only. */
  .project-line {
    position: static;
    width: auto; height: auto;
    margin: 0 0 var(--info-block-gap);
    overflow: visible;
    clip: auto;
    white-space: normal;
    font-size: inherit;
    font-weight: 600;
    line-height: inherit;
  }

  /* In the drop-down a long line may wrap — there's no column beside
     it to run into. */
  #project-index a { display: block; white-space: normal; }  /* full-width tap target */
}

@media (prefers-reduced-motion: reduce) {
  #sidebar,
  #bar,
  #project-index,
  #sidebar .meta,
  .col-text,
  #cursor,
  #cursor::before,
  .cursor-label { transition: none; }
}
