/* ===========================================================================
   site.css
   Page level compositions. tokens.css owns the system, this owns the pages.
   Same rules: no shadow, no gradient between hues, no fade up on scroll.
   =========================================================================== */

/* ===== Home: the opening ================================================= */

.hero{
  position:relative;
  padding:var(--s9) 0 var(--s8);
  overflow:hidden;
}

/* A wall of Amalfi lemon maiolica bleeding off the right edge of the
   viewport, the way a tiled wall runs out of a photograph rather than
   stopping in a frame. This is the largest single gesture on the site.

   The cells are built by tiles.js because the artwork is one hand painted
   tile and is not seamless: alternate columns and rows are mirrored so every
   join is a reflection. See the note at the top of that file. */
.tiles{
  /* 300px, not 220. At 220 each medallion lands at about 50px and the wall
     reads as texture; at 300 the lemons and the eight lobed medallions are
     still legible as what they are, which is the whole point of the
     artwork. */
  --tile:300px;
  position:absolute;
  top:0;
  bottom:0;
  left:58%;
  right:0;
  overflow:hidden;
  pointer-events:none;
}
.tiles i{
  position:absolute;
  display:block;
  background:url("../assets/tile-field.webp?v=539033fe?v=539033fe") center / 100% 100% no-repeat;
}
/* The tiles meet the text on a painted rule rather than on a raw crop. */
.tiles::after{
  content:"";
  position:absolute;
  left:0;
  top:0;
  bottom:0;
  width:3px;
  background:var(--blue);
  z-index:2;
}

/* Laying in. Each tile opens from its own centre, staggered on the diagonal,
   so the wall reads as being set rather than switched on. It is a clip, not a
   fade: there is no opacity animation anywhere on this site. */
@media (prefers-reduced-motion: no-preference){
  .tiles i{
    clip-path:inset(50% 50% 50% 50%);
    transition:clip-path 460ms var(--ease);
  }
  .tiles.is-set i{clip-path:inset(0 0 0 0)}
}

.hero .wrap{position:relative;z-index:1}
.hero-text{max-width:min(100%,600px)}
.hero h1{margin-top:var(--s3)}
.hero .lede{margin-top:var(--s5);max-width:44ch}

.hero-rule{
  border:0;
  border-top:2px solid var(--blue);
  margin:var(--s5) 0 0;
  max-width:120px;
}

/* the three word summary under the name */
.hero-tags{
  display:flex;
  gap:var(--s1);
  flex-wrap:wrap;
  margin-top:var(--s5);
}
.hero-tags li{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  border-radius:var(--r);
  padding:6px 12px;
}
.hero-tags li:nth-child(1){background:var(--w-shell);color:var(--blue)}
.hero-tags li:nth-child(2){background:var(--w-blue);color:var(--blue)}
.hero-tags li:nth-child(3){background:var(--w-leaf);color:var(--green)}

/* Below 900px the wall stops being a panel beside the text and becomes a
   band under it. Absolute positioning would put tiles behind the headline,
   where they would wreck the contrast the whole palette was measured for. */
@media (max-width:900px){
  .hero{padding:var(--s7) 0 0}
  .hero-text{max-width:none}
  .hero .tiles{
    --tile:170px;
    position:relative;
    left:auto;
    right:auto;
    height:170px;
    margin-top:var(--s7);
    border-top:3px solid var(--blue);
  }
  .hero .tiles::after{content:none}
}

/* ===== Home: who she is ================================================== */
/* Note 7: the homepage has to give the full impression on its own, so this
   carries real biography rather than a teaser that defers to About. */

.intro-grid{
  display:grid;
  grid-template-columns:400px 1fr;
  gap:var(--s7);
  align-items:start;
}
.intro-grid .portrait{border-radius:var(--r);overflow:hidden;background:var(--sky)}
@media (max-width:900px){
  .intro-grid{grid-template-columns:1fr;gap:var(--s5)}
  .intro-grid .portrait{max-width:320px}
}

.intro-body p{font-size:17px;max-width:60ch}

