:root {
  --paper: #f7f5ee;
  --ink: #263c35;
  --muted: #64736b;
  --line: #d6dcd1;
  --accent: #c85c39;
  --green: #e4eadf;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: Arial, Helvetica, sans-serif;
  font-size: 16px;
  line-height: 1.6;
}

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

:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

header, main, footer {
  max-width: 1180px;
  margin: auto;
  padding: 0 48px;
}

/* Header and tabs */

/* Pinned to the top so the wordmark and tabs stay reachable while scrolling.
   The background has to be set explicitly, since content now passes underneath
   it; paper matches the body, so only the bottom border marks the edge. The
   header is centred and no wider than main, so nothing shows beside it. */
header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--paper);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
  min-height: 110px;
  border-bottom: 1px solid var(--line);
}

.brand {
  font-size: 36px;
  font-weight: 750;
  letter-spacing: -1px;
  display: flex;
  align-items: flex-end;
  gap: 0;
}

/* A non-none mask makes .mark a stacking context, which paints after inline
   content, so the sphere covers the name unless the name is raised explicitly.
   The 13px gap before the dot lives here rather than on .brand because .brand
   needs a zero gap for the overlap below. */
.wordmark {
  display: flex;
  align-items: baseline;
  gap: 13px;
  line-height: 1;
  margin-bottom: 14px;
  position: relative;
  z-index: 1;
}

/* The logo is a mask rather than an <img>, so its colour comes from CSS and
   can be changed by setting --mark-from and --mark-to. Being a mask, only the
   SVG's alpha is used, so a shaded variant would flatten; RDSpherenoshadow is
   line art and suits it. The reaction-diffusion pattern turns to grey mush
   below about 40px, hence the size.

   The negative margin overlaps the wordmark's "D" onto the sphere's right
   silhouette by 12px. Only the silhouette is overlapped: the maze is
   compressed enough there to read as a contour, whereas type over the open
   pattern competes with it. */
.mark {
  position: relative;
  width: 92px;
  height: 92px;
  margin-right: -12px;
  flex: none;
  -webkit-mask: url("/img/RDSpherenoshadow.svg") center / contain no-repeat;
  mask: url("/img/RDSpherenoshadow.svg") center / contain no-repeat;
  transform: rotate(var(--spin, 0deg));
}

/* The gradient sits on a child rather than on .mark itself so that it can be
   turned back by the same angle logo-spin.js applies above, which leaves the
   colour fixed in place while only the masked pattern turns. The mask still
   applies, because it clips descendants along with the element.

   It is oversized because a square has to be at least as wide as its own
   diagonal to keep covering the masked box at every angle, which is 141.4%.
   The inset below gives 150%, leaving enough margin that subpixel rounding
   cannot expose a hairline at the corners around 45 degrees.

   The stops then pull the two ends of the gradient back in to meet the sphere.
   The mark covers the middle 1/1.5, or 66.67%, of this box, and a centred
   square projects onto a 45 degree gradient line as that same fraction, so the
   range has to start and end (100% - 66.67%) / 2 = 16.67% in from each end.
   Without them the sphere would show only the middle two thirds of the ramp. */
