/* ============================================================
   RUMMEL SECTION PAGE, BLOCK LIBRARY.

   Loads after rummel-page.css. Every run below is one deletable block:
   markup between two fences in the page, one labeled run of CSS here
   including its own breakpoints, and one guarded IIFE in rummel-page.js.

   Delete a block's markup and nothing else needs touching. The rail
   builds itself from data-rail, so its entry disappears and the rest
   renumber on their own.

   Lifted out of academics-c7-hybrid.html, where it was inline only
   because seven concepts were sharing a stylesheet that could not be
   disturbed. Every comment came with it. They are the record of why
   each number is what it is, and without them these get re-broken.

   Renamed on the way out, while it was still free:
     .progs      -> .pathways    (too close to .prog to read at a glance)
     .cards      -> .photo-trio  (collided with template-landing.html)
     .tk         -> .pair
     .tracks     -> .bandsec     (it is just the dark band section)
     .dept-grid  -> .grid9       (the block is generic, "dept" was not)
     .req-wrap   -> .ta-wrap     (table plus aside)
   ============================================================ */


/* ============================================================
   HERO  required
   The whole photograph, nothing cropped away.

   EVERY selector here is scoped to header.open on purpose. a bare .open
   used to match any state class named open, which handed a screen tall
   flex hero to the curriculum accordion and broke it.

   the frame is tied to the WIDTH, not to the viewport height. that is the
   whole trick: the crop then works out identically at every desktop size,
   so the faces always land on the same percentage of the frame and the
   copy can be held at a fixed share of it without ever riding up onto one.
   tying it to vh was what let the headline cross a face, because the copy
   is a fixed pixel height and a short window shrank the frame underneath it.
   ============================================================ */

/* the field behind the header lands on real white early, so even a subpixel edge at a
   fractional boundary blends white into white rather than tint.
   The stops are all OPAQUE on purpose. It used to pass through rgba(255,255,255,.5),
   which is not a pale blue at all: half transparent white over the band composites to a
   grey, and the jump from that grey to #fff two stops later drew a visible hairline
   across the page exactly where the hero meets the white section. Kelly caught it and
   the rule is the house one, transitions are smooth like the sky, never a seam. Mixing
   toward white in opaque steps, with an eased set of stops rather than three, fades it
   out with nothing to catch the eye. (2026-08-15) */