/* ===== Home: the section index =========================================== */
/* A list, not cards. Each row opens on hover or focus to show one line. */

.what-index{border-top:1px solid var(--rule-firm)}
.what-index li{border-bottom:1px solid var(--rule)}

.what-index a{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  grid-template-areas:
    "name date"
    "line line";
  gap:0 var(--s4);
  align-items:baseline;
  padding:var(--s3) var(--s3) var(--s3) 0;
  border-left:3px solid transparent;
  padding-left:var(--s3);
  transition:background-color 200ms var(--ease), border-color 200ms var(--ease);
}
.what-index a:hover{background:var(--sky)}
.what-index li:nth-child(1) a{border-left-color:var(--blue)}
.what-index li:nth-child(2) a{border-left-color:var(--blue)}
.what-index li:nth-child(3) a{border-left-color:var(--lemon)}
.what-index li:nth-child(4) a{border-left-color:var(--green)}
.what-index li:nth-child(5) a{border-left-color:var(--rule-firm)}

.what-index .wi-name{
  grid-area:name;
  font-family:var(--display);
  font-size:clamp(26px,4vw,44px);
  line-height:1.05;
  letter-spacing:-0.02em;
}
.what-index .wi-date{grid-area:date;color:var(--muted);white-space:nowrap}

.what-index .wi-line{
  grid-area:line;
  max-width:58ch;
  color:var(--ink);
  font-size:15px;
  overflow:hidden;
  max-height:0;
  opacity:0;
}
@media (prefers-reduced-motion: no-preference){
  .what-index .wi-line{
    transition:max-height 300ms var(--ease), opacity 240ms var(--ease), margin-top 300ms var(--ease);
  }
}
.what-index a:hover .wi-line,
.what-index a:focus-visible .wi-line{
  max-height:8em;
  opacity:1;
  margin-top:var(--s1);
}
/* Under reduced motion the line is always present. Nothing is hidden behind
   an interaction a reduced motion visitor cannot trigger. */
@media (prefers-reduced-motion: reduce){
  .what-index .wi-line{max-height:none;opacity:1;margin-top:var(--s1)}
}
@media (max-width:700px){
  .what-index a{grid-template-columns:1fr;grid-template-areas:"name" "date" "line"}
  .what-index .wi-date{margin-top:4px}
  .what-index .wi-line{max-height:none;opacity:1;margin-top:var(--s2)}
}

/* ===== Home: contact ===================================================== */

.contact-mail{
  font-family:var(--display);
  font-size:clamp(24px,3.4vw,40px);
  margin-top:var(--s4);
  line-height:1.1;
}
.contact-links{align-self:end}
@media (max-width:900px){.contact-links{margin-top:var(--s6)}}

/* ===== Interior pages: the head ========================================== */

.page-head{
  padding:var(--s8) 0 var(--s7);
  border-bottom:1px solid var(--rule-firm);
  position:relative;
  overflow:hidden;
}
.page-head.tint{background:var(--sky)}
.page-head.w-shell{background:var(--w-shell)}
.page-head.w-blue{background:var(--w-blue)}
.page-head.w-leaf{background:var(--w-leaf)}
.page-head.w-lemon{background:var(--w-lemon)}

/* Interior headline size is set from measurement rather than by eye. The
   content column is 660px at 1440 and the longest authored line on any page
   needs about 9.2px of width per px of font size, so it stops fitting above
   72px. 70px leaves headroom and holds every authored line break at 1440,
   1200 and 1024. Above that the hard breaks silently gain a third line,
   which is the thing they exist to prevent. */
.page-head h1{
  font-size:clamp(34px,5.6vw,70px);
  margin-top:var(--s3);
  max-width:20ch;
}
.page-head .lede{margin-top:var(--s4)}

/* ===== Carousel ==========================================================
   Used on Work, Expression, Compassion and About.

   Built on native scroll snap, so with JavaScript switched off it is still a
   working horizontal scroller with every caption readable. The buttons and
   the counter are progressive enhancement on top of that, and carousel.js
   hides them if there is only one slide.

   Autoplay is deliberately absent. A carousel that moves on its own steals
   reading time and is a WCAG 2.2.2 problem the moment a caption runs past a
   line, and none of these carry anything urgent enough to justify it.
   ========================================================================= */