.mark::before {
  content: "";
  position: absolute;
  inset: -25%;
  background: linear-gradient(
    to top right,
    var(--mark-from, #c0220f) 16.667%,
    var(--mark-to, #f5a623) 83.333%
  );
  transform: rotate(calc(-1 * var(--spin, 0deg)));
}

.brand .dot { color: var(--accent); }

.tabs {
  display: flex;
  gap: 30px;
  /* Six tabs stop fitting on one row below about 590px, and gap applies to
     both axes, so without this the wrapped row would sit 30px down and cost
     the pinned header that much height for nothing. */
  row-gap: 10px;
  flex-wrap: wrap;
  /* Sits the tab labels on the wordmark's baseline, which is 23.8px lower.
     align-items: baseline cannot do it, because the brand's baseline would
     come from .mark, which is empty and only contributes a synthesized one.
     Both items stay centred, so that 23.8px holds however tall the line grows.
     The margin is doubled because centring splits it either side, and it is a
     margin rather than a relative offset so that the header grows to match
     when the nav wraps below the brand instead of spilling past the border. */
  margin-top: 47.6px;
}

.tab {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 2.5px;
  color: var(--muted);
  padding: 6px 0;
  border-bottom: 2px solid transparent;
}

.tab:hover { color: var(--ink); }

.tab[aria-selected="true"] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* Panels. Without JS every panel stays visible, separated by a rule. */

.panel { padding: 56px 0; }

.panel + .panel { border-top: 1px solid var(--line); }

.js .panel { display: none; padding: 56px 0 72px; }

.js .panel + .panel { border-top: 0; }

.js .panel.is-active { display: block; }

/* Typography */

h1 {
  font-family: Georgia, serif;
  font-size: 68px;
  line-height: 1.07;
  font-weight: normal;
  letter-spacing: -3px;
  margin: 0;
}

h1 em { color: var(--accent); font-style: normal; }

h2 {
  font-family: Georgia, serif;
  font-size: 40px;
  font-weight: normal;
  letter-spacing: -1.4px;
  margin: 0 0 8px;
}

h2.section-break {
  margin-top: 64px;
  padding-top: 40px;
  border-top: 1px solid var(--line);
}

h3 {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -.2px;
  margin: 0 0 6px;
}

.lead {
  max-width: 68ch;
  margin: 0 0 32px;
  color: var(--muted);
}

.lead a, .prose a {
  color: var(--accent);
  border-bottom: 1px solid var(--accent);
}

/* About */

.hero {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 72px;
  align-items: center;
  padding-bottom: 64px;
}

/* The clip-path polygons are generated in absolute pixels against the
   image's natural 600x400, so the stage is scaled rather than resized. */
.portrait {
  margin: 0;
  width: 600px;
  height: calc(400px * var(--s, 1));
}

.portrait-stage {
  width: 600px;
  height: 400px;
  transform: scale(var(--s, 1));
  transform-origin: top left;
}

#funnyShape { display: block; width: 600px; height: 400px; }

.shaped { clip-path: polygon(95% 50%, 100% 53%, 95% 56%, 99% 59%); }

.prose { border-top: 1px solid var(--line); padding-top: 35px; }

.prose p {
  max-width: 68ch;
  margin: 0;
  font-size: 17px;
  line-height: 1.8;
  color: var(--muted);
}

/* Narrower than the 68ch used elsewhere: a ch is the width of "0", which is
   wider than average lowercase, so 68ch actually sets about 77 characters.
   57ch measures out at roughly 65, which is the comfortable range. */
.about-intro p { max-width: 57ch; }

.about-intro p + p { margin-top: 20px; }

/* A heading inside the intro, which is an h2 so the panel reads h1 then h2
   rather than skipping a level, but nothing like the 40px an h2 is elsewhere:
   those title a whole tab, and this one sits in a 520px column between
   paragraphs. Georgia at this size keeps it in the same family as the titles
   while reading clearly as a step below them. */
.about-intro h2 {
  max-width: 57ch;
  margin: 34px 0 14px;
  font-size: 23px;
  letter-spacing: -.6px;
  color: var(--ink);
}

/* The paragraph after it has lost the p + p rule above, the heading having
   come between them. */
.about-intro h2 + p { margin-top: 0; }

.about-links {
  display: flex;
  flex-wrap: wrap;
  gap: 26px;
  margin-top: 30px;
}

.about-links a {
  color: var(--accent);
  border-bottom: 1px solid currentColor;
}

/* Intro text with one photograph beside it, which fills what would otherwise be
   a 545px column of dead space to the right of the text, and echoes the hero's
   text-left, image-right split above. The columns match .features below, so the
   photograph lines up exactly with the ceramics photo beneath it. */
.about-body {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 44px;
  align-items: start;
  border-top: 1px solid var(--line);
  padding-top: 35px;
}

/* The rule above the intro belongs to the full band, not the text column, so
   it is moved off .prose and onto .about-body. */
.about-body .prose {
  border-top: 0;
  padding-top: 0;
}

/* The two personal features. Deliberately no card or border: the photos and
   the space around them do the work, so only the captions are styled. */
.features {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 44px;
  margin-top: 72px;
}

.feature { margin: 0; }

.feature img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 5px;
}

.feature figcaption {
  margin-top: 16px;
  max-width: 46ch;
  color: var(--muted);
  font-size: 15px;
  line-height: 1.7;
}

/* Ceramics needs two photographs under one caption, so its figure takes a row
   of its own and splits it in two. The inner gap matches the outer one, so all
   three photographs come out exactly the same width as the single feature
   below, rather than shrinking to fit three across one row. */
.feature.wide { grid-column: span 2; }

/* The paired caption describes both photographs, so it is wider than the 46ch
   used for a single one. It stops at the left photograph's right edge rather
   than running the whole row: the row is 1084px on a wide display, which would
   put lines near 130 characters. Matching the column instead lands at about 77
   and lines the caption up with the image above it. Expressed as a calc of the
   pair's own geometry so it tracks the gap and the column width. */
.feature.wide figcaption { max-width: calc((100% - 44px) / 2); }

.feature-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 44px;
}

