:root{
  --navy: #394763;
  --navy-dark: #1b2130;
  --navy-mid: #232a3d;
  --teal: #83c0c0;
  --teal-dark: #4b8686;
  --sage: #acd1be;
  --orange: #f2a35c;
  --orange-deep: #dd7e34;
  --ink: #e9edf2;
  --muted: #b3bfd2;
  --stage-bg: #141823;
  --e3: 0 20px 50px rgba(0,0,0,.4);
  --font-head: "Poppins", system-ui, sans-serif;
  --font-body: "Lato", system-ui, sans-serif;

  /* Console footprint per breakpoint, used to give every panel's content
     box the exact same height (and therefore identical vertical centering)
     regardless of which panel it is. See .panel-inner below. */
  --console-h: 156px;      /* 3*48px cells + 2*6px gaps */
  --console-bottom: 8dvh;
  --console-clear: 16px;   /* breathing room between content and console */

  /* Gallery-card base metrics (single :root; the breakpoint overrides
     that scale these live in the media queries further down). */
  --gcard-w: 260px; --gcard-gap: 14px; --gcard-visible: 1;
}

*{ box-sizing:border-box; -webkit-tap-highlight-color: transparent; }
html,body{ height:100%; margin:0; overflow:hidden; overscroll-behavior:none; }
body{ font-family: var(--font-body); background: var(--stage-bg); color: var(--ink); }
button{ font-family:inherit; }
a{ color:inherit; text-decoration:none; }

.stage{ position:fixed; inset:0; height:100dvh; width:100dvw; overflow:hidden; }

/* ============ mesh overlay ============ */
/* A thin "mosquito net" crosshatch drifting slowly upward over the navy
   background. It's now a first child INSIDE each panel (not a global
   fixed layer), so paint order naturally puts it above that panel's own
   background but below panel-inner's real content — no z-index needed,
   just DOM order within the same stacking context. Color is the
   gradient's own darkest navy, so the mesh reads as a faint sheen rather
   than a visible foreign-colored grid. pointer-events:none keeps every
   link/button beneath it fully clickable.
   The drift itself lives on a ::after pseudo-element and animates
   `transform` instead of background-position — transform runs on the
   compositor, so the five always-running overlays no longer force a
   repaint every frame. The pseudo overhangs the panel by one tile
   (32px) top and bottom and translates up exactly one tile-height, so
   the loop is seamless and no edge ever shows; the parent's
   overflow:hidden clips the overhang. */
.mesh-overlay{
  position: absolute; inset: 0; pointer-events: none;
  overflow: hidden;
}
.mesh-overlay::after{
  content: "";
  position: absolute; inset: -32px 0;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2732%27%20height%3D%2732%27%3E%3Cpath%20d%3D%27M0%200L32%2032M32%200L0%2032%27%20stroke%3D%27%231b2130%27%20stroke-width%3D%271%27%20stroke-opacity%3D%270.6%27/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 32px 32px;
  animation: meshDrift 8s ease-in-out infinite;
  will-change: transform;
}
@keyframes meshDrift{
  0%   { transform: translateY(0); }
  100% { transform: translateY(-32px); }
}

/* ============ panels ============ */
.panels-layer{
  position:absolute; inset:0; z-index: 1;
  /* Standing transition so the "camera pull-back" (overview mode
     below) animates the scale smoothly in both directions without
     needing an inline style bump. */
  transition: transform .55s cubic-bezier(.19,1,.22,1);
}
.panel{
  position:absolute; inset:0;
  transition: transform .6s cubic-bezier(.19,1,.22,1), opacity .5s ease;
  will-change: transform, opacity;
}
.panel.is-top{ z-index: 5; }

#panel-home{ transform: translate(0,0); z-index: 3; }
#panel-left{ transform: translateX(100%); }
#panel-right{ transform: translateX(-100%); }
#panel-up{ transform: translateY(100%); }
#panel-down{ transform: translateY(-100%); }
/* Privacy rests above the viewport like the card — it's the next step in the
   same downward press direction (Home -> Card -> Privacy). */
#panel-privacy{ transform: translateY(-100%); }

/* Mobile opens on the business card instead of Home — matches script.js's
   initial `current` value below the same 900px line used everywhere else
   in this file. Home's resting position (translateY(100%)) is exactly
   what pressing the Home button from the card would animate it in from
   anyway (see HOME_DIRS.down in script.js), so the very first transition
   still looks correct. */
@media (max-width: 899px){
  #panel-home{ transform: translateY(100%); z-index: auto; }
  #panel-down{ transform: translate(0,0); z-index: 3; }
}

/* Mid-tone stop moved earlier (was 60%/55%) so the gradient reaches its
   lighter blue sooner — the navy-dark mesh color stays legible against
   roughly half the gradient now instead of getting lost after a third. */
