/* ============================================================
   styles.css
   Charcoal throughout. Modules open onto a slightly lifted
   charcoal panel, never a white one.
   Every value worth arguing about is a variable at the top.
   ============================================================ */

:root {
  /* Color */
  --charcoal:      #141618;
  --charcoal-soft: #1c1f22;
  --charcoal-line: #2c3034;
  --ink:           #0e0f10;
  --paper:         #f4f2ef;
  --white:         #ffffff;
  --text:          #f2f0ed;
  --text-dim:      #8b9298;
  --text-dimmer:   #5d646a;

  /* Type */
  --font: "Fira Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

  /* Rhythm */
  --wrap: 1080px;
  --gutter: 28px;
  --spine: 96px;         /* distance from left edge to the timeline spine */
  --radius: 3px;

  /* Where the node sits inside its row, measured from the row's top. On
     wide screens this is the cap height of the company name, so the top of
     the bubble is level with the top of "Angi". Below 900px the date range
     shows above the name and is the first text instead, so it lines up
     with that. The spine is derived from this rather than set separately -
     they drifted apart before and left 22px of line above the first
     bubble with nothing attached to it. */
  --node-top: 29px;
  --node-size: 11px;

  /* Motion */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur: 420ms;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--charcoal);
  color: var(--text);
  font-family: var(--font);
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

/* ---------------- Fixed lockup ---------------- */

/* Portrait, name and year readout are one fixed object on the right.
   No mix-blend-mode here any more: it looked fine on type but it
   wrecks a photograph. */
.yearRail {
  position: fixed;
  top: 50%;
  right: clamp(20px, 2.6vw, 44px);
  transform: translateY(-50%);
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
  pointer-events: none;
}

.yearRail__portrait {
  margin: 0;
  width: clamp(76px, 8vw, 104px);
}

.yearRail__portrait img,
.hero__portrait img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  /* The scroll handler is throttled, so interpolate between its steps.
     A rotated circle is still a circle, which is why the crop holds. */
  transition: transform 140ms linear;
}


.yearRail__name {
  margin: 2px 0 0;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text);
  text-align: right;
  max-width: 120px;
}

.yearRail__year {
  margin-top: 6px;
  /* So a word override reads like the years it sits among. */
  text-transform: uppercase;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--white);
}

.yearRail__track {
  width: 1px;
  height: 120px;
  background: var(--charcoal-line);
  position: relative;
  margin-top: 4px;
}

.yearRail__fill {
  position: absolute;
  top: 0; left: 0;
  width: 1px;
  height: 0%;
  background: var(--white);
  transition: height 120ms linear;
}

/* The hero carries its own copy of the portrait and name for narrow
   screens, where the rail is hidden. One or the other, never both. */
@media (min-width: 900px) {
  /* Scoped to .hero so it beats the plain .hero__top rule defined
     later in this file; a media query adds no specificity of its own. */
  .hero .hero__top { display: none; }
}

/* ---------------- Hero ---------------- */

.hero {
  padding: clamp(80px, 16vh, 180px) 0 clamp(90px, 14vh, 150px);
}

/* A small avatar next to the name, at roughly LinkedIn's scale. Sitting
   it with the name keeps it a label rather than a design feature: the
   page is not about the photo. */
.hero__top {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: clamp(28px, 5vh, 52px);
}

.hero__portrait {
  margin: 0;
  flex: 0 0 auto;
  width: 44px;
}

.hero__portrait img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
}