.carousel{margin-top:var(--s5);position:relative}

.car-top{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:var(--s3);
  padding-bottom:var(--s2);
}
/* The controls row is empty when the track fits, and an empty flex row still
   takes its padding. Collapsing it keeps the spacing honest. */
.car-top:has(.car-controls[hidden]){padding-bottom:0}
.car-title{
  font-family:var(--display);
  font-size:var(--t-h3);
  line-height:1.1;
}
.car-controls{display:flex;align-items:center;gap:var(--s1)}
/* An author `display` always beats the UA sheet's `[hidden]{display:none}`,
   so the rule above was quietly re-showing controls that carousel.js had
   already hidden: the markup said hidden, the JS said hidden, and the row
   rendered anyway with a next button that could not move. */
.car-controls[hidden]{display:none}
.car-count{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  color:var(--muted);
  min-width:5ch;
  text-align:right;
}
.car-btn{
  width:36px;height:36px;
  border:1px solid var(--rule-firm);
  border-radius:var(--r);
  color:var(--ink);
  display:grid;
  place-items:center;
  transition:background-color 160ms var(--ease), border-color 160ms var(--ease), color 160ms var(--ease);
}
.car-btn:hover:not([disabled]){background:var(--ink);border-color:var(--ink);color:var(--cream)}
.car-btn[disabled]{color:var(--rule-firm);cursor:default}
/* Chevrons drawn as a rotated square corner. No icon font, no SVG request. */
.car-btn i{
  width:8px;height:8px;
  border-top:1.5px solid currentColor;
  border-right:1.5px solid currentColor;
  display:block;
}
.car-btn.prev i{transform:rotate(-135deg);margin-left:3px}
.car-btn.next i{transform:rotate(45deg);margin-right:3px}

/* Flex rather than grid. `grid-auto-columns: minmax(0, 33%)` sets a maximum,
   not a width: the tracks are free to shrink below it, so five slides
   compressed themselves to fit the container instead of overflowing it and
   the scroller never scrolled. `flex: 0 0 <width>` is the thing that actually
   holds a slide at its width and pushes the rest out of view. */
.car-track{
  display:flex;
  gap:var(--s3);
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  scroll-behavior:smooth;
  /* the scrollbar is left alone rather than hidden: hiding it removes the
     only affordance a trackpad user has when the buttons are off screen */
  padding-bottom:var(--s2);
}
.car-slide{flex:0 0 calc(33.333% - var(--s3) * 2 / 3)}
@media (prefers-reduced-motion: reduce){
  .car-track{scroll-behavior:auto}
}
@media (max-width:1000px){
  .car-slide{flex-basis:calc(50% - var(--s3) / 2)}
}
@media (max-width:640px){
  .car-slide{flex-basis:86%}
}

.car-slide{scroll-snap-align:start;min-width:0}
.car-slide .photo{aspect-ratio:4 / 3}
.car-slide .photo img{width:100%;height:100%;object-fit:cover}
.car-slide figcaption{
  font-size:15px;
  color:var(--ink);
  padding-top:var(--s2);
  max-width:44ch;
}
.car-slide figcaption .cap-meta{
  display:block;
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--muted);
  margin-bottom:6px;
}

/* ---- The empty slot -----------------------------------------------------
   Every carousel on this build is waiting on photographs. An empty slot is
   drawn as a labelled panel that says what belongs in it, at the exact ratio
   the real photograph will occupy, so the page does not reflow when the
   image lands and nobody has to guess what to send.

   It is deliberately NOT hidden behind the draft gate. The client asked for
   these carousels and is supplying the photographs, so the fastest way to get
   them is to show her precisely which ones are missing. */
.slot{
  aspect-ratio:5 / 3;
  border:1px dashed var(--rule-firm);
  border-radius:var(--r);
  background:var(--w-lemon);
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:var(--s1);
  padding:var(--s4);
}
.slot .slot-k{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--blue);
}
.slot .slot-w{
  font-family:var(--display);
  font-size:var(--t-h3);
  line-height:1.15;
  color:var(--ink);
}
.slot .slot-n{font-size:14px;color:var(--muted);max-width:34ch}

