* {
  box-sizing: border-box;
}

html.is-loaded {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: 'Public Sans', 'Public Sans fallback', system-ui, sans-serif;
  background: #fbf3e8;
  color: #4a2b30;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a {
  color: #b8112a;
  text-decoration: none;
  transition: color .2s ease;
}

a:hover {
  color: #e80202;
}

::selection {
  background: #b8112a;
  color: #fff;
}

@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.reveal {
  opacity: 0;
}

.reveal.in {
  animation: riseIn .6s cubic-bezier(.22, .61, .36, 1) forwards;
}

/* Portraits do not "arrive" like a line of prose. They settle: a shallow
     scale from 97%, no vertical travel, slightly slower so a wall of 44 faces
     resolves calmly rather than marching. */
@keyframes plateIn {
  from {
    opacity: 0;
    transform: scale(.97);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.fac.reveal.in,
.fac.in,
.stagger.roster>.in,
.roster>li.reveal.in {
  animation: plateIn .7s cubic-bezier(.22, 1, .36, 1) forwards;
}

.navlink {
  position: relative;
}

.navlink::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -6px;
  height: 1.5px;
  width: 100%;
  background: #e80202;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .3s cubic-bezier(.22, .61, .36, 1);
}

.navlink:hover::after {
  transform: scaleX(1);
}

.lift {
  transition: transform .35s var(--ease-soft), box-shadow .35s var(--ease-soft);
}

.lift:hover {
  transform: translateY(-3px);
}

/* Faculty roster. The plate is the brand material; the portrait sits on it
     and desaturates at rest so 35 different lighting set-ups read as one wall,
     returning to full colour on hover. */
.fac {
  margin: 0;
}

/* See the home page for why this is flex rather than an auto-fit grid: a
     two-person group left its tiles stranded against the left edge. */
.roster {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: clamp(18px, 2.2vw, 28px);
  list-style: none;
  margin: 0;
  padding: 0;
}

.roster>li {
  width: clamp(9.375rem, 17vw, 12.25rem);
}

/* Names and places align to the tallest name IN THEIR OWN ROW rather than
     every card reserving two lines whether it needs them or not — on a row
     where nothing wraps that reservation was 22px of dead space under the
     name. Tracks are a fixed width, not max-content, or a long name would
     widen its whole column and the cards would stop being uniform. */
/* Below 360px a 150px track cannot hold a long name once text-only zoom is
     turned up; one column is honest there. */
@media (max-width: 359px) {
  .roster {
    grid-template-columns: 1fr !important;
  }

  .roster>li {
    max-width: 240px;
  }
}

@supports (grid-template-rows: subgrid) {
  .roster {
    display: grid;
    justify-content: start;
    grid-template-columns: repeat(auto-fill, clamp(9.375rem, 17vw, 12.25rem));
  }

  /* row-gap: 0 is load-bearing. A subgrid child inherits the parent's
       gutters, so the roster's 28px `gap` — meant to separate CARDS — was also
       being inserted between each card's own three rows. That put 40px between
       a portrait and its name and 29px between the name and the city, none of
       it authored. Overriding the gutter here leaves the spacing to the
       margins below, and the gap BETWEEN cards still comes from the parent. */
  .roster>li {
    width: auto;
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    row-gap: 0;
  }

  .roster>li .fac__name {
    min-height: 0;
  }
}

.fac__name {
  min-height: 2.6em;
}

/* Search. Sits on paper, so the field carries a real border rather than the
     translucent treatment the crimson forms use. */
/* Clears the 73px fixed nav when anything scrolls it into view. */
.fac-find {
  max-width: 480px;
  margin: 0 auto 8px;
  scroll-margin-top: 92px;
}

/* Clears the fixed masthead when the nav menu jumps to a section. */
.roster-sec {
  scroll-margin-top: 88px;
}

.fac-find__label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #e80202;
  margin: 0 0 10px;
  text-align: center;
}

.fac-find__row {
  position: relative;
  display: flex;
  align-items: center;
}

.fac-find__icon {
  position: absolute;
  left: 16px;
  width: 18px;
  height: 18px;
  color: #b8112a;
  pointer-events: none;
}

.fac-find input {
  width: 100%;
  padding: 14px 16px 14px 46px;
  border-radius: 999px;
  border: 1px solid rgba(184, 17, 42, .32);
  background: #fff;
  color: #3a0410;
  font: inherit;
  font-size: 0.9375rem;
}

.fac-find input::placeholder {
  color: #8a6a4f;
  opacity: 1;
}

.fac-find__clear {
  position: absolute;
  right: 8px;
  padding: 8px 16px;
  border: 0;
  border-radius: 999px;
  background: rgba(184, 17, 42, .1);
  color: #e80202;
  font: inherit;
  font-size: 0.84375rem;
  font-weight: 700;
  cursor: pointer;
}

.fac-find__clear:hover {
  background: rgba(184, 17, 42, .18);
}

.fac-find__count {
  text-align: center;
  font-size: 0.875rem;
  color: #7a5c44;
  margin: 12px 0 0;
  min-height: 1.4em;
}

