/* ============================================================
   Mohammed Essam — portfolio
   Red, flat, loud. No halftone, no sunburst, no gold, no drop
   shadows: those read as 1970s pastiche, which reads as old.
   Two registers only:
     home  = flat red field, off-white ticket, black type
     rest  = off-white gallery
   ============================================================ */

/* ---- MONIQA, and why it is switched off ---------------------
   Free typeface by Rajesh Rajput, SIL Open Font License 1.1.
   The files are NOT in this repo and never have been.

   This used to be a live @font-face naming six candidate files, so
   every page load asked the server for five fonts that do not exist
   and got five 404s, on every page, for every visitor. A browser
   fetches a webfont as soon as something on the page asks for that
   family, and "Moniqa" was first in the home headline stack, so it
   asked on every single load. Syne is the chosen look now, not a
   placeholder, so the cost was being paid for nothing.

   TO TURN IT ON: get the files from the designer's own page
   (behance.net/gallery/114431757), drop them into a "fonts" folder
   next to this file, uncomment the block below, and put "Moniqa"
   back at the front of --display-home. Name only the file you
   actually have, or the missing ones are 404s again.

@font-face{
  font-family:"Moniqa";
  src:url("fonts/Moniqa.woff2") format("woff2");
  font-weight:100 900;
  font-style:normal;
  font-display:swap;
}
   -------------------------------------------------------------- */

:root{
  --red:#E3261A;
  --red-deep:#B7150B;
  --red-hot:#FF3B2B;
  --ink:#0B0A09;
  --paper:#F6F3EE;
  --ink-60:#0B0A0999;
  --ink-30:#0B0A094d;
  --ink-12:#0B0A091f;

  /* page (non-home) palette — off-white gallery */
  --bg:#F2EEE5;
  --fg:#0B0A09;
  --fg-dim:#0B0A09a8;
  /* Raised from #0B0A0961 on 9 Sep 2026. At 38% ink it composited to 2.44:1
     against a 4.5 requirement, and it was not decoration: it set the job title
     under his name in the nav on EVERY page, the serial on every work card,
     "See all work" and the copyright line. Four visible things, all failing.
     0xB4 is 71% ink, which measures 4.6:1 on the lightest ground the site uses
     (#F1E4DC) and better on the others, so it clears on all of them.
     Still visibly quieter than --fg-dim, which is the job it was doing. */
  --fg-faint:#0B0A09b4;
  --line:#0B0A091f;
  --well:#E4DFD2;
  /* the sticky nav's own ground: the same paper, nearly opaque. It was
     hard-coded to the default #F2EEE5 on every page, so it sat slightly wrong
     on all three that change their paper. A token means it follows. */
  --nav-bg:#F2EEE5f2;

  /* the home headline. Syne IS the look. "Moniqa" is not listed here on
     purpose: naming a family with no file behind it is what made the browser
     ask for five fonts that do not exist on every page load. See the top of
     this file for how to turn it back on. */
  --display-home:"Syne","Bricolage Grotesque",sans-serif;
  --display:"Bricolage Grotesque","Hanken Grotesk",sans-serif;
  --mono:"Barlow Condensed","Hanken Grotesk",sans-serif;

  --ease:cubic-bezier(.2,.8,.2,1);
  --wrap:1240px;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  background:var(--bg);
  color:var(--fg);
  font-family:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  font-size:17px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit}

.wrap{width:min(100% - 44px,var(--wrap));margin-inline:auto}

/* THE PAGE GROWS WITH THE SCREEN.
   1240 was the whole story at every width, which is right on a laptop and
   wrong on a big monitor: at 2700px it left about 750px of empty paper down
   each side and the work grid, two 4:3 cards across, came out smaller than
   it does on a 15 inch laptop. A portfolio's images are the argument, so on
   a large screen they should be large.

   Only the container moves. Every block of words already carries its own
   measure in characters (62ch on a project, 60ch under a heading, 44ch on a
   card, 32em in Japanese), so none of this widens a line of text: it widens
   the pictures and the space around them, which is what was too small.

   Smooth rather than stepped: breakpoints would make the grid jump as he
   drags a window across them. 86vw is the share of the screen the page
   takes; 1240 is the floor, so nothing below about 1440 changes at all and
   the laptop layout is exactly what it was; 2160 is the ceiling, because
   past that a two-column grid starts making single images bigger than
   anyone wants rather than showing more work.

   The floor cannot hurt a phone: .wrap takes the SMALLER of this and the
   screen less its margins, so at 400px it still comes out 356. */
:root{ --wrap:clamp(1240px, 86vw, 2160px) }

/* mono voice: labels, serials, small print */
.mono,.serial,.tk-top,.tk-terms,.page-nav,.sticker,.project-meta,
.next-label,.exp-time,.skill-tag,.terms,.note,.tk-band,.tk-badge,.joke{
  font-family:var(--mono);
  text-transform:uppercase;
  letter-spacing:.14em;
  font-weight:600;
}

h1,h2,h3,.big{
  font-family:var(--display);
  font-weight:800;
  letter-spacing:-.03em;
  line-height:.95;
  margin:0;
}

/* ============================ THE WAY IN ============================
   The ticket's own perforation is punched across the red field, left to
   right, and when the row is complete the field tears along it and the two
   halves travel off, each carrying its scalloped edge away with it.

   Chosen from preview-intro.html on 22 Aug 2026 over three others (a drawn
   rule, a validation stamp, and the game number set enormous). It won because
   it is the only one built out of something the site already owns, and the
   only one that is honestly a LOADING screen: the perforation crossing the
   screen is a progress bar that does not look like one.

   It replaced two tumbling dice, which were his idea and then his complaint:
   "the intro is soooooooooo bad, need to look elegant and decent, like the
   cabinet". Do not put the dice back.

   All of it is built by the inline script at the top of index.html, so with
   JavaScript off there is no overlay at all.
   ================================================================== */
.boot{display:none}
html.is-booting .boot{
  display:block;position:fixed;inset:0;z-index:9999;
  /* NO background of its own. The two halves ARE the red field. Give this a
     red as well and it parts to reveal more red, which is exactly the trap a
     card wipe on this site fell into once already. */
  background:transparent;
}

/* THE TICKET IS NOT HIDDEN. It is behind the halves from the first frame and
   the tear uncovers it, so there is no class anywhere whose removal the page's
   visibility depends on, and the headline can measure itself and the foil can
   be painted the whole time this is on top of them. */

.boot-half{
  position:absolute;left:0;right:0;
  /* a pixel of overlap each. Two halves of an odd number of pixels leave a
     sub-pixel seam down the middle, and one hairline of paper showing through
     before the perforation is punched gives the whole thing away. */
  height:calc(50% + 1px);
  background:var(--red);
  will-change:transform;
}
.boot-half.top{top:0}
.boot-half.bot{bottom:0}

/* The punched holes. A window that grows in steps over a strip that does not
   move, rather than an animated mask: the strip keeps its own width so the
   holes are revealed one after another instead of being stretched, and a
   growing width is understood by everything that has ever rendered CSS. */
.boot-punch{
  position:absolute;left:0;width:0;height:14px;overflow:hidden;
}
.boot-half.top .boot-punch{bottom:-7px}
.boot-half.bot .boot-punch{top:-7px}
.boot-punch i{
  position:absolute;left:0;top:0;height:14px;width:100vw;
  background-image:radial-gradient(circle at 11px 7px,var(--paper) 5px,transparent 5.5px);
  background-size:22px 14px;
}

/* steps(), so they are punched one at a time rather than wiped across.
   Every duration below, and the 1270ms trigger in index.html, is the original
   divided by 1.15 at his request: the sequence ends at 1.98s instead of 2.28s.
   They have to be scaled TOGETHER or the punch and the tear stop meeting. */
html.is-booting .boot.is-punching .boot-punch{animation:bootPunch .826s steps(28,end) .13s both}
@keyframes bootPunch{from{width:0}to{width:100%}}
/* someone who clicks before it has finished gets the row completed at once,
   not a half-punched ticket torn in half */
html.is-booting .boot.is-skipped .boot-punch{animation:none;width:100%}

/* The tear. Eased IN rather than out: paper holds, gives all at once, and
   then it is gone. An ease-out here reads as two panels sliding, which is a
   slideshow transition and not a ticket being torn. */
html.is-booting .boot.is-tearing .boot-half.top{animation:bootTearUp   .713s cubic-bezier(.55,0,.28,1) both}
html.is-booting .boot.is-tearing .boot-half.bot{animation:bootTearDown .713s cubic-bezier(.55,0,.28,1) both}
@keyframes bootTearUp  {from{transform:translateY(0)}to{transform:translateY(-101%)}}
@keyframes bootTearDown{from{transform:translateY(0)}to{transform:translateY(101%)}}

/* ============================ HOME: THE TICKET ============================ */

body.is-home{
  background:var(--red);
  min-height:100vh;min-height:100svh;
  display:flex;align-items:center;justify-content:center;
  padding:40px 22px;
}

.ticket{
  width:min(100%,940px);
  background:var(--paper);
  color:var(--ink);
  position:relative;
  border-radius:4px;
  display:flex;flex-direction:column;
}
/* perforated edges, drawn as plain dots. no texture, no shadow. */
.perf{height:14px;position:relative;background:var(--paper);border-radius:4px 4px 0 0;flex:none}
.perf.bot{border-radius:0 0 4px 4px}
.perf::before{
  content:"";position:absolute;left:0;right:0;height:14px;
  background-image:radial-gradient(circle at 7px 7px,var(--red) 5px,transparent 5.5px);
  background-size:22px 14px;
}
.perf.top::before{top:-7px}
.perf.bot::before{bottom:-7px}

/* the badge sits at the top right of the ticket now; the black band it used
   to ride in is gone, which gives the name the whole width of the left. */
.tk-badge{
  background:var(--red);color:var(--paper);
  font-size:11px;padding:6px 11px;border-radius:0;
  margin:0;white-space:nowrap;flex:none;
  font-family:var(--mono);text-transform:uppercase;letter-spacing:.14em;font-weight:600;
  /* it became a link when it started opening the cabinet, and picked up the
     browser's underline with it. The red block is the affordance. */
  text-decoration:none;
}
/* the barcode is a link to the playlist for the same reason, and a rule under
   a barcode reads as part of the barcode */
a.barcode{text-decoration:none}

/* The name lockup, and the reason anyone knows whose site this is: the home
   has no nav bar. Name left and big, his line and the award count pushed to
   the far side of the same row, which is what the black band used to do. */
.tk-namerow{
  display:grid;
  grid-template-columns:1fr auto;
  align-items:start;
  column-gap:24px;
  margin:0 0 26px;
}
/* The name is set in the condensed voice, not the display one. It used to be
   the same face and the same weight 800 as the headline, sitting 26px above
   it, and two heavy blocks that close together read as one mass however far
   apart their point sizes are. A different register stops the fight without
   making the name small. */
.tk-name{
  grid-column:1;
  font-family:var(--mono);
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.16em;
  /* scales with the viewport rather than sitting on a floor: on a phone the
     headline can only be about 30px before "copywriter" runs off the edge,
     and a name pinned at 24px would be nearly the same size as it */
  font-size:clamp(15px,4.4vw,44px);
  line-height:1;
  color:var(--ink);
  margin:0;
}
.tk-aside{
  grid-column:2;grid-row:1;
  display:flex;flex-direction:column;align-items:flex-end;gap:9px;
  text-align:right;padding-top:4px;
}
.tk-aside > span:first-child{
  font-family:var(--mono);text-transform:uppercase;
  letter-spacing:.16em;font-weight:600;font-size:12px;color:var(--ink-60);
}

.tk-body{padding:44px 46px 34px;flex:1;display:flex;flex-direction:column}

.tk-top{
  display:flex;justify-content:space-between;gap:16px;
  font-size:11px;color:var(--ink-60);margin-bottom:46px;
}

/* the headline: Syne. */
.tk-title{
  font-family:var(--display-home);
  font-weight:800;
  letter-spacing:-.035em;
  line-height:.92;
  margin:0 0 22px;
  font-size:clamp(52px,10.5vw,124px);
  color:var(--ink);
}
/* one line each, always: the script below measures them and shrinks the
   headline to fit, which only works if the browser is not allowed to wrap */
.tk-title span{display:block;white-space:nowrap}
.tk-title span:last-child{color:var(--red)}

/* The sub-line and the tabs share the left column; the stamp holds the right
   and stands beside both. The stamp used to sit in a row of its own next to a
   one-line paragraph, which opened 188px of empty paper under the copy while
   the stamp floated across from it. Stretching the column and pushing the
   tabs to the bottom of it puts the two blocks on the same baseline and the
   hole closes. */
.tk-mid{
  display:flex;align-items:stretch;justify-content:space-between;
  gap:clamp(20px,4vw,56px);margin:0 0 40px;
}
.tk-midleft{
  flex:1;min-width:0;display:flex;flex-direction:column;
}
.tk-midleft .tk-nav{flex:1;margin-top:clamp(14px,2.4vh,30px);margin-bottom:0}
.tk-sub{font-size:18px;line-height:1.5;color:var(--ink-60);margin:0;max-width:46ch}

/* ============================ THE STAMP ============================
   A postage stamp, not a rubber one. Red field bled to the edge, the
   perforation biting into it in the ticket's own paper colour, a duotone
   portrait, the issuing department curved round it, a value and the name
   along the bottom. The whole thing is one link to the About page.
   No texture and no filters: the duotone is a blend mode over a flat
   colour, which is how two-ink printing actually works.
   =================================================================== */