.hero__name {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.hero__headline {
  margin: 0;
  font-weight: 300;
  font-size: clamp(30px, 5.2vw, 60px);
  line-height: 1.1;
  letter-spacing: -0.025em;
  max-width: 22ch;
  color: var(--text-dim);
}

/* The lead phrase carries the weight; the rest sits back. */
.hero__headline .lead {
  color: var(--white);
  font-weight: 500;
}

/* The paragraph that does the real work of explaining the headline. */
.hero__intro {
  margin: clamp(30px, 5vh, 48px) 0 0;
  max-width: 62ch;
  font-size: clamp(16px, 1.55vw, 19px);
  line-height: 1.62;
  color: #b9bfc4;
}

.hero__subhead {
  margin: clamp(22px, 3vh, 32px) 0 0;
  max-width: 52ch;
  font-size: clamp(15px, 1.4vw, 17px);
  line-height: 1.6;
  color: var(--text-dim);
}

.hero__cue {
  margin-top: clamp(44px, 8vh, 80px);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dimmer);
  display: flex;
  align-items: center;
  gap: 12px;
}

.hero__cue::after {
  content: "";
  width: 1px;
  height: 40px;
  background: linear-gradient(var(--text-dimmer), transparent);
  animation: cueDrift 2.4s var(--ease) infinite;
}

@keyframes cueDrift {
  0%, 100% { opacity: 0.3; transform: translateY(0); }
  50%      { opacity: 1;   transform: translateY(6px); }
}

/* ---------------- Timeline ---------------- */

.timeline {
  list-style: none;
  margin: 0;
  padding: 0 0 clamp(80px, 14vh, 160px);
  position: relative;
}

/* The spine */
.timeline::before {
  content: "";
  position: absolute;
  left: 5px;
  top: calc(var(--node-top) + var(--node-size) / 2);
  bottom: 0;
  width: 1px;
  background: var(--charcoal-line);
}

.job {
  position: relative;
  padding-left: var(--spine);
}

.job + .job { margin-top: 4px; }

/* The node on the spine */
.job::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--node-top);
  width: var(--node-size);
  height: var(--node-size);
  border-radius: 50%;
  background: var(--charcoal);
  border: 1px solid var(--charcoal-line);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
  z-index: 2;
}

.job.is-open::before,
.job:hover::before {
  background: var(--white);
  border-color: var(--white);
  transform: scale(1.15);
}

/* ---- The collapsed header (always visible, always scannable) ---- */

.job__toggle {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-top: 1px solid var(--charcoal-line);
  color: inherit;
  font: inherit;
  padding: 26px 56px 26px 0;
  cursor: pointer;
  position: relative;
  transition: opacity 200ms var(--ease);
}

.job:first-child .job__toggle { border-top: 0; }

.job__toggle:focus-visible {
  outline: 1px solid var(--white);
  outline-offset: 4px;
}

/* Collapsed row: words on the left, a piece of creative on the right,
   so the timeline is scannable and has something to look at without
   anything being unfolded. */
.job__head {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  align-items: start;
}

@media (min-width: 720px) {
  .job__head { grid-template-columns: minmax(0, 1fr) clamp(140px, 20vw, 220px); }
}

.job__text { display: block; min-width: 0; }

.job__thumb {
  display: block;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--charcoal-soft);
  aspect-ratio: 16 / 10;
}

.job__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.78;
  transition: opacity 260ms var(--ease);
}

.job__toggle:hover .job__thumb img,
.job.is-open .job__thumb img { opacity: 1; }

.job__range {
  display: block;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dimmer);
  font-variant-numeric: tabular-nums;
  margin-bottom: 12px;
  transition: color 200ms var(--ease);
}

/* The scroller beside the module already shows the date range, so
   repeating it above the company name said the same thing twice. It stays
   in the markup and comes back below 900px, where the scroller is hidden
   and this is the only place the dates appear at all. */
@media (min-width: 900px) {
  .job__range { display: none; }
}

.job__company {
  display: block;
  font-size: clamp(26px, 3.6vw, 42px);
  font-weight: 400;
  letter-spacing: -0.025em;
  line-height: 1.04;
  color: var(--white);
}

.job__title {
  display: block;
  margin-top: 8px;
  font-size: clamp(14px, 1.3vw, 16px);
  font-weight: 400;
  color: var(--text-dim);
}

.job__oneLine {
  display: block;
  margin-top: 16px;
  max-width: 58ch;
  font-size: clamp(14px, 1.35vw, 17px);
  line-height: 1.55;
  color: #b9bfc4;
}