.fac-find__count strong {
  color: #e80202;
}

[data-fac-hidden] {
  display: none !important;
}

@keyframes refiltered {
  from {
    opacity: 0;
  }
}

.is-refiltered {
  animation: refiltered .26s var(--ease-soft) both;
}

.fac__plate {
  position: relative;
  overflow: hidden;
  border-radius: 16px;
  aspect-ratio: 4 / 5;
  background: #e80202;
  box-shadow: 0 10px 26px rgba(122, 5, 18, 0.16);
}

/* The portrait's alpha ramp dissolves it into the plate over the last 30%.
     On a dark suit that lands in crimson on its own; on a white coat it fades
     through a milky pink instead, because what is dissolving is light. This
     tints the dissolve zone back to the plate's own bottom tone so every
     portrait ends in the same material regardless of what the subject wore.
     It also covers the residual saturation step where the alpha finally dies:
     the image renders at saturate(.35) and the plate beneath is full crimson,
     so the two never meet in the same tone by themselves. */
.fac__plate::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 36%;
  pointer-events: none;
  background: linear-gradient(to top, rgba(90, 4, 20, .88) 0%,
      rgba(90, 4, 20, .46) 38%, rgba(90, 4, 20, 0) 100%);
}

.fac__plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  display: block;
  filter: saturate(.35) contrast(1.04);
  transition: filter .45s cubic-bezier(.22, .61, .36, 1), transform .45s cubic-bezier(.22, .61, .36, 1);
}

.fac:hover .fac__plate img {
  filter: saturate(1) contrast(1);
  transform: scale(1.03);
}

.fac__plate--empty {
  display: flex;
  align-items: flex-end;
  padding: 18px;
}

.fac__plate--empty span {
  font-family: 'Spectral', 'Spectral fallback', Georgia, serif;
  font-size: 1.1875rem;
  line-height: 1.25;
  color: #e80202;
}

.fac__name {
  font-family: 'Spectral', 'Spectral fallback', Georgia, serif;
  font-weight: 600;
  font-size: 1.03125rem;
  line-height: 1.3;
  color: #3a0410;
  margin: 16px 0 4px;
}

.fac__place {
  font-size: 0.84375rem;
  color: #5a3a3f;
  margin: 0;
  font-weight: 500;
}

@media (max-width: 1000px) {
  [data-two-col] {
    grid-template-columns: 1fr !important;
  }

  [data-footer-grid] {
    grid-template-columns: 1fr 1fr !important;
  }
}

@media (max-width: 860px) {
  [data-desktop-nav] {
    display: none !important;
  }

  [data-mobile-btn] {
    display: flex !important;
  }
}

@media (max-width: 720px) {
  [data-footer-grid] {
    grid-template-columns: 1fr !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1 !important;
    animation: none !important;
  }
}

/* --- design tokens ------------------------------------------------------ */
/* Generated from DESIGN.md at build time, so the documented palette and the
   shipped one cannot drift. The compiled markup still carries literal
   values inline; these exist for anything written from here on. */
:root {
  --color-contrast-crimson: #b8112a;
  --color-oxblood: #e80202;
  --color-oxblood-deep: #47030d;
  --color-cath-black: #2a0208;
  --color-ceremonial-gold: #e80202;
  --color-lamp-gold: #e80202;
  --color-gold-deep: #cf9a2e;
  --color-gold-ink: white;
  --color-ground-deep: #3d0410;
  --color-oxblood-mid: #5a0414;
  --color-footer-black: #160104;
  --color-paper: #fbf3e8;
  --color-paper-lit: #fdf6ec;
  --color-paper-shade: #f7ead9;
  --color-ink: #3a0410;
  --color-ink-body: #4a2b30;
  --color-ink-muted: #5a3a3f;
  --color-ink-quiet: #7a5c44;
  --color-white: #ffffff;
  --radius-chip: 999px;
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  --radius-hero: 28px;
  --radius-panel: 32px;
  --radius-circle: 50%;
  --space-xs: 8px;
  --space-sm: 14px;
  --space-md: 22px;
  --space-lg: 30px;
  --space-xl: 44px;
  --space-section: clamp(56px, 7vw, 100px);
  --space-gutter: clamp(16px, 4vw, 52px);
}

/* --- hidden panels ----------------------------------------------------- */
/* `hidden` hides an element through a UA rule (`[hidden]{display:none}`), and
   an inline style outranks it. The mobile menu carries `display:flex` inline,
   so the attribute alone left a fixed, full-viewport, z-index:110 overlay
   sitting invisibly over the whole desktop page and eating every click — the
   hero's atlas buttons were being intercepted by the menu's Register link.
   This has to be !important to outrank that inline declaration. */
[data-panel][hidden] {
  display: none !important;
}

/* --- fixed nav legibility ---------------------------------------------- */
/* The nav is translucent (rgba(43,3,9,.55) -> .3) over a backdrop blur. That
   reads beautifully over the crimson hero, but the page below it is cream, and
   measured from rendered pixels the links fell to 2.17-2.49:1 there — WCAG AA
   wants 4.5:1, and even large text wants 3:1. The tint is deepened so the
   plate stays dark whatever passes under it; the gradient, the blur and the
   saturation boost are all kept, so the treatment still reads as glass.
   Re-measured after this change: 4.9-5.6:1 over cream, 8-9:1 over the hero.
   Inline styles outrank a stylesheet, hence !important. */