.stamp{
  flex:0 0 auto;position:relative;display:block;text-decoration:none;
  /* sized off the viewport HEIGHT as well as its width. The home page does
     not scroll, and a stamp measured only in vw grows past the bottom of a
     short laptop screen: 1.24 of a too-wide stamp is a very tall one. */
  width:clamp(130px,min(21vw,19.5vh),268px);
  aspect-ratio:1/1.24;
  /* everything inside is measured against the stamp, not the window, so the
     name cannot outgrow the band it sits in. The px values are the fallback
     for anything that does not know cqw and are simply overridden below. */
  container-type:inline-size;
  background:var(--red);
  color:var(--paper);
  transform:rotate(-3.2deg);
  transition:transform .28s var(--ease);
}
.stamp:hover,.stamp:focus-visible{transform:rotate(0deg) scale(1.045)}

/* the perforation: half-circles of ticket paper eating the four edges */
.stamp-perf{
  position:absolute;inset:-1px;z-index:3;pointer-events:none;
  background-image:
    radial-gradient(circle at 50% 0,   var(--paper) 4.6px, transparent 5px),
    radial-gradient(circle at 50% 100%,var(--paper) 4.6px, transparent 5px),
    radial-gradient(circle at 0 50%,   var(--paper) 4.6px, transparent 5px),
    radial-gradient(circle at 100% 50%,var(--paper) 4.6px, transparent 5px);
  background-size:13px 9px, 13px 9px, 9px 13px, 9px 13px;
  background-position:top center, bottom center, left center, right center;
  background-repeat:repeat-x, repeat-x, repeat-y, repeat-y;
}

/* the field. The photo is the stamp now and the red is only a frame round it,
   so the inner rule is gone and the portrait is most of the surface. */
.stamp-in{
  position:absolute;inset:7px 7px 23% 7px;z-index:2;display:block;
}

.stamp-arc{position:absolute;inset:1.5% 1.5% auto;width:97%;height:auto;z-index:3}
.stamp-arc-t{
  fill:var(--paper);font-family:var(--mono);font-weight:700;
  font-size:10.5px;letter-spacing:.02em;
}

/* duotone: a flat red fill with the photo screened over it, which is what
   two-ink printing is, rather than a filter stack pretending to be ink.
   Circular, and large enough that the portrait reads as the subject of the
   stamp rather than a vignette sitting inside a frame. */
.stamp-photo{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:72%;aspect-ratio:1/1;border-radius:50%;
  overflow:hidden;background:var(--red-deep);
}
.stamp-photo img{
  width:100%;height:100%;object-fit:cover;display:block;
  filter:grayscale(1) contrast(1.26) brightness(1.14);
  mix-blend-mode:screen;
}

/* the band under the field: the line, and the place under it */
.stamp-band{
  position:absolute;left:7px;right:7px;bottom:7px;height:calc(23% - 14px);
  z-index:2;display:flex;flex-direction:column;
  align-items:center;justify-content:center;
  background:var(--paper);color:var(--ink);padding:0 6%;text-align:center;
}
.stamp-value{
  flex:none;font-family:var(--display-home);font-weight:800;
  line-height:1;letter-spacing:-.05em;color:var(--red);
  font-size:20px;
  font-size:14cqw;
}
.stamp-value:empty{display:none}
.stamp-names{display:block;min-width:0;width:100%}
.stamp-name{
  display:block;
  font-family:var(--display);font-weight:800;
  text-transform:uppercase;letter-spacing:-.02em;line-height:1.04;
  font-size:11px;
  font-size:7.2cqw;
}
.stamp-foot{
  display:block;margin-top:.25em;
  font-family:var(--mono);text-transform:uppercase;font-weight:600;
  letter-spacing:.14em;line-height:1.2;color:var(--ink-30);
  /* one line, always: it is the last thing in a fixed-height band and a wrap
     pushes it straight out through the bottom of the stamp */
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  font-size:6px;
  font-size:4cqw;
}
.stamp-foot:empty{display:none}

/* franked once he wins: the field goes to ink and the value follows */
body.has-won .stamp{background:var(--ink)}
body.has-won .stamp-photo{background:var(--ink)}
body.has-won .stamp-value{color:var(--ink)}

/* nav as punch boxes. The link row is 1fr, not auto, so on the home page the
   punches grow into the height the stamp leaves beside them instead of
   sitting at a fixed height with empty paper above. */
.tk-nav{
  display:grid;grid-template-columns:repeat(3,1fr);
  grid-template-rows:auto 1fr;
  gap:10px 12px;margin-bottom:34px;
}
.tk-nav > a{grid-row:2}
/* The note sits on top of the covered box, as its caption.

   NO text-transform. It used to be forced to capitals, so "...think you can
   give it a shot?" came out shouting and every note he wrote came out in the
   same voice whatever he typed. He asked for his own casing back, and he is
   right: this is the one line up there that is him speaking rather than the
   ticket's furniture, and a copywriter choosing lower case has chosen it.
   The tracking comes down a little to suit mixed case, which needs less air
   between letters than a run of capitals does. */
.scratch-cap{
  grid-column:3;grid-row:1;align-self:end;
  font-family:var(--mono);
  letter-spacing:.08em;font-weight:600;font-size:11px;
  color:var(--ink-30);line-height:1.4;
}
.tk-nav a{
  border:2px solid var(--ink);border-radius:3px;
  padding:15px 17px;text-decoration:none;color:var(--ink);
  /* number up in the corner, label along the bottom, the way a coupon is set.
     When these grow tall beside the stamp the height is then doing something
     rather than leaving the label stranded at the foot of an empty box. */
  display:grid;grid-template-columns:1fr auto;grid-template-rows:auto 1fr;
  gap:8px;
  font-family:var(--display);font-weight:800;
  letter-spacing:-.02em;font-size:19px;
}
.tk-nav a i{
  grid-column:2;grid-row:1;justify-self:end;
  font-style:normal;font-family:var(--mono);font-size:12px;letter-spacing:.14em;opacity:.45;
  /* the number sits at the TOP, so the rising fill reaches it last. Its own
     delay, longer than the label's, or it brightens over bare paper. */
  transition:opacity .16s var(--ease);
}
.tk-nav a:hover i,
.tk-nav a:focus-visible i{transition:opacity .16s var(--ease) .17s}
.tk-nav a b{grid-column:1/-1;grid-row:2;align-self:end;font-weight:inherit}
/* a sensible ceiling: past this they stop reading as buttons */
.tk-midleft .tk-nav a{max-height:150px}

/* ---- THE FILL ON HOVER ----
   Shared by the home punches and the résumé button on About, so the two
   pages answer the pointer the same way.

   The red rises from the bottom of the box rather than appearing all at once.
   These were already going red on hover and he asked for a hover state
   anyway, which is the honest verdict on a colour that swaps in one frame:
   nothing appears to have happened, it is simply a different colour now. A
   fill with a direction reads as the box being filled in, which is what a
   coupon is for.

   It is a pseudo-element and not a background, because `background` cannot
   be given a direction. That means the box has to own a stacking context
   (position) and clip the fill (overflow), and the real children have to be
   lifted above it or the fill covers the words.

   `border-color` was missing from the old transition, so the outline snapped
   red while the fill faded. Both are timed together here. */
.tk-nav a,
.resume-btn{
  position:relative;overflow:hidden;
  /* LEAVING. No delay on the way out: the fill drains downwards and uncovers
     the top of the box first, so the words have to be dark again before the
     red has gone from behind them. */
  transition:color .16s var(--ease),border-color .18s var(--ease);
}
/* ARRIVING. The colour waits for the fill to reach the words. Without the
   delay the type turns off-white while the red is still climbing, so for
   about a fifth of a second it is off-white on off-white and the label
   disappears. Dark type on red is perfectly legible; pale type on pale paper
   is not, so the change is timed to land as the fill completes. */
.tk-nav a:hover,
.tk-nav a:focus-visible,
.resume-btn:hover,
.resume-btn:focus-visible{
  transition:color .16s var(--ease) .11s,border-color .18s var(--ease);
}
.tk-nav a::before,
.resume-btn::before{
  content:"";position:absolute;inset:0;z-index:0;
  background:var(--red);
  transform:scaleY(0);transform-origin:50% 100%;
  transition:transform .26s cubic-bezier(.2,.75,.3,1);
}
.tk-nav a > *,
.resume-btn > *{position:relative;z-index:1}
.tk-nav a:hover,
.tk-nav a:focus-visible{border-color:var(--red);color:var(--paper)}
.tk-nav a:hover::before,
.tk-nav a:focus-visible::before{transform:scaleY(1)}
.tk-nav a:hover i,
.tk-nav a:focus-visible i{opacity:.8}
/* pressed: it sits down into the fill rather than bouncing */
.tk-nav a:active::before{transform:scaleY(1)}
.tk-nav a:active{transform:translateY(1px)}

/* the covered box: foil sits over the punch until it is scratched off */
.tk-nav a.covered{position:relative;overflow:hidden;border-color:var(--red)}
.tk-nav a.covered .foil{
  position:absolute;inset:0;width:100%;height:100%;
  cursor:crosshair;touch-action:none;z-index:2;
}
/* The covered one does NOT fill. The fill would sit under the foil and show
   through the moment the first stripe came off, which hands over the ending
   before the joke has been told. The outline going hot red is the whole of
   its hover, on purpose. */
.tk-nav a.covered:hover{background:none;border-color:var(--red-hot);color:var(--ink)}
.tk-nav a.covered:hover::before,
.tk-nav a.covered:focus-visible::before{transform:scaleY(0)}

.tk-nav a.scratched{
  background:var(--red);border-color:var(--red);color:var(--paper);
  animation:pop .34s var(--ease);
}
.tk-nav a.scratched i{opacity:.75}
/* once it is open it is a button like the others, but it is already red, so
   the fill that arrives is the hot one or nothing appears to happen at all */
.tk-nav a.scratched:hover::before,
.tk-nav a.scratched:focus-visible::before{background:var(--red-hot)}
@keyframes pop{
  0%{transform:scale(.97)}
  55%{transform:scale(1.025)}
  100%{transform:scale(1)}
}

.tk-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:26px;margin-top:auto}
/* explicit box: in the stacked layout the flex axis turns vertical,
   so a flex-basis here would size the height and collapse the width */
/* the barcode is a link now, so it needs to say so under the cursor and
   lift a little on hover. Nothing else about it changes. */
.barcode{
  flex:0 0 auto;width:132px;height:44px;display:block;
  transition:opacity .18s var(--ease),transform .18s var(--ease);
  background-image:repeating-linear-gradient(90deg,
    var(--ink) 0 2px,transparent 2px 4px,var(--ink) 4px 5px,transparent 5px 9px,
    var(--ink) 9px 12px,transparent 12px 14px);
}
a.barcode:hover,a.barcode:focus-visible{opacity:.62;transform:translateY(-2px)}
.tk-terms{font-size:10.5px;line-height:1.55;color:var(--ink-30);letter-spacing:.09em;margin:0;text-align:right}

/* ---- TAP TARGETS ON A PHONE ----
   Two of the ticket's links are real links and neither is big enough to hit.
   On the mobile artboard the badge (to the awards cabinet) comes out 93x22
   and the barcode (to the playlist) 132x31, against the 44px a finger needs.

   They are the right SIZE to look at; it is only the hit area that is wrong.
   So the area is extended with a pseudo-element and not one pixel of the
   drawing moves. VERTICAL ONLY, and no wider than the element already is:
   every box on the artboard is absolutely positioned, so growing these
   sideways would reach across whatever is beside them.

   Coarse pointers only, because on a mouse a 22px target is already fine and
   an invisible 44px box would just make the cursor change early. */
@media (pointer:coarse){
  .tk-badge,a.barcode{position:relative}
  .tk-badge::after,a.barcode::after{
    content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:44px;
  }
}

/* the awards switch: when he finally wins something */
body.has-won .tk-badge{background:var(--ink)}
.won-list{margin:0 0 30px;padding:0;list-style:none}
.won-list li{
  font-family:var(--mono);text-transform:uppercase;
  letter-spacing:.14em;font-size:13px;font-weight:600;
  padding:9px 0;border-bottom:1px solid var(--ink-12);
}

/* ---- fill the screen on a landscape display ----
   The ticket takes 95% of the viewport both ways and the body
   spreads its blocks down the height, so it reads as one printed
   object filling the screen instead of a card floating in red. */