/* Four carousels stacked on Work read as one wall of lemon if every empty
   slot is the same colour. The tone only ever applies to the placeholder, so
   it disappears entirely once the photographs land. */
.carousel[data-tone="blue"] .slot{background:var(--w-blue)}
.carousel[data-tone="blue"] .slot-k{color:var(--blue)}
.carousel[data-tone="green"] .slot{background:var(--w-leaf)}
.carousel[data-tone="green"] .slot-k{color:var(--green)}
.carousel[data-tone="shell"] .slot{background:var(--w-shell)}

/* ===== Work ============================================================== */

.feature-head{
  display:flex;
  justify-content:space-between;
  align-items:flex-start;
  gap:var(--s5);
  flex-wrap:wrap;
}

/* ===== Research ========================================================== */

.spec{border-top:1px solid var(--rule-firm);margin-top:var(--s5)}
.spec div{
  display:grid;
  grid-template-columns:200px 1fr;
  gap:var(--s5);
  padding:var(--s3) 0;
  border-bottom:1px solid var(--rule);
}
.spec dt{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:0.14em;
  text-transform:uppercase;
  color:var(--muted);
  padding-top:5px;
}
.spec dd{max-width:58ch}
.spec .is-key dd{font-weight:600}
@media (max-width:700px){
  .spec div{grid-template-columns:1fr;gap:var(--s1)}
  .spec dt{padding-top:0}
}

.open-q{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--s3);
  margin-top:var(--s5);
}
/* Uniform padding on all four sides of each cell. An earlier pass aligned the
   first cell's text with the section text by removing its left padding, which
   inside a filled panel reads as the text being jammed against the edge. */
.open-q > div{
  border-radius:var(--r);
  padding:var(--s5) var(--s4);
}
.open-q > div:nth-child(1){background:var(--w-blue)}
.open-q > div:nth-child(2){background:var(--w-leaf)}
.open-q h3{margin:var(--s2) 0 var(--s2)}
@media (max-width:800px){
  .open-q{grid-template-columns:1fr}
}

/* the single outbound link to the paper, set as the object it is */
.paper-link{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s4);
  flex-wrap:wrap;
  border:1px solid var(--rule-firm);
  border-radius:var(--r);
  padding:var(--s4);
  margin-top:var(--s5);
  background:var(--white);
  transition:border-color 160ms var(--ease), background-color 160ms var(--ease);
}
a.paper-link:hover{border-color:var(--blue);background:var(--w-shell)}
.paper-link .pl-t{
  font-family:var(--display);
  font-size:clamp(22px,3vw,32px);
  line-height:1.1;
  max-width:26ch;
}
.paper-link .pl-go{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--blue);
  white-space:nowrap;
}

/* ===== Shared: two columns =============================================== */

.two-col{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--s7);
}
@media (max-width:900px){.two-col{grid-template-columns:1fr;gap:var(--s6)}}

/* ===== Compassion: the timeline ========================================== */

.timeline{
  position:relative;
  padding-left:var(--s7);
  margin-top:var(--s6);
}
.timeline::before{
  content:"";
  position:absolute;
  left:0;
  top:6px;
  bottom:6px;
  width:2px;
  background:var(--rule-firm);
}
.tl-ev{position:relative;padding:0 0 var(--s7) 0}
.tl-ev::before{
  content:"";
  position:absolute;
  left:calc(var(--s7) * -1);
  top:14px;
  width:var(--s3);
  height:2px;
  background:var(--blue);
}
.tl-ev .yr{
  font-family:var(--display);
  line-height:0.94;
  letter-spacing:-0.02em;
  display:block;
}
.tl-ev .place{display:block;margin:var(--s1) 0 var(--s2)}
.tl-ev p{max-width:54ch}