/* The open/close affordance */
.job__sign {
  position: absolute;
  right: 4px;
  top: 30px;
  width: 15px;
  height: 15px;
}

.job__sign::before,
.job__sign::after {
  content: "";
  position: absolute;
  background: var(--text-dim);
  transition: transform var(--dur) var(--ease), background 200ms var(--ease);
}

.job__sign::before { left: 0; top: 7px; width: 15px; height: 1px; }
.job__sign::after  { left: 7px; top: 0; width: 1px; height: 15px; }

.job.is-open .job__sign::after { transform: rotate(90deg) scaleX(0); }
.job.is-open .job__sign::before,
.job__toggle:hover .job__sign::before,
.job__toggle:hover .job__sign::after { background: var(--white); }

/* The collapsed row and the rail would otherwise show the same picture
   twice, because the thumb is one of the pieces in the rail. Once the
   module is open the rail is doing that job, so the thumb steps aside. */
.job__thumb {
  transition: opacity 240ms var(--ease), max-height 300ms var(--ease),
              margin 300ms var(--ease);
}

.job.is-open .job__thumb {
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  pointer-events: none;
}

@media (min-width: 720px) {
  /* Give the reading column the full width once the thumb has gone. */
  .job.is-open .job__head { grid-template-columns: 1fr; }
  .job.is-open .job__thumb { display: none; }
}

/* Client wall: twelve marks, two columns, six rows.

   Desaturated rather than flattened. An earlier version used
   brightness(0) invert(1) for pure white silhouettes, which normalises
   beautifully but destroys any logo with a background plate: Zephyr is
   type on a coloured pill, so the pill became a solid white blob. Now
   the marks keep their internal contrast, and Zephyr's plate was removed
   from the file itself instead.

   Two columns rather than three: a wordmark needs width, and three
   columns left the cells too narrow for the long names to reach full
   height before hitting the cell edge.

   Unlike the other thumbnails this one is not locked to 16/10, because
   seven rows of marks will not fit in a 128px box without touching. It
   is capped instead: tall enough to breathe, close enough to the other
   modules' 16/10 visuals that the row does not read as a different
   shape. 138px against their 128px. Raising --wallMax gives the marks
   more air and makes this row taller than the rest; lowering it does
   the reverse. */
.job__thumb--logos {
  --wallMax: 138px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(var(--rows, 6), 1fr);
  place-items: center;
  gap: 3px 6px;
  padding: 9px 8px;
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius);
  aspect-ratio: auto;
  align-self: start;
  height: var(--wallMax);
}

.job__thumb--logos img {
  /* Height drives it, width follows the artwork, so the lettering lands
     at a consistent size. --s lifts marks whose box encloses more than
     the letters. */
  height: calc(9.2px * var(--s, 1));
  width: auto;
  max-width: 100%;
  object-fit: contain;
  filter: grayscale(1) brightness(1.9) contrast(1.15);
  opacity: 0.9;
  transition: opacity 200ms var(--ease);
}

/* Marks that ship dark artwork need flipping to read on charcoal. */
.job__thumb--logos img.is-dark {
  filter: grayscale(1) invert(1) brightness(1.3) contrast(1.1);
}


.job--static:hover .job__thumb--logos img { opacity: 1; }

/* A row with nothing behind it: no pointer, no plus sign, no hover
   promise of more. Iowa is the only one. */
/* Keep the same right padding as the interactive rows even though there
   is no plus sign, or this row's creative sits out of line with the rest. */
.job__toggle--static { cursor: default; }
.job--static:hover::before {
  background: var(--charcoal);
  border-color: var(--charcoal-line);
  transform: none;
}

/* ---- The fold-out panel ---- */

.job__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur) var(--ease);
}

.job.is-open .job__panel { grid-template-rows: 1fr; }

.job__panelInner {
  overflow: hidden;
  min-height: 0;
}