.hero-field{background:linear-gradient(180deg,
  var(--ink-band) 0%,
  var(--ink-band) 78%,
  color-mix(in srgb, var(--ink-band) 78%, #fff) 86%,
  color-mix(in srgb, var(--ink-band) 48%, #fff) 91%,
  color-mix(in srgb, var(--ink-band) 22%, #fff) 94.5%,
  color-mix(in srgb, var(--ink-band) 8%,  #fff) 96.5%,
  #fff 98.5%,#fff 100%);}

/* the min-height needs the same viewport ceiling as the copy offset, and for a less
   obvious reason. the wrap is bottom aligned, so whenever min-height exceeds what the
   content actually needs, the surplus is inserted ABOVE the copy and pushes it back
   down the screen. capping the offset alone moved the headline up 160px and the hero
   handed 109px of that straight back. */
header.open{position:relative;display:flex;align-items:flex-end;overflow:hidden;
  width:100%;min-height:min(clamp(560px,58vw,1000px), 96vh);
  background:var(--ink-band);}
/* rests on the complete frame and breathes from there. it never loads cropped.
   the page sets its own image and focal point inline, because a centre crop is
   right for one photograph and wrong for the next. */
header.open .ph{position:absolute;inset:0;
  background-size:cover;background-position:center;}
/* ---- the hero rotation, optional ----
   Extra frames live INSIDE .ph rather than beside it, deliberately. .ph is the
   positioned box in both layouts (absolute on desktop, relative with a 3:2 ratio
   on phones), so children pinned to its inset fill exactly the photo area and
   never the copy area, and .ph::after (the mobile dissolve) is generated last and
   so still paints over the top of them, which keeps that seam soft.

   The base .ph keeps its own inline photograph as frame one. With no JavaScript
   the hero is simply that photograph, which is the right floor.

   ---- Ken Burns, per photo, not shared ----
   Used to be one heroDrift scale(1)->scale(1.04) running on .ph continuously,
   inherited by whichever layer sat on top. That meant every photo drifted
   whatever phase a single shared 20s clock happened to be in when it faded up,
   never its own clean zoom, and Kelly found the result barely visible and asked
   for real Ken Burns and more of a zoom (2026-08-18). Each photo now gets its
   OWN scale(1)->scale(1.15) run, restarted from zero the moment it becomes the
   visible frame (rummel-page.js toggles the .kb class off and back on with a
   reflow in between, which is what makes a CSS animation restart on the same
   element instead of picking up where the last one left off). ease-out and
   forwards so it settles into the zoomed frame rather than snapping back. */
header.open .ph .ph-layer{position:absolute;inset:0;
  background-size:cover;background-position:center;
  opacity:0;transition:opacity 1.25s ease;}
header.open .ph .ph-layer.on{opacity:1;}
header.open .ph.kb,
header.open .ph .ph-layer.kb{animation:heroKen 14s ease-out forwards;}
@media(prefers-reduced-motion:reduce){
  header.open .ph .ph-layer{transition:none;}
  header.open .ph.kb,
  header.open .ph .ph-layer.kb{animation:none;}
}
@keyframes heroKen{
  from{transform:scale(1);}
  to{transform:scale(1.15);}
}
/* the wash rises from the bottom rather than coming in from the left. it darkens
   only the band the copy sits in, leaves every face in clear air, and still lands
   on white so the join to the section below stays seamless.

   two layers, on purpose. the old version ran blue and white through ONE gradient,
   so the stops had to interpolate from opaque blue straight to translucent white.
   they crossed at rgb(230,236,238), a cool grey, and the ramp did not reach opaque
   white until the final row. that grey step against the pure white of #below was
   the line. split apart, the dissolve is its own layer and reaches real #fff at
   97.5%, so the bottom 2.5% of the panel is exactly the white underneath it and
   there is nothing left to see a seam in. the white is held near zero until 84%
   because the subline runs to 86.7% and has to stay on dark ground. */
header.open .wash{position:absolute;inset:0;
  background:
    linear-gradient(180deg,
      rgba(255,255,255,0)   84%,
      rgba(255,255,255,.16) 89%,
      rgba(255,255,255,.58) 93%,
      rgba(255,255,255,.90) 96%,
      #fff 97.5%,
      #fff 100%),
    linear-gradient(180deg,
      rgb(var(--ink-band-rgb) / .30) 0%,
      rgb(var(--ink-band-rgb) / .08) 16%,
      rgb(var(--ink-band-rgb) / 0)   34%,
      rgb(var(--ink-band-rgb) / 0)   52%,
      rgb(var(--ink-band-rgb) / .30) 60%,
      rgb(var(--ink-band-rgb) / .78) 71%,
      rgb(var(--ink-band-rgb) / .90) 86%,
      rgb(var(--ink-band-rgb) / .90) 100%);}
header.open .glow{position:absolute;left:-12%;top:-18%;width:62vw;height:62vw;pointer-events:none;
  background:radial-gradient(circle,rgba(117,178,221,.20),transparent 62%);}
/* the dot field, at its original weight. white at 16% under .45 opacity, so it
   lands near 7% and you feel it rather than see it. it was never too faint, it was
   switched off below 1081; that is fixed in the media query instead. do not
   strengthen this, the point is that it stays under the photograph. (Kelly) */
header.open .dots{position:absolute;inset:0;background:var(--dots);
  background-size:13px 13px;opacity:.45;pointer-events:none;}
/* this padding is what holds the copy off the faces. 37.5vw inside a frame that is
   58vw tall lands the headline near 62% of the frame, clear of a seated subject's
   chin, and it scales with the frame instead of fighting it. measured clearance is
   33px at 1100 and 76px at 1440. the narrow end sets the number: the copy is close
   to a fixed pixel height, so it eats the largest share of the smallest frame.

   bounded by BOTH axes. 37.5vw alone is what held the copy off the faces, but it
   is width driven, so a wide window pushed it down regardless of how short that
   window was: at 1920x800 the headline began at y=805 and its second line ended 111px
   below the fold. that is the shape of a real laptop in Chrome, wide and short once
   the browser takes its own chrome off the top.

   the vh term is the ceiling that keeps the whole headline on screen. 240px is the
   nav plus two lines of display type plus a little air, so the min() only ever bites
   on windows that are too short for the width based value, and normal windows are
   completely unchanged. */
header.open .wrap{position:relative;z-index:4;width:100%;
  padding-top:min(37.5vw, calc(100vh - 240px));padding-bottom:2rem;}
/* capped locally. --t-display is the shared page title size and is not touched here.
   24ch sets a headline in two lines, which keeps the block compact. the clearance
   itself is the wrap's padding-top, not this. */
header.open .display{color:#fff;max-width:24ch;font-size:clamp(2.3rem,3.7vw,3.5rem);
  text-shadow:0 6px 40px rgba(6,23,48,.55);}
header.open .display em{color:var(--rb);}
header.open .sub{font-size:var(--t-lede);line-height:1.6;color:rgba(255,255,255,.95);max-width:54ch;margin-top:1.1rem;}
header.open .acts{display:flex;gap:1rem;margin-top:1.25rem;flex-wrap:wrap;}

/* ---- the stacked header, 1080 and down ----
   the photo box is 3:2, so its height falls with the window, but the subline and
   the button are fixed sizes and do not. below about 1080 the copy therefore eats
   more of a shorter frame than the face-free band can hold, and the headline lands
   back on a subject's face. rather than tune the type per breakpoint, the copy
   comes off the photograph entirely and sits underneath it on the band.
   same breakpoint as the rail strip, so the page changes character in one place. */
@media(max-width:1080px){
  header.open{height:auto;min-height:0;display:block;}
  header.open .ph{position:relative;inset:auto;width:100%;aspect-ratio:3/2;}
  /* Ken Burns stays a desktop thing, same as the drift it replaced: off here. */
  header.open .ph.kb,
  header.open .ph .ph-layer.kb{animation:none;}
  /* the photo dissolves into the band rather than stopping on a hard edge */
  header.open .ph::after{content:"";position:absolute;left:0;right:0;bottom:0;height:34%;
    background:linear-gradient(180deg,rgb(var(--ink-band-rgb) / 0),var(--ink-band));}
  /* same rule on the stacked layout: reach real white before the edge, not at it. */
  header.open .wash{top:auto;bottom:0;height:120px;
    background:linear-gradient(180deg,
      rgb(var(--ink-band-rgb) / 0) 0%,rgba(255,255,255,.55) 62%,rgba(255,255,255,.92) 84%,
      #fff 93%,#fff 100%);}
  /* the glow goes, the dots stay. they were switched off together, which is why
     the pattern vanished on anything under 1081. */
  header.open .glow{display:none;}
  header.open .wrap{padding-top:2.4rem;padding-bottom:7rem;}
  /* balance only down here, where the headline runs to three lines and the wrap
     otherwise strands a one or two letter word on a line of its own. On desktop
     it is left off on purpose: at two lines the natural break lands exactly on
     the colour change, and balancing pulls a word back across it. */
  header.open .display{max-width:20ch;text-wrap:balance;}
}
@media(max-width:760px){
  header.open .wrap{padding-top:2rem;padding-bottom:6rem;}
  header.open .acts .btn{width:100%;}
}


/* ============================================================
   RAIL  required
   Off screen until the header is behind you.

   a diagonal wash, not a vertical band. (Kelly: the old one was too harsh
   and did not feel expensive.)

   the old version banded because the gradient was doing structural work. it
   held a light band for exactly 88px so a navy title would sit on true
   #75B2DD, then dropped to --rb-deep by 146px because the first white link
   starts at 154px and white needs --rb-deep or darker. three stops inside
   58px is what you saw as the hard edge, and pixel stops cannot survive a
   turn to 45 degrees anyway, since a diagonal gradient's length depends on
   width and height together.

   so the gradient stops carrying contrast. the base is deep everywhere,
   which frees the wash to be purely about depth and lets every word go white.
   ============================================================ */
.rail{position:fixed;left:0;top:0;bottom:0;width:var(--rail-w);z-index:120;
  background:
    /* ONE gradient down the diagonal Kelly drew, not a corner glow over a base. she
       marked a line from the top left falling to roughly three fifths of the way down
       and slightly right, which is about 16 degrees off vertical, hence 164deg. a
       single ramp is also what makes it subtle: layering a radial over a base always
       leaves a faint boundary where the two disagree, and there is nothing to
       disagree here.

       the first stop is TRUE --rb, the brand blue itself, not a tint of it. Kelly
       asked for that three times and I kept diluting it to protect a contrast number.
       Her call, made repeatedly, so it stands.

       what that costs, stated plainly rather than quietly designed around: white on
       the bright top measures about 2.8:1 for the title and the top two nav labels
       land near 3.5:1, where small text wants 4.5. the shadows below are built to
       keep both crisp.

       --rail-end rather than --ink-band directly, because a band outside the blue
       family cannot be the landing point: Columbia Blue falling to red interpolates
       through purple. Athletics overrides it to navy. */
    linear-gradient(164deg,
      var(--rb)       0%,
      var(--rb-mid)   10%,
      var(--rb-deep)  20%,
      var(--rb-steel) 48%,
      var(--rail-end) 100%);
  color:#fff;display:flex;flex-direction:column;padding:2.2rem 1.9rem;overflow:hidden;
  transform:translateX(-100%);transition:transform .5s cubic-bezier(.16,1,.3,1);
  box-shadow:18px 0 60px -30px rgba(0,0,0,.75);}
.rail.in{transform:none;}
.rail::after{content:"";position:absolute;inset:0;background:var(--dots);
  background-size:13px 13px;opacity:.4;pointer-events:none;}
.rail > *{position:relative;z-index:2;}
/* the R sits alone at the foot. Apply was tried here and cut: application season
   is one time of year and it did not earn the permanent slot. (Kelly) */
.rail .brand{display:flex;align-items:center;gap:.8rem;margin-top:auto;min-height:44px;}
.rail .brand img{height:42px;flex:none;}
/* white, because the base under it is deep enough to carry it. the shadow is the
   depth device Kelly asked for: a solid offset in the darker blue, the same word
   sitting just behind itself. one shadow, no blur. it had three layers including an
   18px soft one, and a wide halo behind type always reads cheap: that is the glow
   effect, not depth. a single solid offset holds the letterform edges against the
   bright corner better than a blur ever did.

   tracked to .032em rather than the headings' .01em. this sits at 30px against their
   48px, and the same em value at a smaller size reads tighter, so matching the number
   would have looked like nothing changed. */
.rail .rtitle{font-family:var(--slab);font-size:1.9rem;line-height:1.05;
  text-transform:uppercase;color:#fff;margin-bottom:1.5rem;letter-spacing:.032em;
  text-shadow:0 1.5px 0 rgb(var(--ink-band-rgb) / .55);}
.rail .prog-line{height:3px;background:transparent;margin-bottom:1.6rem;}
.rail .prog-line i{display:block;height:100%;width:0;background:var(--accent-band);transition:width .25s ease;}
.rail nav{list-style:none;display:flex;flex-direction:column;gap:.15rem;margin-top:1.4rem;}
.rail a.spy{display:flex;gap:.85rem;align-items:baseline;padding:.72rem .6rem;
  min-height:44px;border-left:3px solid transparent;transition:all .18s ease;}
/* the labels carry a whisper of shadow because the fall keeps the top of the rail
   genuinely bright, and the first two sit around 3.5:1 rather than the 4.5 small text
   wants. a tight dark offset does not change the measured ratio, but it holds the
   letterforms' edges so they stay crisp instead of dissolving into the blue. */
.rail a.spy .num{font-family:var(--slab);font-size:.84rem;letter-spacing:.1em;color:#fff;
  text-shadow:0 1px 2px rgba(11,34,64,.55);}
.rail a.spy .lab{font-family:var(--sans);font-weight:500;font-size:1rem;
  letter-spacing:.06em;text-transform:uppercase;color:#fff;
  text-shadow:0 1px 2px rgba(11,34,64,.55);}
.rail a.spy:hover{background:rgba(255,255,255,.1);}
.rail a.spy.on{border-left-color:var(--accent-band);background:rgba(255,255,255,.14);}
.rail a.spy.on .num,.rail a.spy.on .lab{color:#fff;}

/* everything below the header slides over to make room.
   one continuous white behind the whole run. the white sections each used to
   paint their own box on a dark body, so every boundary between them was a seam
   the browser antialiased into a faint grey hairline at fractional device pixel
   positions. with white already behind, the blend is white on white and the
   seams disappear. the dark sections still paint over this. */
#below{background:#fff;transition:padding-left .5s cubic-bezier(.16,1,.3,1);}
body.railed #below{padding-left:var(--rail-w);}

/* the footer moves over too, and it has to, because it sits OUTSIDE #below and
   so never inherited that shift. The rail is fixed, so it was simply covering
   the footer's left 290px: the motto is centred on the full width, which put it
   off centre in the part you can actually see, and the red to blue hairline
   along the top started underneath the rail.

   margin-left rather than padding-left on purpose. It narrows the footer's own
   box, so the centred motto, the watermark, the breathing glow and both
   decorative layers all recentre themselves inside the visible width. Padding
   would have moved the text and left every absolutely positioned layer behind.
   Same curve and duration as #below, so the two travel as one. */
.motto-footer{transition:margin-left .5s cubic-bezier(.16,1,.3,1);}
body.railed .motto-footer{margin-left:var(--rail-w);}
/* the facts rail sits directly under the motto band and has to travel with it,
   or the two go out of line the moment the section rail slides in. */
.mf-facts{transition:margin-left .5s cubic-bezier(.16,1,.3,1);}
body.railed .mf-facts{margin-left:var(--rail-w);}

/* 1080 and down: the rail strip is retired and IN THIS SECTION takes over.
   The row's own styles live in rummel-page.css, because Academics is on the
   frozen academics-concepts sheet and never loads this file. But the two
   rules that UNDO desktop layout have to live HERE, next to the desktop rules
   they cancel, or source order lets the desktop rule win: rummel-page.css
   loads first, so its padding-left:0 was being beaten by the padding-left:
   var(--rail-w) below and the row rendered 103px wide at left 290. */
@media(max-width:1080px){
  body.railed #below{padding-left:0;}
  body.railed .motto-footer{margin-left:0;}
  body.railed .mf-facts{margin-left:0;}
}

/* ============================================================
   DRAWER  optional, but this is the signature move
   Turns a card that would have been a dead end into the best thing
   on the page.

   the four Academics program cards used to link to four pages that do not
   exist, so every one of them was a 404. clicking one opens this instead.
   it sits beside the rail rather than over it, so you never lose your place.

   the spine is the identity: Rummel red, with the name in SquareSlab turned
   on its side, so it reads as a tab you have pulled out rather than a modal
   that has landed on top of you.

   the spine stays red on every page, including a page whose band is red. the
   drawer is a white panel, so red on it is red on white either way.
   ============================================================ */
.pd-scrim{position:fixed;inset:0;z-index:140;background:rgba(6,23,48,.55);
  backdrop-filter:blur(2px);opacity:0;pointer-events:none;
  transition:opacity .5s cubic-bezier(.16,1,.3,1);}
body.pd-open .pd-scrim{opacity:1;pointer-events:auto;}

/* the offset follows the rail's ACTUAL state rather than assuming it is there.
   the rail is only in once you are past the hero, and pinning the drawer at the
   rail width unconditionally leaves a 290px window onto the page whenever it is
   not, which looks like a mistake rather than a design. flush against whatever is
   beside it. the same easing and duration the rail uses, so the page moves as one. */
.pd{position:fixed;top:0;bottom:0;left:0;right:0;z-index:150;display:flex;
  background:#fff;transform:translateX(-102%);visibility:hidden;
  transition:transform .5s cubic-bezier(.16,1,.3,1),visibility 0s linear .5s;
  box-shadow:26px 0 80px -40px rgba(6,23,48,.8);}
body.pd-open .pd{transform:none;visibility:visible;transition-delay:0s;}
/* the normal case: the cards live below the hero, so the rail is already in by the
   time one can be clicked, and the drawer sits beside it exactly as intended. */
body.railed .pd{left:var(--rail-w);}

/* top aligned, not centred, and the padding is the rail's own 2.2rem so the spine
   title starts on exactly the same line as the rail title beside it. */
.pd-spine{flex:none;width:68px;background:var(--red);position:relative;
  display:flex;align-items:flex-start;justify-content:center;overflow:hidden;
  padding:2.2rem 0 1.5rem;}
/* vertical-rl with no flip, so it reads downward and the start of the word sits at
   the top. the size is set by the script, not here: turned on its side the word is
   bounded by the WINDOW HEIGHT, so a short laptop would run it off the end.

   two notes on the shadow, because it is not simply copied from the rail title.
   the offset is on X, not Y: text-shadow offsets stay physical in a vertical writing
   mode rather than rotating with the glyphs, so the Y offset that sits under each
   letter horizontally would sit beside them here. and the shade is navy, not a
   darker red, because deepening Rummel red is how you end up in Brother Martin's
   crimson. on a red field the offset has to be a neutral. */
.pd-spine span{writing-mode:vertical-rl;
  font-family:var(--slab);text-transform:uppercase;letter-spacing:.06em;
  color:#fff;white-space:nowrap;font-size:2rem;line-height:1;
  text-shadow:-1.5px 0 0 rgba(11,34,64,.45);}

/* stable, NOT both-edges. both-edges evens the gutters by reserving the bar's width
   on the left as well, which pushes the photograph off the red spine and opens a
   white gap between them. the photo has to run flush into the spine, so the reserve
   stays on the right only and the text compensates instead: --pd-sb is the measured
   scrollbar width, and .pd-inner takes it back off its right padding. flush image,
   even text gutters, no magic number. */
.pd-body{flex:1 1 auto;min-width:0;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-gutter:stable;}
/* the photo rotation. one frame per track, so the header actually shows what the
   panel is describing rather than repeating one image.

   the crossfade goes THROUGH the dot pattern instead of straight from photo to
   photo. the outgoing frame drifts up in scale as it leaves, the incoming one
   settles down into place, and the dot field swells in the middle of the handover,
   so the picture appears to break into the texture and reform out of it. it is the
   same dot the hero and the rail already use, which is why it reads as belonging
   here rather than as an effect.

   roughly three quarters of the opening view is photograph, which is what Kelly
   asked for. 72vh rather than a literal 75 because at exactly 75 the heading landed
   below the fold, which defeats the one thing it is there to do. the cap wins over
   the ratio and the frames crop, which is what cover is for.

   the base is white, not the band color. any pixel the frames do not cover,
   including the hairline a fractional height leaves at the foot, then blends white
   into the white below it instead of showing a dark edge. */
.pd-pic{display:block;width:100%;aspect-ratio:1100/733;max-height:72vh;position:relative;
  overflow:hidden;background:#fff;}
/* the stack is spelled out rather than left to DOM order, so the incoming frame
   always fades in ON TOP of the outgoing one. left implicit, the wrap from the last
   frame back to the first reverses that and the handover looks different.

   two elements, not one: the slide owns the crossfade and the frame inside it owns
   the Ken Burns. on a single element the transition and the animation would both be
   driving transform, and the animation always wins, so the drift would kill the
   settle. split, they compose. */
.pd-slide{position:absolute;inset:0;z-index:1;opacity:0;
  transition:opacity 1.1s cubic-bezier(.4,0,.2,1);will-change:opacity;}
.pd-slide.on{opacity:1;z-index:2;}
/* every frame drifts, all the time, not just the visible one. scoping the animation
   to .on was the bounce: the moment a slide lost the class its animation died and the
   transform snapped from wherever the drift had reached back to 1.03, and because the
   slide was still half opaque on its way out you SAW it jump backwards. left running,
   there is nothing to snap. the cost is two idle animations on compositor-only
   properties, which is nothing. */
.pd-frame{position:absolute;inset:0;background-size:cover;background-repeat:no-repeat;
  transform:scale(1.03);will-change:transform;
  animation:pdDrift 26s ease-in-out infinite alternate;}
@keyframes pdDrift{
  from{transform:scale(1.03) translate3d(0,0,0);}
  to{transform:scale(1.085) translate3d(-1.1%,-0.9%,0);}
}
/* the dot layer sits above the frames and below the fade to white. it rests at a
   whisper and only swells while a handover is running. opened up from 9px to 16px so
   the field breathes instead of reading as noise, and the dot itself pulled from 55%
   white down to 32%. the old one was tight and bright, which is busy rather than
   sleek. */
.pd-grain{position:absolute;inset:0;z-index:3;pointer-events:none;
  background:radial-gradient(rgba(255,255,255,.32) 1px, transparent 1.7px);
  background-size:16px 16px;opacity:.10;
  transition:opacity .6s ease;}
.pd-pic.swapping .pd-grain{opacity:.40;}
/* the photo dissolves into the panel rather than stopping on a hard edge. it reaches
   real #fff at 86% and HOLDS it, exactly the fix the hero needed: a ramp that only
   arrives at white on its final row leaves the last pixels a shade off, and against
   the true white underneath that shows as a thin line. the last 14% is now the same
   white as the panel, so there is nothing to see a join in. */
.pd-pic::after{content:"";position:absolute;left:0;right:0;bottom:0;height:44%;z-index:4;
  background:linear-gradient(180deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,.55) 52%,
    rgba(255,255,255,.92) 74%,
    #fff 86%,
    #fff 100%);}
.pd-head{position:relative;}
/* the top padding is tighter than the sides on purpose. with the photograph taking
   most of the view, the heading and description have only the last quarter to land
   in, and 2.6rem of it was going to empty space above the title. */
.pd-inner{padding:1.4rem calc(3.4rem - var(--pd-sb,0px)) 4rem 3.4rem;}
.pd-inner h2{font-family:var(--slab);font-weight:700;font-size:clamp(1.9rem,2.6vw,2.6rem);
  line-height:1.05;text-transform:uppercase;color:var(--navy);margin-bottom:1rem;}
.pd-inner h2 em{font-style:normal;color:var(--rb-deep);}
/* capping the whole block was the wrong lever: it fixed the long lines and put 440px
   of dead white straight back. so the block stays full width and the CONTENT uses it.
   only the lede is capped, at a readable measure, and the tracks run as columns
   underneath, which fills the panel honestly rather than stretching one line of prose
   across it.

   text-wrap:pretty is doing real work here, not decoration. at this measure the lede
   was dropping a 44px fragment onto a line of its own, and the copy length changes
   per entry, so a hand tuned width would only fix the one you can see. */
.pd-inner .pd-lede{font-size:var(--t-lede);line-height:1.66;color:var(--ink-soft);
  max-width:86ch;text-wrap:pretty;margin-bottom:2.4rem;}
.pd-track p, .pd-note{text-wrap:pretty;}
#pdTracks{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
  gap:0 3.4rem;}
/* every track keeps its top rule. side by side in the grid the rules line up across
   the panel and read as one edge, which is the point. */
.pd-track{padding:1.4rem 0;border-top:1px solid var(--line);}
.pd-track h3{font-family:var(--slab);font-size:1.06rem;text-transform:uppercase;
  letter-spacing:.03em;color:var(--navy);margin-bottom:.4rem;}
.pd-track p{font-size:1.06rem;line-height:1.62;color:var(--ink-soft);}
/* A track that carries its own destination gets its own button, set smaller than a
   page level CTA so a column of them reads as a list of choices rather than seven
   competing headlines. Used by the memorial drawer, where each named fund is a
   separate Neon campaign and a shared button would misdirect the gift. */
.pd-track-go{margin-top:.9rem;padding:.72rem 1.3rem;min-height:44px;font-size:.8rem;}
/* the note reads as a sentence, so it is set as one. it used to be appended onto the
   coordinator line and swept up in that line's uppercase, which turned an ordinary
   fact into shouting. */
.pd-note{margin-top:2.2rem;padding-top:1.4rem;border-top:1px solid var(--line);
  font-size:1rem;line-height:1.62;color:var(--ink-soft);}
/* ---- THE CONTACT BLOCK ----
   The person you would actually call, with their face. Portrait from the faculty
   roster's own photo set, so there is one source for a staff photograph on this site
   and it cannot drift. The email and phone are SHOWN, not hidden behind a verb: a
   parent who wants to write the number down should be able to read it. Both are still
   real mailto: and tel: links, so they are tappable as well as legible. */
/* One shape at every width: portrait on the left, role and name to the right of it,
   email and phone on their own full width row underneath. It began as the phone
   layout and moved up to the base rule on Kelly's call (2026-08-15), because the
   block reading the same on a laptop and a phone is worth more than the one line of
   width the desktop arrangement saved.
   row gap 0: a single `gap` here would also become the grid ROW gap and push the
   role away from the name. */
/* A pale blue CARD, not a hairline-ruled afterthought (Kelly, 2026-08-17: "make
   the whole back of the box pale blue to help it stand out"). This is the one
   block in the drawer a parent has to act on, and as a bare block under a rule
   it read like a footnote to the tracks above it. The fill carries it instead,
   so the rule came off: a tinted panel and a top border on top of it is two
   ways of saying the same edge. */
.pd-contact{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 1.05rem;
  align-items:center;margin-top:2rem;
  background:var(--rb-pale);border-radius:10px;
  padding:1.35rem 1.4rem;}
/* display:contents lifts the three text rows out of .pd-cbody so the face can span
   the role and the name while the links span both columns. The markup the drawer
   builds is untouched, which is what makes this reusable on any face + name +
   contact block rather than a one off for this drawer. */
.pd-cbody{display:contents;}
.pd-face{grid-row:span 2;}
/* the face is taller than role + name, so the two spanned rows grow. hugging them to
   the boundary between the rows keeps the pair reading as one unit, centred against
   the portrait, instead of drifting apart by the leftover height */
.pd-crole{grid-column:2;align-self:end;}
.pd-cname{grid-column:2;align-self:start;}
/* a contact with no portrait keeps a single column, so the text is not indented past
   a face that is not there */
.pd-contact:not(:has(.pd-face)){grid-template-columns:minmax(0,1fr);}
.pd-contact:not(:has(.pd-face)) .pd-crole,
.pd-contact:not(:has(.pd-face)) .pd-cname{grid-column:1;}
/* 64, not 78. At 78 the face competed with the name beside it; at 64 it reads as
   a portrait attached to a name, which is the job. No ring and no shadow either:
   a circle on white needs neither, and both were doing decoration. */
/* white, not rb-pale: the card behind it is rb-pale now, so a pale placeholder
   circle vanished into it whenever a portrait was missing. */
.pd-face{width:56px;height:56px;border-radius:50%;overflow:hidden;
  background:#fff;}
.pd-face img{width:100%;height:100%;object-fit:cover;object-position:center 20%;display:block;}
/* the role sits QUIET above the name, in soft ink rather than blue, so the name is
   the only coloured thing in the block */
.pd-crole{font-family:var(--sans);font-weight:500;font-size:.64rem;letter-spacing:.15em;
  text-transform:uppercase;color:var(--ink-soft);line-height:1.4;}
/* RED, and not SquareSlab. Slab in caps at this size was the same unreadable density
   the fact values had, and caps made a person's name read as a header rather than a
   name. Oswald in sentence case at 1.12rem is legible small, and the red makes the
   human the one accent in the block. (Kelly, 2026-08-14) */
.pd-cname{font-family:var(--sans);font-weight:600;font-size:1.12rem;line-height:1.25;
  color:var(--red);letter-spacing:.005em;margin-top:.05rem;}
/* full width, back at the left edge underneath the face. On anything wider than a
   phone the two values still share one line with a hairline between them, because
   there is room for it; the phone rule below stacks them. */
.pd-clinks{grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:center;
  margin-top:.6rem;}
.pd-clink{font-size:.94rem;line-height:1.5;color:var(--rb-deep);text-decoration:none;
  transition:color .18s ease;}
.pd-clink:hover{color:var(--navy);text-decoration:underline;text-underline-offset:3px;}
.pd-clink + .pd-clink{margin-left:.85rem;padding-left:.85rem;
  border-left:1px solid var(--line);}
/* The phone keeps the same shape and only changes what will not fit on it. Beside a
   56px face the name's column is about 228px on a 375px screen and the address needs
   233px to hold one line, which is the reason the links get the full width in the
   base rule rather than sitting in the name's column. */
@media(max-width:600px){
  .pd-contact{column-gap:.95rem;padding:1.15rem 1.05rem;}
  /* stacked: the two values will not share a line at this width, and a hairline
     between two wrapped lines reads as a mistake */
  .pd-clinks{flex-direction:column;align-items:flex-start;gap:.1rem;}
  .pd-clink + .pd-clink{margin-left:0;padding-left:0;border-left:0;}
  /* 36px tall so each is a real target. the clamp lets the address step down a little
     on the narrowest phones rather than break across two lines, and anywhere is the
     last resort net so it can never run past the gutter */
  .pd-clink{min-height:36px;display:flex;align-items:center;
    font-size:clamp(.85rem,4.05vw,.94rem);overflow-wrap:anywhere;}
}

/* ---- THE FACTS LIST ----
   Date, time, place and who it is for, as rows rather than a sentence. Two columns
   on anything wide enough, one on a phone. The label is slab so it scans as a label;
   the value is the reading face so it does not shout. */
/* Four facts a parent takes in at a glance, so it is built for the EYE, not for
   reading (Kelly, 2026-08-14: "a parent needs to glance at this and understand").

   Two things changed to get there. The rules came out: four of them under four
   different-length values never line up, so they read as a ragged edge rather than
   structure. And the label moved ABOVE the value instead of beside it, which lets the
   eye run straight across four values on one line without stepping over a label each
   time. The value is now the loud part and the label is the quiet part, which is the
   reverse of what it was. */
.pd-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));
  gap:1.5rem 2.4rem;margin:1.5rem 0 2rem;}
.pd-fact{display:block;}
.pd-fact dt{font-family:var(--sans);font-weight:500;font-size:.66rem;letter-spacing:.15em;
  text-transform:uppercase;color:var(--ink-soft);margin-bottom:.22rem;}
/* NOT SquareSlab. It is a heavy display slab built for titles, watermarks and big
   headers, and at 17px in caps it stopped being readable (Kelly, 2026-08-14). Oswald
   is the house face for labels and UI, which is exactly what a fact value is, and it
   holds up small. Sentence case rather than caps for the same reason: word shapes are
   what the eye actually catches at this size, and "Saturday, September 12" is read
   faster than the shouted version. Slab stays for the title above. */
.pd-fact dd{font-family:var(--sans);font-weight:500;font-size:1.08rem;line-height:1.35;
  color:var(--navy);letter-spacing:.005em;}
/* the calendar link. quiet until hovered: it is a convenience, not the main action */
.pd-cal{display:inline-flex;align-items:center;gap:.5rem;
  font-family:var(--sans);font-weight:600;font-size:.78rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--rb-deep);
  padding:.6rem .95rem;border:1.5px solid var(--line);border-radius:4px;min-height:44px;
  margin-bottom:1.6rem;transition:border-color .2s ease,background .2s ease,color .2s ease;}
.pd-cal svg{width:17px;height:17px;}
.pd-cal:hover{border-color:var(--rb-deep);background:var(--rb-pale);color:var(--navy);}
@media(max-width:600px){ .pd-cal{width:100%;justify-content:center;} }

/* THE SIGN UP BUTTON, TOP RIGHT.
   It used to sit at the foot, after the reading. That is the logical place and the
   wrong one: a parent who has already decided should not have to read a panel to
   find the button (Kelly, 2026-08-14: "move to the top right so it's super easy to
   find"). So .pd-top runs the title and the button as one row, and the button is
   the first thing level with the headline rather than the last thing under it.

   Gold, not red, because it registers a family. See .btn-gold in rummel-page.css.

   align-items:flex-start keeps the button pinned to the top of the row while a two
   line title grows downward beside it, so it never drifts to the vertical middle. */
.pd-top{display:flex;align-items:flex-start;justify-content:space-between;gap:2.2rem;}
.pd-top .pd-head{flex:1 1 auto;min-width:0;}
.pd-cta{flex:0 0 auto;padding-top:.4rem;}
.pd-cta .btn{white-space:nowrap;}
/* Below 820 the row becomes a stack and the button goes full width and FIRST, above
   the title, so the tap target is still the first thing a thumb meets. */
@media(max-width:820px){
  .pd-top{flex-direction:column;gap:1.2rem;}
  .pd-cta{order:-1;padding-top:0;width:100%;}
  .pd-cta .btn{width:100%;justify-content:center;}
}
.pd-who{margin-top:.9rem;
  font-family:var(--sans);font-size:.84rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--rb-deep);}
/* the address opts out of the uppercase and the tracking. an email is something
   people read character by character, and both of those make that harder. */
.pd-mail{text-transform:none;letter-spacing:0;color:var(--rb-deep);
  text-decoration:underline;text-underline-offset:3px;
  text-decoration-color:rgba(43,110,158,.4);
  margin-left:.5rem;transition:color .18s ease,text-decoration-color .18s ease;}
.pd-mail:hover{color:var(--red);text-decoration-color:var(--red);}
/* 48px, comfortably over the 44px floor, and it sits clear of the photo. it lives
   outside .pd-body on purpose: the body scrolls and the X has to stay put. */
.pd-x{position:absolute;top:1.1rem;right:1.1rem;z-index:3;width:48px;height:48px;
  border:0;border-radius:50%;background:rgba(6,23,48,.55);color:#fff;cursor:pointer;
  font-size:1.35rem;line-height:1;display:flex;align-items:center;justify-content:center;
  transition:background .18s ease,transform .18s ease;}
.pd-x:hover{background:var(--red);transform:translateY(-1px);}
.pd-x:focus-visible{outline:3px solid #fff;outline-offset:2px;}

/* below 1081 the rail is a strip across the top, so there is no side to sit beside.
   the drawer takes the full width and the spine narrows, but it stays, because the
   red edge is what makes this feel like the same object on a phone. */
@media(max-width:1080px){
  /* body.railed has to be matched here, not just .pd. down here the rail is a strip
     ACROSS THE TOP, so there is nothing on the left to sit beside, and the plain
     .pd rule would lose to body.railed .pd on specificity and shove the drawer
     the rail's width off the screen edge. */
  .pd, body.railed .pd{left:0;}
  .pd-spine{width:44px;}
  .pd-inner{padding:2rem 1.6rem 3.4rem;}
}
@media(max-width:560px){
  .pd-spine{width:38px;}
  .pd-inner{padding:1.75rem 1.25rem 3rem;}
}
@media(prefers-reduced-motion:reduce){
  .pd,.pd-scrim{transition-duration:.01ms;}
  .pd-slide{transition-duration:.01ms;}
  .pd-frame{animation:none;}
  .pd-pic.swapping .pd-grain{opacity:.10;}
}


/* ============================================================
   RHYTHM  required
   Extra air above the FIRST block only, whatever that block is.

   the rail slides in over half a second once the hero is behind you, and at
   the shared 6rem the first heading arrived in the same beat, so the two
   motions collided. 10rem gives the rail room to finish before the heading
   reaches reading position. later sections do not have this problem and keep
   the shared spacing.

   written as :first-child rather than named after one block, so it follows
   whichever block a page happens to start with.
   ============================================================ */
#below > :first-child:not(.its),
#below > .its + *{padding-top:10rem;}
/* below 1081 the rail is a top strip rather than a panel sliding in from the side,
   so there is no collision to buy room for. the extra air would read as a hole. */
@media(max-width:1080px){ #below > :first-child:not(.its),#below > .its + *{padding-top:5rem;} }


/* ============================================================
   SURFACES  required
   The white content section, and the dark band section.
   ============================================================ */
.lightsec{background:#fff;}
.lightsec .h2{color:var(--navy);}
.lightsec .lede{color:var(--ink-soft);}
.lightsec em{color:var(--rb-deep);}

.bandsec{background:var(--ink-band);}

/* The dotted marker under a fact awaiting Kelly's yes. INVISIBLE since
   2026-08-17, her call: the site is live now and on a public page a dotted
   underline reads as a broken link to a family. The attribute stays on the
   markup, so `[data-confirm]` still finds every open fact in the editor and in
   the console; it just does not draw. Do not restyle it back on. */
[data-confirm]{border-bottom:0;}

/* Keeps a phrase whole across a line break. Use sparingly, and only on things
   that read wrong when split: a school name, a person's name, a figure and its
   unit. Never on a whole clause, because a phrase wider than its container will
   overflow rather than wrap. */
.nowrap{white-space:nowrap;}


/* ============================================================
   DIVIDER  optional
   The R between two sections.

   Used ONLY between two sections that share a background, since where the
   colour already changes a rule adds a seam rather than easing one. the blue
   sides belong: they are the divider.

   the grey Kelly once saw around this band was never a border, it was the
   browser antialiasing the boundary between three separate white boxes at
   1.5x display scaling. cured by painting one continuous white on #below.
   ============================================================ */
.rdiv .inner{display:flex;align-items:center;gap:1.3rem;max-width:1280px;margin:0 auto;
  padding:clamp(.9rem,2vw,1.4rem) 2.4rem;}
.rdiv .lft,.rdiv .rgt{flex:1;height:2px;}
.rdiv .lft{background:linear-gradient(90deg,transparent,var(--rb));}
.rdiv .rgt{background:linear-gradient(270deg,transparent,var(--rb));}
.rdiv img{width:26px;flex:none;opacity:.95;}
.rdiv.on-white{background:#fff;}
.rdiv.on-white .lft{background:linear-gradient(90deg,transparent,var(--rb-deep));}
.rdiv.on-white .rgt{background:linear-gradient(270deg,transparent,var(--rb-deep));}
@media(max-width:560px){.rdiv .inner{gap:.9rem;padding-left:1.25rem;padding-right:1.25rem;} .rdiv img{width:22px;}}


/* ============================================================
   PATHWAYS  optional
   Four cards, the four things this section most wants to sell.
   Each one can open a DRAWER instead of navigating.

   Four is the number. Three reads thin against a full width row and five
   drops the type below the floor at this column width.
   ============================================================ */
.pathways{background:#fff;}
.pathways .h2{color:var(--navy);}
.pathways .lede{color:var(--ink-soft);}
.pathways em{color:var(--rb-deep);}
.prog-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.5rem;}
.prog{display:flex;flex-direction:column;padding:2.2rem 1.8rem 1.9rem;
  background:#fff;border:1px solid var(--line);border-top:4px solid var(--rb-deep);
  transition:transform .2s ease, box-shadow .2s ease, border-top-color .2s ease;}
.prog:hover{transform:translateY(-6px);box-shadow:var(--shadow-lg);border-top-color:var(--accent);}
/* uppercase because it is SLAB. SquareSlab is never set in lower case anywhere on
   this site: "Sep 12" in a slab face reads sloppy, "SEP 12" reads designed. (Kelly) */
/* Oswald, not slab. Kelly, 2026-08-15: slab is hard to read small, and at .82rem in
   caps this label was exactly that. Oswald 600 holds the same weight on the page and
   stays legible. Slab is for display sizes on this site now. */
.prog .n{font-family:var(--sans);font-weight:600;text-transform:uppercase;
  font-size:var(--t-label);letter-spacing:.2em;color:var(--accent);margin-bottom:1rem;}
.prog .h3{margin-bottom:.75rem;}
.prog p{font-size:var(--t-body);line-height:1.62;color:var(--ink-soft);flex:1;margin-bottom:1.4rem;}
.prog .go{font-family:var(--sans);font-weight:600;font-size:var(--t-label);
  letter-spacing:.13em;text-transform:uppercase;color:var(--rb-deep);
  min-height:44px;display:inline-flex;align-items:center;gap:.45rem;}
.prog:hover .go{color:var(--accent);}
@media(max-width:1080px){ .prog-grid{grid-template-columns:repeat(2,1fr);} }
@media(max-width:760px){ .prog-grid{grid-template-columns:1fr;gap:1.1rem;} }


/* ============================================================
   TIMELINE  optional
   A dated sequence that BRANCHES, for a section where the reader's
   own situation changes which dates apply to them.

   Vertical, not horizontal. A horizontal rail is the prettier first
   sketch and the wrong one: with the side rail in, the column is 914px,
   and seven steps across it leave 130px each, which is a chip that can
   carry a date and about two words. A parent needs a sentence. Vertical
   gives every step the full column, and it is the same shape on a phone,
   so there is no second layout to keep honest.

   Three things carry it:

   1. The spine FILLS to today. .tl-fill is sized in JS from the real
      clock against the step dates, so a family arriving in October sees
      how much of the season is already behind them without reading a
      word. Same gesture as the rail's progress line, so it reads as part
      of the same page rather than a new idea.
   2. The switch re-deals the list. Steps that do not belong to the
      chosen track leave, and the ones that stay renumber and re-fade.
   3. Each card opens the DRAWER, via data-drawer rather than an href,
      because these steps are not pages and never will be.

   The dates lead the block on purpose. This is the one place on an
   admissions page where a parent is hunting for a number, so the date
   is set in slab and the prose gets out of its way.

   The steps are NOT boxed (Kelly, 2026-08-14: "way less clunky, sleek
   and modern"). Twelve bordered cards down one column read as twelve
   objects to get through; the same twelve as type threaded on a single
   hairline read as one sequence, which is what a timeline is. The spine
   already draws the structure, so a border around every step is drawing
   it a second time. Hover is a soft tint and a small slide rather than a
   lift and a shadow, because a row is not a card and should not pretend
   to be liftable.
   ============================================================ */
.tl-switch{display:flex;gap:0;margin-bottom:2.6rem;border-bottom:2px solid var(--line);}

.tl-steps{list-style:none;padding-left:0;}

/* the spine, and the fill, live on .tl-track rather than on the list, because
   an <ol> may only hold <li> and these two are neither. one continuous line
   behind everything, including the gaps between steps. */
.tl-track{position:relative;}
/* the -1px is the spine's own width. left alone, a 2px rule hung off the
   column centre sits one pixel right of the dots it is threading, which is
   invisible until you look at it once and then never stops being visible. */
.tl-track::before{content:"";position:absolute;top:1.05rem;bottom:1.4rem;
  left:calc(var(--tl-when) + var(--tl-gap));margin-left:-1px;
  width:2px;background:var(--line);}
.tl-fill{position:absolute;top:1.05rem;left:calc(var(--tl-when) + var(--tl-gap));
  margin-left:-1px;width:2px;height:0;
  background:linear-gradient(180deg,var(--rb),var(--rb-deep));
  transition:height .6s cubic-bezier(.16,1,.3,1);}

.tl{--tl-when:76px;--tl-gap:1.5rem;--tl-dot:11px;}

.tl-step{position:relative;display:grid;
  grid-template-columns:var(--tl-when) calc(var(--tl-gap)*2) 1fr;
  align-items:start;padding-bottom:.55rem;
  transition:opacity .3s ease,transform .3s ease;}
.tl-step:last-child{padding-bottom:0;}
/* leaving the track. display:none would drop them out of the flow with a
   jump, so they fade and lift first and the JS hides them after. */
.tl-step.leaving{opacity:0;transform:translateY(-6px);}
.tl-step[hidden]{display:none;}

/* Oswald, not slab. Kelly, 2026-08-15: slab is hard to read small and we have discussed
   it. A .7rem month abbreviation in a heavy slab was the worst offender on the page.
   Still uppercase, still the same weight on the eye, but legible at this size. Slab is
   display only on this site now. */
.tl-when{font-family:var(--sans);text-transform:uppercase;line-height:1;text-align:right;padding-top:.62rem;}
.tl-when .m{display:block;font-weight:600;font-size:.72rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--rb-deep);}
.tl-when .d{display:block;font-weight:700;font-size:1.34rem;color:var(--navy);margin-top:.16rem;}
.tl-when .y{display:block;font-family:var(--sans);font-weight:600;font-size:.66rem;
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);margin-top:.22rem;}

/* the dot sits in the middle column, centred on the spine. */
.tl-dot{position:relative;justify-self:center;margin-top:.82rem;
  width:var(--tl-dot);height:var(--tl-dot);border-radius:50%;
  background:#fff;border:2px solid var(--line);z-index:1;
  transition:background .3s ease,border-color .3s ease,box-shadow .3s ease;}
.tl-step.past .tl-dot{background:var(--rb-deep);border-color:var(--rb-deep);}
/* a step with no date is open ended rather than done: the application does not
   arrive and pass, it just stays open. so it is filled, but never marked past. */
.tl-step.tl-open .tl-dot{background:var(--rb);border-color:var(--rb-deep);}
/* the next thing that has not happened yet is the only red on the spine.
   one node, so it reads as "you are here" rather than as decoration. */
.tl-step.next .tl-dot{background:var(--accent);border-color:var(--accent);
  box-shadow:0 0 0 5px rgb(var(--accent-rgb) / .16);}

/* a row, not a card. no border, no fill, no left bar: the spine is the
   structure. the padding is there so the hover tint has somewhere to land and
   so the whole row stays a comfortable target, and the radius keeps that tint
   from reading as a hard rectangle. */
/* font-family:inherit is NOT decoration. This is a <button>, and a button does
   not inherit the page's face from body the way a div does: without this it
   falls back to the browser's own control font (Arial here) while every
   paragraph around it is the reading face, and the step text reads as if it
   came from a different site. Kelly caught it 2026-08-17. The h3, the .tl-at
   and the .go all name their face explicitly, which is why only the paragraph
   looked wrong and the rest looked fine. */
.tl-card{width:100%;text-align:left;background:transparent;cursor:pointer;
  font-family:inherit;font-size:inherit;color:inherit;
  border:0;border-radius:4px;padding:.6rem .9rem .75rem;margin-left:-.9rem;
  transition:background .22s ease,transform .22s ease;}
.tl-card:hover{background:var(--rb-pale);transform:translateX(3px);}
/* Oswald 600 in sentence case. Slab at 1.06rem in caps was dense to read and made a
   step name shout like a section header; these are steps, not headings. Same reasoning
   as the drawer contact name. (Kelly, 2026-08-15) */
.tl-card h3{font-family:var(--sans);font-weight:600;font-size:1.1rem;line-height:1.22;
  letter-spacing:.005em;color:var(--navy);transition:color .22s ease;}
.tl-card:hover h3{color:var(--rb-deep);}
.tl-step.past .tl-card h3{color:var(--ink-soft);}
.tl-step.past .tl-card:hover h3{color:var(--navy);}
.tl-card .tl-at{font-family:var(--sans);font-weight:600;font-size:.68rem;
  letter-spacing:.11em;text-transform:uppercase;color:var(--rb-deep);margin-top:.28rem;}
.tl-card p{font-size:.94rem;line-height:1.58;color:var(--ink-soft);margin-top:.45rem;
  max-width:62ch;}
/* the affordance, twelve times over, so it has to be quiet. no min-height: the
   row itself is the button and already clears the tap floor, and 44px of empty
   space under every step was most of what made the block feel long. */
.tl-card .go{font-family:var(--sans);font-weight:600;font-size:.66rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--rb-deep);
  display:inline-flex;align-items:center;gap:.4rem;margin-top:.5rem;
  opacity:.62;transition:opacity .22s ease,color .22s ease;}
.tl-card:hover .go{opacity:1;color:var(--accent);}
.tl-card .go span{transition:transform .18s ease;}
.tl-card:hover .go span{transform:translateX(4px);}

/* today. a hairline across the card column with the date on it, positioned by
   JS between the last step that has happened and the next one that has not.

   It starts at the CARD's left edge, not the block's, so the label aligns with
   the card titles and never lands on top of a date. ::before is the short tick
   that reaches back to the spine, and its length is exactly --tl-gap because
   the dot column is --tl-gap*2 wide and centred on the spine. */
/* translateY(-50%) so the y the JS sets means the LINE, not the top of the
   label box. Without it the marker hangs its own height below the position it
   was given and clips the card underneath. top transitions on the same curve
   as the fill, so switching tracks moves the two together. */
.tl-now{position:absolute;left:calc(var(--tl-when) + var(--tl-gap)*2);right:0;
  display:flex;align-items:center;gap:.7rem;transform:translateY(-50%);
  pointer-events:none;z-index:2;opacity:0;
  transition:opacity .4s ease, top .6s cubic-bezier(.16,1,.3,1);}
.tl-now.on{opacity:1;}
.tl-now::before{content:"";position:absolute;right:100%;
  width:var(--tl-gap);height:1px;background:var(--accent);}
.tl-now::after{content:"";flex:1;height:1px;
  background:linear-gradient(90deg,var(--accent),rgb(var(--accent-rgb) / 0));}
.tl-now .lab{flex:none;font-family:var(--sans);font-weight:600;font-size:.72rem;
  letter-spacing:.16em;text-transform:uppercase;color:var(--accent);
  background:#fff;padding-right:.2rem;}

/* the closing line under the list, for the thing a date cannot carry. */
.tl-foot{margin-top:2.2rem;padding-left:calc(var(--tl-when) + var(--tl-gap)*2);
  font-size:var(--t-body);line-height:1.62;color:var(--ink-soft);}
.tl-foot strong{color:var(--navy);}

/* a step with nothing more to say is not clickable, and must not look it. */
.tl-card:not([data-drawer]){cursor:default;}
.tl-card:not([data-drawer]):hover{background:transparent;transform:none;}
.tl-card:not([data-drawer]):hover h3{color:var(--navy);}

/* DAYPICK: one event, several dates, one tap each. Lives with the timeline
   because it is the same idea at a smaller scale, and it is what stops a
   six Friday event from being one link and a paragraph asking the reader to
   go find their own date on the next screen. */
.daypick{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:1.2rem;}
.daypick a{display:inline-flex;align-items:center;justify-content:center;
  min-height:52px;padding:.8rem 1.35rem;background:#fff;
  border:1px solid var(--line);border-bottom:3px solid var(--rb-deep);
  font-family:var(--sans);font-weight:600;font-size:.9rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--navy);
  transition:transform .18s ease,background .18s ease,color .18s ease,border-color .18s ease;}
.daypick a:hover{transform:translateY(-3px);background:var(--rb-deep);color:#fff;
  border-color:var(--rb-deep);}
.daypick-lab{font-family:var(--sans);font-weight:600;font-size:var(--t-label);
  letter-spacing:.16em;text-transform:uppercase;color:var(--rb-deep);}

@media(max-width:760px){
  /* the date column would leave the card about 180px, so the date moves
     above the card and the spine keeps the left edge to itself. */
  .tl{--tl-when:0px;--tl-gap:.9rem;}
  .tl-switch{overflow-x:auto;scrollbar-width:none;}
  .tl-switch::-webkit-scrollbar{display:none;}
  .tl-switch .tab-btn{padding:.95rem 1.1rem;font-size:.82rem;}
  .tl-step{grid-template-columns:calc(var(--tl-gap)*2) 1fr;}
  .tl-when{grid-column:2;text-align:left;display:flex;align-items:baseline;
    gap:.45rem;padding-top:0;margin-bottom:.3rem;}
  .tl-when .m,.tl-when .d,.tl-when .y{margin-top:0;}
  .tl-when .d{font-size:1.18rem;}
  .tl-dot{grid-column:1;grid-row:1;margin-top:.28rem;}
  /* the negative margin that lets the hover tint bleed left has nothing to
     bleed into once the date sits above the row, so it comes off. */
  .tl-card{grid-column:2;grid-row:2;margin-left:0;padding-left:0;}
  .tl-card:hover{background:transparent;transform:none;}
  .tl-foot{padding-left:calc(var(--tl-gap)*2);}
}


/* ============================================================
   GRID9  optional
   A grid of small cards. Nine is the comfortable maximum: past that
   the block stops being scannable and ROSTER is the right answer.
   ============================================================ */
.grid9{display:grid;grid-template-columns:repeat(3,1fr);gap:1.4rem;}
.g9{padding:1.7rem 1.5rem;border:1px solid var(--line);border-top:4px solid var(--rb-deep);
  background:#fff;transition:transform .2s ease,box-shadow .2s ease,border-top-color .2s ease;}
.g9:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg);border-top-color:var(--accent);}
.g9 h3{font-family:var(--slab);font-size:1.12rem;text-transform:uppercase;letter-spacing:.02em;
  color:var(--navy);margin-bottom:.5rem;}
.g9 p{font-size:1rem;line-height:1.6;color:var(--ink-soft);}
.g9 .who{display:block;margin-top:.6rem;font-family:var(--sans);font-weight:600;
  font-size:var(--t-label);letter-spacing:.1em;text-transform:uppercase;color:var(--rb-deep);}
@media(max-width:1080px){ .grid9{grid-template-columns:1fr 1fr;} }
@media(max-width:760px){ .grid9{grid-template-columns:1fr;} }


/* ============================================================
   PHOTOTRIO  optional
   Three photo cards standing on their own, well down the page.

   the pull-up that made these break the hero edge in an earlier concept is
   dropped: Kelly wants this row further down, standing on its own.
   ============================================================ */
.photo-trio{background:#fff;padding:.5rem 0 5rem;}
.photo-trio .pt-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6rem;}
.photo-trio .pt-c{background:#fff;box-shadow:var(--shadow-lg);overflow:hidden;
  transition:transform .22s ease,box-shadow .22s ease;}
.photo-trio .pt-c:hover{transform:translateY(-8px);}
/* square, Kelly's pick. the tall 3:4 crop was cutting subjects badly. each card sets
   its own background-position inline, since a centre crop is right for one of three
   and wrong for the other two. */
.photo-trio .pt-c .pic{aspect-ratio:1/1;background-size:cover;background-position:center;}
.photo-trio .pt-c .txt{padding:1.7rem 1.6rem 1.9rem;border-top:4px solid var(--rb-deep);}
.photo-trio .pt-c:nth-child(2) .txt{border-top-color:var(--accent);}
.photo-trio .pt-c h3{margin-bottom:.5rem;}
.photo-trio .pt-c p{font-size:var(--t-body);line-height:1.6;color:var(--ink-soft);}
/* stays square on phones too. a full width square is tall, but it is the crop Kelly
   picked and the subjects hold up in it. */
@media(max-width:900px){ .photo-trio .pt-grid{grid-template-columns:1fr;gap:1.2rem;} }


/* ============================================================
   TABLEASIDE  optional
   A data table on the left, a photo and a tinted callout on the right.
   ============================================================ */
.ta-wrap{display:grid;grid-template-columns:1.25fr .75fr;gap:3rem;align-items:start;}
table.ta{width:100%;border-collapse:collapse;font-size:var(--t-list);}
table.ta th{font-family:var(--sans);font-weight:600;font-size:var(--t-label);letter-spacing:.14em;
  text-transform:uppercase;color:var(--navy);text-align:left;padding:.8rem .7rem;border-bottom:2px solid var(--navy);}
table.ta th:not(:first-child),table.ta td:not(:first-child){text-align:center;width:22%;}
table.ta td{padding:.72rem .7rem;border-bottom:1px solid var(--line);color:var(--ink);}
table.ta tr:last-child td{border-bottom:none;border-top:2px solid var(--navy);
  font-family:var(--sans);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--navy);}
/* the frame above the callout, in the column that would otherwise sit empty */
.ta-pic{aspect-ratio:4/3;background-size:cover;background-position:center;
  margin-bottom:1.4rem;box-shadow:var(--shadow-lg);}
/* .tall for a genuine portrait frame. The column is narrow, so a 3:4 fills it without
   the letterboxing a landscape crop leaves, and a portrait photograph lands in it
   uncropped rather than being centre cut to a wide slot. */
.ta-pic.tall{aspect-ratio:3/4;}
.ta-note{background:var(--rb-pale);border-left:5px solid var(--rb-deep);padding:2rem 1.8rem;}
.ta-note h3{font-family:var(--slab);font-size:1.25rem;text-transform:uppercase;color:var(--navy);margin-bottom:.3rem;}
.ta-note .tag{font-family:var(--sans);font-weight:600;font-size:var(--t-label);letter-spacing:.16em;
  text-transform:uppercase;color:var(--rb-deep);margin-bottom:1rem;}
.ta-note p{font-size:var(--t-body);line-height:1.68;color:var(--ink-soft);}
.ta-note p + p{margin-top:.8rem;}
.ta-note b{color:var(--navy);}
@media(max-width:900px){ .ta-wrap{grid-template-columns:1fr;gap:2rem;} }


/* ============================================================
   CONTACTDROP  optional
   A closed by default panel holding the contacts a family needs once: who to
   call, what the code is, which number to dial.
   Built for the tuition block, where four paragraphs of logistics sat in the
   aside competing with the price. Maddux's note, through Kelly (2026-08-15):
   a family does not want the paragraphs, it wants a LIST with the contact
   information on it.
   <details> on purpose. It is keyboard operable and it opens with no JS, so
   the numbers stay reachable even if the script never loads.
   ============================================================ */
.cdrop{interpolate-size:allow-keywords;margin-top:1.7rem;
  background:var(--rb-pale);border-left:5px solid var(--rb-deep);}
.cdrop > summary{list-style:none;cursor:pointer;display:flex;align-items:center;
  gap:1rem;padding:1.15rem 1.4rem;}
.cdrop > summary::-webkit-details-marker{display:none;}
.cdrop-lab{min-width:0;}
/* block, both of them. as inline spans the label and its line underneath ran
   together into one long strip and the margin between them did nothing. */
.cdrop-lab .t{display:block;font-family:var(--slab);text-transform:uppercase;
  font-size:1.15rem;line-height:1.15;color:var(--navy);}
.cdrop-lab .s{display:block;font-family:var(--sans);font-weight:600;
  font-size:var(--t-label);letter-spacing:.16em;text-transform:uppercase;
  color:var(--rb-deep);margin-top:.32rem;}
/* the chevron is the only moving part, and it TURNS rather than swapping to a
   different glyph, so opening reads as one gesture instead of two states */
.cdrop-chev{margin-left:auto;flex:0 0 auto;width:28px;height:28px;
  display:grid;place-items:center;color:var(--rb-deep);
  transition:transform .34s cubic-bezier(.22,.61,.36,1),color .2s ease;}
.cdrop-chev svg{width:15px;height:15px;display:block;}
.cdrop[open] .cdrop-chev{transform:rotate(180deg);}
.cdrop > summary:hover .cdrop-chev{color:var(--accent);}
.cdrop > summary:focus-visible{outline:2px solid var(--rb-deep);outline-offset:-4px;}
/* opens on a height transition rather than snapping open. where ::details-content
   is not supported the panel simply appears, which is the same content either way */
/* The panel opens with NO animation on its height or its content, deliberately.
   Both were built and both were pulled. Animating the height via ::details-content
   leaves the panel at zero if the transition does not resolve; fading the content in
   leaves it at opacity 0 for as long as the animation sits in its active phase, and
   animation-fill-mode does not save you, because a running animation applies its
   values whatever the fill. Either way the failure is the same and it is the worst
   one available here: the panel opens and the phone numbers are not there.
   A disclosure that opens instantly is what every native <details> does and costs a
   family nothing. The chevron still turns, which is motion the content does not
   depend on. Anything smoother than that has to be watched running on a real screen
   before it goes back in. */
@media(prefers-reduced-motion:reduce){
  .cdrop-chev{transition:none;}
}
.cdrop-body{padding:0 1.4rem 1.5rem;}
.cdrop-lede{font-size:var(--t-body);line-height:1.68;color:var(--ink-soft);
  padding-bottom:1.1rem;border-bottom:1px solid var(--line);}
.cdrop-lede b{color:var(--navy);}
/* one row per place a family might have to call. the number is SHOWN rather than
   hidden behind a verb, for the reason the drawer contact shows it too: somebody
   writing it down has to be able to read it. a row with no number simply leaves
   the second column empty, which costs it nothing. */
.cdrop-list{list-style:none;margin:0;padding:0;}
/* the two rows are declared, not left implicit. Without them the `-1` on the value below
   resolves to the last line of the EXPLICIT grid, which is line 1 when no rows exist, so
   the value landed inside the name's row and stretched it. That is what put a band of
   dead space between a name and its description on exactly the rows whose value wrapped
   to two lines. (Kelly caught it, 2026-08-15) */
.cdrop-list > li{display:grid;grid-template-columns:minmax(0,1fr) auto;
  grid-template-rows:auto auto;
  gap:.22rem 1.5rem;padding:1rem 0;border-bottom:1px solid var(--line);}
.cdrop-list > li:last-child{border-bottom:none;padding-bottom:0;}
.cdrop-who{grid-column:1;font-family:var(--sans);font-weight:600;font-size:1.02rem;
  line-height:1.3;color:var(--navy);letter-spacing:.005em;}
.cdrop-who a{color:inherit;text-decoration:none;
  border-bottom:1px solid rgba(43,110,158,.42);}
.cdrop-who a:hover{color:var(--rb-deep);border-bottom-color:var(--rb-deep);}
.cdrop-what{grid-column:1;font-size:var(--t-list);line-height:1.55;color:var(--ink-soft);}
/* spans both rows of column one so it centres against the pair, the same trick the
   drawer contact uses to hold a face beside a role and a name */
.cdrop-val{grid-column:2;grid-row:1/-1;align-self:center;text-align:right;}
.cdrop-val a{font-size:var(--t-list);line-height:1.5;color:var(--rb-deep);
  text-decoration:none;white-space:nowrap;}
.cdrop-val a:hover{color:var(--navy);text-decoration:underline;text-underline-offset:3px;}
.cdrop-val .sub{display:block;font-size:.86rem;line-height:1.45;color:var(--ink-soft);
  margin-top:.22rem;}
@media(max-width:600px){
  /* the number drops beneath its row rather than squeezing the name into a sliver,
     and gets a real 36px target once it is on its own line */
  .cdrop-list > li{grid-template-columns:minmax(0,1fr);}
  .cdrop-val{grid-column:1;grid-row:auto;text-align:left;margin-top:.4rem;}
  .cdrop-val a{min-height:36px;display:inline-flex;align-items:center;}
  .cdrop-body{padding:0 1.15rem 1.3rem;}
  .cdrop > summary{padding:1.05rem 1.15rem;}
}


/* ============================================================
   PAIR  optional
   Two cards on the dark band. Use for a genuine either/or.
   ============================================================ */
.pair{display:grid;grid-template-columns:1fr 1fr;gap:1.6rem;}
.pair .card{padding:2.4rem 2.2rem;background:rgba(255,255,255,.08);border-left:5px solid var(--rb);}
.pair .card.alt{border-left-color:var(--accent-band);}
.pair .card .h3{color:#fff;}
.pair .card p{font-size:var(--t-body);line-height:1.68;color:rgba(255,255,255,.94);margin-top:.85rem;}
@media(max-width:760px){ .pair{grid-template-columns:1fr;} }


/* ============================================================
   TABS  optional
   One row of buttons, one pane at a time.

   Wrap each group in .tabgroup. That is what lets a page carry TWO
   independent tab groups without them cross wiring, which is exactly
   what Student Life needs. rummel-page.js scopes each controller to
   its own group.

   The category strip on ROSTER reuses these classes verbatim, so it
   costs no new CSS and reads as the same component.
   ============================================================ */
.entry-note{background:var(--rb-pale);border-left:4px solid var(--rb-deep);
  padding:1.15rem 1.35rem;margin-bottom:2rem;font-size:var(--t-body);line-height:1.6;
  color:var(--ink-soft);}
.entry-note strong{color:var(--navy);}
.tabs-nav{display:flex;gap:0;margin-bottom:2rem;border-bottom:2px solid var(--line);}
.tab-btn{font-family:var(--sans);font-weight:600;font-size:.86rem;letter-spacing:.12em;
  text-transform:uppercase;padding:.95rem 1.75rem;min-height:52px;background:none;border:0;
  color:var(--ink-soft);cursor:pointer;border-bottom:3px solid transparent;margin-bottom:-2px;
  white-space:nowrap;transition:color .18s ease,border-color .18s ease;}
.tab-btn:hover{color:var(--navy);}
.tab-btn:focus-visible{outline:3px solid var(--accent);outline-offset:-3px;}
.tab-btn.active{color:var(--navy);border-bottom-color:var(--accent);}
.tab-pane{display:none;}
.tab-pane.active{display:block;}
.track-cols{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;}
.track-col{background:var(--paper-2);border-top:4px solid var(--rb-deep);padding:1.5rem 1.5rem 1.7rem;}
.track-col.alt{border-top-color:var(--accent);}
.track-col h4{font-family:var(--slab);font-size:1.05rem;text-transform:uppercase;color:var(--navy);margin-bottom:.25rem;}
.track-col .tcol-note{font-family:var(--sans);font-weight:600;font-size:var(--t-label);letter-spacing:.1em;
  text-transform:uppercase;color:var(--rb-deep);margin-bottom:1rem;line-height:1.45;}
.track-col.alt .tcol-note{color:var(--accent);}
.track-col ul{list-style:none;}
.track-col li{font-size:var(--t-list);line-height:1.5;color:var(--ink);padding:.5rem 0;border-bottom:1px solid var(--line);}
.track-col li:last-child{border-bottom:none;}
@media(max-width:760px){
  .track-cols{grid-template-columns:1fr;}
  /* the years scroll sideways rather than wrapping into a ragged block */
  .tabs-nav{overflow-x:auto;scrollbar-width:none;}
  .tabs-nav::-webkit-scrollbar{display:none;}
  .tab-btn{padding:.95rem 1.15rem;font-size:.82rem;}
  /* .wrap is for a strip of FOUR (the club categories on Student Life): two
     balanced rows of two, every category on screen, nothing hiding off the
     right edge. Five grade years would land 3+2, so those keep scrolling.
     Each cell carries its own rule so both rows read as tabs. */
  .tabs-nav.wrap{display:grid;grid-template-columns:1fr 1fr;overflow:visible;border-bottom:0;}
  .tabs-nav.wrap .tab-btn{white-space:normal;line-height:1.25;text-align:center;
    padding:.8rem .5rem;font-size:.82rem;letter-spacing:.08em;
    border-bottom:2px solid var(--line);margin-bottom:0;}
  .tabs-nav.wrap .tab-btn.active{border-bottom:3px solid var(--accent);}
}


/* ============================================================
   STATBAND  optional, recommended as the closer
   Four numbers on the dark band.
   ============================================================ */
.statband{background:var(--navy-2);border-top:1px solid rgba(117,178,221,.2);}
.stats{display:grid;grid-template-columns:repeat(4,1fr);}
.stat{padding:2.6rem 1.6rem;text-align:center;}
.stat .n{font-family:var(--slab);font-size:var(--t-stat);line-height:1;color:var(--rb);}
.stat .k{font-family:var(--sans);font-weight:600;font-size:var(--t-statlab);
  letter-spacing:.18em;text-transform:uppercase;margin-top:.7rem;color:rgba(255,255,255,.92);}
.stat .d{font-size:.95rem;color:rgba(255,255,255,.82);margin-top:.35rem;line-height:1.5;}
.statband .stat{border-right:1px solid rgba(117,178,221,.16);}
.statband .stat:last-child{border-right:none;}
.on-light .stat .n{color:var(--rb-deep);}
.on-light .stat .k{color:var(--navy);}
.on-light .stat .d{color:var(--ink-soft);}
@media(max-width:1080px){ .stats{grid-template-columns:repeat(2,1fr);} }
@media(max-width:760px){
  .stats{grid-template-columns:1fr;}
  .stat{padding:2rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.12);}
  .statband .stat{border-right:none;}
}


/* ============================================================
   LINKOUT  optional
   Wide tiles pointing at pages that ALREADY EXIST.

   No drawer here, deliberately. The drawer exists because four cards
   were 404s. Putting a panel in front of a page we already built would
   use the signature move against its own reason for existing.

   The moment instead: the photograph rises out of the dot field on
   hover. Same dot the hero, the rail and the drawer already use, so it
   reads as belonging to the family rather than as a new effect.

   Items group under .lo-group with their own .lo-title, because ten
   destinations that split into places and events are not ten peers, and
   a flat grid of ten equal cards is exactly the correct and forgettable
   failure to avoid.
   ============================================================ */
.lo-group + .lo-group{margin-top:3.4rem;}
.lo-title{font-family:var(--sans);font-weight:600;font-size:var(--t-label);
  letter-spacing:.2em;text-transform:uppercase;color:var(--rb-deep);
  padding-bottom:.8rem;margin-bottom:1.6rem;border-bottom:1px solid var(--line);}
.lo-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1.5rem;}
.lo{display:flex;align-items:stretch;background:#fff;border:1px solid var(--line);
  overflow:hidden;transition:transform .22s ease,box-shadow .22s ease,border-color .22s ease;}
.lo:hover{transform:translateY(-5px);box-shadow:var(--shadow-lg);border-color:transparent;}
.lo-pic{flex:none;width:190px;position:relative;overflow:hidden;
  background-size:cover;background-position:center;background-color:var(--rb-steel);}
/* the dot field sits ON the photo at rest and clears on hover, so the picture
   surfaces rather than merely brightening. */
.lo-pic::before{content:"";position:absolute;inset:0;z-index:2;
  background:var(--dots);background-size:13px 13px;opacity:.5;
  transition:opacity .45s ease;}
.lo:hover .lo-pic::before{opacity:.12;}
.lo-pic::after{content:"";position:absolute;inset:0;z-index:1;
  background:rgb(var(--ink-band-rgb) / .45);transition:opacity .45s ease;}
.lo:hover .lo-pic::after{opacity:0;}
.lo-txt{flex:1 1 auto;min-width:0;padding:1.6rem 1.5rem 1.4rem;
  display:flex;flex-direction:column;}
.lo-txt h3{font-family:var(--slab);font-size:1.18rem;line-height:1.15;text-transform:uppercase;
  letter-spacing:.02em;color:var(--navy);margin-bottom:.45rem;}
/* ---- LINKOUT DATE  additive, 2026-08-17 ----
   Kelly asked for the date on the right hand side of an event tile. Optional:
   wrap the title in .lo-hd and add .lo-when only on tiles that HAVE a date, so
   every tile without one is untouched. The date never breaks mid date; when the
   tile is too narrow to hold both it drops to its own line under the title,
   which is what flex-wrap here is for. */
.lo-txt .lo-hd{display:flex;align-items:baseline;justify-content:space-between;
  gap:.25rem 1rem;flex-wrap:wrap;margin-bottom:.45rem;}
/* The title takes the room that is left and wraps inside it, so the date stays
   pinned to the right on the FIRST line rather than dropping under the title at
   the first sign of a squeeze. It still drops on a very narrow tile, which is
   the honest fallback. */
.lo-txt .lo-hd h3{margin-bottom:0;flex:1 1 7ch;min-width:0;}
.lo-when{flex:none;font-family:var(--sans);font-weight:600;font-size:.72rem;
  letter-spacing:.13em;text-transform:uppercase;color:var(--rb-deep);white-space:nowrap;}
.lo-txt p{font-size:1rem;line-height:1.55;color:var(--ink-soft);flex:1;margin-bottom:1rem;}
.lo-txt .go{font-family:var(--sans);font-weight:600;font-size:var(--t-label);
  letter-spacing:.13em;text-transform:uppercase;color:var(--rb-deep);
  min-height:44px;display:inline-flex;align-items:center;gap:.45rem;}
.lo:hover .lo-txt .go{color:var(--accent);}
.lo-txt .go span{transition:transform .18s ease;}
.lo:hover .lo-txt .go span{transform:translateX(4px);}
@media(max-width:1080px){ .lo-grid{grid-template-columns:1fr;} }
@media(max-width:560px){
  /* the side by side photo drops under 560, where 190px of it would leave the
     title about 120px to live in. */
  .lo{flex-direction:column;}
  .lo-pic{width:100%;aspect-ratio:16/9;}
}

/* ---- LINKOUT FLAT  additive, 2026-08-17 ----
   The text-only tile, for a destination with no honest photograph (Kelly
   pulled the two Association pictures on alumni.html: not the right people).
   Same box, same type, same 44px CTA; a Columbia rule down the left edge does
   the work the photo did, so the tile still reads designed rather than empty,
   and it deepens on hover the way the photo used to surface. */
.lo.lo-flat{border-left:4px solid var(--rb);}
.lo.lo-flat .lo-txt{padding:1.8rem 1.7rem 1.5rem;}
.lo.lo-flat:hover{border-color:transparent;border-left-color:var(--rb-deep);}

/* ---- LINKOUT ROLLS  additive, 2026-08-15 ----
   A quiet row of pills under a LINKOUT group, for the six rolls of ONE page
   (the Award Wall). Six tiles would need six distinct photographs and there
   is one Award Wall photo; six copies of it is the forgettable version. The
   pills sit under the tile grid as their own group so the tile stays a single
   anchor (an <a> inside an <a> is invalid and browsers break the outer one).
   Balanced at every width: 6 per row, then 3, then 2. */
.lo-rolls{display:flex;flex-wrap:wrap;gap:.6rem;}
.lo-roll{flex:1 1 calc(16.666% - .6rem);display:inline-flex;align-items:center;justify-content:center;
  min-height:46px;padding:.6rem .9rem;border:1.5px solid var(--line);border-radius:4px;
  font-family:var(--sans);font-weight:600;font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--rb-deep);text-align:center;background:#fff;
  transition:border-color .2s ease,background .2s ease,color .2s ease,transform .2s ease;}
.lo-roll:hover{border-color:var(--rb-deep);background:var(--rb-pale);color:var(--navy);transform:translateY(-2px);}
@media(max-width:1080px){ .lo-roll{flex-basis:calc(33.333% - .6rem);} }
@media(max-width:560px){ .lo-roll{flex-basis:calc(50% - .6rem);} }

/* ---- CONTACT STRIP  additive, 2026-08-15 ----
   One person, shown: the face, the role quiet above a red Oswald name (the
   drawer contact's own convention), the address and the number as real links
   that are also legible enough to write down. Ported from rummel-sport.css so
   a section front that does not load the sport sheet (Alumni: the Director of
   Alumni) can carry the same block. Same selectors, same values, so pages that
   load both sheets are unchanged. .lo-group + .coach lets it sit under a tile
   grid inside the same section rather than needing a section of its own. */
.coach-strip{padding:3.4rem 0 3.6rem;}
.coach{display:grid; grid-template-columns:auto minmax(0,1fr); gap:1.4rem 2.2rem;
  align-items:center; background:var(--rb-pale); border-left:5px solid var(--rb-deep);
  padding:1.9rem 2.2rem 1.9rem 2rem;}
.lo-group + .coach{margin-top:3.4rem;}
.coach .face{width:98px; height:98px; border-radius:50%; overflow:hidden; background:#fff;
  box-shadow:0 12px 26px -14px rgba(11,34,64,.5);}
.coach .face img{width:100%; height:100%; object-fit:cover; object-position:center 22%; display:block;}
.coach .role{font-family:var(--sans); font-weight:500; font-size:.7rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--ink-soft);}
.coach .name{font-family:var(--sans); font-weight:600; font-size:1.55rem; line-height:1.15;
  color:var(--red); letter-spacing:.005em; margin-top:.15rem;}
.coach .what{font-family:var(--serif); font-size:1rem; line-height:1.6; color:var(--ink-soft);
  margin-top:.5rem; max-width:56ch;}
/* The two ways to reach him, as quiet outlined chips rather than two runs of
   blue text (Kelly, 2026-08-17: "clean this up more"). Two obviously separate,
   obviously tappable things, and the extension rides the number as a lighter
   trailing note instead of a comma splice inside the label. Mirrored verbatim
   in rummel-sport.css, which carries the same block for the Athletic Director.
   Change one, change both. */
.coach .links{display:flex; flex-wrap:wrap; align-items:center; gap:.6rem .7rem; margin-top:.9rem;}
.coach .links a{font-family:var(--sans); font-weight:600; font-size:.95rem; letter-spacing:.02em;
  color:var(--navy); text-decoration:none; min-height:46px; display:inline-flex; align-items:center;
  gap:.6rem; white-space:nowrap; padding:0 1.05rem; border-radius:999px;
  background:#fff; border:1px solid var(--line);
  transition:border-color .18s ease, color .18s ease, transform .18s ease, box-shadow .18s ease;}
.coach .links a svg{width:17px; height:17px; flex:none; color:var(--rb-deep); opacity:1;}
.coach .links a .ext{font-weight:500; color:var(--ink-soft); letter-spacing:.02em;}
.coach .links a:hover{border-color:var(--rb-deep); transform:translateY(-2px);
  box-shadow:0 10px 22px -14px rgba(11,34,64,.55);}
@media(max-width:960px){
  .coach .links{gap:.6rem;}
}
@media(max-width:560px){
  .coach{padding:1.4rem 1.2rem; gap:1rem 1rem;}
  .coach .face{width:74px; height:74px;}
  .coach .name{font-size:1.32rem;}
  /* full width chips on a phone: one obvious tap target each, stacked, with air
     between them rather than the flush 44px rows that read as one lump */
  .coach .links{flex-direction:column; align-items:stretch; gap:.55rem;}
  .coach .links a{white-space:normal; justify-content:flex-start;
    font-size:clamp(.88rem,4vw,.98rem); padding:.72rem 1rem;}
}


/* ============================================================
   ROSTER  optional
   The complete list, when the count is far past what GRID9 can hold:
   fifteen sports, fifty clubs.

   The drawer does not extend here and the count is the argument. It is a
   full screen panel with a red spine, a photo rotation and Ken Burns
   drift, a heavyweight once per visit gesture. Firing it fifty times
   turns a signature moment into a chore, and each opening would need a
   photo set that does not exist for fifty clubs.

   So: dense rows, expanding IN PLACE, one at a time. In place for the
   same reason the drawer sits beside the rail rather than over it. The
   reader is scanning to find one thing and must not lose their place.

   Pair this with PATHWAYS above it: the drawer for the four you want to
   sell, the roster for everything.
   ============================================================ */
.roster{display:grid;grid-template-columns:repeat(3,1fr);gap:0 1.5rem;align-items:start;}
.ritem{grid-column:span 1;border-bottom:1px solid var(--line);}
/* two row types. .ritem-btn expands; .ritem-row does not.
   a list where some rows open and some do nothing would be worse than either,
   so a page picks ONE and uses it throughout. Student Life ships .ritem-btn
   (every club opens to its description and moderator); so do the Admissions
   scholarships and the Support rows. .ritem-row is for a list with nothing
   behind it. */
/* font-family:inherit for the same reason .tl-card carries it: these are
   <button> elements, and a button does not inherit the page face. .ri-name
   names the slab explicitly so it always looked right, while .ri-sub named
   nothing and fell through to the browser's control font. Kelly caught the
   scholarships sub-line 2026-08-17, the second sighting of this bug. Any new
   block built on a button needs this line. */
.ritem-btn,.ritem-row{width:100%;text-align:left;background:none;border:0;
  font-family:inherit;font-size:inherit;color:inherit;
  display:flex;flex-direction:column;justify-content:center;gap:.15rem;
  padding:.85rem .6rem;min-height:56px;
  border-left:3px solid transparent;transition:border-color .18s ease,background .18s ease;}
/* A row that opens says so at rest: a chevron on the right, deep blue, that
   turns over and goes red when the row is open (Kelly, 2026-08-17: make it
   clear the items are clickable). Drawn, not a glyph, so it is the same mark
   at every size and never falls back to a font's arrow. */
.ritem-btn{cursor:pointer;position:relative;padding-right:2.6rem;}
.ritem-btn::after{content:"";position:absolute;right:1rem;top:50%;width:.6rem;height:.6rem;
  border-right:2px solid var(--rb-deep);border-bottom:2px solid var(--rb-deep);
  transform:translateY(-70%) rotate(45deg);
  transition:transform .24s cubic-bezier(.16,1,.3,1),border-color .18s ease;}
.ritem-btn:hover{background:var(--rb-pale);border-left-color:var(--rb-deep);}
.ritem-btn:hover::after{transform:translateY(-50%) rotate(45deg);}
.ritem-btn:focus-visible{outline:3px solid var(--accent);outline-offset:-3px;}
.ritem.open .ritem-btn{background:var(--rb-pale);border-left-color:var(--accent);}
.ritem.open .ritem-btn::after{border-color:var(--accent);transform:translateY(-30%) rotate(225deg);}
.ri-name{font-family:var(--slab);font-size:1.02rem;line-height:1.2;text-transform:uppercase;
  letter-spacing:.02em;color:var(--navy);}
.ri-sub{font-size:.94rem;line-height:1.4;color:var(--ink-soft);}
.ritem-body{padding:0 .6rem 1.1rem 1.05rem;}
.ritem-body p{font-size:1rem;line-height:1.6;color:var(--ink-soft);}
.ritem-body p + p{margin-top:.6rem;}
.ritem-body .who{display:block;margin-top:.7rem;font-family:var(--sans);font-weight:600;
  font-size:var(--t-label);letter-spacing:.1em;text-transform:uppercase;color:var(--rb-deep);}
.ritem-body .who a{text-transform:none;letter-spacing:0;text-decoration:underline;
  text-underline-offset:3px;text-decoration-color:rgba(43,110,158,.4);}

/* the sports variant: a taller row carrying a small square frame, since a sport
   without a picture of it is a word on a list. */
.roster.sports{grid-template-columns:repeat(2,1fr);}
.roster.sports .ritem-btn{flex-direction:row;align-items:center;gap:1rem;min-height:76px;}
.roster.sports .ri-pic{flex:none;width:56px;height:56px;background-size:cover;
  background-position:center;background-color:var(--rb-pale);}
.roster.sports .ri-text{display:flex;flex-direction:column;gap:.15rem;min-width:0;}

/* .roster.pairs  TWO COLUMNS, AND THE LAST ROW IS ALWAYS FULL.

   Kelly, 2026-08-17: every club tab has to end on a full row. The four club
   counts are 10, 7, 6 and 6, and no single column count divides all four,
   because 7 is prime. Three columns leaves 3+3+3+1 and 3+3+1. Two columns
   clears 10, 6 and 6 outright and leaves only the odd one.

   For that one, a lone last item spans both columns rather than sitting in a
   half empty row. :last-child:nth-child(odd) matches only when the count is
   odd, so this corrects itself as clubs are added or dropped and there is no
   number to remember. Rebalancing the categories was the other way to fix it,
   but those categories come off the school club sheet and are not ours to
   reshuffle for a layout.

   Scoped to .pairs, not to .roster, because the sport pages use the plain
   three column roster and are not part of this. */
.roster.pairs{grid-template-columns:repeat(2,1fr);}
.roster.pairs > .ritem:last-child:nth-child(odd){grid-column:1 / -1;}
@media(max-width:1080px){ .roster{grid-template-columns:repeat(2,1fr);} }
@media(max-width:760px){ .roster,.roster.sports,.roster.pairs{grid-template-columns:1fr;} }


/* ============================================================
   HISTORY STRIPE  optional
   A single row of photographs across the decades, each under its year.

   Built for Student Council on clubs-activities.html, 2026-08-17: seven
   frames, 1964 to today, mined out of the yearbooks.

   ONE ROW, ALWAYS. It never wraps. Seven frames wrapping to four and three
   would end on a broken row, and a half row of history reads like the scan
   ran out rather than like a deliberate stop. Below 900 it becomes a
   sideways scroller instead, the same move .tabs-nav makes.

   The frames are deliberately NOT colour matched to each other. The run from
   grey 1964 to full colour today IS the passage of time, and flattening them
   would throw away the one thing the stripe is saying.
   ============================================================ */
.stripe .sh-rail{display:grid;grid-template-columns:repeat(7,1fr);gap:.75rem;}
.sh-f{margin:0;}
.sh-f .pic{aspect-ratio:4/3;background-size:cover;background-position:center;
  border-radius:10px;box-shadow:var(--shadow-lg,0 14px 34px -18px rgba(11,34,64,.5));
  background-color:var(--rb-pale,#e8f1f8);
  transition:transform .28s ease,box-shadow .28s ease;}
.sh-f:hover .pic{transform:translateY(-5px);box-shadow:0 22px 44px -20px rgba(11,34,64,.55);}
.sh-f figcaption{margin-top:.62rem;text-align:center;
  font-family:var(--slab),'Roboto Slab',serif;text-transform:uppercase;
  font-size:1.02rem;letter-spacing:.05em;color:var(--rb-deep,#2B6E9E);}
/* --rb-type is the PALE on-dark tint (#C7E3F7). The stripe sits on white,
   so the year takes --rb-deep, the same blue the section headings use. */
.stripe .sh-note{margin-top:1.15rem;font-size:.95rem;color:var(--ink-soft);max-width:62ch;}

@media(max-width:900px){
  /* still one row, it just travels sideways. 62% leaves the next frame
     peeking in, so the row reads as scrollable without a scrollbar hint. */
  .stripe .sh-rail{display:flex;overflow-x:auto;gap:.75rem;padding-bottom:.7rem;
    scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;}
  .stripe .sh-rail > .sh-f{flex:0 0 62%;scroll-snap-align:start;}
}
@media(max-width:560px){
  .stripe .sh-rail > .sh-f{flex:0 0 78%;}
  .sh-f figcaption{font-size:.85rem;}
}

/* ============================================================
   CTA  optional
   The closing band. One headline, one button, nothing else.
   ============================================================ */
.cta{background:var(--ink-band);text-align:center;}
.cta .h2{color:#fff;}
.cta .lede{color:rgba(255,255,255,.92);margin:1.1rem auto 2rem;max-width:56ch;}
.cta .acts{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;}
@media(max-width:760px){ .cta .acts .btn{width:100%;} }


/* ============================================================
   LANDMARK STOPS. Used by landmarks.html only.
   A walking tour: photo beside story, alternating sides, with a
   ghost stop numeral. Added 2026-08-08; additive, no existing
   block touched.
   ============================================================ */
.lm-stop .wrap{
  display:grid; grid-template-columns:1fr 1fr;
  gap:clamp(2rem,5vw,4.5rem); align-items:center;
}
.lm-stop.rev .lm-pic{order:2;}
.lm-pic{
  aspect-ratio:4/3.2; background-size:cover; background-position:center;
  border-radius:18px; box-shadow:0 24px 56px -24px rgba(11,34,64,.55);
}
.lm-pic.tall{aspect-ratio:3/3.6;}
.lm-story{position:relative;}
.lm-num{
  font-family:var(--slab); font-weight:700; line-height:1;
  font-size:clamp(4rem,9vw,6.5rem); color:rgb(var(--ink-band-rgb) / .10);
  display:block; margin-bottom:-.35em; user-select:none;
}
.lm-story .h2{margin-bottom:.9rem;}
.lm-story p{font-size:1.05rem; line-height:1.7; color:var(--ink-soft); margin-bottom:1rem;}
.lm-story .who{
  display:block; font-family:var(--sans); font-weight:600; font-size:.78rem;
  letter-spacing:.16em; text-transform:uppercase; color:var(--accent);
  margin-top:1.2rem;
}
@media(max-width:900px){
  .lm-stop .wrap{grid-template-columns:1fr; gap:1.6rem;}
  .lm-stop.rev .lm-pic{order:0;}
  .lm-pic{aspect-ratio:16/10;}
  .lm-pic.tall{aspect-ratio:4/3.4;}
}