/* each year set at a size proportional to its weight */
.w-sm  .yr{font-size:clamp(30px,4vw,46px)}
.w-md  .yr{font-size:clamp(38px,6vw,72px)}
.w-lg  .yr{font-size:clamp(48px,8.5vw,104px)}
.w-xl  .yr{font-size:clamp(60px,13vw,160px)}

@media (max-width:700px){
  .timeline{padding-left:var(--s5)}
  .tl-ev::before{left:calc(var(--s5) * -1);width:var(--s2)}
}

/* ===== Compassion: the Sva block =========================================
   Note 5. The foundation gets one honest block and a link out. It is one part
   of what she does, not the thing her site leads with. */

.sva-card{
  display:grid;
  grid-template-columns:1fr auto;
  gap:var(--s6);
  align-items:start;
  background:var(--white);
  border:1px solid var(--rule-firm);
  border-radius:var(--r);
  padding:var(--s6);
}
.sva-lockup img{width:64px;height:auto;display:block;margin-bottom:10px}
.sva-lockup{
  flex:none;
  border-left:2px solid var(--blue);
  padding-left:var(--s3);
  margin:0;
  max-width:none;
}
.sva-mark{
  display:block;
  font-family:var(--display);
  font-size:clamp(38px,4.6vw,58px);
  line-height:0.94;
  letter-spacing:-0.03em;
  color:var(--ink);
}
.sva-strap{
  display:block;
  margin-top:var(--s2);
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:0.14em;
  text-transform:uppercase;
  color:var(--muted);
  line-height:1.9;
}
@media (max-width:800px){
  .sva-card{grid-template-columns:1fr;gap:var(--s5);padding:var(--s4)}
}

/* ===== About ============================================================= */

.bio-grid{
  display:grid;
  grid-template-columns:400px 1fr;
  gap:var(--s7);
  align-items:start;
}
.bio-grid .portrait{border-radius:var(--r);overflow:hidden;background:var(--sky)}
@media (max-width:900px){
  .bio-grid{grid-template-columns:1fr;gap:var(--s5)}
  .bio-grid .portrait{max-width:340px}
}

/* ===== Shared bits ======================================================= */

.next-page{
  border-top:1px solid var(--rule-firm);
  padding:var(--s7) 0;
}
.next-page a{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  gap:var(--s4);
  flex-wrap:wrap;
}
.next-page .np-title{
  font-family:var(--display);
  font-size:clamp(30px,5vw,58px);
  letter-spacing:-0.02em;
  line-height:1;
}
.next-page a:hover .np-title{color:var(--blue)}

/* full bleed left */
.bleed-left{
  margin-left:calc((100vw - min(100vw, var(--maxw))) / -2 - var(--gutter));
  width:calc(100% + (100vw - min(100vw, var(--maxw))) / 2 + var(--gutter));
  max-width:none;
}
@media (max-width:900px){
  .bleed-left{margin-left:calc(var(--gutter) * -1);width:calc(100% + var(--gutter) * 2)}
}

/* ===== The maiolica band =================================================
   The running border does the structural work on every page: it closes each
   page head, opens each tinted section and opens the footer. One 190px
   artwork, drawn at 56px tall and repeated on x.

   56px is a floor, not a preference. Below about 40px the guilloche stops
   resolving and reads as noise, so the band is either large enough to be a
   design element or it is not used at all. Nothing is ever set on top of it.
   ========================================================================= */

.band{
  height:56px;
  background:url("../assets/tile-band.webp?v=730776d2?v=730776d2") repeat-x;
  background-size:84px 56px;
}

/* Page heads close on the band instead of a hairline. */
.page-head{
  padding-bottom:calc(var(--s7) + 56px);
  border-bottom:0;
}
.page-head::after{
  content:"";
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:56px;
  background:url("../assets/tile-band.webp?v=730776d2?v=730776d2") repeat-x;
  background-size:84px 56px;
}

/* Every interior page head carries a block of the same wall, in the right
   hand columns the grid keeps empty. It echoes the homepage and puts
   something in the space the plates used to occupy, so the pattern is the
   through line of the site rather than a homepage flourish.

   The content column ends at 760px at 1440 and this starts at 893px, so the
   two never meet at any width where it is shown. */
