/* tommylu.com — a calling card.
   Light by default because that is what Tommy liked; dark follows the system,
   and an explicit choice overrides both. Every colour is a token defined on
   bare :root first, so no state can render one theme's text on the other's
   ground. Jost at 200 gives the name an airy, architectural set; IBM Plex Mono
   carries the T_LU mark and the small labels. */

:root {
  --bg:      #ffffff;
  --ink:     #14171d;
  --dim:     #6a7280;
  --faint:   #9aa1ac;
  --line:    rgba(0, 0, 0, .10);
  --raise:   #fafafa;
  --hover:   #f4f5f7;

  --sans: "Jost", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:    #0b0d11;
    --ink:   #eef1f6;
    --dim:   #939cab;
    --faint: #666e7c;
    --line:  rgba(255, 255, 255, .11);
    --raise: #0f1218;
    --hover: #151922;
  }
}
:root[data-theme="dark"] {
  --bg:    #0b0d11;
  --ink:   #eef1f6;
  --dim:   #939cab;
  --faint: #666e7c;
  --line:  rgba(255, 255, 255, .11);
  --raise: #0f1218;
  --hover: #151922;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 300;
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
a:focus-visible, button:focus-visible { outline: 1px solid var(--ink); outline-offset: 5px; border-radius: 2px; }

/* ── flow field ──────────────────────────────────────────────────────────── */
#flow {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  z-index: 0;
  display: block;
}

/* ── intro ───────────────────────────────────────────────────────────────
   There is no intro overlay any more. The old one drew its own T_LU on a
   full-bleed layer and then scaled the underscore to scale(80,620) -- a bar
   painted in var(--ink), so on a dark theme the last thing before the homepage
   was a white screen. That is the loader feeling this replaces.

   Now the hero wordmark IS the intro: the real <h1 class="name"> starts drawn
   compact as T_LU and expands into TOMMY LU. Nothing is duplicated, so there
   is no swap to mis-time and the final frame is the resting hero by
   construction -- it is the same element with its transforms removed.

   Everything below only applies while html.tlu-anim is set, which the document
   head decides before the first paint and site.js removes when the expansion
   lands. Without JS the class is never set and the page renders at rest. */
html.tlu-anim .name span,
html.tlu-anim .top,
html.tlu-anim .links,
html.tlu-anim .bottom { opacity: 0; }
/* Invisible is not the same as absent: opacity:0 still takes clicks, and the
   nav sits over the wordmark's own space for the second the intro runs. Nothing
   the visitor cannot see should be able to receive a tap. */
html.tlu-anim .top,
html.tlu-anim .links,
html.tlu-anim .bottom { pointer-events: none; }

/* The canvas is faded up under the wordmark rather than switched on, so the
   contour field emerges out of the ground instead of appearing. */
html.tlu-anim #flow { opacity: 0; }

/* The bar that stands in for the underscore. Absolutely positioned so it can
   never take part in layout: the hero's final metrics must not depend on an
   element that only exists during the intro. site.js creates and removes it. */
.name { position: relative; }
.nm-bar {
  /* An underscore sits BELOW the baseline, and the baseline in a line-height:1
     box of this face is around 78% down. At the box's centre the bar reads as a
     hyphen -- captured at 64% it gave "T—LU" -- so it is set under the caps
     where the glyph it stands in for would be. */
  position: absolute; top: 82%; left: 0;
  height: .055em; width: 1px;
  background: currentColor;
  transform-origin: 50% 50%;
  pointer-events: none;
  will-change: transform, opacity;
}

/* Content waits behind the intro, then rises once it is through. */
.things { opacity: 0; }
body.ready .things { opacity: 1; transition: opacity .7s ease; }

/* The per-letter entrance is the non-intro path only. When the intro ran, the
   letters have just been animated into place by hand and re-running this would
   fade the finished wordmark out and back in. */
/* After the intro the furniture has less distance to travel and less time to
   do it in: the wordmark has already carried the reveal, and the site's
   standard .8s entrance would leave the page still assembling at ~2s. */
body.ready.intro-ran .top    { animation: rise .52s cubic-bezier(.22,.61,.36,1) .02s both; }
body.ready.intro-ran .links  { animation: rise .52s cubic-bezier(.22,.61,.36,1) .12s both; }
body.ready.intro-ran .bottom { animation: rise .52s cubic-bezier(.22,.61,.36,1) .20s both; }