#panel-home{ background: linear-gradient(160deg, var(--navy-dark), var(--navy-mid) 35%, var(--navy)); }
#panel-left{ background: linear-gradient(200deg, var(--navy-dark), var(--navy-mid) 35%, var(--navy)); }
#panel-right{ background: linear-gradient(120deg, var(--navy-dark), var(--navy-mid) 35%, var(--navy)); }
#panel-up{ background: linear-gradient(210deg, var(--navy-dark), #202840 32%, var(--navy)); }
#panel-down{ background: linear-gradient(150deg, var(--navy-dark), #202840 32%, var(--navy)); }
#panel-privacy{ background: linear-gradient(170deg, var(--navy-dark), var(--navy-mid) 35%, var(--navy)); }

/* Every panel's content box is the SAME height (viewport minus the
   console's footprint, offset and a clearance margin) so all five
   panels center their content identically between the top of the
   viewport and the top of the console — regardless of panel type. */
.panel-inner{
  position:absolute; top:0; left:0; right:0;
  height: calc(100dvh - var(--console-bottom) - var(--console-h) - var(--console-clear));
  display:flex; align-items:center; justify-content:center;
  padding: 4dvh 6vw 0;
  text-align:center;
}
.panel-copy{ max-width: 920px; }

/* ============ breadcrumb (About / Expertise / Projects) ============ */
/* Keeps the domain — my name — visible on every panel that isn't Home
   or the card, since those two already show it another way. */
.breadcrumb{
  display:flex; align-items:center; justify-content:center; gap: 8px;
  font-family: var(--font-head); font-weight:400;
  font-size: clamp(12px,1.3vw,14px);
  margin: 0 0 14px;
}
.crumb-domain{ color: var(--muted); font-weight:500; }
.crumb-sep{ color: var(--muted); opacity:.55; font-weight:400; }
.crumb-current{
  color: var(--orange); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
}
.expertise-inner .breadcrumb{ margin-bottom: 18px; }

.eyebrow{
  font-family: var(--font-head); font-weight:600; letter-spacing:.09em; text-transform:uppercase;
  font-size: clamp(13px,1.4vw,16px); color: var(--orange); margin: 0 0 16px;
}
.headline{
  font-family: var(--font-head); font-weight:700; color:#fff;
  font-size: clamp(30.6px, 5.76vw, 75.6px);
  line-height: 1.08; margin: 0 0 22px; letter-spacing:-.01em;
}
.roles{
  font-size: clamp(15px, 2vw, 20px); letter-spacing:.05em; text-transform:uppercase;
  color: var(--teal); font-weight:600; margin: 0 0 22px;
}
.summary{
  font-size: clamp(16px, 2.1vw, 23px); line-height:1.55; color: var(--muted);
  max-width: 62ch; margin: 0 auto;
}

/* ============ projects gallery (Up) ============ */
.gallery-inner{ flex-direction:column; }
/* Wrapper around the heading + sub-line. Transparent to layout by default
   (display:contents) so both behave exactly as before; only the landscape-
   phone view below turns it into a one-line flex row. */
.gallery-titleline{ display: contents; }
.gallery-heading{
  font-family: var(--font-head); font-weight:700; color:#fff;
  font-size: clamp(24px, 4vw, 38px); margin: 0 0 10px;
}
.gallery-sub{ color: var(--muted); font-size: clamp(13px,1.6vw,15px); margin: 0 0 28px; }

.gallery-viewport{
  overflow: hidden;
  width: calc(var(--gcard-w, 260px) * var(--gcard-visible, 1) + var(--gcard-gap, 14px) * (var(--gcard-visible, 1) - 1));
  max-width: 92vw;
}
.gallery-track{
  display:flex;
  align-items: stretch;
  gap: var(--gcard-gap, 14px);
  transition: transform .45s cubic-bezier(.19,1,.22,1);
}
.gcard{
  flex: 0 0 var(--gcard-w, 260px);
  display:flex;
  flex-direction:column;
  border: 1px solid rgba(255,255,255,.1);
  border-radius: 14px;
  overflow:hidden;
  background: rgba(255,255,255,.04);
  text-align:left;
}
.gcard-media{
  width:100%;
  aspect-ratio: 890 / 419;
  overflow:hidden;
  background: rgba(255,255,255,.06);
}
.gcard-media img{ display:block; width:100%; height:100%; object-fit:cover; }
.gcard-body{
  flex:1;
  display:flex;
  flex-direction:column;
  padding: 16px 18px 20px;
}
.gcard h3{
  font-family: var(--font-head); font-weight:700; color:#fff;
  font-size: clamp(16px, 1.9vw, 21px); line-height:1.25;
  margin: 0 0 8px;
  /* Reserve two lines so single-line headings (e.g. the zenbike card)
     keep the body copy aligned across cards — replaces an old
     <br/>&nbsp; spacer that used to force the second line in markup. */
  min-height: 2.5em;
}
.gcard p{
  font-family: var(--font-body); color: var(--muted);
  font-size: clamp(12.5px, 1.3vw, 14px); line-height:1.5;
  margin:0;
}
/* The concrete-metric line at the bottom of each favorite — bold, warm
   accent color, pushed to the bottom of the card body so cards with
   different paragraph lengths still line up along a common baseline. */