/* Stays charcoal. Set back from the page by a hair and a hairline, not
   by inverting to white: the module should feel like the same object
   opening, not a document pasted on top. */
.panel {
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius);
  margin: 10px 0 34px;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 300ms var(--ease) 60ms, transform 300ms var(--ease) 60ms;

  display: grid;
  grid-template-columns: 1fr;
  align-items: stretch;
}

.job.is-open .panel { opacity: 1; transform: none; }

/* Text left, creative right, so the work is visible the moment it opens
   rather than buried under the reading. */
@media (min-width: 900px) {
  .panel { grid-template-columns: minmax(0, 1fr) clamp(260px, 34%, 400px); }
}

.panel__main {
  padding: clamp(24px, 3vw, 38px);
  min-width: 0;
}

.panel__meta {
  margin: 0 0 clamp(22px, 2.6vw, 30px);
  padding-bottom: 16px;
  border-bottom: 1px solid var(--charcoal-line);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

/* ---- Sections: one after another, top to bottom ---- */

.sect + .sect { margin-top: clamp(22px, 2.6vw, 30px); }

.sect__label {
  margin: 0 0 12px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dimmer);
}

.sect__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 9px;
}

.sect__list li {
  position: relative;
  padding-left: 20px;
  font-size: clamp(14.5px, 1.32vw, 16px);
  line-height: 1.5;
  color: var(--text);
  max-width: 62ch;
}

/* The track label carries the structure, so give it weight and let the
   sentence after it stay plain. */
.sect__track {
  color: var(--white);
  font-weight: 600;
  margin-right: 2px;
}

.sect__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.68em;
  width: 9px;
  height: 1px;
  background: var(--text-dimmer);
}

/* ---- Creative rail: slides continuously ---- */

.rail {
  position: relative;
  border-top: 1px solid var(--charcoal-line);
}

@media (min-width: 900px) {
  .rail {
    border-top: 0;
    border-left: 1px solid var(--charcoal-line);
  }
}

/* An ordinary column. This used to be a 360px window with the track
   taken out of flow and translated -50% forever, against a duplicated
   set so the wrap was seamless. It worked, and it was still the wrong
   idea: only about three pieces ever cleared the window, so anything
   past the third - Tend's app demo, the Lizzo spread - was in the file,
   was paid for on load, and was never seen by anybody.

   Now the rail is as tall as its contents and every piece is on the
   page. The cost is that a module with a lot of creative makes for a
   long rail, so the fix is editorial: put fewer, better pieces in the
   carousel rather than trusting a scroll to reveal them. */
.rail__track {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 22px;
}

.rail__item {
  margin: 0;
  flex: 0 0 auto;
}

.rail__item img,
.rail__item video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 2px;
  background: var(--charcoal);
}

/* A placeholder should read as a gap in the material, not as artwork. */
.is-missing img { opacity: 0.6; }

/* ---------------- Footer ---------------- */

.footer {
  border-top: 1px solid var(--charcoal-line);
  padding: clamp(56px, 10vh, 110px) 0 clamp(56px, 10vh, 110px);
}

.footer__line {
  margin: 0;
  max-width: 38ch;
  font-size: clamp(20px, 2.6vw, 30px);
  font-weight: 300;
  line-height: 1.28;
  letter-spacing: -0.02em;
  color: var(--white);
}

.footer__links {
  margin-top: 32px;
  display: flex;
  gap: 28px;
  flex-wrap: wrap;
}

.footer__links a {
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 1px solid var(--charcoal-line);
  padding-bottom: 3px;
  transition: color 180ms var(--ease), border-color 180ms var(--ease);
}

.footer__links a:hover {
  color: var(--white);
  border-color: var(--white);
}

/* ---------------- Small screens ---------------- */

/* Above ~1240px the rail sits in the natural margin outside the wrap.
   Below that it would land on top of the content, so reserve a gutter
   for it rather than letting the year collide with the plus signs. */