@media (orientation:landscape) and (min-width:900px){
  body.is-home{padding:2.5vh 2.5vw}
  .ticket{width:95vw;max-width:none;min-height:95vh}
  .tk-body{padding:clamp(30px,3.4vh,58px) clamp(34px,3.6vw,72px) clamp(24px,2.6vh,40px)}

  /* The blocks used to be spread down the height by space-between, which put
     an identical gap everywhere and left the tabs floating away from the copy
     they belong to. Now the top half flows on its own margins, the tabs sit
     right under the sub-line, and only the barcode and the small print are
     pushed to the bottom edge by the auto margin. All the slack collects in
     one place, between the tabs and the foot, instead of being shared out. */
  .tk-top    {margin-bottom:clamp(12px,2.2vh,32px)}
  .tk-namerow{margin-bottom:clamp(8px,1.5vh,20px)}
  /* enough to clear the descenders on 'copywriter' and no more: the old
     gap was not this margin, it was the row centring the short sub-line
     against the tall stamp beside it */
  .tk-title  {margin-bottom:clamp(10px,1.7vh,22px)}
  .tk-sub{font-size:clamp(17px,1.32vw,22px);max-width:none;white-space:nowrap}
  .tk-mid    {margin-bottom:clamp(10px,1.6vh,22px)}
  /* height-first so the row is a known size and the page cannot grow */
  .stamp{width:auto;height:min(31vh,320px)}
  .tk-nav    {margin-bottom:0}
  .tk-foot   {margin-top:auto;padding-top:clamp(10px,1.7vh,30px)}

  /* width AND height: a headline measured only in vw is far too tall on a
     short wide laptop screen, which is what pushed the page into scrolling */
  .tk-title{font-size:min(9.4vw,16vh,208px)}
  .tk-name{font-size:clamp(26px,2.9vw,56px)}
  .tk-nav{max-width:min(100%,1100px)}
  .tk-nav a{font-size:clamp(19px,1.5vw,26px);padding:clamp(16px,2vh,26px) 20px}
}

/* ============================ SHARED PAGE CHROME ============================ */

/* EVERYTHING IN THIS HEADER WAS SCALED BY 15% ON 12 SEP 2026, at his request:
   "make the header with my name and the tabs bigger by 15%". Every number in
   the nav is the old one times 1.15, at every width, including the two phone
   blocks near the foot of this file. The bar's height went with the type, or
   bigger words in the same box is not a bigger header, it is a tighter one.

   They are one set. Scaling some and not others is how a header ends up with
   a 15% name over 0% links, which is the crowding this was meant to undo. */
.page-nav{
  position:sticky;top:0;z-index:50;
  background:var(--nav-bg);backdrop-filter:blur(9px);
  border-bottom:1px solid var(--line);
  /* 12px condensed caps read as fine print rather than as navigation on any
     screen bigger than a laptop. Up enough to be read at a glance, not so far
     that the header starts competing with the first row of work. */
  font-size:16.7px;   /* 14.5 x 1.15 */
}
.page-nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:20px;height:80px}
/* the lockup: name in capitals, the job under it and deliberately not bold.
   site.js builds the two lines, so editing the HTML alone will not change it. */
.page-nav .logo{
  text-decoration:none;display:block;line-height:1;
  font-family:var(--display);text-transform:none;
}
.page-nav .logo b{
  display:block;font-weight:800;letter-spacing:.01em;
  font-size:26.5px;text-transform:uppercase;   /* 23 x 1.15 */
  white-space:nowrap;      /* the name is one line or it is not a lockup */
}
.page-nav .logo span{
  display:block;margin-top:3.5px;
  font-family:var(--mono);font-weight:500;
  text-transform:lowercase;letter-spacing:.16em;
  font-size:15px;color:var(--fg-faint);        /* 13 x 1.15 */
  white-space:nowrap;
}
/* The links are set BIGGER than the rest of the header (his ask, 7 Sep 2026).
   14.5px condensed caps read as fine print next to a 23px name, so the one
   part of the header you are meant to use was the quietest thing in it.

   A clamp, not a number, because this row is the tightest piece of layout on
   the site: below 520px it drops to its own line under the name, and at 375px
   it has 331px to fit three items in. The floor is what keeps that true. */
.page-nav .links{display:flex;gap:27.5px;flex:none;
                 font-size:clamp(17.25px,1.5vw,20.7px)}   /* all x 1.15 */
/* .55 put the caret at 3.3:1 against a 4.5 requirement, and the caret is the
   ONLY thing telling anyone the Work menu opens. Opacity on the link is a
   group opacity, so a child cannot undo it: the fix has to be here, not on
   `.nav-caret`. .72 measures past 4.5 and is still visibly quieter than the
   current page's link, which is the whole job of the dimming. */
.page-nav .links a{text-decoration:none;opacity:.72;transition:opacity .16s;white-space:nowrap}
.page-nav .links a:hover,.page-nav .links a.on{opacity:1;color:var(--red)}

/* ---- the work menu ----
   Hover The work and the section drops down. A section tier was tried instead
   and it only appeared once you were already on a work page, which made it a
   step too many; this is one move from anywhere.

   It is built to READ as a dropdown, which the first two passes did not. Four
   things do that work and all four are load-bearing:
     a caret on the parent, so you know there is something to open before you
       open it, and it turns over when the menu is down;
     a panel with its own width, so it is a surface rather than a stray link;
     a notch pointing back up at the parent, which is what says "this belongs
       to that" without a line joining them;
     rows that fill on hover, because a menu row is a target, not a word.
   The one elevation on the site is here. It is soft and low-contrast, not the
   1970s hard offset the art direction rules out: without any lift at all the
   panel reads as part of the page rather than over it.

   Aligned to the LEFT edge of the link it belongs to, not centred under it. */
.page-nav .nav-item{position:relative;display:inline-block}

/* the caret. Drawn in type rather than as an image so it takes the link's own
   colour and weight, and turned with a transform so the shape stays crisp. */
.page-nav .nav-item > a .nav-caret{
  display:inline-block;margin-left:6px;font-size:.72em;line-height:1;
  transform:translateY(-1px);
  transition:transform .18s var(--ease);
}
.page-nav .nav-item.is-open > a .nav-caret{transform:translateY(-1px) rotate(180deg)}
/* the parent stays lit while its menu is down, so the header shows where you
   are even before you pick anything out of it */
.page-nav .nav-item.is-open > a{opacity:1;color:var(--red)}

.page-nav .nav-menu{
  position:absolute;left:-14px;top:100%;
  /* the padding is the bridge: with a margin instead, the gap between link
     and panel is dead ground and the menu shuts as the pointer crosses it */
  padding-top:12px;
  display:block;
  opacity:0;visibility:hidden;pointer-events:none;
  transform:translateY(-6px);
  transition:opacity .16s var(--ease),transform .16s var(--ease),visibility .16s;
  z-index:60;
}
.page-nav .nav-menu-in{
  display:flex;flex-direction:column;
  min-width:212px;
  background:var(--bg);
  border:1px solid var(--line);
  box-shadow:0 12px 30px -14px rgba(11,10,9,.34);
  position:relative;
}
/* the notch. Two of them stacked: the outer one is the hairline, the inner is
   the paper sitting a pixel over it, so the border appears to open around the
   arrow instead of being drawn across its base. */
.page-nav .nav-menu-in::before,
.page-nav .nav-menu-in::after{
  content:"";position:absolute;left:24px;width:11px;height:11px;
  transform:rotate(45deg);
}
.page-nav .nav-menu-in::before{
  top:-6px;background:var(--line);
}
.page-nav .nav-menu-in::after{
  top:-5px;background:var(--bg);
}
.page-nav .nav-menu a{
  display:block;white-space:nowrap;text-decoration:none;
  padding:11px 18px;color:var(--fg);opacity:.72;
  border-bottom:1px solid var(--line);
  transition:opacity .14s var(--ease),color .14s var(--ease),background .14s var(--ease);
}
.page-nav .nav-menu a:last-child{border-bottom:0}
.page-nav .nav-menu a:hover,
.page-nav .nav-menu a:focus-visible{opacity:1;color:var(--red);background:var(--well)}
.page-nav .nav-menu a.on{opacity:1;color:var(--red)}

/* .is-open is the authority, set by site.js with a grace period on closing.
   :hover is kept alongside it only so the menu still works if the script has
   not run yet, and NOT with pointer-events, which is the trap: the moment
   :hover goes false pointer-events flips to none in the same frame and the
   panel cannot be re-entered. */
.page-nav .nav-item:hover .nav-menu,
.page-nav .nav-item:focus-within .nav-menu{
  opacity:1;visibility:visible;pointer-events:auto;transform:translateY(0);
}
.page-nav .nav-item.is-open .nav-menu{
  opacity:1;visibility:visible;pointer-events:auto;transform:translateY(0);
}

.page-head{padding:88px 0 44px}
.page-head h1{font-size:clamp(52px,11vw,138px)}
.page-head .note{margin:22px 0 0;font-size:12px;color:var(--fg-dim);max-width:60ch;letter-spacing:.12em}

/* ============================ FOOTER ============================
   One line, and that is the whole footer. The tear-off stub with its
   columns, barcode and giant wordmark was built and then killed: the owner
   wanted the sentence and nothing else. Contact still sits in the top nav
   on every page, so nothing is lost by keeping this quiet.
   ============================================================== */

.foot{margin-top:120px;border-top:1px solid var(--line);padding:34px 0 38px;position:relative}

/* ---- arriving at the footer ----
   The footer is the last thing on the page, so reaching it should feel like
   arriving rather than like running out of content. Three beats, in order:
   the rule draws itself across, the line is uncovered from below, then the
   icons land one after another. It resets when the footer leaves the screen,
   so it plays again every time he scrolls back down to it. */
.foot::after{
  content:"";position:absolute;left:0;top:-1px;height:2px;width:100%;
  background:var(--red);transform:scaleX(0);transform-origin:left center;
}
.foot.is-near::after{transition:transform .78s cubic-bezier(.62,.02,.2,1);transform:scaleX(1)}
/* THE LINE NO LONGER UNCOVERS ITSELF. That reveal was written for a 58px
   headline, where a clip-path wipe is a real gesture. On an 11px copyright
   line it is invisible work: it animates about eleven pixels of height and
   nobody can tell it happened. The rule drawing across is the arrival now.
   Its prefers-reduced-motion override went with it, in the same pass — that
   pair has been separated twice before and each time the people who asked
   for less motion got the animation anyway. */
.foot .foot-social a{opacity:0;transform:translateY(10px) scale(.86)}
.foot.is-near .foot-social a{
  opacity:1;transform:none;
  transition:opacity .3s linear,transform .42s cubic-bezier(.2,1.5,.4,1);
}
.foot.is-near .foot-social a:nth-child(1){transition-delay:.46s}
.foot.is-near .foot-social a:nth-child(2){transition-delay:.54s}
.foot.is-near .foot-social a:nth-child(3){transition-delay:.62s}
.foot.is-near .foot-social a:nth-child(4){transition-delay:.70s}
.foot.is-near .foot-social a:nth-child(5){transition-delay:.78s}
/* the hover lift has to win over the entrance transform once it has landed */
.foot.is-near .foot-social a:hover{transform:translateY(-2px)}
.foot-in{display:flex;align-items:baseline;justify-content:space-between;gap:30px}

/* THE COPYRIGHT LINE. His words, in the system voice, at the size the
   ticket's own small print is set (.tk-terms is 10.5px). Sentence case as he
   typed it, NOT uppercased: "all copywriters reserved" shouted is a slogan,
   and lowercase is what makes it read as the boilerplate it is pretending
   to be. The joke only works if it looks like something nobody wrote. */
.foot p{
  margin:0;font-family:var(--mono);font-weight:600;
  font-size:11.5px;letter-spacing:.11em;line-height:1.5;
  color:var(--fg-faint);max-width:none;
}
/* the mark sits just off the line, close enough to belong to it */
.foot-c{margin-right:.55em;font-size:1.1em;line-height:1}
.foot p a{
  color:var(--red);text-decoration:none;
  border-bottom:1px solid currentColor;
  transition:color .16s var(--ease);
}
.foot p a:hover{color:var(--red-hot)}

/* THE RIGHT-HAND SIDE CARRIES THE CONTACT DETAILS. It used to carry four
   23px icons and 370px of nothing, while the address existed on no page in
   any readable form: the only mailto down here was one word inside the joke,
   and the icon row skips mail on purpose.

   The address is set in the SYSTEM voice, not the display one. It is
   information, the same kind of thing as the serial and the small print, and
   it was briefly tried at 56px in the display face: that made a gmail with a
   number in it the loudest element on the page, which is the opposite of
   what it should do. Big enough to read and copy, quiet enough not to be the
   thing you notice first. */
/* A ROW, not a column. It was a stack because it held three things of three
   different sizes; with the résumé gone it is the address and the icons, and
   two things do not need a column. Everything sits on one baseline with the
   copyright line, so the whole footer is a single rule of type. */
.foot-side{display:flex;align-items:baseline;gap:26px;flex:none}
.foot-mail{
  font-family:var(--mono);font-weight:600;
  font-size:16px;letter-spacing:.09em;
  color:var(--fg);text-decoration:none;
  border-bottom:1px solid var(--line);padding-bottom:2px;
  transition:color .16s var(--ease),border-color .16s var(--ease);
}
.foot-mail:hover,.foot-mail:focus-visible{color:var(--red);border-bottom-color:var(--red)}
/* `.foot-cv` was here and is gone with the résumé button, 29 Aug 2026. The
   résumé itself is untouched: `contact.resume` still feeds `.resume-btn` on
   the About page. */

/* the social row: quiet, and the same weight as the small print elsewhere.
   A box of icons has no text baseline to sit on, so it is centred against
   the row rather than aligned to it: baseline on this would hang the icons
   off the bottom of the line. */
.foot-social{display:flex;align-items:center;gap:15px;flex:none;align-self:center}
.foot-social a{
  display:block;color:var(--fg-faint);text-decoration:none;
  transition:color .16s var(--ease),transform .16s var(--ease);
}
.foot-social a:hover{color:var(--red);transform:translateY(-2px)}
.foot-social svg{
  width:23px;height:23px;display:block;
  fill:none;stroke:currentColor;stroke-width:1.7;
}
.foot-social svg .fill{fill:currentColor;stroke:none}
/* a link with no icon still shows, as its name */
.foot-social a.txt{
  font-family:var(--mono);text-transform:uppercase;
  letter-spacing:.14em;font-weight:600;font-size:11px;
}