nav[style] {
  background: linear-gradient(135deg, rgba(43, 3, 9, 0.88), rgba(43, 3, 9, 0.64)) !important;
}

/* --- motion layer ------------------------------------------------------ */
/* One easing for the whole system. Ease-out-quint: decisive, no bounce. */
:root {
  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.22, .61, .36, 1);
}

/* Press feedback. There was none anywhere on the site: every control had a
   hover state and then nothing happened when you actually pressed it. 90ms,
   under the ~80-100ms threshold where feedback still reads as instant. */
a[class*="lift"],
button,
.fac,
[data-action] {
  -webkit-tap-highlight-color: transparent;
}

a[class*="lift"]:active,
button[data-action]:active {
  transform: translateY(-1px) scale(.985);
  transition-duration: .09s;
}

/* List choreography. Staggering siblings within one list is legitimate;
   the same fade on every section is the AI tell. Only real lists get this,
   and the stagger is capped so a 35-name roster does not take 2 seconds. */
/* ------------------------------------------------------------- scrollbar
   Styling ::-webkit-scrollbar turns it into a CLASSIC, space-taking scrollbar.
   That makes it a layout decision, not a cosmetic one: a page that styles it
   is 11px narrower than a page that does not, and every centred thing on it —
   including the fixed masthead's link row — sits 11px further left.

   These rules were pasted into seven page <style> blocks and missed by the
   eighth, so past-editions rendered with a 0px overlay scrollbar and its whole
   nav shifted right by 11px against every other page. The home page was a
   third value again at 10px. One declaration here, one width everywhere. */
::-webkit-scrollbar {
  width: 11px;
}

::-webkit-scrollbar-track {
  background: #f4e7d8;
}