.gcard-body{ position: relative; }
/* Prefixed with .gcard so this rule (0,2,0) beats the earlier
   ".gcard p { color: var(--muted); }" rule (0,1,1) — otherwise the
   muted body-paragraph color wins the cascade and the mint claim
   never appears. */
.gcard .gcard-claim{
  margin-top: auto;
  padding-top: 10px;
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--sage);
  font-size: clamp(12.5px, 1.35vw, 14.5px);
  letter-spacing: .01em;
}
/* Links inside a claim keep the claim's mint color and bold weight, and
   only the anchor text itself is underlined — not the surrounding "online
   store" copy — so the underline reads as "click this word." */
.gcard .gcard-claim a{
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.gcard .gcard-claim a:hover{ text-decoration-color: var(--sage); }
.gcard-placeholder{ opacity:.75; }

/* Queue-it logo overlay for the high-traffic card — vertically
   centered on the left third of the top image, with a soft white halo
   drop shadow in all directions so it stays legible against darker
   parts of the photo.
   Selector prefixed with .gcard-media to outrank the generic
   ".gcard-media img { width:100%; height:100%; object-fit:cover }"
   rule earlier in this file — without that, the browser stretches the
   logo to fill the card and object-fit:cover crops it to a zoomed
   letter. Same trick is used on every logo overlay below. */
.gcard-media-queueit{ position: relative; }
.gcard-media img.qit-logo{
  position: absolute;
  left: 20%;
  right: 20%;
  top: 50%;
  transform: translateY(-50%);
  width: 60%;
  height: auto;
  object-fit: contain;
  filter:
    drop-shadow(0 0 3px rgba(255,255,255,.9))
    drop-shadow(3px 0 3px rgba(255,255,255,.9))
    drop-shadow(-3px 0 3px rgba(255,255,255,.9))
    drop-shadow(0 3px 3px rgba(255,255,255,.9))
    drop-shadow(0 -3px 3px rgba(255,255,255,.9));
  pointer-events: none;
}

/* University of St. Thomas logo overlay for the teaching card —
   centered in the media with ~25% padding on each side. */
.gcard-media-teaching{ position: relative; }
.gcard-media img.stu-logo{
  position: absolute;
  left: 25%;
  top: 25%;
  width: 50%;
  height: 50%;
  object-fit: contain;
  pointer-events: none;
}

/* zenbike logo overlay — heavy yellow wordmark centered over the card's
   media graphic. */
.gcard-media-zenbike{ position: relative; }
.zenbike-logo{
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-head);
  font-weight: 800;
  color: #f7d417;
  font-size: clamp(22px, 3.4vw, 34px);
  letter-spacing: -.01em;
  text-shadow: 0 2px 8px rgba(0,0,0,.5);
  line-height: 1;
  pointer-events: none;
}
.zenbike-logo sup{
  font-size: .45em;
  font-weight: 700;
  top: -.9em;
  margin-left: 1px;
}

/* EIX + FamilyBusiness on a diagonal — EIX pinned to top-left, FamBiz
   pinned to bottom-right. Both marks are wide, so a side-by-side layout
   truncated them; splitting them diagonally lets each occupy roughly
   half the card width without overlap. Percent-based sizing keeps the
   composition responsive to the card's own scale. */