/* ============================ WORK GRID ============================ */
/* one uniform grid. no alternating sizes, no two lists, no tabs.
   the sticker carries the status instead. */

.work-list{display:grid;grid-template-columns:repeat(2,1fr);gap:56px 40px}
.piece{text-decoration:none;display:block;color:inherit}

.frame{
  position:relative;overflow:hidden;border-radius:3px;
  aspect-ratio:4/3;background:var(--well);
  border:1px solid var(--line);
}
.frame img{width:100%;height:100%;object-fit:cover;transition:transform .5s var(--ease)}
.piece:hover .frame img{transform:scale(1.035)}
.frame.empty::after{
  content:"NO IMAGE YET";position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);letter-spacing:.2em;
  font-size:12px;color:var(--fg-faint);
}

/* THE STICKER — what actually happened to this piece */
/* Up from 11px/7px-12px on 12 Sep 2026, his call: "never left the deck sticker
   is very little". Measured before: that sticker came to 141x38 on a 600px
   card, under a quarter of its width, for the one element carrying the only
   piece of information the grid gives you about what happened to a piece.
   The widest, "Three films, three markets", goes 182px to roughly 240px, and
   the max-width below still holds it inside a 572px card. */
.sticker{
  position:absolute;top:14px;right:14px;z-index:2;
  background:var(--red);color:var(--paper);
  font-size:14px;padding:9px 16px;border-radius:100px;
  transform:rotate(-3deg);
  max-width:calc(100% - 28px);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* spec work: ink on paper, so it reads as a different kind of label */
.sticker.spec{background:var(--ink);color:var(--paper);transform:rotate(2.5deg)}

.piece .serial{display:block;margin:16px 0 7px;font-size:11px;color:var(--fg-faint)}
.piece h2{font-size:clamp(21px,2.3vw,29px);margin-bottom:6px}
.piece:hover h2{color:var(--red)}
.piece p{margin:0;font-size:15px;color:var(--fg-dim);max-width:44ch}

.reveal{opacity:0;transform:translateY(16px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
.reveal.is-visible{opacity:1;transform:none}

/* ============================ PROJECT PAGE ============================ */

.project{padding:56px 0 0}
.back-link{
  display:inline-block;text-decoration:none;margin-bottom:44px;
  font-family:var(--mono);text-transform:uppercase;
  letter-spacing:.16em;font-size:12px;font-weight:600;color:var(--fg-dim);
}
.back-link:hover{color:var(--red)}

/* ---- the language switch ----
   Top of a project page, opposite the way back, on the line the back link was
   already holding. It only appears on a project that HAS another language, so
   it can never offer a door into a page that is still in English.
   The two share the row, so .back-link's own bottom margin comes off here and
   goes onto the row instead, or the switch sits 44px above the rule it
   belongs to. */
.project-top{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:20px;margin-bottom:44px;
}
.project-top .back-link{margin-bottom:0}

/* ---- the résumé button ----
   Under the experience list, which is where a CV belongs: he has just read
   the four jobs and this is the long version of them.

   Built like a home punch and it fills the same way (the shared rules are up
   with .tk-nav a), so the one button on this page and the three on the home
   page answer the pointer identically. The arrow drops a couple of pixels on
   hover, which is the only part that says "download" without a word. */
#aboutResume{margin:38px 0 0}
.resume-btn{
  display:inline-flex;align-items:center;gap:16px;
  border:2px solid var(--fg);border-radius:3px;
  padding:15px 22px;text-decoration:none;color:var(--fg);
  font-family:var(--display);font-weight:800;
  letter-spacing:-.02em;font-size:19px;
}
.resume-btn:hover,
.resume-btn:focus-visible{color:var(--paper);border-color:var(--red)}
.resume-btn:hover::before,
.resume-btn:focus-visible::before{transform:scaleY(1)}
.resume-btn:active{transform:translateY(1px)}
.resume-arrow{
  font-family:var(--mono);font-size:17px;line-height:1;opacity:.5;
  transition:opacity .18s var(--ease),transform .22s var(--ease);
}
.resume-btn:hover .resume-arrow,
.resume-btn:focus-visible .resume-arrow{opacity:1;transform:translateY(3px)}
/* on a phone it takes the width rather than sitting as a small tab in a
   large empty space under the list */
@media (max-width:560px){
  .resume-btn{display:flex;justify-content:space-between;width:100%}
}

.project-meta{font-size:11.5px;color:var(--fg-faint);margin:0 0 16px}
.status-produced{color:var(--red)}
.status-pitched{color:var(--fg-dim)}
.project-title{font-size:clamp(42px,8vw,104px);margin-bottom:40px}

.project-body{max-width:62ch;margin-bottom:60px}
.project-body p{font-size:19px;line-height:1.62;margin:0 0 22px;color:var(--fg-dim)}

/* Row gap opened up from 28px on 13 Sep 2026, his call. Side by side, 28px
   reads as one pair; stacked, it read as one tall picture cut in half. The
   columns keep 28 because that gap is doing the opposite job: holding two
   images together as a pair. */
/* SIX columns, not two (13 Sep 2026, his ask for a 1/3 option). Six is the
   smallest number that divides cleanly by 2 and by 3, so full, two-thirds,
   half and third are all whole numbers of tracks and every combination still
   lines up on the same vertical rails. Two columns could not express a third
   at all. */
.media-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:52px 28px;margin-bottom:70px}
.project-media{margin:0;grid-column:span 6}
.project-media[data-layout="full"]{grid-column:span 6}
.project-media[data-layout="two-thirds"]{grid-column:span 4}
.project-media[data-layout="half"]{grid-column:span 3}
.project-media[data-layout="third"]{grid-column:span 2}

/* THE SPACER, his idea: an empty block he can drop into the media list where
   a particular project needs air, instead of every project paying for it in
   the stylesheet. Deliberately ONE small fixed size so two or three of them
   is the way to ask for more, which is a decision he can see and count rather
   than a number he has to guess.

   Full width it makes vertical space. Half or third it takes a cell and
   pushes what follows across, which is the only way to leave a deliberate
   hole in a grid. Those two have no height of their own: the picture beside
   them sets the row. */
.media-spacer{grid-column:span 6;height:48px}
.media-spacer[data-layout="two-thirds"]{grid-column:span 4;height:0}
.media-spacer[data-layout="half"]{grid-column:span 3;height:0}
.media-spacer[data-layout="third"]{grid-column:span 2;height:0}
/* NO PIECE OF MEDIA IS TALLER THAN THE SCREEN (his ask, 12 Sep 2026:
   "i need to make sure that each image in the media is in one scroll").

   Measured on Tuborg at 1440x900 before this: the two portrait stills came out
   908px tall in a 900px viewport, 101%, so neither could ever be seen whole,
   and a portrait in a full-width slot would have been 1860px, over twice the
   screen. 80vh leaves a fifth of the screen showing what comes next, which is
   what tells you there is more without you having to find out.

   It SHRINKS, it does not crop. `object-fit:cover` would hold the full width
   but cut the image, and the thing being cut is somebody's poster: the
   headline lives at the top of most of them. A wide image therefore comes out
   slightly narrower than its slot on a short screen and is centred, which is
   the price of never mangling the work. */
/* TWO BLOCKS IN A ROW ARE THE SAME SIZE (13 Sep 2026, his ask: the red gif
   "alligend size-wise with the gif next to it").

   Before: `width:auto` meant an image smaller than its column drew at its own
   pixel size, so the fries carton sat at 560px beside a phone squeezed to
   335px by the 80vh ceiling. Two things of unrelated scale in one row.

   Now each block fills its cell and the picture is CONTAINED inside it, so a
   row is one size and nothing is cropped. On these transparent cutouts the
   letterbox is invisible: there is no visible box for the artwork to sit in,
   it simply centres. A row whose pictures are the same shape is unchanged,
   because the cell was already that shape. */
/* NOT display:flex. Making the figure and the button flex containers stretches
   the image to the container's cross size, the container asks its content how
   tall it is, and every image on every project measured 0px. Block layout,
   intrinsic height, and matchRowHeights() in work.html evens the row up after
   the pictures know their own size. */
.project-media img{
  display:block;
  width:100%;max-width:100%;
  height:auto;max-height:80vh;
  object-fit:contain;
  margin-inline:auto;
  /* LIFT, NOT A FRAME (13 Sep 2026, his ask: the work "looks so white on
     white"). This is `filter: drop-shadow`, deliberately NOT `box-shadow`.
     A box-shadow is drawn around the element's RECTANGLE, so on the
     transparent cutouts it would put a hard-edged shadow round a box with
     artwork floating loose inside it, which is precisely the border he had
     removed a day earlier. drop-shadow follows the alpha channel, so a
     cutout is shadowed along its own silhouette and a photograph along its
     edge, with one rule and no per-image flag.

     Soft and low-contrast on purpose. The art direction rules out the hard
     1970s offset, and the only other elevation on the site, the nav panel,
     is the same kind of quiet lift. */
  filter:drop-shadow(0 8px 22px rgba(11,10,9,.20));
}
/* opaque and rectangular, so the cheap shadow is the right one here */
.video-wrap{box-shadow:0 8px 22px rgba(11,10,9,.20)}
/* The film gets the same ceiling, but through its WIDTH, so the 16:9 stays
   exactly 16:9 instead of being clipped inside a box that is no longer 16:9.
   padding-top is a percentage of the width, so clamping the width is all it
   takes. At 900px tall this computes to 1280px and the 1240px column is
   already inside it: nothing moves on an ordinary screen. */
.video-wrap{max-width:calc(80vh * 16 / 9);margin-inline:auto}
.project-media figcaption{
  margin-top:11px;font-size:12px;color:var(--fg-faint);
  font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase;
}
.video-wrap{position:relative;padding-top:56.25%;border-radius:3px;overflow:hidden;background:#000}
.video-wrap iframe,.video-wrap video{position:absolute;inset:0;width:100%;height:100%;border:0}

.credits{margin-bottom:70px}
.credits-head{
  font-family:var(--mono);text-transform:uppercase;
  letter-spacing:.18em;font-size:12px;font-weight:600;
  color:var(--fg-faint);margin-bottom:18px;
}
.credits-list{list-style:none;padding:0;margin:0;max-width:640px}
.credits-list li{display:flex;gap:20px;padding:11px 0;border-top:1px solid var(--line);font-size:15px}
.credits-list li span{
  flex:0 0 40%;color:var(--fg-faint);
  font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;
}

/* BACK AND FORWARD ARE THE SAME SIZE (13 Sep 2026, his call), and colour
   carries the hierarchy instead of type size.

   The next link used to be clamp(28px,5vw,58px) display type under a red
   rule. At the foot of a project page that is the call-to-action band every
   marketing site has, which is the thing he has objected to twice, and it
   made the second-loudest thing on a project page the name of a DIFFERENT
   project. Both are now the system voice on one rail: back is quiet ink,
   forward is red. You can still tell instantly which way the page wants you
   to go, and neither one shouts over the work above it. */
.proj-nav{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:18px 28px;flex-wrap:wrap;
  border-top:2px solid var(--red);padding:24px 0 0;
}
/* The words "Previous" and "Keep going" are gone (13 Sep 2026, his call), so
   each side is an arrow and a project name. The arrow already says which
   direction, and the label was saying it a second time. 13.2px is the old
   12px plus his 10%: with the labels off there is room for the names to
   carry a little more weight. */
.prev-project,
.next-project{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  text-decoration:none;
  font-family:var(--mono);font-size:13.2px;font-weight:500;
  text-transform:uppercase;letter-spacing:.12em;
}
/* back: quiet */
.prev-project{color:var(--fg-faint)}
.prev-project .prev-title{color:var(--fg);letter-spacing:.06em}
.prev-project:hover,
.prev-project:hover .prev-title{color:var(--red)}

/* forward: red. margin-left:auto so it still sits right on the first
   project, where there is no back link to push it there. */
.next-project{color:var(--red);margin-left:auto}
.next-project .next-title{color:var(--red);letter-spacing:.06em}
.next-project:hover,
.next-project:hover .next-title,
.next-project:hover .next-arrow{color:var(--red-hot)}
.next-label{color:var(--fg-faint)}
.next-arrow{color:var(--red)}

/* ============================ ABOUT ============================ */

/* The picture box is the same height as the text beside it, and it stays that
   way on its own: the photograph is taken out of flow (absolute), so it
   contributes nothing to the row's height. The row is therefore measured from
   the words alone and the frame stretches to whatever that comes to, so
   rewriting the bio in admin re-sizes the picture with no numbers to update.
   The floor stops a very short bio from squashing it into a letterbox. */
/* THE WHOLE ABOUT PAGE HOLDS ONE COLUMN, and it is capped rather than
   growing with the screen, because this is a page you read.

   Capping the two blocks alone left them hard against the left with 800px of
   nothing to the right of them, which is not a margin, it is a lean. So the
   heading is capped to the same 1200 and every part of it is centred, and the
   heading, the photo and the jobs stay on one left edge.

   1200 comes from the reading column rather than from eye: the text side is
   1.15 of 2, so 1200 less the gap puts about 660px under the words, which is
   the 62ch above at 19px. The photo takes the rest.

   EVERY block inside the page goes in this list. The résumé button was left
   out of it and was the only child of the container still running full width,
   so it started 217px to the left of the heading, the photo and the jobs,
   which all start together. Add a block to the page, add it here. */
   LEFT ALIGNED, NOT CENTRED (13 Sep 2026, found in an alignment sweep).
   `margin-inline:auto` centred this 1200px column inside a `.wrap` that grows
   with the screen, so About was the only page whose content did not start on
   the same rail as its own nav. Measured against the nav's left edge before
   the fix: 16px out at 1280, 20px at 1440, 121px at 1680, 224px at 1920 and
   480px at 2560. On his own monitor his name in the header and the headline
   under it were half a screen apart. The 1200px measure is kept, because that
   is a reading decision; only the centring goes. */
body.page-about .page-head .wrap,
body.page-about .about-grid,
body.page-about .exp-list,
body.page-about .lang-list,
body.page-about .blk-head,
body.page-about #aboutResume{max-width:1200px;margin-inline:0}
.about-grid{display:grid;grid-template-columns:.85fr 1.15fr;gap:56px;align-items:stretch;margin-bottom:96px}
/* The About portrait is printed the same way the stamp is: a flat red
   ground with a grey photograph screened over it, so it is two inks rather
   than a filter pretending to be two inks. Same treatment as .stamp-photo,
   at a size where the face actually reads. The frame is red rather than
   black now so the picture belongs to the ticket. */
.about-portrait{
  width:100%;height:100%;object-fit:cover;
  /* his face sits high in the frame, so crop from the middle upward rather
     than from the centre, which would take the top of his head off */
  object-position:50% 32%;
  border-radius:2px;transform:rotate(-1.6deg);
  display:block;background:var(--red-deep);
  /* on a screen blend the DARK greys are what let the red through, so
     lifting the picture washes it pink. Pull it down and open the contrast
     and the face gets its form back in two inks. */
  filter:grayscale(1) contrast(1.42) brightness(.93);
  mix-blend-mode:screen;
}
/* The About picture is a stamp, built the same way as the one on the ticket:
   a red field, perforated edges eaten out of it, and the photograph printed
   over the red as two inks. The black box frame it had before belonged to
   nothing else on the site. The perforations are punched in the PAGE's own
   ground, not in the ticket's paper, so they read as holes rather than as
   white dots sitting on top of the colour. */
.about-shot{
  background:var(--red);
  border:0;border-radius:2px;overflow:hidden;
  transform:rotate(-1.6deg);
  line-height:0;
  position:relative;min-height:340px;
  padding:14px;
}
/* the perforation: half-circles of the page eating the four edges. At inset:0
   rather than -1px because the stamp clips its own overflow, and anything
   hanging outside the box would simply be cut off. */
.about-shot::before{
  content:"";position:absolute;inset:0;z-index:3;pointer-events:none;
  background-image:
    radial-gradient(circle at 50% 0,   var(--bg) 6.2px, transparent 6.7px),
    radial-gradient(circle at 50% 100%,var(--bg) 6.2px, transparent 6.7px),
    radial-gradient(circle at 0 50%,   var(--bg) 6.2px, transparent 6.7px),
    radial-gradient(circle at 100% 50%,var(--bg) 6.2px, transparent 6.7px);
  background-size:18px 12px, 18px 12px, 12px 18px, 12px 18px;
  background-position:top center, bottom center, left center, right center;
  background-repeat:repeat-x, repeat-x, repeat-y, repeat-y;
}
/* Out of flow, so the row's height is the text's height and nothing else.
   Inset by the padding, so the red field shows as the stamp's margin the way
   it does on the ticket, and printed over it rather than sitting on it. */
.about-shot .about-portrait{
  position:absolute;left:14px;top:14px;transform:none;border:0;
  /* sized, not inset. An <img> is a replaced element: give it four insets and
     width:auto and it keeps its INTRINSIC size instead of stretching to them,
     so the photograph stood 661px tall inside a 470px stamp and hung out of
     the bottom of it. */
  width:calc(100% - 28px);height:calc(100% - 28px);
  background:var(--red-deep);
  filter:grayscale(1) contrast(1.26) brightness(1.14);
  mix-blend-mode:screen;
}
/* with more than one photo the frame is a control, so it says so: the cursor
   changes and the frame lifts a hair. No count printed on it, no arrows, no
   dots. Finding out it changes is the whole of the joke. */
.about-shot.has-roll{cursor:pointer;transition:transform .2s var(--ease)}
.about-shot.has-roll:hover,
.about-shot.has-roll:focus-visible{transform:rotate(-1.6deg) scale(1.012)}
.about-shot.has-roll:focus-visible{outline:2px solid var(--red);outline-offset:4px}
/* THE ABOUT PAGE IS FOR READING, SO IT DOES NOT GET WIDER WITH THE SCREEN.
   The work grid should: images are the argument and they want the room. Words
   are the opposite. When --wrap was allowed to grow, this paragraph had no
   measure of its own and went with it: 109 characters a line on a 2400px
   screen, against the 55 to 75 anything is comfortable to read at. The
   Japanese version had a measure and the English one never did.

   So the reading column is set here, and the two blocks that hold it are
   capped, which is also what puts the margins back. */
.about-text{font-size:19px;line-height:1.62;margin:0 0 22px;color:var(--fg-dim);max-width:62ch}
/* ---- the manipulating piece ----
   One column of type and a lot of paper. Set at reading size in the body
   face, not at feature size: it is six lines long and blowing them up would
   make it look like it thinks it is an essay.
   The measure is the same 62ch the About bio uses, so the two pages read as
   the same voice at the same width. */
/* the eyebrow, above the heading rather than under it, so the first thing
   read is what you did and the second is what it was called */
.manip-label{font-family:var(--mono);font-size:11px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--fg-faint);margin:0 0 10px}

/* THE PAGE IS CAPPED AND CENTRED, unlike every other page on the site.
   The site's 1240px container is built for pages that FILL it: the work grid
   has two columns, About has a photo beside its text. This page is one column
   of reading and nothing else, so in that container it sat hard left with
   about 460px of dead paper down the right at 1440px, which read as a page
   that had failed to load its other half rather than as a narrow column.

   The nav deliberately keeps the full width. It is chrome, it is the same on
   every page, and re-centring it here would make this page look like a
   different site.

   860px is set by the heading, not chosen: "Manipulating." runs 779px of ink
   at the 138px ceiling of its clamp, and the clamp means it can never get
   bigger than that at any screen width. Below about 900px the wrap's own
   `100% - 44px` is already the smaller number, so phones never see this. */
body.page-manipulating .page-head .wrap,
body.page-manipulating > .wrap{max-width:860px}

/* The measure moved OFF the article and onto the paragraphs. It used to sit
   on .manip, which capped everything inside it at 62ch, including the drawing
   below: that is why the cartoon's own max-width never once applied and it
   never actually broke out of the column the way the note below says it
   should. The words still read at 62ch; the article is just no longer a lid
   on top of them. */
.manip{max-width:none;margin:0 0 40px;padding-bottom:80px}
/* Direct children only. The manifesto's paragraphs are nested inside
   .manip-creed, and a bare `.manip p` reaches them too: at the same
   specificity the later rule wins, so the phone override further down
   this file was quietly resetting the manifesto back to body size. */
.manip > p{font-size:19px;line-height:1.62;margin:0 0 26px;color:var(--fg-dim);max-width:62ch}
/* the last line before the way out lands on its own, so it gets the weight */
.manip > p:nth-last-child(2){color:var(--fg)}
/* ---- the cartoon ----
   Wider than the 62ch reading column, because a drawing squeezed into a text
   measure looks like an accident, but it keeps the column's LEFT EDGE and
   grows to the right only. A negative left margin was tried first and it put
   the drawing at -30px, off the side of the screen, because the container
   only has 30px of margin to borrow at 1300px wide. Growing rightwards
   cannot overflow anything.
   `currentColor` on the strokes means the ink is the page's ink: change the
   text colour and the drawing follows it.

   760, not the old 620. That number was written to beat a 590px column and
   was never reached anyway, because .manip capped everything inside it at
   62ch. With the measure now resolving against the paragraph's own 19px the
   column is 660, so 620 would have quietly turned the drawing into the
   squeezed thing this note exists to prevent. 760 puts it 100px past the
   words and still 100px inside the 860 block. */
.manip-fig{margin:46px 0;max-width:760px}
.manip-fig svg{width:100%;height:auto;display:block;color:var(--fg)}
.manip-fig figcaption{
  margin-top:14px;font-size:16px;line-height:1.5;color:var(--fg-dim);
  text-align:center;max-width:44ch;margin-left:auto;margin-right:auto
}
@media (max-width:900px){
  /* nothing to break out of on a phone: the column is already the screen */
  .manip-fig{margin:34px 0;max-width:100%}
  .manip-fig figcaption{font-size:15px}
}
/* ---- the manifesto ----
   Set apart from the prose around it and a size up, with a rule top and
   bottom. That is the whole of the styling: no numbers, no bullets, no
   heading announcing that a manifesto is coming. A manifesto that introduces
   itself is a mission statement. */
.manip-creed{
  margin:44px 0;padding:34px 0;max-width:62ch;
  /* the rules are capped with the prose rather than running the full 860,
     so they read as a pause in the column and not as a page divider */
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)
}
.manip-creed p{
  font-size:23px;line-height:1.45;margin:0 0 24px;color:var(--fg);
  letter-spacing:-.01em;max-width:34ch
}
.manip-creed p:last-child{margin-bottom:0}
@media (max-width:900px){
  .manip-creed{margin:32px 0;padding:26px 0}
  .manip-creed p{font-size:19px;margin-bottom:20px;max-width:none}
}

.manip-out{margin-top:46px}
.manip-out a{
  font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--red);text-decoration:none;
  border-bottom:1px solid transparent;transition:border-color .16s var(--ease)
}
.manip-out a:hover,
.manip-out a:focus-visible{border-bottom-color:var(--red)}
.manip-out span{display:inline-block;transition:transform .16s var(--ease)}
.manip-out a:hover span{transform:translateX(4px)}
@media (max-width:900px){
  .manip{padding-bottom:40px}
  .manip > p{font-size:17px;margin-bottom:22px}
}