::-webkit-scrollbar-thumb {
  background: linear-gradient(#b8112a, #e80202);
  border-radius: 999px;
  border: 2px solid #f4e7d8;
}

/* ------------------------------------------------------------------- base
   These were duplicated verbatim in every page's own <style> block, which
   means a page that does not repeat them gets none of them: the new
   past-editions page fell back to the browser's default serif for its body,
   nav and footer, and every link kept its underline. Same failure as the
   riseIn keyframes below. Defining them once here makes them structural
   rather than something each new page has to remember.
   A page may still override any of it — page CSS is emitted after this. */
body {
  margin: 0;
  font-family: 'Public Sans', 'Public Sans fallback', system-ui, sans-serif;
  color: #4a2b30;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a {
  color: #b8112a;
  text-decoration: none;
  transition: color .2s ease;
}

a:hover {
  color: #e80202;
}

::selection {
  background: #b8112a;
  color: #fff;
}

/* The keyframes live here, not only in each page's own <style>. `.stagger > *`
   sets opacity:0 and relies on `riseIn` to bring it back — and an animation
   naming a keyframe that does not exist on that page simply does nothing, so
   the opacity:0 sticks and the content ships invisible. That is how the new
   past-editions page shipped 29 photographs nobody could see. Defining them in
   the shared sheet means a page cannot forget them. */
@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.stagger>* {
  opacity: 0;
  animation: riseIn .55s var(--ease) forwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}

.stagger.in>* {
  opacity: 0;
}

/* ...but a wall of faces is not a list of sentences. The same 12px rise on
   every revealed thing — 34 elements on the home page, 44 on the faculty page —
   is the uniform reflex the brand register calls a tell. Portraits settle in
   place instead: a shallow scale, no vertical travel, a slightly longer curve
   so 44 faces resolve calmly rather than marching up the screen. */
/* The `to` is not optional. `.stagger > *` sets opacity:0, and with
   fill-mode forwards an animation with only a `from` settles on the
   element's own style — i.e. invisible. Written without it, 42 portraits
   finished their entrance and stayed blank. */
@keyframes plateIn {
  from {
    opacity: 0;
    transform: scale(.965);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.stagger.roster>* {
  animation-name: plateIn;
  animation-duration: .7s;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}

/* Hover: everything that responds does so on the same curve and duration, so
   the page feels like one material rather than a collection of components. */
a,
button,
.fac__plate img,
.comp,
.lift {
  transition-timing-function: var(--ease-soft);
}

/* The mobile menu appeared instantly — no transition at all. It now fades and
   settles 8px. `hidden` cannot be transitioned, so js/app.js drives an
   `is-open` class and only sets `hidden` after the exit finishes. */
[data-panel="menu"] {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity .26s var(--ease-soft), transform .26s var(--ease-soft);
}

[data-panel="menu"].is-open {
  opacity: 1;
  transform: none;
}

/* The registration thank-you swapped in instantly. Exit is ~75% of entrance. */
[data-panel="register-success"] {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .34s var(--ease), transform .34s var(--ease);
}

[data-panel="register-success"].is-open {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .stagger>* {
    opacity: 1 !important;
    animation: none !important;
  }

  [data-panel] {
    transition: none !important;
    transform: none !important;
  }

  a[class*="lift"]:active,
  button[data-action]:active {
    transform: none;
  }
}

/* --- notch and rounded corners ------------------------------------------ */
/* `env()` only resolves once the viewport opts in with viewport-fit=cover.
   Without both, content slides under the notch and the home indicator in
   landscape on every modern iPhone. `max()` keeps the existing gutter on
   devices that report no inset. */
body {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

nav,
[data-panel="menu"] {
  padding-left: max(0px, env(safe-area-inset-left));
  padding-right: max(0px, env(safe-area-inset-right));
}

footer {
  padding-bottom: max(28px, env(safe-area-inset-bottom));
}

/* --- skip link -------------------------------------------------------- */
/* The page opens with a fixed nav and a full-height hero; without this a
   keyboard user tabs the whole navigation before reaching any content. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
}

.skip-link:focus {
  left: 12px;
  top: 12px;
  width: auto;
  height: auto;
  padding: 12px 20px;
  background: #e80202;
  color: #e80202;
  border-radius: 0 0 14px 14px;
  font-weight: 700;
  font-size: 0.9375rem;
}

/* --- focus visibility -------------------------------------------------- */
/* The design styles hover but never focus. Every control here sits on either
   a crimson or a cream ground, so the ring is drawn in gold with a dark
   halo — legible on both without a second rule. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
#main:focus,
[data-panel="register-success"]:focus {
  outline: 3px solid #e80202;
  outline-offset: 3px;
  border-radius: 6px;
}

/* The form controls were the only interactive elements without the ring: the
   design gave them a 1px border tint that measured 1.43:1 against the ground,
   where WCAG 1.4.11 needs 3:1. The inline `outline:none` on each control is
   what suppressed it, so this has to out-specify that.
   The two programmatic targets are named individually rather than matched as
   `[tabindex="-1"]:focus`. That blanket selector also caught the complication
   map's points: they use a roving tabindex, so every point except the current
   one carries `tabindex="-1"`, and `:focus` matches on a plain MOUSE click
   where `:focus-visible` does not. Clicking a dot therefore drew a 3px ring
   with a 3px offset around the whole point — a ~136x33 gold rectangle beside
   the dot, including its invisible label box, which stayed after the pointer
   left. That is what read as the dot moving on click. The points are
   `<button>`s, so `button:focus-visible` still rings them for the keyboard.
   The thank-you panel takes focus programmatically and was otherwise painting
   Chrome's default BLUE ring on drenched crimson — the only fourth hue on the
   site, on the last screen a converting reader sees. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid #e80202 !important;
  outline-offset: 2px !important;
}

/* An iframe is a tab stop the moment it exists, and it does not inherit the
   rule above. Once the map is loaded a keyboard reader lands on it with no
   indication of where they are.
   `:focus` is the wrong selector and silently never matches: focusing an
   iframe moves focus INTO its browsing context, so the element itself is not
   `:focus` even while `document.activeElement` returns it. `:focus-within`
   does match, cross-origin included. Inset so the ring is not clipped by the
   rounded card the frame sits in. */
iframe:focus,
iframe:focus-within,
iframe:focus-visible {
  outline: 3px solid #e80202;
  outline-offset: -3px;
}

/* --- map facade --------------------------------------------------------- */
/* The Google embed was 822KB across 37 third-party requests on contact.php —
   91% of the page weight, loaded before the reader had scrolled, for a page
   whose own content is 62KB. It also sets Google's cookies on arrival.
   The facade carries the address and a maps link with no third-party contact
   at all, and loads the embed only when someone asks for it. */
/* ------------------------------------------------------------ back to top
   Sits clear of the safe-area inset so it is not under a phone's home bar. It
   is `hidden` in the markup and revealed by script once the page has been
   scrolled, so with scripting off it never appears rather than appearing and
   doing nothing. */
.totop {
  position: fixed;
  z-index: 90;
  right: max(18px, env(safe-area-inset-right));
  bottom: max(18px, env(safe-area-inset-bottom));
  width: 48px;
  height: 48px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, .34);
  background: #e80202;
  color: white;
  box-shadow: 0 10px 26px rgba(58, 4, 16, .34);
  opacity: 0;
  translate: 0 12px;
  transition: opacity .35s var(--ease-soft), translate .45s var(--ease),
    box-shadow .3s var(--ease-soft);
}

.totop[hidden] {
  display: none;
}

.totop.is-on {
  opacity: 1;
  translate: 0 0;
}

.totop:hover {
  box-shadow: 0 16px 34px rgba(58, 4, 16, .44);
}

/* The icon set only has a right-pointing arrow; this is the same glyph turned
   to face up rather than a second nearly-identical path in the defs. */
.totop svg {
  width: 20px;
  height: 20px;
  rotate: -90deg;
}

@media (prefers-reduced-motion: reduce) {
  .totop {
    transition: opacity .2s var(--ease-soft);
    translate: none;
  }
}

/* ------------------------------------------------------------- arrow links
   Every link that ends in the arrow glyph slides it on hover. This was only
   wired up on the explore cards, so "Meet the faculty", "Read more about the
   course", "Venue & contact details" and every `.lift` button sat inert while
   the four cards beside them moved.

   `:has()` rather than a class on each link: the rule then applies to any link
   that ends in an arrow, including ones added later, and there is no markup to
   remember. `translate` is an independent property, so it cannot be clobbered
   by an entrance animation that fills `transform` — which is how `.lift` and
   the card hovers shipped dead. It is also not a layout property, so the row
   the link sits in does not reflow.

   `.ways__go` keeps its own rule; the values agree, so the two never disagree
   about how far the arrow travels. */
a:has(svg use[href="#ic-arrow"]) svg {
  transition: translate .45s var(--ease);
}

a:has(svg use[href="#ic-arrow"]):hover svg,
a:has(svg use[href="#ic-arrow"]):focus-visible svg {
  translate: 4px 0;
}

@media (prefers-reduced-motion: reduce) {
  a:has(svg use[href="#ic-arrow"]) svg {
    transition: none;
  }

  a:has(svg use[href="#ic-arrow"]):hover svg,
  a:has(svg use[href="#ic-arrow"]):focus-visible svg {
    translate: none;
  }
}

/* ------------------------------------------------------------------ footer
   The footer links carried a fixed alpha and no hover state, so nothing on the
   surface said they were live until the cursor changed. They now lift to full
   white, which also puts them clear of the skyline behind them. */
.foot-link {
  color: rgba(255, 255, 255, .72);
  transition: color .25s var(--ease-soft);
}

.foot-link:hover,
.foot-link:focus-visible {
  color: #fff;
}

.foot-link--sm {
  color: rgba(255, 255, 255, .78);
}

.foot-link--sm:hover,
.foot-link--sm:focus-visible {
  color: #fff;
}

/* inline-flex, not inline-block: the build appends an external-link marker to
   every off-site link, and against a block image it dropped onto its own line
   under the logo. */
.foot-pco {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  transition: opacity .25s var(--ease-soft);
  opacity: .9;
}

.foot-pco:hover,
.foot-pco:focus-visible {
  opacity: 1;
}

.foot-pco img {
  width: 150px;
  height: auto;
  display: block;
}

/* ---------------------------------------------------------------- skyline
   Chennai, as a watermark along the bottom of every dark header and the
   footer. tools/make_skyline.py strips the client's white card to alpha,
   colours the ink gold and bakes the upward fade into the alpha channel, so
   this is an ordinary background image.

   It began as a CSS `mask-image`, which is tidier — the tint would come from
   the page — but a mask is a cross-origin local read, so it is blocked when
   the built HTML is opened straight off disk. Reviewing that way is exactly
   what the client does, and the skyline was simply invisible to them. A
   background image has no such restriction and needs no vendor prefixes.

   `aspect-ratio` matches the artwork exactly (2200x421); the band is anchored
   to the bottom, so any mismatch would crop the temple's tower first. It is
   decorative, so it is aria-hidden and never a tab stop. */
.skyline {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  width: 100%;
  aspect-ratio: 2200 / 421;
  pointer-events: none;
  background-image: url(../assets/skyline-chennai.png);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: bottom center;
  opacity: .30;
}

/* The footer ground is nearly black, so the same strength that reads as a
   watermark on the hero's crimson disappears against it. */
.skyline--footer {
  opacity: .24;
}

/* --------------------------------------------------------------- masthead
   Solid brand material rather than glass. The bar is the crimson the site is
   built from, so the header belongs to the page instead of floating over it,
   and there is no backdrop-filter repainting on every scroll frame. */
.mast {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: black;
  border-bottom: 1px solid rgba(247, 217, 138, .26);
  box-shadow: 0 6px 28px rgba(28, 0, 6, .34);
}

.mast__strip {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  height: 32px;
  overflow: hidden;
  border-bottom: 1px solid rgba(255, 255, 255, .08);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: #e80202;
  transition: height .45s cubic-bezier(.22, 1, .36, 1), opacity .3s ease;
}

.mast__sep {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(228, 184, 74, .6);
}

.mast__where {
  color: rgba(255, 255, 255, .66);
}

/* Compacted state: the dates have been read by the time you are scrolling. */
.mast.is-scrolled .mast__strip {
  height: 0;
  opacity: 0;
  border-bottom-color: transparent;
}

.mast__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 11px clamp(16px, 4vw, 52px);
  transition: padding .45s cubic-bezier(.22, 1, .36, 1);
}

.mast.is-scrolled .mast__bar {
  padding-top: 8px;
  padding-bottom: 8px;
}

.mast__brand {
  display: flex;
  align-items: center;
  flex: none;
  padding: 6px 11px;
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, .78));
}