.page-head .tiles{
  --tile:190px;
  top:0;
  bottom:56px;
  left:auto;
  right:0;
  width:min(38%,420px);
}
@media (max-width:900px){
  .page-head .tiles{display:none}
}

main > section.tint:not(.page-head){
  position:relative;
  overflow:hidden;
  padding-top:calc(var(--s8) + 56px);
}
main > section.tint:not(.page-head)::before{
  content:"";
  position:absolute;
  left:0;
  right:0;
  top:0;
  height:56px;
  background:url("../assets/tile-band.webp?v=730776d2?v=730776d2") repeat-x;
  background-size:84px 56px;
}

.site-foot{
  border-top:0;
  padding-top:calc(56px + var(--s6));
  position:relative;
  overflow:hidden;
}
.site-foot::before{
  content:"";
  position:absolute;
  left:0;
  right:0;
  top:0;
  height:56px;
  background:url("../assets/tile-band.webp?v=730776d2?v=730776d2") repeat-x;
  background-size:84px 56px;
}

@media (max-width:640px){
  .band,
  .page-head::after,
  main > section.tint:not(.page-head)::before,
  .site-foot::before{height:44px;background-size:66px 44px;--band-w:66px}
  .page-head{padding-bottom:calc(var(--s6) + 44px)}
  main > section.tint:not(.page-head){padding-top:calc(var(--s7) + 44px)}
  .site-foot{padding-top:calc(44px + var(--s6))}
}

/* ===== Motion pass =======================================================
   Everything here is inside prefers-reduced-motion: no-preference, animates
   only clip-path and transform (both composited, neither triggers layout),
   and is reversible. Nothing loops in the reader's field of view except the
   frieze, which moves one tile width per fifty seconds.
   ========================================================================= */

@media (prefers-reduced-motion: no-preference){

  /* ---- The frieze drifts -------------------------------------------------
     The band is drawn wider than its container and translated by exactly one
     tile width, so the loop point is invisible. Fifty seconds per tile is
     slow enough that it reads as a detail noticed rather than an animation
     played. It is transform, not background-position, so it composites on the
     GPU and never repaints the strip. */
  @keyframes frieze{
    from{transform:translate3d(0,0,0)}
    to{transform:translate3d(calc(var(--band-w) * -1),0,0)}
  }
  .page-head::after,
  main > section.tint:not(.page-head)::before,
  .site-foot::before{
    --band-w:84px;   /* one tile at 56px tall: 944 / 628 * 56 */
    left:calc(var(--band-w) * -2);
    right:calc(var(--band-w) * -2);
    width:auto;
    animation:frieze 50s linear infinite;
  }
  

  /* ---- The nav draws its underline -------------------------------------
     Only the nav. A body link keeps its persistent underline, because that
     line is the only affordance it has that does not depend on colour, and
     hiding it until hover would fail anyone who cannot hover at all. */
  .mast nav a{position:relative}
  .mast nav a::after{
    content:"";
    position:absolute;
    left:0; right:0; bottom:0;
    height:1px;
    background:currentColor;
    transform:scaleX(0);
    transform-origin:left;
    transition:transform 240ms var(--ease);
  }
  .mast nav a:hover::after,
  .mast nav a:focus-visible::after{transform:scaleX(1)}
  .mast nav a[aria-current="page"]::after{transform:scaleX(1)}

  /* ---- Things you can click lift ---------------------------------------- */
  .car-slide,
  .paper-link,
  .sva-card{transition:transform 220ms var(--ease), border-color 220ms var(--ease)}
  .car-slide:hover,
  a.paper-link:hover{transform:translateY(-3px)}

  .what-index a{transition:background-color 200ms var(--ease),
                           border-color 200ms var(--ease),
                           padding-left 200ms var(--ease)}
  .what-index a:hover,
  .what-index a:focus-visible{padding-left:var(--s4)}

  .hero-tags li{transition:transform 200ms var(--ease)}
  .hero-tags li:hover{transform:translateY(-2px)}

  /* ---- The timeline draws itself ---------------------------------------
     The rule down the left of Compassion scales from the top as the section
     arrives, so the years are joined by a line being drawn rather than one
     that was always there. reveal.js already puts .is-in on .tl-ev, so the
     spine keys off the first event rather than needing its own observer. */
  .timeline::before{
    transform:scaleY(0);
    transform-origin:top;
    transition:transform 1100ms var(--ease);
  }
  .timeline:has(.tl-ev.is-in)::before{transform:scaleY(1)}

  .tl-ev::before{
    transform:scaleX(0);
    transform-origin:left;
    transition:transform 420ms var(--ease) 240ms;
  }
  .tl-ev.is-in::before{transform:scaleX(1)}
}