.skills-list{display:flex;flex-wrap:wrap;gap:9px;margin-top:32px}
.skill-tag{border:1px solid var(--line);border-radius:100px;padding:6px 13px;font-size:11px;color:var(--fg-dim)}
/* A skill that is a door. It must not announce itself: the whole joke is that
   you reached for it without being told you could, so at rest it is exactly
   the same pill as the six beside it and only the cursor gives it away. */
a.skill-tag{text-decoration:none;cursor:pointer;
  transition:color .16s var(--ease),border-color .16s var(--ease)}
a.skill-tag:hover,
a.skill-tag:focus-visible{color:var(--red);border-color:var(--red)}
.exp-list{margin-bottom:40px}
/* tight rows: the job title was inheriting the body's 1.6 line-height, which
   put half a blank line above and below every role and made the list twice
   as tall as it needed to be */
/* ONE LINE A JOB, and the cap is the point.
   The dates used to sit at the far side of a 1200px row with two lines of
   type on the left, so every row had a void down the middle of it. Narrowing
   the row is what fixes that: at 820 the gap between the job title and the
   dates reads as a rail, at 1200 it reads as a hole. brianlovin.com, which is
   where this arrangement comes from, runs its column at 640.
   Left aligned rather than centred so it keeps the same left edge as the
   heading and the photo above it. */
.exp-row{display:flex;justify-content:space-between;align-items:baseline;gap:28px;padding:13px 0;border-top:1px solid var(--line);max-width:760px}
/* the agency and the title travel together and wrap together */
.exp-lead{display:flex;align-items:baseline;gap:12px;min-width:0;flex-wrap:wrap}
/* The company is the top line and takes the display treatment; the job title
   is the quiet second line. Swapped round on 25 Aug 2026: with the role on
   top the column read "Copywriter, Copywriter, Copywriter, Copywriter" and
   the four names that actually differ were set small underneath. */
.exp-org{font-family:var(--display);font-weight:800;letter-spacing:-.02em;font-size:20px;line-height:1.2;color:var(--fg)}
.exp-org b{font-weight:800}
/* The city rides along with the agency but does not compete with it: same
   face, no weight, no tracking, quieter colour, a size down. It INHERITS the
   family rather than naming one, so it can never drift off the row's face. */
.exp-place{font-family:inherit;font-weight:400;font-size:16px;letter-spacing:0;color:var(--fg-dim)}
.exp-role{font-size:15px;line-height:1.2;color:var(--fg-dim)}
.exp-time{font-size:11.5px;color:var(--fg-dim);white-space:nowrap}

/* ============================ RESPONSIVE ============================ */

/* short landscape screens: a 13in laptop is about 720px tall, and a name, a
   two-line headline, a sub-line, a stamp, three tabs, a barcode and the small
   print do not all fit at full size. Everything steps down together rather
   than one element being singled out. */
@media (orientation:landscape) and (min-width:900px) and (max-height:820px){
  .tk-title{font-size:min(8vw,14.5vh)}
  .tk-name{font-size:clamp(20px,2.4vw,40px)}
  .tk-top{margin-bottom:clamp(10px,1.8vh,24px)}
  .tk-nav a{padding:clamp(11px,1.5vh,18px) 18px;font-size:clamp(16px,1.25vw,21px)}
  .stamp{height:min(26vh,320px)}
}