.mast__brand img {
  height: 31px;
  width: auto;
  display: block;
}

.mast__links {
  display: flex;
  align-items: center;
  gap: 30px;
}

/* The link underline is drawn from the centre out, and the active page keeps
   it drawn. transform only — animating width would reflow the whole row. */
.mast__link {
  position: relative;
  color: rgba(255, 255, 255, .86);
  font-weight: 600;
  font-size: 0.90625rem;
  letter-spacing: .01em;
  padding: 6px 0;
}

.mast__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.5px;
  background: #e80202;
  transform: scaleX(0);
  transition: transform .4s var(--ease);
}

.mast__link:hover {
  color: #fff;
}

.mast__link:hover::after {
  transform: scaleX(1);
}

/* While the pointer (or focus) is anywhere in the item — including down inside
   the open panel — the parent stays lit and its rule stays drawn. Otherwise the
   heading you opened goes dark the moment you move onto its own sub-items and
   nothing on screen says which section you are in. */
.mast__item:hover>.mast__link,
.mast__item:focus-within>.mast__link {
  color: #e80202;
}

.mast__item:hover>.mast__link::after,
.mast__item:focus-within>.mast__link::after {
  transform: scaleX(1);
}

/* The active item keeps the SAME weight as the rest. At 700 against 600 its
   label is wider, so every other link in the row moved as you navigated —
   up to 2.2px of drift between pages, which reads as the whole nav twitching.
   Gold plus the drawn rule already say which page you are on; the weight
   change only ever cost layout stability. */