/* ── shell ───────────────────────────────────────────────────────────────── */
.shell {
  position: relative; z-index: 2;
  min-height: 100vh; min-height: 100svh;
  display: grid; grid-template-rows: auto 1fr auto;
  padding: clamp(1.3rem, 3.4vw, 2.3rem);
}

.top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.mark {
  font-family: var(--mono);
  font-size: .8rem; letter-spacing: .34em;
  color: var(--ink);
}
.cursor { animation: blink 1.5s steps(2, start) infinite; }
@keyframes blink { to { opacity: 0; } }

.top-right { display: flex; align-items: center; gap: 1.1rem; }
.place {
  font-family: var(--mono);
  font-size: .68rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--faint);
}
.theme {
  appearance: none; background: none; border: 0; padding: .2rem;
  color: var(--faint); cursor: pointer; line-height: 0;
  transition: color .2s ease, transform .35s ease;
}
.theme:hover { color: var(--ink); transform: rotate(25deg); }
:root[data-theme="dark"] .theme .rays,
:root:not([data-theme="light"]) .theme .rays { opacity: 1; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .theme .rays { opacity: .55; } }

/* ── centre ──────────────────────────────────────────────────────────────── */
.center {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
}

.name {
  /* Set as a studio wordmark rather than a heading: Jost is a Futura
     descendant, and wide-tracked geometric caps is the register design practices
     have used for their own identity for decades. Uppercase does the work here —
     at this tracking, mixed case reads as a sentence, caps read as a mark. */
  font-weight: 200;
  font-size: clamp(1.9rem, 8vw, 5.4rem);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: .42em;
  text-indent: .42em;              /* re-centres against the trailing space */
  margin: 0;
  display: flex; flex-wrap: nowrap; justify-content: center;
  white-space: nowrap;
}
.name .gap { width: .72em; }
.name span { display: inline-block; }

body.ready:not(.intro-ran) .name span {
  animation: letter .85s cubic-bezier(.22, .61, .36, 1) both;
}
.name span:nth-child(1) { animation-delay: .02s; }
.name span:nth-child(2) { animation-delay: .06s; }
.name span:nth-child(3) { animation-delay: .10s; }
.name span:nth-child(4) { animation-delay: .14s; }
.name span:nth-child(5) { animation-delay: .18s; }
.name span:nth-child(7) { animation-delay: .24s; }
.name span:nth-child(8) { animation-delay: .28s; }
@keyframes letter { from { opacity: 0; transform: translateY(.35em); } to { opacity: 1; transform: none; } }

.line {
  font-size: clamp(.95rem, 2.2vw, 1.08rem);
  color: var(--dim);
  max-width: 34ch;
  margin: clamp(1.7rem, 4vw, 2.4rem) 0 0;
  text-wrap: balance;
}
body.ready .line { animation: rise .8s cubic-bezier(.22,.61,.36,1) .34s both; }

.links {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: clamp(1.2rem, 3.5vw, 2.3rem);
  margin-top: clamp(3.2rem, 8vw, 5rem);
}
body.ready .links { animation: rise .8s cubic-bezier(.22,.61,.36,1) .44s both; }
.links a {
  font-family: var(--mono);
  font-size: .76rem; letter-spacing: .11em;
  color: var(--dim);
  padding-bottom: .45rem;
  position: relative;
}
.links a::after {
  content: ""; position: absolute; left: 50%; bottom: 0;
  width: 0; height: 1px; background: var(--ink);
  transition: width .32s cubic-bezier(.22,.61,.36,1), left .32s cubic-bezier(.22,.61,.36,1);
}
.links a:hover { color: var(--ink); }
.links a:hover::after { width: 100%; left: 0; }

@keyframes rise { from { opacity: 0; transform: translateY(.6rem); } to { opacity: 1; transform: none; } }

.bottom { display: flex; justify-content: center; }
body.ready .bottom { animation: rise .8s cubic-bezier(.22,.61,.36,1) .54s both; }
.toggle {
  font-family: var(--mono);
  font-size: .68rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--faint);
  display: inline-flex; flex-direction: column; align-items: center; gap: .55rem;
}
.toggle::after { content: ""; width: 1px; height: 1.4rem; background: linear-gradient(var(--faint), transparent); }
.toggle:hover { color: var(--dim); }