@media (max-width:900px){
  .work-list{grid-template-columns:1fr;gap:44px}
  .about-grid{grid-template-columns:1fr;gap:34px}
  /* The dates come off the end of the row and sit under the job. Beside it
     they were taking about a third of a phone's width and leaving the title
     a column too narrow to hold it, which broke "Creative Copywriter" across
     two lines in English and broke クリエイティブコピーライター in the middle
     of a long-vowel mark in Japanese, which is a line a Japanese typesetter
     may not begin. Full width fixes both without shrinking any type. */
  .exp-row{flex-direction:column;align-items:flex-start;gap:6px}
  .exp-time{order:-1}
  /* stacked, there is no text beside it to match, so the frame carries its
     own portrait shape instead of stretching to a row it now owns alone */
  .about-shot{min-height:0;aspect-ratio:4/5}
  /* One column on a phone, so every block spans it whatever it was asked to
     be. A half or third SPACER is meaningless once nothing sits beside it,
     so it goes rather than leaving a mystery gap; the full-width one still
     does its job, which is vertical air. */
  .media-grid{grid-template-columns:1fr}
  .project-media,
  .project-media[data-layout="full"],
  .project-media[data-layout="two-thirds"],
  .project-media[data-layout="half"],
  .project-media[data-layout="third"]{grid-column:1/-1}
  .media-spacer{grid-column:1/-1}
  .media-spacer[data-layout="two-thirds"],
  .media-spacer[data-layout="half"],
  .media-spacer[data-layout="third"]{display:none}
  .tk-body{padding:30px 18px 22px}
  .tk-mid{flex-direction:column;align-items:flex-start;gap:26px}
  /* the tabs only stretch when they are standing next to the stamp; 
     stacked on a phone they go back to their own height */
  .tk-midleft{width:100%}
  .tk-midleft .tk-nav{flex:none;margin-top:26px}
  .tk-midleft .tk-nav a{max-height:none}
  .tk-nav{grid-template-rows:auto auto}
  .stamp{width:clamp(150px,48vw,220px)}
  .tk-nav{grid-template-columns:1fr;grid-template-rows:none;gap:9px}
  .tk-nav > a{grid-row:auto}
  .scratch-cap{grid-column:1;grid-row:auto}
  .tk-foot{flex-direction:column;align-items:flex-start;gap:16px}
  .tk-terms{text-align:left}
  .foot{margin-top:86px;padding:30px 0 34px}
  /* THE ROW BECOMES A STACK, and the right-hand side has to become a column
     again with it. Left as a baseline row it put the address and four icons
     on one line inside 344px: measured 435px of content in a 344px box, with
     the icons off the right edge. Everything goes back to the page's left
     edge like the rest of the site. */
  .foot-in{flex-direction:column;align-items:flex-start;gap:22px}
  .foot-side{flex-direction:column;align-items:flex-start;gap:16px}
  .page-nav .links{gap:19.5px}                        /* 17 x 1.15 */
  .page-nav{font-size:14.4px;letter-spacing:.11em}    /* 12.5 x 1.15 */
  body.is-home{padding:16px 10px}


}
/* The dropdown is closed with visibility, not display, because it has to be
   able to transition. A hidden box still takes part in layout, so a panel
   nobody can see was hanging 28px off the right of a phone and making every
   page draggable sideways. Anchor it, and its notch, to the right edge of its
   link instead of the left. Nothing changes above 900px, where it opens under
   a link that is nowhere near the edge. */
@media (max-width:900px){
  .page-nav .nav-menu{left:auto;right:-14px}
  .page-nav .nav-menu-in::before,
  .page-nav .nav-menu-in::after{left:auto;right:24px}
}

/* THE NAV HAS TO FIT A PHONE.
   One row cannot: the logo is 190px, the links 170, the gap 20, and a 375px
   screen leaves 331 of usable width. "Say hi" was cut in half at the right
   edge and the whole page could be dragged sideways after it. So below 520px
   the links drop to their own row under the name. Nothing is removed: the
   alternative was dropping "Say hi" on phones, and the point of a portfolio
   is that the way to reach him is never the thing that gets cut. */
@media (max-width:520px){
  .page-nav .wrap{flex-wrap:wrap;height:auto;padding-top:16px;padding-bottom:14px;gap:9px 20px}
  .page-nav .links{width:100%;gap:20.7px}             /* 18 x 1.15 */
}

@media (max-width:520px){
  .credits-list li{flex-direction:column;gap:3px}
  .credits-list li span{flex:none}
  .page-nav .logo b{font-size:21.3px}                 /* 18.5 x 1.15 */
  .page-nav .logo span{font-size:13.2px}              /* 11.5 x 1.15 */
  .page-nav .links{gap:16px}                          /* 14 x 1.15 */
  .page-nav{font-size:13.2px;letter-spacing:.1em}     /* 11.5 x 1.15 */
  .tk-band{padding:9px 18px;font-size:10.5px}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important}
  .reveal{opacity:1;transform:none}
  /* the footer's entrance starts from a hidden state, so it has to be handed
     its finished state outright rather than merely having its timing zeroed */
  .foot::after{transform:scaleX(1)}
  /* the line's own clip-path reveal was removed with the 58px setting it was
     written for, so its override went at the same time. The pair had been
     separated twice before, and each time the result was that people who
     asked for less motion were the only ones still getting the animation. */
  .foot .foot-social a{opacity:1;transform:none}
}

/* ============================ THE HOME CANVAS ============================
   The owner asked for a free canvas: drag anything anywhere, any size, add
   boxes and images. He was told the trade (absolute positions describe one
   screen) and chose it anyway, so the compromise is in the units: every
   position is a PERCENTAGE of the ticket, never a pixel. The ticket then
   scales as one piece and the composition holds its proportions at any
   window size. Type is sized as a percentage of the ticket height and set
   in pixels by applyLayout() on load and on resize.

   Two artboards, `layout.desktop` and `layout.mobile`, chosen at 900px.
   An artboard with no items falls through to the flow layout below, which
   is the hand-built one and still the default. So an untouched phone keeps
   the layout that was tested down to 360px, and deleting `layout` in admin
   puts everything back. Nothing here applies until he designs something.
   ------------------------------------------------------------------------ */
.ticket.is-canvas .tk-body{position:relative}

/* the flow wrappers stop existing so their children can be placed directly.
   display:contents keeps the elements and their JS, and removes only the box */
.ticket.is-canvas .tk-namerow,
.ticket.is-canvas .tk-mid,
.ticket.is-canvas .tk-midleft,
.ticket.is-canvas .tk-foot,
/* these four used to be single draggable blocks holding two or more separate
   sentences: the game line with the serial, his line with the award badge,
   the two halves of the headline, and all three punches. Every one of those
   is its own piece of writing and he places them separately. Inherited type
   still passes through a display:contents box, so the voices survive. */
.ticket.is-canvas .tk-top,
.ticket.is-canvas .tk-aside,
.ticket.is-canvas .tk-title,
.ticket.is-canvas .tk-nav{display:contents}

.ticket.is-canvas [data-slot]{
  position:absolute;margin:0;max-width:none;min-width:0;
}
/* a slot the artboard does not place is a slot he deleted: in canvas mode
   there is no flow left to fall back into, so it must not land at random */
.ticket.is-canvas [data-slot]:not([data-placed]){display:none}
/* the headline and the sub-line measure themselves against their own box now */
.ticket.is-canvas .tk-title span{white-space:nowrap}
.ticket.is-canvas .tk-sub{white-space:normal;max-width:none}
.ticket.is-canvas .tk-nav{max-width:none}
.ticket.is-canvas .tk-nav a{max-height:none}
/* On the canvas a punch is sized by the artboard, but its padding, its gap
   and its little number were all fixed pixel values, so they did not shrink
   with the rest of the composition: on a small ticket the furniture ate the
   box and the label spilled out of it. Expressed against the punch's own type
   they scale with everything else. Same ratios as the flow layout (15/19,
   17/19, 8/19, 12/19), so a full-size ticket is unchanged. */
.ticket.is-canvas .tk-nav a{padding:.79em .9em;gap:.42em}
.ticket.is-canvas .tk-nav a i{font-size:.63em}
.ticket.is-canvas .stamp{width:100%;height:100%}
.ticket.is-canvas .barcode{width:100%;height:100%}
.ticket.is-canvas .tk-aside{align-items:inherit}

/* elements he adds in the designer that the flow layout has no place for */
.tk-free{position:absolute;margin:0}
.tk-free.free-box{background:var(--red)}
.tk-free.free-img{overflow:hidden}
.tk-free.free-img img{width:100%;height:100%;display:block;object-fit:cover}
.tk-free.free-text{line-height:1.2;margin:0}
.tk-free a{color:inherit;text-decoration:none}

/* an element switched off in the designer, on this artboard only */
.ticket.is-canvas [data-slot].slot-off,
.tk-free.slot-off{display:none}

/* An artboard is a surface, and every child of it is absolutely positioned,
   so there is no content left to give the ticket a height. On a wide screen
   the stylesheet already pins it to 95vh and this never showed; below that
   breakpoint it does not, and the ticket collapsed to a couple of hundred
   pixels. Type is sized as a percentage of that height, so the headline came
   out at 1.6px. The artboard has to be told its height at every width. */
.ticket.is-canvas{min-height:calc(100svh - 80px);min-height:calc(100vh - 80px)}
@supports (height:100svh){
  .ticket.is-canvas{min-height:calc(100svh - 80px)}
}
@media (orientation:landscape) and (min-width:900px){
  .ticket.is-canvas{min-height:95vh}
}

/* ---- the composition keeps the shape it was drawn at ----
   Positions are a percentage of the ticket's WIDTH, type is a percentage of
   its HEIGHT. While the ticket has the shape it was composed on those two
   agree. Let the ticket stretch to any window and they pull apart: open the
   devtools and the ticket gets taller (so the type grows) and narrower (so
   the boxes shrink), and the band, the small print and the tab labels all
   wrap and collide at once. Measured on his own layout: it needs an aspect of
   1.89 for the terms and the band to stay on one line, and it was being shown
   at 1.18.

   So a layout that records the shape it was drawn at gets that shape back.
   The ticket is fitted inside the screen like a poster and scales as ONE
   piece, which makes type and boxes scale by the same number again. The red
   field is what grows around it, and the ticket already sits on red, so the
   margin reads as the field rather than as a letterbox.

   A layout with no aspect keeps the old stretch-to-fill behaviour exactly,
   so nothing that exists regresses.

   The ticket is laid out ONCE at a fixed natural size and then scaled to fit.
   Sizing it in vw/vh instead meant every position, every type size and the
   headline fit had to be recomputed on resize, which only happened 120ms after
   the drag stopped: nothing moved, then everything jumped. A scale is one
   number, costs no layout, and follows the mouse. */
/* Out of flow, and centred by its own transform. A scaled box still occupies
   its UNSCALED size in layout, so left in the flow a 1600x848 ticket grew the
   body past the viewport and the page scrolled 31px behind a ticket that
   visually fitted. Taking it out of flow means the composition's natural size
   cannot affect the page at all, at any scale. */
.ticket.is-canvas.is-locked{
  position:fixed;left:50%;top:50%;
  width:var(--tk-nat-w);
  height:var(--tk-nat-h);
  min-height:0;max-width:none;flex:none;
  transform:translate(-50%,-50%) scale(var(--tk-k,1));
}
/* the home page is a single screen by design, and now nothing can make it
   otherwise */
body.is-home.home-locked{overflow:hidden}

/* ============================ PER-PAGE COLOUR ============================
   Each page sits on its own ground with one small element of its own, so
   they read as pages of the same thing rather than one page repeated. The
   red and the type do not change: only the paper and one mark. The tokens
   are the same ones the gallery already uses, so nothing else needs to know.
   ------------------------------------------------------------------------ */