.gcard-media-eix{
  position: relative;
  background: linear-gradient(180deg, #ffffff 0%, #f4f2ee 100%);
}
/* Prefix with .gcard-media so these outrank the generic
   ".gcard-media img { width:100%; height:100%; object-fit:cover }" rule
   at the top of the gallery section — otherwise the browser blows both
   logos up to fill the card and crops them into fragments. */
.gcard-media img.eix-logo-img,
.gcard-media img.fambiz-logo-img{
  position: absolute;
  display: block;
  height: auto;
  object-fit: contain;
}
.gcard-media img.eix-logo-img{
  top: 8%;
  left: 6%;
  width: 25%;
}
.gcard-media img.fambiz-logo-img{
  bottom: 10%;
  right: 6%;
  width: 66.66%;
}

/* FMSC card — the source PNG is a near-white logo shape on transparency.
   Recolor it to FMSC blue with a mask (the mask uses the PNG's alpha, so
   only the logo shape is painted) and let the media box's white background
   show through the logo's negative space. */
.gcard-media-fmsc{ position: relative; background: #fff; }
.gcard-media-fmsc .fmsc-logo{
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 58%;
  aspect-ratio: 118 / 54;
  background-color: rgba(35, 97, 146, 0.8);
  -webkit-mask: url("rsc/favorites/fmsc-logo.png") center / contain no-repeat;
          mask: url("rsc/favorites/fmsc-logo.png") center / contain no-repeat;
  pointer-events: none;
}

@media (min-width: 640px){ :root{ --gcard-w: 300px; --gcard-gap: 18px; --gcard-visible: 2; } }
@media (min-width: 900px){ :root{ --gcard-w: 340px; --gcard-gap: 22px; --gcard-visible: 3; } }

/* ============ bio (Left) ============ */
.bio-inner{ flex-direction:column; }
.bio-photo{
  width: 144px; height:144px; object-fit:cover; border-radius:50%;
  box-shadow: var(--e3); border: 3px solid rgba(255,255,255,.85);
  margin: 0 0 18px;
}
.bio-lede{
  font-family: var(--font-head); font-weight:600; color:#fff;
  font-size: clamp(19px, 3vw, 28px); line-height:1.3;
  max-width: 46ch; margin: 0 0 16px;
}
.bio-detail{
  color: var(--muted); font-size: clamp(14px, 1.7vw, 17px); line-height:1.55;
  max-width: 50ch; margin: 0 0 22px;
}
/* LinkedIn link now runs on as part of the sentence — same typography as
   the surrounding paragraph, just a subtle underline to show it's a link. */
.bio-detail a{
  color: inherit; font: inherit;
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: rgba(154,167,189,.5);
  transition: color .2s, text-decoration-color .2s;
}
.bio-detail a:hover{ color: var(--orange); text-decoration-color: var(--orange); }

/* Short call-to-action line beneath the bio — the friendly, informal
   "Let's LinkedIn" invite, given its own line so it reads as a distinct
   handshake rather than an inline aside. Always underlined so it's
   obviously clickable. */
.bio-cta{
  margin: 4px 0 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(15px, 1.9vw, 19px);
  letter-spacing: .01em;
}
.bio-cta a{
  color: var(--orange);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-color: rgba(242,163,92,.6);
  transition: color .2s, text-decoration-color .2s;
}
.bio-cta a:hover{ color: #fff; text-decoration-color: #fff; }

/* ============ expertise (Right) ============ */
.expertise-inner{ flex-direction:column; }
.expertise-grid{
  display:grid; grid-template-columns: repeat(3, 1fr);
  gap: clamp(8px, 1.4vw, 16px);
  max-width: 960px;
  width: 92vw;
}
.ecard{
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-radius: 12px;
  padding: clamp(10px,1.4vw,16px);
  text-align:left;
  transition: background .2s;
}
.ecard-toggle{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  width:100%; background:none; border:none; padding:0; margin:0;
  cursor:default; text-align:left;
}
.ecard h3{
  font-family: var(--font-head); font-weight:600; color:#fff;
  font-size: clamp(15px, 2vw, 20px); margin: 0 0 8px;
}
.ecard-chev{ width:16px; height:16px; flex:none; color: var(--muted); display:none; transition: transform .25s ease; }
.ecard .chips{ display:flex; flex-wrap:wrap; gap:6px; }
.ecard .chips span{
  font-size: clamp(10px,1vw,11.5px); background: rgba(221,126,52,.10); color: #f6b97e;
  padding: 4px 8px; border-radius:999px; white-space:nowrap;
}

/* Mobile: one category per row, collapsed by default, accordion-style —
   only one card's chips are shown at a time (see script.js). */
@media (max-width: 640px){
  .expertise-grid{ grid-template-columns: 1fr; }
  .ecard-toggle{ cursor:pointer; }
  .ecard-toggle:hover h3{ color: var(--orange); }
  .ecard-chev{ display:block; }
  .ecard .chips{ display:none; padding-top:2px; }
  .ecard.is-expanded{ background: rgba(255,255,255,.08); }
  .ecard.is-expanded .ecard-toggle h3{ margin-bottom:0; }
  .ecard.is-expanded .chips{ display:flex; margin-top:8px; }
  .ecard.is-expanded .ecard-chev{ transform: rotate(180deg); color: var(--orange); }
}

/* ============ business card panel (Down) ============ */
/* Height comes from the shared .panel-inner rule now (see above) so
   every panel is centered the same way. Card gets slightly tighter
   side padding than the others so the (now larger) card has room to
   breathe without overflowing on phones. */
.card-inner{ flex-direction:column; padding-left: 3vw; padding-right: 3vw; }

.card-static{
  width: min(92vw, 502px); /* previous cap (456px) raised another 10% */
  aspect-ratio: 1050/600;
  background: #efe6d3; /* card's own paper tone — shows as a blank card until the photo loads in */
  border-radius: 20px;
  box-shadow: var(--e3);
  overflow: hidden;
}
.card-photo{
  display:block; width:100%; height:100%;
  object-fit:cover;
  opacity: 0;
  transition: opacity .4s ease;
}
.card-photo.is-loaded{ opacity: 1; }

/* Desktop: vertically centered between the top of the viewport and the
   top edge of the (now smaller) console. Console top = 100dvh - 7dvh
   (bottom offset) - 190px (3*58px + 2*8px gap), so its midpoint is
   (93dvh - 190px) / 2 = 46.5dvh - 95px. Sizing formula is the prior
   pass scaled up another 10% (min(49.2vw,...,900px) -> min(54.12vw,...,990px)). */
@media (min-width: 900px){
  .card-inner{ position:relative; }
  .card-inner{
    --card-w: min(54.12vw, calc((93dvh - 270px) * 1.155), 990px);
  }
  .card-static{
    position:absolute; left:50%; top: calc(46.5dvh - 95px);
    transform: translate(-50%, -50%);
    width: var(--card-w);
    border-radius: 28px;
  }
}

/* ============ privacy (one step beyond the business card) ============ */
/* Utility panel holding the privacy policy, trademark attribution, and the
   copyright notice (the console's copyright line is hidden at several
   breakpoints, so this is its reliable home).

   NOTE ON SCROLLING: this is the ONE place the site permits a scrollbar, and
   it's a deliberate exception. Legal text has to be readable in full — silently
   clipping a privacy policy would be worse than a scroll region. Only the inner
   .privacy-body scrolls; the panel itself, and every other panel, still never
   scroll. On desktop the two-column layout means it usually doesn't scroll at
   all. */
.privacy-inner{ flex-direction: column; justify-content: flex-start; }
.privacy-body{
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px 30px;
  align-content: start;
  max-width: 940px;
  width: 92vw;
  text-align: left;
  /* min-height:0 lets this flex child actually shrink so overflow can kick in */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
  overscroll-behavior: contain;
}
.privacy-block{ margin: 0 0 12px; break-inside: avoid; }
.privacy-block h3{
  font-family: var(--font-head); font-weight: 600; color: #fff;
  font-size: clamp(13px, 1.4vw, 15px);
  margin: 0 0 4px;
  letter-spacing: .01em;
}
.privacy-block p{
  color: var(--muted);
  font-size: clamp(11.5px, 1.15vw, 13px);
  line-height: 1.5;
  margin: 0 0 6px;
}
.privacy-block strong{ color: var(--ink); font-weight: 700; }
.privacy-links a{
  color: var(--orange);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.privacy-links a:hover{ color: #fff; }
.privacy-block a{
  color: var(--orange);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.privacy-block a:hover{ color: #fff; }
#privacyCopyright{ color: var(--ink); font-weight: 700; }
.privacy-updated{ opacity: .7; font-style: italic; }

/* Narrow screens: single column (the grid would be unreadable at 2-up). */
@media (max-width: 700px){
  .privacy-body{ grid-template-columns: 1fr; }
}

/* Subtle scrollbar so the exception doesn't look like a bug. */
.privacy-body::-webkit-scrollbar{ width: 6px; }
.privacy-body::-webkit-scrollbar-thumb{
  background: rgba(255,255,255,.18); border-radius: 999px;
}
.privacy-body{ scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.18) transparent; }

/* ============ console ============ */
/* Sized 25% smaller than the previous pass across every breakpoint. */
.console{
  position: fixed; left:50%; bottom: 8dvh; transform: translateX(-50%);
  z-index: 50;
  display:grid;
  grid-template-columns: 48px 48px 48px;
  grid-template-rows: 48px 48px 48px;
  gap: 6px;
}
.cbtn{
  display:flex; align-items:center; justify-content:center;
  background: rgba(20,24,35,.68);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: 14px;
  color: var(--muted);
  cursor:pointer;
  backdrop-filter: blur(8px);
  transition: background .2s, color .2s, transform .15s, border-color .2s, opacity .2s;
}
.cbtn:hover{ background: rgba(102,168,168,.22); color:#fff; border-color: rgba(102,168,168,.5); }
.cbtn:active{ transform: scale(.94); }
.cbtn:disabled{ opacity:.25; pointer-events:none; }

/* Hover tooltips on the console buttons — only surfaced when the user
   is looking at the Home panel, so they act as an orientation legend
   ("this button goes to About / Expertise / Some Favorites / Business
   Card") rather than pestering visitors on every other panel where
   context already tells them what each direction does.
   Positioned outside each button so they don't get clipped by the
   button's own bounds, and pointer-events:none keeps them from
   blocking clicks. */
.cbtn{ position: relative; }
.cbtn::after{
  content: attr(data-tip);
  position: absolute;
  pointer-events: none;
  font-family: var(--font-head);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(20,24,35,.92);
  border: 1px solid rgba(102,168,168,.5);
  padding: 5px 9px;
  border-radius: 6px;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .15s ease, transform .15s ease;
  z-index: 60;
}
/* Reveal all four tooltips together the moment ANY control button is
   hovered/focused — a labeled-legend behavior rather than a per-button
   probe. Saves the user a second hover before committing to a click
   because the surrounding labels come along for the ride.
   Uses :has() (Chrome/Safari/Edge 105+, Firefox 121+); older browsers
   simply skip the labels rather than misbehave. */
.console[data-panel="home"]:has(.cbtn:hover) .cbtn::after,
.console[data-panel="home"]:has(.cbtn:focus-visible) .cbtn::after{
  opacity: 1;
  transform: translateY(0);
}
/* Directional placement so each tooltip sits on the same side as its
   button's arrow points. */
.cbtn-up::after{    left: 50%; bottom: calc(100% + 8px); transform: translate(-50%, 4px); }
.console[data-panel="home"]:has(.cbtn:hover) .cbtn-up::after,
.console[data-panel="home"]:has(.cbtn:focus-visible) .cbtn-up::after{ transform: translate(-50%, 0); }

.cbtn-down::after{  left: 50%; top: calc(100% + 8px);    transform: translate(-50%, -4px); }
.console[data-panel="home"]:has(.cbtn:hover) .cbtn-down::after,
.console[data-panel="home"]:has(.cbtn:focus-visible) .cbtn-down::after{ transform: translate(-50%, 0); }

.cbtn-left::after{  top: 50%; right: calc(100% + 8px);   transform: translate(4px, -50%); }
.console[data-panel="home"]:has(.cbtn:hover) .cbtn-left::after,
.console[data-panel="home"]:has(.cbtn:focus-visible) .cbtn-left::after{ transform: translate(0, -50%); }

.cbtn-right::after{ top: 50%; left: calc(100% + 8px);    transform: translate(-4px, -50%); }
.console[data-panel="home"]:has(.cbtn:hover) .cbtn-right::after,
.console[data-panel="home"]:has(.cbtn:focus-visible) .cbtn-right::after{ transform: translate(0, -50%); }

/* Home button doesn't need a tip on Home (it's already home), and its
   role is obvious elsewhere. Suppress it entirely. */
.cbtn-home::after{ display: none; }

/* Touch devices don't really "hover" — skip tooltips there to avoid
   sticky states after a tap. */
@media (hover: none){
  .cbtn::after{ display: none; }
}
.cbtn svg{ width: 18px; height:18px; }

/* Icon swap on the left/right buttons when we're viewing the Favorites
   gallery: the panel-nav single chevron gives way to a "scroll" double
   chevron, and the button gets a slightly lighter blue wash so its
   change of role (carousel scroller instead of panel jumper) reads at a
   glance. The button footprint is unchanged so landscape/side-mounted
   layouts stay intact. */
.cbtn-ic-scroll{ display: none; }
.console[data-panel="up"] .cbtn-left .cbtn-ic-nav,
.console[data-panel="up"] .cbtn-right .cbtn-ic-nav{ display: none; }
.console[data-panel="up"] .cbtn-left .cbtn-ic-scroll,
.console[data-panel="up"] .cbtn-right .cbtn-ic-scroll{ display: block; }
.console[data-panel="up"] .cbtn-left,
.console[data-panel="up"] .cbtn-right{
  background: rgba(58, 82, 120, .78);
  border-color: rgba(120, 160, 200, .35);
  color: #fff;
}
.console[data-panel="up"] .cbtn-left:hover,
.console[data-panel="up"] .cbtn-right:hover{
  background: rgba(80, 110, 150, .85);
  border-color: rgba(140, 180, 220, .55);
}

.cbtn-up{ grid-column:2; grid-row:1; }
.cbtn-left{ grid-column:1; grid-row:2; }
.cbtn-home{ grid-column:2; grid-row:2; border-radius: 50%; color: var(--orange); }
.cbtn-home svg{ width:22px; height:22px; } /* 2x the surrounding directional icons */
.cbtn-right{ grid-column:3; grid-row:2; }
.cbtn-down{ grid-column:2; grid-row:3; }

/* ============ console label ============ */
.console-label{
  position:absolute; top:100%; left:50%; transform:translateX(-50%);
  margin-top: 12px;
  font-family: var(--font-head); font-weight:600; letter-spacing:.15em;
  text-transform:uppercase; font-size: 11px; color: var(--muted);
  opacity:.6; white-space:nowrap;
}
.console-copyright{
  position:absolute; top:100%; left:50%; transform:translateX(-50%);
  margin-top: 30px;
  font-family: var(--font-body); font-size: 10px; color: var(--muted);
  opacity:.35; white-space:nowrap;
}

@media (min-width: 900px){
  :root{ --console-h: 190px; --console-bottom: 7dvh; }
  .console{
    grid-template-columns: 58px 58px 58px;
    grid-template-rows: 58px 58px 58px;
    gap: 8px;
    bottom: 7dvh;
  }
  .cbtn svg{ width:21px; height:21px; }
  .cbtn-home svg{ width:24px; height:24px; }
}

/* ============ short / small screens: keep the console from crowding content ============ */
/* Mobile portrait: shrink the favorites card's top graphic further so
   the whole card ends up ~20px shorter. object-fit:cover keeps the
   image's aspect ratio (no skew) — it just crops a bit more image at
   the top and bottom edges — while everything below the graphic
   (heading, paragraph, mint claim) rides up 20px and stops clipping
   against the bottom of the panel on standard portrait phones. */
@media (orientation: portrait) and (max-width: 639px){
  .gcard-media{
    aspect-ratio: auto;
    height: 102px;
  }
}

@media (max-height: 620px){
  :root{ --console-bottom: 14px; }
  .panel-inner{ padding-top: 2dvh; }
  .headline{ font-size: clamp(23.4px, 5.4vw, 41.4px); margin-bottom:12px; }
  .summary{ font-size: clamp(13px,2vw,16px); }
  .console-label{ display:none; }
  .console-copyright{ display:none; }
  .console{ bottom: 14px; }
  .bio-photo{ width: 96px; height:96px; margin-bottom: 10px; }
  .bio-lede{ font-size: clamp(15px,2.4vw,20px); margin-bottom:10px; }
  .bio-detail{ display:none; }
  .gallery-sub{ margin-bottom: 16px; }
  .gcard-body{ padding: 12px 16px 16px; }
}

@media (max-width: 380px){
  :root{ --console-h: 134px; } /* 3*42px cells + 2*4px gaps */
  .console{
    grid-template-columns: 42px 42px 42px;
    grid-template-rows: 42px 42px 42px;
    gap: 4px;
  }
}

/* ============ mobile landscape: side-mounted console ============ */
/* A bottom-anchored console eats a big share of an already-short
   landscape viewport and was overlapping content. Below a phone-
   landscape height ceiling, move it to a vertically centered strip
   on the right edge instead — like a handheld game controller — so
   content reclaims the full height and only needs right-side
   clearance rather than bottom clearance. */
@media (orientation: landscape) and (max-height: 500px){
  .console{
    left: auto; right: 3vw; bottom: auto; top: 50%;
    transform: translateY(-50%);
  }

  .panel-inner{ height: 92dvh; padding: 2dvh 190px 0 6vw; }
  .card-inner{ height: 92dvh; padding: 2dvh 190px 0 3vw; }
  .expertise-inner{ height: 92dvh; padding: 2dvh 190px 0 6vw; }
  /* Top-anchor the Favorites content so the breadcrumb stays near the top
     (with the title line hidden, the default centering pushed it down). */
  .gallery-inner{ justify-content: flex-start; }

  .headline{ font-size: clamp(19.8px, 4.5vw, 34.2px); margin-bottom: 8px; }
  .roles{ margin-bottom: 10px; }
  .summary{ font-size: clamp(12px,1.8vw,15px); }
  .console-label{ display:none; }
  .console-copyright{ display:none; }
  .bio-photo{ width: 64px; height:64px; margin-bottom: 8px; }
  .bio-lede{ font-size: clamp(14px,2.2vw,18px); margin-bottom: 8px; }
  .bio-detail{ display:none; }

  /* Keep the gallery and expertise grid inside the space left of the
     console rather than sizing off the full (now partly-blocked) width. */
  .gallery-viewport{ max-width: calc(100vw - 220px); }
  .expertise-grid{ max-width: calc(100vw - 220px); width: auto; }

  /* Horizontal favorites card — LANDSCAPE PHONE ONLY. The stacked
     portrait layout is too tall for this short viewport, so the card body
     was clipping and only the graphic showed while scrolling. Here the
     card turns on its side: graphic on the left half, text on the right
     half, one card in view at a time. Portrait and iPad are untouched. */
  :root{ --gcard-w: min(480px, calc(100vw - 240px)); --gcard-gap: 10px; --gcard-visible: 1; }
  .gcard{ flex-direction: row; height: min(69dvh, 218px); }
  .gcard-media{ flex: 0 0 50%; width: 50%; height: 100%; aspect-ratio: auto; }
  .gcard-body{ flex: 0 0 50%; width: 50%; padding: 10px 12px; }
  .gcard h3{ font-size: 13px; margin-bottom: 4px; min-height: 0; }
  .gcard p{ font-size: 11px; }
  .gcard .gcard-claim{ padding-top: 6px; font-size: 11px; }

  /* Cramped view: hide the "Some History / Worked at / worked on." line
     entirely to give the cards the vertical room. Portrait and iPad still
     show it normally (the wrapper is display:contents outside this view). */
  .gallery-titleline{ display: none; }
  .gallery-inner .breadcrumb{ margin-bottom: 6px; }

  /* --- landscape mobile fixes for content that was clipping on
         very-short viewports (≤500 dvh) --- */

  /* Business card: shrink to 80% of the mobile-portrait cap so the
     top and bottom of the card stop clipping against the tight
     landscape viewport. */
  .card-static{ width: min(74vw, 402px); }

  /* (Favorites card media is handled by the horizontal-card rules above —
     it fills the left half of the card at full card height.) */

  /* Expertise: collapse every category down to just its heading and
     lay out the five cards in a 3-column grid, filling columns first.
     Clicking a heading uses the same is-expanded class the mobile-
     portrait accordion already toggles (see script.js) — but here the
     expanded card gets pinned to the full left column and the other
     four collapsed cards float into the two right columns. The result:
     one deep column of chips + two narrow columns of category labels,
     all fitting inside the ~350px of landscape height without
     scrolling. */
  .expertise-grid{
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    grid-auto-flow: column dense;
    gap: 6px;
  }
  .ecard{ padding: 6px 10px; }
  .ecard-toggle{ cursor: pointer; }
  .ecard-toggle:hover h3{ color: var(--orange); }
  .ecard-chev{ display: block; }
  .ecard h3{ font-size: 12px; margin-bottom: 0; }
  .ecard .chips{ display: none; gap: 4px; padding-top: 2px; }
  .ecard .chips span{ font-size: 10px; padding: 3px 6px; }
  .ecard.is-expanded{
    grid-column: 1;
    grid-row: 1 / -1;
    background: rgba(255,255,255,.08);
  }
  .ecard.is-expanded .ecard-toggle h3{ margin-bottom: 0; }
  .ecard.is-expanded .chips{ display: flex; margin-top: 6px; }
  .ecard.is-expanded .ecard-chev{ transform: rotate(180deg); color: var(--orange); }
}

/* ============ overview ("poster") mode ============ */
/* A one-shot "camera pull-back" that scales the whole panel layer down
   to reveal the five-panel spatial layout — About on the left, Expertise
   on the right, Favorites on top, Business Card on the bottom, Home in
   the middle — holds briefly, then zooms back into Home. Turns the
   site into a briefly-visible poster. Wired to the top-right button
   and the 'M' key in script.js.
   Positions of the panels during the poster are set inline by JS (with
   a transition:none snap) so no panel sweeps through home mid-zoom;
   this block only handles the whole-layer scale, the console dim, and
   the button chrome. */
.stage.is-overview .panels-layer{
  transform: scale(0.32);
  transform-origin: 50% 50%;
}
.stage.is-overview .panel{ pointer-events: none; }
.stage.is-overview .console{
  opacity: .35;
  pointer-events: none;
  transition: opacity .3s;
}
/* During the poster hold the overview button stays fully clickable —
   pressing it (or 'M' / Escape) exits early. Dim it just slightly so
   it doesn't dominate the poster while still reading as a live target. */
.stage.is-overview .btn-overview{ opacity: .85; }

.btn-overview{
  position: fixed;
  top: 14px; right: 14px;
  z-index: 60;
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  background: rgba(20,24,35,.68);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: 12px;
  color: var(--muted);
  cursor: pointer;
  backdrop-filter: blur(8px);
  transition: background .2s, color .2s, transform .15s, border-color .2s, opacity .2s;
}
.btn-overview:hover{
  background: rgba(102,168,168,.22);
  color: #fff;
  border-color: rgba(102,168,168,.5);
}
.btn-overview:active{ transform: scale(.94); }
.btn-overview svg{ width: 18px; height: 18px; }
.btn-overview:disabled{ opacity: .3; pointer-events: none; }

/* ============ tablet landscape (iPad-ish): side-mounted console ============ */
/* The side-mounted "game controller" console is a LANDSCAPE-only
   behavior: in portrait — phone, tablet, or desktop — the console
   always sits at the bottom, same as the full desktop site.
   Phone landscape (≤500px tall) is handled by the block above; this
   block covers taller landscape touch devices (iPads and friends),
   where the bottom-anchored console was blocking the lower edge of
   panel content. `pointer: coarse` keeps desktop/laptop landscape
   viewports (fine pointer) on the normal bottom console; min-height
   501px keeps this from double-applying on phones. */
@media (orientation: landscape) and (pointer: coarse) and (min-height: 501px) and (max-width: 1400px){
  .console{
    left: auto; right: 3vw; bottom: auto; top: 50%;
    transform: translateY(-50%);
  }

  .panel-inner{ height: 96dvh; padding: 4dvh 230px 0 6vw; }
  .card-inner{ height: 96dvh; padding: 4dvh 230px 0 3vw; }
  .expertise-inner{ height: 96dvh; padding: 4dvh 230px 0 6vw; }

  .console-label{ display:none; }
  .console-copyright{ display:none; }

  /* Reserve the right edge for the console; size gallery/expertise off
     the remaining content column instead of the whole viewport. Two
     cards fit comfortably left of the console on every iPad size. */
  :root{ --gcard-w: 300px; --gcard-gap: 18px; --gcard-visible: 2; }
  .gallery-viewport{ max-width: calc(100vw - 260px); }
  .expertise-grid{ max-width: calc(100vw - 260px); width: auto; }

  /* At ≥900px wide the desktop rule absolutely positions the business
     card assuming a BOTTOM console; with the console on the right,
     return the card to normal flow so .card-inner's flex centering
     (which respects the 230px right padding) places it correctly. */
  .card-static{
    position: static; left: auto; top: auto; transform: none;
    width: min(64vw, 560px);
    border-radius: 20px;
  }
}

/* ============ screen-reader-only utility ============ */
/* Visually hidden but available to assistive tech — used by the
   #panelAnnounce aria-live region so panel changes are announced. */
.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;
}