.mast__link.is-on {
  color: #e80202;
}

.mast__link.is-on::after {
  transform: scaleX(1);
}

/* Deliberately not a pill: a squared plate reads as masthead furniture rather
   than a floating SaaS button, and it distinguishes the persistent header CTA
   from the in-page ones. */
/* Section menu on the faculty item. Hover OR focus-within, so it is reachable
   with a keyboard; a hover-only menu simply does not exist for a tab user.
   Opacity + visibility rather than display:none, so the open and close can be
   eased and the links stay out of the tab order while it is shut. */
/* The item's own hover area is extended downwards over the gap between the
   link and the panel. Without it the pointer crosses 16px of dead space on the
   way to the menu, :hover is lost, and the menu shuts before you arrive —
   which is exactly what happened with only a 14px bridge on the panel. */
.mast__item {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding-bottom: 18px;
  margin-bottom: -18px;
}

.mast__caret {
  display: none;
  color: rgba(255, 255, 255, .62);
  line-height: 0;
  transition: rotate .3s var(--ease), color .3s var(--ease-soft);
}

.mast__caret svg {
  width: 13px;
  height: 13px;
  display: block;
}

.mast__item[data-has-menu="1"] .mast__caret {
  display: block;
}

.mast__item:hover .mast__caret,
.mast__item:focus-within .mast__caret {
  rotate: 180deg;
  color: #e80202;
}

.mast__menu {
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% 0;
  margin-top: 12px;
  min-width: 17rem;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-radius: 14px;
  background: #fbf3e8;
  border: 1px solid rgba(184, 17, 42, .16);
  box-shadow: 0 18px 44px rgba(28, 0, 6, .34);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .26s var(--ease-soft), translate .26s var(--ease), visibility .26s;
}

.mast__item[data-has-menu=""] .mast__menu {
  display: none;
}

.mast__item:hover .mast__menu,
.mast__item:focus-within .mast__menu {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  translate: -50% 4px;
}

/* The gap between the link and the panel would drop the hover halfway across
   it; this bridges it so the menu does not flicker shut on the way down. */
.mast__menu::before {
  content: '';
  position: absolute;
  left: -12px;
  right: -12px;
  top: -22px;
  height: 22px;
}

.mast__menu-link {
  padding: 9px 12px;
  border-radius: 9px;
  color: #5a3a3f;
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.3;
}

.mast__menu-link:hover,
.mast__menu-link:focus-visible {
  background: rgba(184, 17, 42, .09);
  color: #e80202;
}

/* Mobile menu sub-items. */
.mast__sub {
  color: rgba(255, 255, 255, .72);
  font-size: 1rem;
  font-weight: 600;
  padding: 9px 0 9px 18px;
  border-left: 1px solid rgba(247, 217, 138, .3);
  margin-left: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .mast__menu {
    transition: none;
  }
}

.mast__cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 20px;
  border-radius: 10px;
  font-weight: 700;
  font-size: 0.90625rem;
  color: white;
  background: #e80202;
  box-shadow: 0 6px 18px rgba(228, 184, 74, .32);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease-soft);
}

.mast__cta svg {
  width: 16px;
  height: 16px;
}

.mast__cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 26px rgba(228, 184, 74, .42);
}

.mast__burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  cursor: pointer;
  border: 1px solid rgba(255, 255, 255, .24);
  background: rgba(255, 255, 255, .1);
  color: #fff;
}

@media (max-width: 860px) {
  .mast__strip {
    font-size: 0.6875rem;
    letter-spacing: .08em;
    gap: 10px;
  }
}

@media (prefers-reduced-motion: reduce) {

  .mast__strip,
  .mast__bar,
  .mast__link::after,
  .mast__cta {
    transition: none;
  }
}