/* ── things ──────────────────────────────────────────────────────────────── */
.things {
  position: relative; z-index: 2;
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding: clamp(4rem, 10vw, 6.5rem) clamp(1.3rem, 5vw, 3rem);
}
.things > * { max-width: 52rem; margin-left: auto; margin-right: auto; }
.label {
  font-family: var(--mono);
  font-size: .66rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--faint); margin: 0 auto 1rem;
}
.things h2 {
  font-weight: 200; font-size: clamp(1.6rem, 4.4vw, 2.2rem);
  letter-spacing: .01em; line-height: 1.25; margin: 0 auto 2.8rem;
}
.tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 1px;
}
/* The hairline grid is drawn by each tile's outline rather than by a container
   background showing through the gaps. Outlines take no space and adjacent ones
   meet inside the 1px gap, so the lines look identical -- but a row that is not
   full no longer paints a grey slab where the missing cells would be. The old
   approach broke whenever the tile count did not divide by the column count. */
.tiles a {
  background: var(--bg); padding: 1.6rem 1.4rem 1.8rem;
  display: flex; flex-direction: column; gap: .25rem;
  outline: 1px solid var(--line);
  transition: background .25s ease;
}
.tiles a:hover { background: var(--hover); }
.t-n { font-family: var(--mono); font-size: .66rem; color: var(--faint); letter-spacing: .12em; }
.t-t { font-size: 1.02rem; margin-top: .35rem; }
.t-d { font-size: .87rem; color: var(--dim); }

.back {
  display: block; margin: 2.8rem auto 0;
  font-family: var(--mono); font-size: .68rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--faint);
}
.back:hover { color: var(--dim); }


/* ── work ────────────────────────────────────────────────────────────────────
   Type-led and rule-separated, matching the rest of the page: no cards, no
   shadows, no new colours. A project is a heading, a sentence, the interesting
   part, and what it is built from. Everything here uses the tokens already
   defined at the top of this file. */
.projects { list-style: none; margin: 0 auto; padding: 0; }
.project {
  border-top: 1px solid var(--line);
  padding: clamp(1.9rem, 4vw, 2.7rem) 0;
  display: flex; flex-direction: column; gap: .7rem;
}
/* The section's own h2 already sits above the first project, so a rule there
   would double up. */
.project:first-child { border-top: 0; padding-top: 0; }
.project:last-child { padding-bottom: 0; }

.p-t {
  font-weight: 300;
  font-size: clamp(1.15rem, 3vw, 1.45rem);
  letter-spacing: .015em;
  margin: 0;
}
.p-d { margin: 0; max-width: 46ch; text-wrap: pretty; }
.p-hook {
  margin: 0; font-size: .92rem; color: var(--dim);
  max-width: 54ch; text-wrap: pretty;
}

.p-tags {
  list-style: none; margin: .4rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .45rem;
}
.p-tags li {
  font-family: var(--mono);
  font-size: .63rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint);
  border: 1px solid var(--line);
  padding: .24rem .55rem;
  white-space: nowrap;
}

/* Same underline-grow as the header links, so a case-study link reads as the
   same kind of control wherever it appears. */
.p-links { margin: .55rem 0 0; display: flex; flex-wrap: wrap; gap: 1.4rem; }
.p-links a {
  font-family: var(--mono);
  font-size: .72rem; letter-spacing: .11em;
  color: var(--dim);
  padding-bottom: .4rem;
  position: relative;
}
.p-links a::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 0; height: 1px; background: var(--ink);
  transition: width .32s cubic-bezier(.22,.61,.36,1);
}
.p-links a:hover { color: var(--ink); }
.p-links a:hover::after { width: 100%; }

/* ── engineering notes ───────────────────────────────────────────────────── */
.noteslist { list-style: none; margin: 0 auto; padding: 0; }
.note { border-top: 1px solid var(--line); }
.note:first-child { border-top: 0; }
.note a {
  display: flex; flex-direction: column; gap: .3rem;
  padding: 1.4rem 0;
}
.note:first-child a { padding-top: 0; }
.n-t { font-size: 1.02rem; transition: color .2s ease; }
.n-d { font-size: .88rem; color: var(--dim); max-width: 58ch; text-wrap: pretty; }
.note a:hover .n-t { color: var(--dim); }

@media (max-width: 620px) {
  .name { letter-spacing: .26em; text-indent: .26em; font-size: clamp(1.5rem, 9vw, 2.6rem); }
  .name .gap { width: .5em; }
  .links { gap: 1.2rem; }
  .p-hook { font-size: .88rem; }
  .p-tags { gap: .35rem; }
}
@media (prefers-reduced-motion: reduce) {
  .name span, .line, .links, .bottom { animation: none !important; }
  .cursor { animation: none; }
}