/* Under reduce every one of the above is simply its finished state. */
@media (prefers-reduced-motion: reduce){
  .timeline::before,.tl-ev::before,.tiles i{transform:none !important;clip-path:none !important}
  .mast nav a[aria-current="page"]::after{transform:none}
}

/* ===== Image grids and the lightbox =======================================
   One grid component for every set of images on the site: the certificates on
   About, and the photograph grids. It takes one image or five without being
   told which, because the number varies per section and will keep changing.
   ========================================================================= */

.gallery{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(400px,100%),1fr));
  gap:var(--s3);
  margin-top:var(--s5);
}
/* A single image should not stretch to the full column, and two should not
   become three on a wide screen. */
.gallery[data-n="1"]{grid-template-columns:minmax(0,520px)}
.gallery[data-n="2"]{grid-template-columns:repeat(2,minmax(0,1fr));max-width:860px}

.gallery figure{margin:0}

/* The trigger is a real button, so the grid is keyboard operable whether or
   not zoom.js loads. */
.zoom-open{
  display:block;
  width:100%;
  padding:0;
  border:1px solid var(--rule-firm);
  border-radius:var(--r);
  background:var(--white);
  overflow:hidden;
  cursor:zoom-in;
  transition:border-color 180ms var(--ease), transform 180ms var(--ease);
}
/* Certificates and photographs arrive at every aspect ratio there is, so the
   thumbnail is a fixed box and the image is contained inside it. Without this
   a portrait certificate makes its grid row twice the height of its
   neighbours and the row reads as broken rather than as varied. */
.zoom-open img{
  width:100%;
  aspect-ratio:4 / 3;
  object-fit:contain;
  display:block;
  padding:10px;
  background:var(--white);
}
.zoom-open:hover{border-color:var(--blue)}
@media (prefers-reduced-motion: no-preference){
  .zoom-open:hover{transform:translateY(-2px)}
}
.gallery figcaption{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--muted);
  padding-top:var(--s1);
  line-height:1.6;
}

/* ---- the dialog ---- */
.zoomdlg{
  border:0;
  padding:0;
  max-width:100vw;
  max-height:100vh;
  width:100%;
  height:100%;
  background:transparent;
  overflow:hidden;
}
.zoomdlg::backdrop{background:rgba(30,42,56,.88)}
.zoomdlg figure{
  margin:0;
  height:100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:var(--s2);
  padding:var(--s6) var(--s4) 84px;
}
.zoomdlg img{
  max-width:min(1100px,92vw);
  max-height:calc(100vh - 190px);
  width:auto;
  height:auto;
  object-fit:contain;
  border-radius:var(--r);
  background:var(--white);
}
.zoomdlg figcaption{
  color:#EAF1F8;
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  text-align:center;
  max-width:60ch;
}
.zoom-bar{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:var(--s2);
  padding:var(--s3);
}
.zoom-count{
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  color:#EAF1F8;
  min-width:5ch;
  text-align:center;
}
.zoom-btn{
  width:40px;height:40px;
  border:1px solid rgba(234,241,248,.45);
  border-radius:var(--r);
  color:#EAF1F8;
  font-size:22px;
  line-height:1;
  display:grid;
  place-items:center;
  transition:background-color 160ms var(--ease), border-color 160ms var(--ease);
}
.zoom-btn:hover{background:rgba(234,241,248,.16);border-color:#EAF1F8}
.zoom-btn:focus-visible{outline:2px solid #EAF1F8;outline-offset:2px}
.zoom-btn i{
  width:9px;height:9px;
  border-top:1.5px solid currentColor;
  border-right:1.5px solid currentColor;
  display:block;
}
.zoom-btn.prev i{transform:rotate(-135deg);margin-left:3px}
.zoom-btn.next i{transform:rotate(45deg);margin-right:3px}

@media (prefers-reduced-motion: no-preference){
  .zoomdlg[open]{animation:zin 200ms var(--ease)}
  @keyframes zin{from{opacity:0}to{opacity:1}}
  .zoomdlg.no-anim[open]{animation:none}
}
@media (max-width:640px){
  .zoomdlg figure{padding:var(--s4) var(--s2) 76px}
  .zoomdlg img{max-height:calc(100vh - 170px)}
}

/* ===== Subject boxes ======================================================
   A section like "Away from a desk" is a list of subjects, and a subject may
   have no photographs yet, or one, or four. So the box is the constant and
   its contents vary: an empty box states what is still wanted, a filled box
   holds a small grid of that subject's photographs.

   Grouping this way rather than as one flat run of images keeps the three
   Kenya frames together and separate from the two underwater ones, which is
   how someone reads them anyway.
   ========================================================================= */

.subjects{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(340px,100%),1fr));
  gap:var(--s3);
  margin-top:var(--s5);
}