/* the base for both work pages, and what All work keeps: the ordinary paper */
body.page-work  { --bg:#EFE9DC; --well:#E2DACA; --nav-bg:#EFE9DCf2; }
body.page-about { --bg:#F1E4DC; --well:#E4D2C6; --nav-bg:#F1E4DCf2; }
/* ticket.terms, at the foot of the About page (9 Sep 2026). Set in the system
   voice at the size the ticket sets its own small print, above a hairline, so
   it reads as a disclaimer and not as a closing statement. It is aligned to
   the Milestones list rather than the bio column, because it belongs to the
   page rather than to the paragraph above it. */
/* WHERE TO FIND HIM, on the About page (13 Sep 2026, his ask).
   The footer carries the same links at 23px on every page, which is the right
   size for furniture at the bottom of a screen. About is the page somebody is
   reading while they decide, so here the icons are 34px and the address is
   set to be read and copied rather than found. The footer is unchanged. */
.about-contact{
  margin-top:70px;padding-top:26px;
  border-top:2px solid var(--red);
  max-width:1200px;
}
.about-contact .blk-head{margin-bottom:14px}
.about-mail{
  display:inline-block;
  font-family:var(--mono);font-size:clamp(18px,2vw,24px);
  letter-spacing:.02em;color:var(--fg);text-decoration:none;
  border-bottom:1px solid var(--line);padding-bottom:2px;
  transition:color .16s var(--ease),border-color .16s var(--ease);
}
.about-mail:hover{color:var(--red);border-color:var(--red)}
.about-social{display:flex;align-items:center;gap:26px;margin-top:22px;flex-wrap:wrap}
.about-social a{
  display:block;color:var(--fg-faint);text-decoration:none;
  transition:color .16s var(--ease),transform .16s var(--ease);
}
.about-social a:hover{color:var(--red);transform:translateY(-2px)}
.about-social svg{
  width:34px;height:34px;display:block;
  fill:none;stroke:currentColor;stroke-width:1.6;
}
.about-social svg .fill{fill:currentColor;stroke:none}
.about-social a.txt{
  font-family:var(--mono);font-size:14px;text-transform:uppercase;letter-spacing:.1em;
}

body.page-about .about-terms{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.13em;
  text-transform:uppercase;line-height:1.7;color:var(--fg-dim);
  max-width:1200px;margin:64px auto 0;padding-top:16px;
  border-top:1px solid var(--line);
}
body.page-awards{ --bg:#E6E7E2; --well:#D6D8D1; --nav-bg:#E6E7E2f2; }
/* Work that worked, and ONLY that page: a dusty blue. The work that ran is the
   one part of the site that is finished, so it is the one ground that is a
   colour rather than a paper. All work stays on the ordinary paper above and
   carries the registration cross; a sage was tried on it on 22 Aug 2026 and
   taken back out, because the difference belongs on the page that earned it,
   not on the default one.

   Grey-leaning rather than a true blue, and LIGHT: the campaign stills have to
   sit on it and the red has to stay the loudest thing on the page. #A3BAD1 was
   tried first and it was too heavy, closer to a painted wall than to paper. */
body.page-work-ran { --bg:#CFDCE6; --well:#B9CBDA; --nav-bg:#CFDCE6f2; }
/* Work I want to do, the other half of the same door (6 Sep 2026). A warm sand
   against the dusty blue, so the two filtered views are opposite temperatures
   and cannot be confused with each other or with the paper of All work.

   Same discipline as the blue: light, low-chroma and grey-leaning, because the
   stills sit on it and the red stays the loudest thing on the page. It is NOT
   given a mark of its own; the ground is the difference, exactly as the tick
   was rejected on Work that worked for being a second signal saying one thing. */
body.page-work-want{ --bg:#E8DFCD; --well:#D8CDB6; --nav-bg:#E8DFCDf2; }
/* The manipulating piece keeps the About paper, because it is a room off the
   About page rather than a section of its own, and it gets NO mark at all.
   Every other page has one small drawn thing; this is the one that does not,
   and that is the point of it. You clicked a confession expecting something
   and arrived somewhere emptier than where you came from. */
body.page-manipulating { --bg:#F1E4DC; --well:#E4D2C6; --nav-bg:#F1E4DCf2; }

/* the hint of an element: one quiet mark per page, behind everything,
   printed in the page's own well colour so it never fights the work */
body.page-work::before,
body.page-about::before,
body.page-awards::before{
  content:"";position:fixed;pointer-events:none;z-index:0;
  opacity:.55;
}
/* work: a printer's registration cross in the corner */
body.page-work::before{
  right:34px;top:96px;width:74px;height:74px;
  background:
    linear-gradient(var(--well),var(--well)) center/2px 100% no-repeat,
    linear-gradient(var(--well),var(--well)) center/100% 2px no-repeat,
    radial-gradient(circle,transparent 22px,var(--well) 22px,var(--well) 24px,transparent 24px);
}
/* Work that worked had a ticked box here for one pass on 22 Aug 2026 and it is
   gone at the owner's call. The colour is the difference now, and that is
   enough: the page keeps the registration cross like every other work page,
   so there is still exactly one mark on it. Do not put the tick back. */

/* about: one perforation run down the left edge, like the ticket's */
body.page-about::before{
  left:18px;top:0;bottom:0;width:12px;
  background-image:radial-gradient(circle at 6px 6px,var(--well) 4px,transparent 4.5px);
  background-size:12px 20px;
}
/* awards: a faint rule at the height of an empty shelf */
body.page-awards::before{
  left:0;right:0;top:63%;height:2px;background:var(--well);opacity:.5;
}
/* ================= THE PAPER: PRINTED, NOT SCRATCHED =================
   7 Sep 2026. The ticket's 22 diagonal drags were ported here for one pass and
   he killed them on sight, and he was right about why: a scratch is DAMAGE.
   It says somebody dragged a key across the page. What this paper wants to say
   is that it came out of a machine, so the texture has to be REGULAR, and its
   irregularity has to be the machine's, not a scribble.

   So every option below is built from repeats, and the only randomness is the
   fibre in the stock itself. All are `fixed` so they keep their size on a page
   that scrolls, and all sit at z-index 0 under the content rule further down.

   Set one class on <body> to pick. `tex-fibre` is the paper on its own and
   every other option lays its press over the same fibre. */

/* the stock: fractal noise, the same tile the ticket uses for its grain */
body[class*="tex-"]::after{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
  background-repeat:repeat;opacity:.30;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* the press: a second fixed layer over the stock, one repeat each.
   ::before is taken on this page by the perforation run, so the press rides on
   its own element rather than a pseudo, added by the page that wants it. */
.tex-press{position:fixed;inset:0;pointer-events:none;z-index:0}

/* 1. BANDS. A thermal head printing one line at a time. The most literal
      "came out of a machine" there is, and the one a real ticket looks like. */
body.tex-bands .tex-press{
  background-image:repeating-linear-gradient(180deg,
    rgba(11,10,9,.045) 0 1px, transparent 1px 3px);
}
/* 2. DOTS. An offset screen. It rhymes with the portrait, which is already a
      halftone, so the page and the picture are printed the same way. */
body.tex-dots .tex-press{
  background-image:radial-gradient(circle at 1px 1px,rgba(11,10,9,.075) .6px,transparent .75px);
  background-size:3px 3px;
}
/* 3. INK. Banding with an uneven period, so the head reads as a real one that
      does not lay ink down perfectly evenly rather than as a CSS repeat. */
body.tex-ink .tex-press{
  background-image:repeating-linear-gradient(180deg,
    rgba(11,10,9,.038) 0 1px, transparent 1px 4px,
    rgba(11,10,9,.020) 4px 5px, transparent 5px 9px,
    rgba(11,10,9,.028) 9px 10px, transparent 10px 16px);
}
/* 4. WEAVE. Bands both ways: the paper's own grid, closer to laid stock. */
body.tex-weave .tex-press{
  background-image:
    repeating-linear-gradient(180deg,rgba(11,10,9,.035) 0 1px,transparent 1px 4px),
    repeating-linear-gradient(90deg, rgba(11,10,9,.022) 0 1px,transparent 1px 5px);
}
/* 5. FIBRE. The stock and nothing else. */

/* Everything on the page sits above the texture. NOT the nav: this selector is
   (0,1,1) and .page-nav is (0,1,0), so it was winning and quietly turning the
   sticky header into a relative one with z-index 1 on all three of these
   pages. The header stopped sticking, and page content painted over the work
   menu, which is why the menu vanished the moment the pointer reached it.

   NOT the press either, and that exclusion is load-bearing for the same
   reason: this rule is (0,2,1) against the (0,1,0) of `.tex-press`, so it
   would win, turn the overlay into an in-flow block and drop an empty
   viewport-tall div into the top of the page. */
/* `.lb` is excluded for the same reason `.tex-press` is, and it is the third
   time this rule has caught something: it is (0,2,1), so it beats a plain
   class and it was overriding the lightbox's OWN `position:fixed` back to
   `relative` and its z-index:200 down to 1. The overlay then laid itself out
   in the document flow 382px down the page instead of covering the viewport,
   which looked like a blank screen. Anything that has to escape the page has
   to be named here. */
body.page-work   > *:not(.page-nav):not(.lb),
body.page-about  > *:not(.page-nav):not(.tex-press):not(.lb),
body.page-awards > *:not(.page-nav):not(.lb){position:relative;z-index:1}

/* ============================ THE CABINET ============================
   One screen, and it does not scroll. The page is a column that fills the
   viewport under the nav: the cabinet takes whatever height is left after the
   line and the way back have taken theirs, and it is sized by HEIGHT rather
   than width so a short laptop window shrinks it instead of pushing the words
   off the bottom. There is no footer on this page for the same reason. */
body.page-awards{height:100svh;overflow:hidden;display:flex;flex-direction:column}
body.page-awards #page{
  flex:1 1 auto;min-height:0;display:flex;flex-direction:column;
}
.cab-wrap{
  flex:1 1 auto;min-height:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  /* tight under the cabinet, so the sentence sits up close to it */
  gap:clamp(2px,.8vh,12px);
  padding:clamp(12px,2.4vh,34px) 0 clamp(16px,3vh,40px);
  text-align:center;
}
.cab-stage{
  flex:1 1 auto;min-height:0;
  display:flex;justify-content:center;align-items:center;width:100%;
}
/* height first: the cabinet gives way, the words never do.

   The 460px cap was doing two bad things at once on a wide screen. It held
   the case to 23% of a 2000px window, and because the box was then 460 wide
   by 662 tall while the drawing inside is 520:560, the SVG letterboxed
   itself: 460x495 of case with 83px of dead band above and below it, INSIDE
   its own element. Measured, not guessed.

   Height-driven with a generous cap, so the box matches the drawing's own
   ratio and there is nothing left over. */
.cab{width:auto;height:100%;max-width:min(100%,820px);max-height:100%;overflow:visible}

.cab .ink-1{fill:var(--well)}
.cab .ink-2{fill:#0B0A09}
.cab .ink-3{fill:#1a1815}
.cab .shelf-top{fill:var(--well);opacity:.85}

/* ---- the case settles in ---- */
.cab{opacity:0;transform:scale(.965) translateY(10px);
  transition:opacity .6s ease,transform .7s cubic-bezier(.22,.85,.28,1)}
.cab.is-in{opacity:1;transform:none}

/* ---- the shelves draw themselves in ----
   Left to right, one after another, so the emptiness is something you watch
   being built rather than something that was simply already there. */
.cab .shelf{
  transform-box:fill-box;transform-origin:left center;
  transform:scaleX(0);opacity:0;
  transition:transform .52s cubic-bezier(.3,.85,.3,1),opacity .3s ease;
}
.cab.is-drawn .shelf{transform:scaleX(1);opacity:1}
.cab.is-drawn .shelf[data-row="0"]{transition-delay:0s}
.cab.is-drawn .shelf[data-row="1"]{transition-delay:.13s}
.cab.is-drawn .shelf[data-row="2"]{transition-delay:.26s}

/* the doors: glass, so the empty shelves show through before they swing */
.cab .glass{fill:#ffffff10}
.cab .frame{fill:none;stroke:var(--well);stroke-width:3}
.cab .glint{fill:#ffffff14;transition:opacity .8s ease}
.cab .knob{fill:var(--red)}
.cab .door{
  transform-box:fill-box;transform-origin:left center;
  transition:transform 1.05s cubic-bezier(.26,.86,.24,1);
}
.cab .door-r{transform-origin:right center}
/* the slack before the swing: a door takes up its play against the catch
   before it gives, and that half-beat is what stops it reading as a flap */
.cab.is-unlatch .door-l{transform:perspective(900px) rotateY(5deg);transition-duration:.18s}
.cab.is-unlatch .door-r{transform:perspective(900px) rotateY(-5deg);transition-duration:.18s}
.cab.is-open .door-l{transform:perspective(900px) rotateY(-106deg)}
.cab.is-open .door-r{transform:perspective(900px) rotateY(106deg)}
/* the reflection dies as the glass turns away from the room */
.cab.is-open .glint{opacity:0}

/* it holds its space from the first frame: only the opacity waits, so the
   cabinet is never shoved upwards by the words arriving underneath it */
.cab-say{opacity:0;transition:opacity .55s ease}
.cab-say.in{opacity:1}
/* sized against the HEIGHT as well as the width: this page cannot scroll, so
   a headline measured only in vw runs off the bottom of a short window */
.cab-line{
  font-family:var(--display);font-weight:800;letter-spacing:-.03em;line-height:1.04;
  font-size:clamp(22px,min(5vw,4.4vh),52px);
  margin:0;max-width:22ch;margin-inline:auto;
}
/* the booking link inside it: underlined because it is the one thing on the
   page to press, and it has to look like it */
.cab-line a{
  color:var(--red);text-decoration:underline;text-underline-offset:.14em;
  text-decoration-thickness:.06em;
  transition:color .16s var(--ease);
}
.cab-line a:hover{color:var(--red-hot)}
/* the line and the way back are one flex child between them, so the gap on
   .cab-wrap does not separate them and this has to. Deliberately much larger
   than that gap: the sentence belongs to the cabinet, the way back does not. */
.cab-back{margin:clamp(30px,5.5vh,64px) 0 0}
.cab-back a{
  font-family:var(--mono);text-transform:uppercase;letter-spacing:.14em;
  font-weight:600;font-size:13px;color:var(--fg-dim);text-decoration:none;
}
.cab-back a:hover{color:var(--red)}

/* The way back off the produced-only view, at the FOOT of the grid.
   A rule across the column above it, so it reads as the list ending rather
   than as one more thing in the list. Set in the small-print voice and not
   in red: it is a way out, and a way out should never be the loudest thing
   on a page of somebody's work. */
.all-work-end{
  margin:60px 0 0;padding-top:20px;
  border-top:1px solid var(--line);
}
.all-work{
  font-family:var(--mono);text-transform:uppercase;letter-spacing:.14em;
  font-weight:600;font-size:11.5px;color:var(--fg-faint);text-decoration:none;
  transition:color .15s var(--ease);
}
.all-work:hover{color:var(--red)}

@media (prefers-reduced-motion:reduce){
  /* the staged open starts from hidden states, so they have to be handed
     their finished values outright rather than merely having timing zeroed */
  .cab,.cab .door,.cab .shelf,.cab .glint,.cab-say{transition:none}
  .cab{opacity:1;transform:none}
  .cab .shelf{transform:scaleX(1);opacity:1}
}


/* ====== LANGUAGES, MILESTONES, AND THE PROPORTIONS (5 Sep 2026) ======

   The photograph was 501 by 566, a ratio of 0.89, because align-items was
   stretching it to the height of the text column: the picture was being
   deformed by the LENGTH OF THE BIO, so every time the words got shorter the
   portrait got squarer. A fixed column and an explicit 3:4 holds it whatever
   the copy does.

   The two lists then break OUT of the right column and run the full content
   width, pulled left by exactly the left column plus the gap, so they start
   on the photograph's edge and finish on the bio's. Three elements, two
   rails, no third edge.

   No rule through the gap between a title and its date. One was tried and it
   was a second line doing the job the row's own border already does. */
body.page-about .about-grid{grid-template-columns:360px minmax(0,62ch);
  gap:64px;align-items:start;margin-bottom:0}
body.page-about .about-shot{aspect-ratio:3/4}
.lists{margin-left:-424px;width:calc(100% + 424px)}
.lists .blk-head{margin-top:38px}
body.page-about .exp-list{max-width:none;margin:0}
body.page-about .exp-row{max-width:none}

.blk-head{font-family:var(--mono);font-weight:700;font-size:11.5px;
          letter-spacing:.16em;text-transform:uppercase;color:var(--fg-dim);
          margin:0 0 2px}

/* Languages is the same component as Milestones: same padding, same gap,
   same rule, same space-between. The language is bold and the level is not,
   at his call, which puts the display face on the left of both lists and the
   quiet type on the right of both. */
.lang-list{margin:0 0 8px}
.lang-row{display:flex;justify-content:space-between;align-items:baseline;
          gap:28px;padding:13px 0;border-top:1px solid var(--line)}
.lang-name{font-family:var(--display);font-weight:800;font-size:16px;
           letter-spacing:-.02em;color:var(--fg);flex:none}
/* Still descending, so competence is measured by the eye before it is read.
   SIZE and never opacity: faded small text has failed the contrast floor on
   this site three times. */
.lang-say{font-family:var(--body);font-weight:400;color:var(--fg-dim);
          line-height:1.2;text-align:right;font-size:15px}
.lang-row:nth-child(1) .lang-say{font-size:17px}
.lang-row:nth-child(2) .lang-say{font-size:15.5px}
.lang-row:nth-child(3) .lang-say{font-size:14px}
.lang-row:nth-child(4) .lang-say{font-size:13px}
/* Arabic inside a left-to-right row resolves its spaces and punctuation
   against the paragraph rather than the words without this. */
.lang-say[dir]{unicode-bidi:isolate}

@media (max-width:900px){
  body.page-about .about-grid{grid-template-columns:1fr;gap:30px}
  body.page-about .about-shot{aspect-ratio:4/5}
  .lists{margin-left:0;width:auto}
  .lang-row{flex-direction:column;align-items:flex-start;gap:4px}
  .lang-say{text-align:left}
}

/* ==================== THE PROJECT TICKET HEADER ====================
   Screen 03 of the brutalist system, ported to the real project page
   (9 Sep 2026). Behind ?hdr=1 in work.html while he decides.

   Why it exists: the home page is a full-screen lottery ticket and a project
   page opened with a plain back-link and an <h1>, so the site's whole voice
   dropped the moment anyone clicked a piece of work. This is the one place a
   ticket has a JOB rather than being decoration: it carries the back link,
   the serial, the metadata and the two actions.

   HEADER ONLY. The case study below stays on the gallery paper, because the
   art direction says the work carries the colour and a red ticket wrapped
   round someone else's campaign stills fights them.

   The values are lifted from preview-brutalist.html rather than re-invented,
   so the two cannot drift into looking almost-the-same. */
:root{
  --pj-ink:#1A1A18;
  --pj-paper:#F1E4DC;      /* the ticket's paper, exactly */
  --pj-red:#E3261A;
  --pj-mono:"Share Tech Mono",ui-monospace,monospace;
  --pj-disp:"Archivo Black","Arial Black",Impact,sans-serif;
}
.pj-ticket{
  background:var(--pj-paper);color:var(--pj-ink);
  border:3px solid var(--pj-ink);
  margin:0 0 42px;position:relative;
}
/* the same perforated edge the home card has: a MASK, so it cuts the border
   into scallops and the page behind shows through the holes. */
/* NOT `.perf`. styles.css already has one, at line 229: the footer's scalloped
   tear strip, `height:14px`. `.pj-ticket.perf` inherited that height and the
   whole header collapsed to a 14px scalloped sliver with its band and body
   overflowing invisibly. Exactly the collision the `stub` class caused once
   before. Its own name, so it cannot happen again. */
.pj-ticket.pj-perf{
  --perfR:9px; --perfP:24px;
  -webkit-mask-image:
    radial-gradient(circle var(--perfR) at left center,transparent 96%,#000 97%),
    radial-gradient(circle var(--perfR) at right center,transparent 96%,#000 97%);
          mask-image:
    radial-gradient(circle var(--perfR) at left center,transparent 96%,#000 97%),
    radial-gradient(circle var(--perfR) at right center,transparent 96%,#000 97%);
  -webkit-mask-size:100% var(--perfP);  mask-size:100% var(--perfP);
  -webkit-mask-repeat:repeat-y;         mask-repeat:repeat-y;
  -webkit-mask-composite:source-in;     mask-composite:intersect;
}
.pj-band{
  background:var(--pj-ink);color:var(--pj-paper);
  display:flex;justify-content:space-between;align-items:center;gap:18px;
  padding:12px 22px;font-family:var(--pj-mono);font-size:12px;
  letter-spacing:.2em;text-transform:uppercase;
}
.pj-back{color:inherit;text-decoration:none;display:inline-flex;align-items:center}
.pj-back b{font-weight:400;border:2px solid currentColor;padding:1px 6px 0;margin-right:10px}
.pj-back:hover b{background:var(--pj-red);border-color:var(--pj-red)}
.pj-serial{opacity:.72;white-space:nowrap}

.pj-body{padding:26px 24px 28px}
.pj-chips{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.pj-chips span{
  font-family:var(--pj-mono);font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;border:1px solid var(--pj-ink);padding:5px 9px;
}
/* the one chip that is a fact rather than a label, so it takes the ink */
.pj-chips .pj-status.is-ran{background:var(--pj-ink);color:var(--pj-paper)}
.pj-chips .pj-status.is-spec{background:transparent;border-style:dashed}

.pj-title{
  font-family:var(--pj-disp);font-size:clamp(28px,4.6vw,66px);
  line-height:.92;letter-spacing:-.02em;text-transform:uppercase;margin:0;
}
.pj-tag{
  font-family:var(--pj-disp);font-size:clamp(15px,1.5vw,22px);
  line-height:1.3;max-width:48ch;margin:14px 0 0;
}
.pj-acts{display:flex;gap:14px;flex-wrap:wrap;padding-top:22px}
.pj-btn{
  font-family:var(--pj-mono);font-size:12px;letter-spacing:.2em;
  text-transform:uppercase;text-decoration:none;
  background:var(--pj-paper);color:var(--pj-ink);
  border:2px solid var(--pj-ink);padding:11px 15px;
  box-shadow:5px 5px 0 var(--pj-ink);
}
.pj-btn:hover{background:var(--pj-red);color:var(--pj-paper);border-color:var(--pj-red)}
.pj-btn.is-solid{background:var(--pj-ink);color:var(--pj-paper)}
.pj-btn.is-solid:hover{background:var(--pj-red);border-color:var(--pj-red)}

@media (max-width:700px){
  .pj-body{padding:20px 18px 22px}
  .pj-band{padding:10px 16px;font-size:11px;letter-spacing:.14em}
  /* the serial is furniture and the back link is not, so the serial goes
     first when there is not room for both */
  .pj-serial{display:none}
  .pj-acts .pj-btn{flex:1 1 100%;text-align:center}
}

/* ==================== THE LIGHTBOX ====================
   Click a still on a project page and it opens full size (9 Sep 2026, his
   ask). Several of these are posters and caseboards whose type is simply not
   readable at column width, so the page was showing work nobody could read.

   Videos are deliberately NOT included: an iframe cannot be reopened bigger
   without reloading it, which restarts a film someone is halfway through. */
.project-media .zoomable{
  display:block;width:100%;padding:0;border:0;background:none;
  cursor:zoom-in;line-height:0;
}
/* This rule is more specific than the one that sets the 80vh ceiling, so it
   has to carry the ceiling too or the stills, which are the only things that
   are ever tall, would be the one kind of media that ignores it. */
.project-media .zoomable img{
  display:block;
  width:100%;max-width:100%;
  height:auto;max-height:80vh;
  object-fit:contain;
  margin-inline:auto;
  filter:drop-shadow(0 8px 22px rgba(11,10,9,.20));
}
/* the affordance. Nothing else on a project page reacts to a pointer, so a
   small lift is enough to say this one does. */
.project-media .zoomable{transition:filter .18s ease}
/* `filter` is ONE property: a second declaration replaces the first rather
   than adding to it, so the hover state has to restate the shadow or the
   picture drops flat the moment you point at it. */
.project-media .zoomable:hover img,
.project-media .zoomable:focus-visible img{
  filter:drop-shadow(0 8px 22px rgba(11,10,9,.20)) brightness(.92);
}
.project-media .zoomable:focus-visible{outline:2px solid var(--red);outline-offset:3px}

.lb{
  position:fixed;inset:0;z-index:200;display:none;
  align-items:center;justify-content:center;gap:10px;
  padding:clamp(14px,4vw,54px);
  background:rgba(11,10,9,.94);
  /* the site's own paper, so the viewer is not a foreign black box */
  color:var(--bg);
}
.lb.is-open{display:flex}
.lb-stage{margin:0;display:flex;flex-direction:column;align-items:center;gap:12px;
          max-width:100%;max-height:100%;min-width:0}
.lb-stage img{
  display:block;max-width:100%;
  /* the caption needs room, so the picture may not have the whole height */
  max-height:calc(100vh - 132px);
  width:auto;height:auto;object-fit:contain;
}
.lb-cap{
  font-family:var(--display);font-size:13px;letter-spacing:.02em;
  text-align:center;max-width:70ch;opacity:.8;
}
.lb-x,.lb-nav{
  border:1px solid currentColor;background:transparent;color:inherit;
  font-family:var(--display);cursor:pointer;line-height:1;
  display:flex;align-items:center;justify-content:center;flex:none;
}
/* 44px, because these are the only controls in the overlay and a miss puts
   the reader back on a page they cannot see. */
/* ---- THE ARROWS DO NOT MOVE (his ask, 9 Sep 2026) ----
   They were flex siblings of the picture, so their position was set by the
   picture's width: a tall image made a narrow stage and the arrows walked
   inwards, a wide one pushed them out. Clicking through a set meant chasing
   two buttons around the screen.

   `fixed` to the VIEWPORT instead. Same two places on every image, whatever
   its shape, so the pointer can stay still while the pictures change. */
.lb-nav{
  position:fixed;top:50%;transform:translateY(-50%);
  width:46px;height:46px;font-size:19px;border-radius:50%;
}
.lb-prev{left:clamp(10px,2.2vw,28px)}
.lb-next{right:clamp(10px,2.2vw,28px)}
/* the ends of the set. Not hidden: a control that vanishes moves the ones
   beside it and makes the reader wonder what they did. It stays, and stops. */
.lb-nav[disabled]{opacity:.28;cursor:default;pointer-events:none}
.lb-x{position:absolute;top:clamp(12px,2.4vw,26px);right:clamp(12px,2.4vw,26px);
      width:44px;height:44px;font-size:26px;border-radius:50%}
.lb-x:hover,.lb-nav:hover{background:var(--red);border-color:var(--red);color:#fff}
.lb-x:focus-visible,.lb-nav:focus-visible{outline:2px solid var(--red);outline-offset:3px}

@media (max-width:700px){
  /* On a phone they come off the flanks: at 375px a 46px control on each side
     takes a quarter of the width away from the picture. They are still FIXED,
     just to the bottom of the viewport rather than its sides, so they still do
     not move when the image does. */
  .lb{padding:60px 12px 86px}
  .lb-nav{top:auto;bottom:20px;transform:none}
  .lb-prev{left:50%;margin-left:-56px}
  .lb-next{right:50%;margin-right:-56px}
  .lb-stage img{max-height:calc(100vh - 210px)}
}
@media (prefers-reduced-motion:reduce){
  .project-media .zoomable{transition:none}
}
/* how far into the set. Fixed to the viewport like the arrows, so it does not
   move with the picture either. */
.lb-count{position:fixed;top:clamp(14px,2.6vw,28px);left:50%;transform:translateX(-50%);
          font-family:var(--display);font-size:12px;letter-spacing:.18em;opacity:.6}