/* ------------------------------------------------------------------ cards
   One interaction for every card family on the site: it rises on hover and
   falls back, and the surface trades fill for outline as it does. Two skins
   because the site has two materials — paper cards on the light sections,
   outline cards on the crimson ones.

   Only transform, box-shadow, background and border move; no layout property
   is animated, so a grid of twenty of these does not reflow on hover. The
   fall is slower than the rise, which is what stops it feeling springy. */
.card {
  position: relative;
  border-radius: 24px;
  transition: translate .5s cubic-bezier(.22, 1, .36, 1),
    box-shadow .5s cubic-bezier(.22, 1, .36, 1),
    background-color .4s ease, border-color .4s ease;
}

.card--paper {
  background: linear-gradient(155deg, rgba(255, 255, 255, .9), rgba(255, 255, 255, .6));
  border: 1px solid rgba(255, 255, 255, .85);
  box-shadow: 0 16px 42px rgba(122, 5, 18, .1);
}

.card--paper:hover,
.card--paper:focus-within {
  translate: 0 -6px;
  border-color: rgba(184, 17, 42, .34);
  box-shadow: 0 28px 62px rgba(122, 5, 18, .17);
}

.card--outline {
  background: rgba(255, 255, 255, .045);
  border: 1px solid rgba(255, 255, 255, .17);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}

.card--outline:hover,
.card--outline:focus-within {
  translate: 0 -6px;
  background: rgba(255, 255, 255, .105);
  border-color: rgba(247, 217, 138, .46);
  box-shadow: 0 26px 56px rgba(20, 0, 4, .34);
}

/* The rise is the whole effect, so on reduced motion the surface still
   answers — it just does not move. */
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: background-color .2s ease, border-color .2s ease;
  }

  .card--paper:hover,
  .card--paper:focus-within,
  .card--outline:hover,
  .card--outline:focus-within {
    translate: none;
  }
}

/* An "announced soon" panel: copy left at a readable measure, status right. */
.note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 3vw, 48px);
  flex-wrap: wrap;
}

.note__text {
  font-family: 'Spectral', 'Spectral fallback', Georgia, serif;
  font-size: clamp(1.125rem, 1.7vw, 1.375rem);
  line-height: 1.6;
  color: #4a2b30;
  margin: 0;
  flex: 1 1 28rem;
  min-width: 0;
  max-width: 46rem;
  text-wrap: pretty;
}

.note .chip-soon {
  flex: none;
  align-self: center;
}

@media (max-width: 700px) {
  .note {
    align-items: flex-start;
  }

  .note .chip-soon {
    align-self: flex-start;
  }
}

/* Pending status. Reads as a state, not a button, so nobody clicks it. */
.chip-soon {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: .01em;
  color: #e80202;
  background: rgba(184, 17, 42, .09);
  border: 1px solid rgba(184, 17, 42, .22);
}

.chip-soon::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #b8112a;
  flex: none;
}

.mapf {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 28px;
  text-align: center;
}

.mapf__where {
  margin: 0;
  font-family: 'Spectral', 'Spectral fallback', Georgia, serif;
  font-size: 1.25rem;
  line-height: 1.4;
  color: #3a0410;
}

.mapf__where span {
  display: block;
  margin-top: 6px;
  font-family: 'Public Sans',
    'Public Sans fallback', system-ui, sans-serif;
  font-size: 0.9375rem;
  color: #5a3a3f;
}

.mapf__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  align-items: center;
}

.mapf__load {
  padding: 12px 22px;
  border-radius: 999px;
  border: 0;
  cursor: pointer;
  background: #b8112a;
  color: #fff;
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 700;
  transition: background .2s var(--ease-soft, ease);
}

.mapf__load:hover {
  background: #e80202;
}

.mapf__acts a {
  font-size: 0.9375rem;
  font-weight: 600;
}

.mapf__note {
  margin: 0;
  max-width: 34ch;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: #7a5c44;
}

.mapf iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The always-on embed. It fills the card it sits in; the overlay link sits
   above it so "Open in Google Maps" survives the iframe swallowing clicks. */
.mapf__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Top-right, not bottom-left: Google's own attribution and logo sit along the
   bottom edge of the embed and must not be covered. */
.mapf__open {
  position: absolute;
  right: 14px;
  top: 14px;
  z-index: 2;
  padding: 10px 18px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .96);
  color: #e80202;
  font-size: 0.875rem;
  font-weight: 700;
  box-shadow: 0 6px 18px rgba(58, 4, 16, .22);
}

.mapf__open:hover {
  background: #fff;
}