/* Project and resume cards */

.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 24px;
}

.card {
  background: var(--green);
  border-radius: 5px;
  padding: 26px 28px;
  display: flex;
  gap: 18px;
  align-items: flex-start;
}

.card-thumb {
  flex: none;
  width: 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 5px;
  display: block;
}

.resume-actions {
  display: flex;
  align-items: center;
  gap: 22px;
  flex-wrap: wrap;
  margin-bottom: 22px;
}

.resume-actions a:not(.button) {
  color: var(--accent);
  border-bottom: 1px solid currentColor;
}

/* The rest of the site is underlined text links, so this stays the only
   filled control: enough to read as a button without introducing a whole
   button language. */
.button {
  background: var(--accent);
  color: var(--paper);
  padding: 11px 20px;
  border-radius: 5px;
  font-weight: 600;
}

.button:hover { background: var(--ink); }

/* An image of the page rather than an embedded PDF: Chrome's viewer cannot be
   stripped of its toolbar and side panel, and phones often refuse to render a
   framed PDF at all. scripts/render_resume.py produces this at 200dpi, a bit
   over 2x the width below, so it stays sharp on dense screens. The real PDF is
   one click away above, which is also where screen readers are sent. */
.resume-link { display: block; max-width: 816px; }

.resume-page {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: 5px;
}

/* A page each, rather than one tall image: stacked flush they would read as a
   single long sheet, and the border above would frame the pair instead of
   either one. The gap lets the paper show through between them. */
.resume-page + .resume-page { margin-top: 22px; }

/* The thumbnails repeat the link already on the title, purely as a larger
   click target, so they are kept out of the tab order and hidden from
   assistive tech rather than announcing the same destination twice. Sizing
   stays on the image; the anchor only has to not disturb the card's flex row
   or the write-up's 100px grid column. */
.thumb-link {
  flex: none;
  display: block;
}

.thumb-link:hover img {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.card-links {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  font-size: 15px;
}

.card-links li + li { margin-top: 10px; }

.card-links a {
  color: var(--accent);
  border-bottom: 1px solid currentColor;
}

.card-links span {
  display: block;
  color: var(--muted);
  font-size: 14px;
}

.card h3 a { border-bottom: 1px solid transparent; }

.card h3 a:hover { border-bottom-color: var(--accent); color: var(--accent); }

.card p { margin: 0; color: var(--muted); font-size: 15px; }

/* Patents reuse the project cards. The numbers are the link text, so they are
   read as a column of identifiers rather than prose: tabular figures keep the
   digits on a common width, and the tracking separates the country prefix from
   the number without needing a space that is not in the official form. */
.patent-grants a {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.4px;
}

/* The overseas grants are the same invention again, so they are introduced
   rather than listed as peers of the US ones. Sized like .writeup-date, which
   plays the same subordinate role under a heading.

   Qualified by .card because `.card p` above would otherwise win on
   specificity, flattening both the margin and the size back to body copy. */
.card .patent-elsewhere {
  margin: 18px 0 0;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 1.4px;
  color: var(--muted);
}

/* The list that follows it is already introduced, so it does not need the
   14px that separates the first list from the summary above it. */
.patent-elsewhere + .card-links { margin-top: 8px; }

/* Write-ups: a small square thumbnail beside the text, as the Tumblr table
   of contents presents them. The source images are only 100x100, so they are
   shown at that size rather than upscaled. */

.writeups {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
  gap: 30px 44px;
}

.writeup {
  display: grid;
  grid-template-columns: 100px 1fr;
  gap: 20px;
  align-items: start;
}

.writeup-thumb {
  width: 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 5px;
  background: var(--green);
  display: block;
}

.writeup-text h3 a { border-bottom: 1px solid transparent; }

.writeup-text h3 a:hover { color: var(--accent); border-bottom-color: var(--accent); }

.writeup-date {
  margin: 0 0 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1.6px;
  color: var(--muted);
}

.writeup-text p:last-child {
  margin: 0;
  font-size: 15px;
  color: var(--muted);
}

.link-list {
  list-style: none;
  margin: 32px 0 0;
  padding: 0;
  columns: 2;
  column-gap: 48px;
}

.link-list li {
  break-inside: avoid;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}

.link-list a:hover { color: var(--accent); }

/* Videos */

.videos {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 36px 28px;
}

.video-play {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin: 0 0 12px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  overflow: hidden;
  background: var(--green);
  cursor: pointer;
}

.video-play img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.video-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 58px;
  height: 40px;
  transform: translate(-50%, -50%);
  border-radius: 8px;
  background: rgba(38, 60, 53, .78);
  transition: background .15s;
}