/* A subject with more than one photograph takes two columns where there is
   room, so the filled boxes lead and the empty ones fall in behind. */
@media (min-width:1000px){
  .subject-wide{grid-column:span 2}
}

.subject{
  margin:0;
  border:1px solid var(--rule-firm);
  border-radius:var(--r);
  background:var(--white);
  padding:10px;
  display:flex;
  flex-direction:column;
}

/* An empty subject keeps the brief that was there before: what the photograph
   should show, so the gap explains itself rather than just being a gap. */
.subject.is-empty{
  border-style:dashed;
  background:var(--w-lemon);
  justify-content:center;
  gap:var(--s1);
  padding:var(--s4);
  min-height:200px;
}
.subject.is-empty .slot-k{
  font-family:var(--mono);font-size:var(--t-meta);letter-spacing:.14em;
  text-transform:uppercase;color:var(--blue);
}
.subject.is-empty .slot-w{
  font-family:var(--display);font-size:var(--t-h3);line-height:1.15;color:var(--ink);
}
.subject.is-empty .slot-n{font-size:14px;color:var(--muted)}

/* ---- the grid inside a filled box ---- */
.minigrid{display:grid;gap:6px}
.minigrid[data-n="1"]{grid-template-columns:1fr}
.minigrid[data-n="2"]{grid-template-columns:repeat(2,1fr)}
.minigrid[data-n="3"]{grid-template-columns:repeat(3,1fr)}
.minigrid[data-n="4"]{grid-template-columns:repeat(2,1fr)}
.minigrid[data-n="5"],
.minigrid[data-n="6"]{grid-template-columns:repeat(3,1fr)}

/* A single photograph gets the full box and a taller crop, because at one
   across there is room for it and it is the only thing in there. */
.minigrid[data-n="1"] .zoom-open img{aspect-ratio:4 / 3}
.minigrid .zoom-open{border:0;border-radius:2px}
/* Two photographs read better at 4:3 than square: a square crop of a wide
   frame throws away most of what is in it. Three or more go square, where
   the cells are small enough that consistency matters more. */
.minigrid[data-n="2"] .zoom-open img{aspect-ratio:4 / 3}
.minigrid .zoom-open img{
  aspect-ratio:1 / 1;
  object-fit:cover;
  padding:0;
  background:var(--sand);
}
.minigrid .zoom-open:hover{outline:2px solid var(--blue);outline-offset:-2px}

.subject figcaption{
  font-size:14px;
  color:var(--ink);
  padding:var(--s2) 4px 4px;
  line-height:1.5;
}
.subject figcaption .subj-k{
  display:block;
  font-family:var(--mono);
  font-size:var(--t-meta);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--muted);
  margin-bottom:4px;
}