@media (max-width: 1420px) and (min-width: 721px) {
  .wrap { padding-right: calc(var(--gutter) + 128px); }
}

@media (max-width: 899px) {
  :root { --spine: 40px; --gutter: 20px; --node-top: 26px; }
  /* On a phone the lockup costs more room than it earns. The hero
     copy of the portrait and name takes over. */
  .yearRail { display: none; }
  .job::before { top: 26px; }
  .job__sign { top: 26px; }
  .job__toggle { padding-right: 34px; }
}

/* ---------------- Header entrance ---------------- */

/* Built as a transition away from a temporary offset, not as an
   animation into place. The difference matters: with a filled animation
   the resting state is the *start* frame, so anything that stops frames
   from running leaves the header permanently invisible and the portrait
   stuck upside down. Here the resting state is the finished state, and
   the entrance is an offset removed on the next tick. Worst case you get
   no animation rather than no page. */

.hero__name,
.hero__headline .lead,
.hero__headline [data-edit="headline.rest"],
.hero__intro {
  transition: transform 620ms var(--ease);
}

.hero__headline .lead                       { transition-delay: 70ms; }
.hero__headline [data-edit="headline.rest"] { transition-delay: 150ms; }
.hero__intro                                { transition-delay: 260ms; }

.hero__portrait,
.yearRail__portrait {
  transition: transform 900ms cubic-bezier(0.34, 1.26, 0.64, 1);
}

@media (prefers-reduced-motion: no-preference) {
  /* Transform only, deliberately. Fading from opacity 0 looked better,
     but a fade that never completes leaves the header invisible, and a
     rise that never completes leaves it 9px low. Only one of those is
     worth risking. */
  .is-entering .hero__name,
  .is-entering .hero__headline .lead,
  .is-entering .hero__headline [data-edit="headline.rest"],
  .is-entering .hero__intro {
    transform: translateY(9px);
  }

  /* The half-turn that lands the portrait upside down on purpose. It
     sits on the figure; the scroll turn sits on the img inside it, so
     the two compose instead of overwriting each other. */
  .is-entering .hero__portrait.is-flipIn,
  .is-entering .yearRail__portrait.is-flipIn {
    transform: rotate(-180deg) scale(0.9);
  }
}

/* The headline spans are inline, which transforms cannot offset, so make
   them block-level in their own right without changing the line breaks. */
.hero__headline .lead,
.hero__headline [data-edit="headline.rest"] {
  display: inline-block;
}

/* ---------------- Password gate ---------------- */

/* While locked, the shell is empty anyway – the copy has not been
   decrypted yet. This just stops the chrome flashing up behind. */
.is-locked .hero,
.is-locked main,
.is-locked .footer,
.is-locked .yearRail { display: none; }

.gate {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--gutter);
}

.gate__form {
  width: 100%;
  max-width: 320px;
}

.gate__name {
  margin: 0 0 clamp(28px, 6vh, 44px);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.gate__label {
  display: block;
  margin-bottom: 10px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dimmer);
}

.gate__input {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--charcoal-line);
  color: var(--white);
  font-family: var(--font);
  font-size: 17px;
  font-weight: 300;
  padding: 8px 0 12px;
  border-radius: 0;
  transition: border-color 180ms var(--ease);
}

.gate__input:focus {
  outline: 0;
  border-bottom-color: var(--white);
}

.gate__input:disabled { opacity: 0.4; }

.gate__submit {
  margin-top: 26px;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 12px 22px;
  cursor: pointer;
  transition: border-color 180ms var(--ease), color 180ms var(--ease);
}

.gate__submit:hover:not(:disabled) {
  border-color: var(--white);
  color: var(--white);
}

.gate__submit:disabled { opacity: 0.4; cursor: default; }

.gate__msg {
  margin: 20px 0 0;
  min-height: 1.4em;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text-dim);
}

.gate__msg.is-error { color: #e8705f; }

/* ---------------- Reduced motion ---------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 1ms !important;
    animation: none !important;
  }
}