.video-icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-40%, -50%);
  border-style: solid;
  border-width: 7px 0 7px 12px;
  border-color: transparent transparent transparent #fff;
}

.video-play:hover .video-icon { background: var(--accent); }

.video iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-bottom: 12px;
  border: 0;
  border-radius: 5px;
  display: block;
}

.video h3 { font-size: 15px; font-weight: 700; }

.video h3 a:hover { color: var(--accent); }

.video-note { margin: 2px 0 0; font-size: 13px; color: var(--muted); }

/* Contact */

.contact-email { margin: 0 0 8px; }

.email {
  font-family: Georgia, serif;
  font-size: 32px;
  letter-spacing: -1px;
  color: var(--accent);
  border-bottom: 1px solid var(--accent);
}

.link-list.contact-links { columns: 1; }

/* Footer */

footer {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  border-top: 1px solid var(--line);
  padding-top: 22px;
  padding-bottom: 26px;
  font-size: 11px;
  color: var(--muted);
}

.footer-links { display: flex; gap: 20px; }

.footer-links a:hover { color: var(--accent); }

/* Responsive */

@media (max-width: 1120px) { .portrait { --s: .86; width: 516px; } }

/* Narrower than this the intro's text column falls below about 55 characters,
   which is too tight for body copy, so the photograph drops beneath the text
   and keeps the width it has in the ceramics row. */
@media (max-width: 1000px) {
  .about-body { grid-template-columns: 1fr; gap: 40px; }
  .about-body > .feature { max-width: 520px; }
}

@media (max-width: 940px) {
  .hero { grid-template-columns: 1fr; gap: 32px; }
  .portrait { --s: 1; width: 600px; max-width: 100%; }
  h1 { font-size: 53px; letter-spacing: -2px; }
}

@media (max-width: 850px) {
  header, main, footer { padding-left: 25px; padding-right: 25px; }
  header { padding-top: 22px; padding-bottom: 18px; }
  h2 { font-size: 32px; }

  /* Around this width the tabs no longer fit beside the wordmark and wrap
     underneath it, which pushed the pinned header to a third of a phone screen.
     The lockup is scaled down as a unit, keeping the proportions settled at
     full size, and the baseline margin goes: it exists only to seat the tabs on
     the wordmark's baseline, which means nothing once they sit below it. */
  header { gap: 14px; min-height: 0; }
  .brand { font-size: 25px; }
  .mark { width: 64px; height: 64px; margin-right: -8px; }
  .wordmark { gap: 9px; margin-bottom: 10px; }
  .tabs { margin-top: 0; }
  .link-list { columns: 1; }
  .writeups { grid-template-columns: 1fr; }
}

@media (max-width: 430px) {
  .writeup { grid-template-columns: 1fr; gap: 12px; }
}

@media (max-width: 700px) {
  .portrait { --s: .8; width: 480px; }
  /* Stacked, so the ceramics figure no longer needs the extra column. Its two
     photographs stay side by side down to 560px, below which they stack too. */
  .features { grid-template-columns: 1fr; gap: 40px; }
  .feature.wide { grid-column: span 1; }
  .feature-pair { gap: 14px; }
}

@media (max-width: 560px) {
  .portrait { --s: .62; width: 372px; }
  .email { font-size: 24px; }

  /* All five tabs come to 390px at the usual tracking, against 340px of room on
     a 390px phone, so they wrapped to a second row and cost the pinned header
     another 43px. Tightening the tracking and gap brings them to 316px and onto
     one row. Below about 330px they wrap again, which is unavoidable. */
  .tabs { gap: 10px; }
  .tab { letter-spacing: 1.2px; }
  /* Below this a side-by-side pair would be under 250px a side, smaller than
     the single photograph above it. Stacking them keeps every photograph on
     the page the same width; the caption avoids left/right wording so the
     change of arrangement does not contradict it. */
  .feature-pair { grid-template-columns: 1fr; gap: 14px; }
}

@media (max-width: 440px) { .portrait { --s: .48; width: 288px; } }