@media (pointer: coarse) {
  .mapf__open {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

@media (pointer: coarse) {
  .mapf__load {
    min-height: 44px;
  }
}

/* --- metric-matched font fallbacks -------------------------------------- */
/* The entire layout shift on this site was the font swap. Measured on a
   throttled connection: CLS 0.1397 with fonts, 0 with fonts blocked, and
   unchanged with images blocked. Spectral sets a line 152px tall where Georgia
   sets 114px at the same size, so every block below the h1 moved when the web
   font arrived.
   These faces re-describe the local fallbacks with Spectral's and Public
   Sans's own metrics, so the fallback occupies the same space and the swap
   costs nothing. Values were tuned against measured block heights, not
   guessed. */
@font-face {
  font-family: 'Spectral fallback';
  src: local('Georgia'), local('Times New Roman'), local('Times');
  size-adjust: 100.25%;
  ascent-override: 107.9%;
  descent-override: 43.7%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Public Sans fallback';
  src: local('Arial'), local('Helvetica Neue'), local('Helvetica');
  size-adjust: 104.31%;
  ascent-override: 88.5%;
  descent-override: 24.6%;
  line-gap-override: 0%;
}

/* --- touch input -------------------------------------------------------- */
/* Screen width does not tell you the input method: a laptop can have a touch
   screen and a tablet can have a mouse. `pointer: coarse` asks the real
   question. Targets that clear WCAG 2.5.8 via the spacing exception are still
   uncomfortable under a thumb, so on a coarse pointer they get the full 44px
   the platform guidelines ask for. */
@media (pointer: coarse) {
  .cmap__pt {
    min-height: 44px;
    padding-top: 10px;
    padding-bottom: 10px;
  }

  .faq__item summary {
    padding-top: 26px;
    padding-bottom: 26px;
  }

  footer a,
  .ways__go,
  a.navlink {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .fac-find__clear {
    min-height: 40px;
  }
}

/* Hover is not available on touch, so nothing may depend on it. The map
   previews on hover and selects on tap; this makes the tap state visible
   rather than leaving touch users with only the pressed dot to go on. */
@media (hover: none) {
  .cmap__pt:active .cmap__dot {
    background: #e80202;
    transform: scale(1.2);
  }

  .lift:active {
    transform: translateY(-1px) scale(.99);
  }
}

/* --- words longer than their container ---------------------------------- */
/* At a large root font a single word can be wider than the column holding it —
   "complications" measured 376px inside a 288px box at a 24px root, and no
   amount of wrapping helps because there is nowhere to break. `break-word`
   only splits a word that genuinely does not fit, so ordinary prose is
   untouched; `hyphens: auto` makes the split read as typesetting rather than
   as damage. This is what the overflow at 320px was, not a layout bug. */
h1,
h2,
h3,
h4,
p,
li,
dd,
figcaption,
summary,
.fac__name,
.ways__title {
  overflow-wrap: break-word;
  hyphens: auto;
}

/* --- long unbreakable tokens -------------------------------------------- */
/* An email address, an Instagram handle and a URL have no spaces, so on a
   narrow card they cannot wrap and simply push out of it — taking the
   external-link glyph beside them past the viewport edge. Only applied where
   such tokens actually live, not globally, since `anywhere` would let ordinary
   prose break mid-word. */
a[href^="mailto:"],
a[href^="tel:"],
a[href*="instagram"],
.fac-find__count,
.faq__a {
  overflow-wrap: anywhere;
}

/* --- grid overflow ------------------------------------------------------ */
/* Grid and flex items default to `min-width: auto`, which means an item is
   never allowed to be narrower than its own content — so one wide child can
   push a track past its container and the whole page sideways. Invisible at a
   16px root; at a 24px root the contact map's track resolved to 434px inside a
   358px column. This is the standard fix and it costs nothing. */
/* Three fixed columns cannot hold their own content once the reader turns the
   font up: the tier cards fell to 154px wide holding text needing 257px. An
   auto-fit track with a rem minimum collapses the row when the type outgrows
   it, instead of squeezing. */
[data-three-col] {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)) !important;
}

[data-two-col]>*,
[data-three-col]>*,
[data-four-col]>*,
[data-hero-grid]>*,
[data-footer-grid]>*,
.sec-head>*,
.chair>*,
.ways>*,
.roster>*,
.comp>*,
.fac-find__row>*,
.hero-cd>* {
  min-width: 0;
}

/* --- links that leave the site ----------------------------------------- */
/* The cue is an icon AND text: an icon alone is not a signal for a reader who
   cannot see it, and text alone is noise for a reader who can. */
.ext-mark {
  width: .78em;
  height: .78em;
  margin-left: .34em;
  vertical-align: -.04em;
  opacity: .75;
  flex-shrink: 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --- placeholders ------------------------------------------------------ */
/* Measured from rendered pixels at .45 alpha: 3.30 / 3.43 / 3.37:1 on the
   three fields that carry guidance, against a 4.5:1 floor. These are not
   decorative — they hold the only format hint each field has. */
::placeholder {
  color: rgba(255, 255, 255, .72) !important;
  opacity: 1;
}

/* --- text wrapping ----------------------------------------------------- */
h1,
h2,
h3 {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

/* --- reduced motion ---------------------------------------------------- */
/* The design silences the marquee and the ECG trace. These are the rest:
   the floating atlas card, the pulsing hero bloom, the spinning dashed rings
   and the reveal animation all run indefinitely or move a long distance. */
@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    /* Smooth scrolling is precisely the vestibular trigger this query exists
       for, and it was still on: every in-page anchor and the skip link glided
       the whole document. It is not covered by animation or transition. */
    scroll-behavior: auto !important;
  }

  html {
    scroll-behavior: auto !important;
  }

  .reveal {
    opacity: 1 !important;
  }
}