/* ============================================================
   Boxy grid layout.
   The whole site is one column framed by two full-height side
   rails, divided into stacked full-width boxes by horizontal
   rules. No graph-paper background, no ticks — just structure.
   Zero radius everywhere. Dark-mode aware (class-based .dark).
   ============================================================ */

:root {
  --frame-line: #e5e7eb; /* gray-200 — rails + dividers */
  --frame-strong: #d1d5db; /* gray-300 — hover emphasis */
  --surface: #ffffff;
  --surface-2: #fafafa; /* hover fill */
  --ink: #111111; /* focus ring + selection fill — ink on paper */
  /* One soft-corner scale (shape-consistency lock): cells/media use --r,
     every other bordered control/box uses --r-sm, pills stay pills. */
  --r: 14px;
  --r-sm: 10px;
}

.dark {
  --frame-line: #262626; /* neutral-800 */
  --frame-strong: #404040; /* neutral-700 */
  --surface: #0a0a0a;
  --surface-2: #171717; /* neutral-900 */
  --ink: #fafafa;
}

/* Signature micro-label: small mono "coordinate" text used for dates, footer,
   and section metadata. Kept sentence-case so full phrases don't shout; add
   `uppercase` at the call site for short labels that want it. */
.coord {
  font-family: "Geist Mono", ui-monospace, SFMono-Regular, monospace;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
}

/* ============================================================
   Floating nav dock: a translucent pill fixed to the bottom-centre
   of the viewport, replacing the top nav bar.
   ============================================================ */
/* App shell: content scrolls inside .app-scroll; the nav is a separate bar
   (.app-nav) pinned below it, so the nav never overlaps the content. */
html {
  overflow: hidden;
}
body.app-shell {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.app-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-behavior: smooth;
  position: relative;
  z-index: 1;
}
.app-nav {
  flex: 0 0 auto;
  z-index: 41;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  border-bottom: 1px solid var(--frame-line);
}
.dark .app-nav {
  background: rgba(10, 10, 10, 0.72);
}
@media (prefers-reduced-transparency: reduce) {
  .app-nav {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}
.nav-inner {
  max-width: 56rem;
  margin: 0 auto;
  padding: 0.55rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
/* Inline "hooking" proof links. Underline plus a small forward-leaning arrow
   so every outbound claim in prose reads as a checkable receipt, matching the
   blog-content treatment. Applied to inline links, not chips or card CTAs
   (those carry their own affordance). */
.ilink {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: #d1d5db;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--dur-2) var(--ease-out-quart);
}
.ilink:hover {
  color: #000;
  text-decoration-color: currentColor;
}
.dark .ilink:hover {
  color: #fff;
}
.ilink::after {
  content: "\2197"; /* ↗ */
  display: inline-block;
  margin-left: 0.05em;
  font-size: 0.7em;
  font-weight: 600;
  line-height: 1;
  vertical-align: 0.3em;
  color: #a3a3a3;
  transition:
    transform var(--dur-1) var(--ease-out-quint),
    color var(--dur-1) var(--ease-out-quint);
}
.ilink:hover::after {
  transform: translate(1.5px, -1.5px);
  color: currentColor;
}
/* The arrow reads as a trailing glyph, so keep it attached to the last word:
   an ilink wrapping inline <code> shouldn't wrap the arrow to a new line. */
.ilink > code {
  white-space: nowrap;
}
.dark .ilink {
  text-decoration-color: #525252;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.65rem;
  border-radius: 9999px;
  color: inherit;
  background: transparent;
  cursor: pointer;
  transition:
    background-color var(--dur-2) var(--ease-out-quint),
    color var(--dur-2) var(--ease-out-quint);
}
.nav-link:hover {
  background: var(--surface-2);
  color: #000;
}
.dark .nav-link:hover {
  color: #fff;
}
.nav-link[aria-current="page"] {
  color: #000;
  background: var(--surface-2);
}
.dark .nav-link[aria-current="page"] {
  color: #fff;
}
.nav-more {
  position: relative;
}
.nav-more__chev {
  width: 0.85em;
  height: 0.85em;
  transition: transform var(--dur-2) var(--ease-out-quint);
}
.nav-more__btn[aria-expanded="true"] .nav-more__chev {
  transform: rotate(180deg);
}
.nav-more__menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  min-width: 11rem;
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--frame-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: 0 14px 32px -12px rgba(0, 0, 0, 0.35);
  z-index: 50;
}
.nav-more__menu[hidden] {
  display: none;
}
.nav-more__item {
  padding: 0.5rem 0.65rem;
  border-radius: var(--r-sm);
  color: #374151;
  transition: background-color var(--dur-2) var(--ease-out-quart);
}
.dark .nav-more__item {
  color: #d1d5db;
}
.nav-more__item:hover {
  background: var(--surface-2);
  color: #000;
}
.dark .nav-more__item:hover {
  color: #fff;
}
.nav-more__item[aria-current="page"] {
  color: #000;
}
.dark .nav-more__item[aria-current="page"] {
  color: #fff;
}
.nav-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem;
  border-radius: 9999px;
  color: inherit;
  transition: background-color var(--dur-2) var(--ease-out-quart);
}
.nav-icon-btn:hover {
  background: var(--surface-2);
}

/* ============================================================
   Pointer affordances, applied globally so no control has to
   remember. The cursor is the cheapest signal that something is
   interactive, and the browser default is wrong: <button> ships
   with `cursor: default`. Anchors already point on their own.
   ============================================================ */
button,
summary,
label[for],
select,
[role="button"],
[role="tab"],
[role="option"] {
  cursor: pointer;
}

/* Say "no" before the click, not after. */
button:disabled,
input:disabled,
select:disabled,
textarea:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

abbr[title] {
  cursor: help;
}

/* Chrome is for clicking, not reading: a double-click on a button label
   shouldn't leave a smear of selected text. Prose and code keep their
   selection. Children inherit, so labels inside a control are covered. */
button,
summary,
.nav-link,
.cmdk-item,
[role="button"] {
  -webkit-user-select: none;
  user-select: none;
}

/* Touch: drop the legacy 300ms tap delay and the grey flash overlay, so a
   tap lands as immediately as a hover reads on desktop. */
a,
button,
summary,
[role="button"] {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* The keyboard's cursor. :focus-visible only, so a mouse click never leaves
   a ring behind but tabbing always shows where you are. Outline (not border)
   keeps it out of layout, and it inherits each control's own radius. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Selection and caret in the site's ink rather than the OS blue. */
::selection {
  background: var(--ink);
  color: var(--surface);
}
input,
textarea {
  caret-color: var(--ink);
}

/* Soft corners, applied consistently without editing every template. Elements
   carrying Tailwind's full-box `border` token get the control radius; elements
   that already declare a `rounded-*` utility (pills, tags) keep their own. */
.site-frame [class~="border"]:not([class*="rounded"]) {
  border-radius: var(--r-sm);
}
button:not([class*="rounded"]),
input,
textarea,
select {
  border-radius: var(--r-sm);
}
.cell {
  border-radius: var(--r);
}
.cmdk-panel {
  border-radius: var(--r);
}
.cmdk-trigger,
.cmdk-item,
.ai-btn {
  border-radius: var(--r-sm);
}

/* ---- "Read with" AI buttons on blog posts. Monochrome; the brand mark is
        carried in currentColor and darkens to ink on hover. ---- */
.ai-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--frame-line);
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  font-weight: 500;
  color: #374151; /* gray-700 */
  background: var(--surface);
  transition:
    color var(--dur-2) var(--ease-out-quint),
    border-color var(--dur-2) var(--ease-out-quint),
    background-color var(--dur-2) var(--ease-out-quint);
}
.ai-btn svg {
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
}
.ai-btn:hover {
  color: #000;
  border-color: var(--frame-strong);
  background: var(--surface-2);
}
.dark .ai-btn {
  color: #d1d5db;
}
.dark .ai-btn:hover {
  color: #fff;
}

/* ============================================================
   Frame: one centred column with two full-height side rails.
   Opened in header.html, closed in footer.html.
   ============================================================ */
/* Side gutters beside the frame: a calm, static perforated dot field — the
   margin of engineering graph paper. Monochrome, no motion, no JS; the blue
   binary rain is gone (color is not the brand's voice). Hidden when there's no
   real gutter (tablet/mobile). A faint slash hatch overlays the dots at low
   opacity for a touch of blueprint texture. */
.binary-gutter {
  position: fixed;
  top: 0;
  bottom: 0;
  width: calc((100vw - 56rem) / 2 - 1px);
  z-index: 0;
  pointer-events: none;
  user-select: none;
  --dot: rgba(17, 17, 17, 0.14); /* graphite dots on light */
  --dot-bright: rgba(17, 17, 17, 0.5); /* dots lit under the cursor */
  --slash: rgba(17, 17, 17, 0.04);
  background-image:
    repeating-linear-gradient(-45deg, var(--slash) 0 1px, transparent 1px 22px),
    radial-gradient(var(--dot) 1.1px, transparent 1.2px);
  background-size:
    auto,
    18px 18px;
  background-position:
    0 0,
    center;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
}
.dark .binary-gutter {
  --dot: rgba(255, 255, 255, 0.12); /* light dots on near-black */
  --dot-bright: rgba(255, 255, 255, 0.55);
  --slash: rgba(255, 255, 255, 0.035);
}
.binary-gutter--left {
  left: 0;
}
.binary-gutter--right {
  right: 0;
}

/* Cursor-reactive dots: a brighter dot layer revealed in a soft circle that
   follows the pointer. --mx/--my (viewport px, set by micro.js) drive the mask;
   the right gutter offsets the x into its own box. When the cursor is away from
   an edge the mask sits off that gutter, so the base field stays calm. */
.binary-gutter::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--dot-bright) 1.4px, transparent 1.5px);
  background-size: 18px 18px;
  background-position: center;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(circle 150px at var(--gx, -9999px) var(--gy, -9999px), #000 0%, transparent 72%);
  mask-image: radial-gradient(circle 150px at var(--gx, -9999px) var(--gy, -9999px), #000 0%, transparent 72%);
}
.binary-gutter--left::after {
  --gx: calc(var(--mx, -9999) * 1px);
  --gy: calc(var(--my, -9999) * 1px);
}
.binary-gutter--right::after {
  --gx: calc(var(--mx, -9999) * 1px - (100vw + 56rem) / 2 - 1px);
  --gy: calc(var(--my, -9999) * 1px);
}
@media (max-width: 960px) {
  .binary-gutter {
    display: none;
  }
}

/* ============================================================
   Interactive cat easter egg in the footer corner. Sleeps by
   default; micro.js adds .is-awake / .is-happy and sets --px/--py
   so the eyes track the cursor as it approaches.
   ============================================================ */
footer {
  position: relative;
  padding-bottom: 3.75rem; /* room for the cat below the footer row */
}
.cat {
  position: absolute;
  width: 54px;
  z-index: 2;
  color: #f7f6f3; /* white cat */
  --cat-ear: #e2a7b1;
  --cat-nose: #d9808f;
  --cat-stripe: rgba(0, 0, 0, 0.13); /* stripes on white fur */
  --cat-whisker: rgba(0, 0, 0, 0.42); /* dark whiskers over light bg */
  cursor: grab;
  touch-action: none; /* let the cat be dragged without scrolling */
}
.dark .cat {
  --cat-whisker: rgba(255, 255, 255, 0.5); /* light whiskers over dark bg */
}
.cat.is-dragging {
  cursor: grabbing;
  z-index: 80;
}
/* Footer: tucked in the bottom-right corner below the footer row. */
.cat--footer {
  right: 1.25rem;
  bottom: 0.3rem;
}
/* Hero: perched at the bottom-right corner of the profile photo. */
.cat--hero {
  width: 46px;
  right: -14px;
  bottom: -16px;
}
@media (max-width: 640px) {
  .cat {
    display: none;
  }
}
.cat__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  /* Soft shadow + tight edge so the white cat reads on light backgrounds. */
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.22))
    drop-shadow(0 0 0.6px rgba(0, 0, 0, 0.4));
}
.cat__svg * {
  transform-box: view-box;
}
.cat__body {
  transform-origin: 24px 45px;
}
.cat__tail {
  transform-origin: 38px 33px;
}
.cat__ear {
  transition: transform var(--dur-3) var(--ease-out-quint);
}
.cat__ear--l {
  transform-origin: 15px 11px;
}
.cat__ear--r {
  transform-origin: 33px 11px;
}
.cat__pupils {
  transition: transform var(--dur-1) var(--ease-out-quint);
  transform: translate(calc(var(--px, 0) * 1.7px), calc(var(--py, 0) * 1.4px));
}
.cat__eyes,
.cat__eyes-closed {
  transition: opacity var(--dur-2) var(--ease-out-quart);
}
.cat__heart {
  transform-origin: 24px 7px;
  opacity: 0;
  transform: scale(0);
}

/* ---- Sleeping (default): eyes closed, z's rise, gentle breathing ---- */
.cat.is-sleeping .cat__eyes {
  opacity: 0;
}
.cat:not(.is-sleeping) .cat__eyes-closed {
  opacity: 0;
}
.cat.is-sleeping .cat__body {
  animation: cat-breathe 3.8s ease-in-out infinite;
}
.cat__z {
  position: absolute;
  bottom: 68%;
  font-family: "Geist Mono", ui-monospace, monospace;
  font-weight: 600;
  line-height: 1;
  color: currentColor;
  opacity: 0;
  animation: cat-z 3s ease-in-out infinite;
  animation-play-state: paused;
}
.cat.is-sleeping .cat__z {
  animation-play-state: running;
}
.cat__z--1 {
  right: 2px;
  font-size: 8px;
  animation-delay: 0s;
}
.cat__z--2 {
  right: 7px;
  font-size: 10px;
  animation-delay: 1s;
}
.cat__z--3 {
  right: 13px;
  font-size: 13px;
  animation-delay: 2s;
}
@keyframes cat-z {
  0% {
    opacity: 0;
    transform: translateY(8px);
  }
  30% {
    opacity: 0.85;
  }
  100% {
    opacity: 0;
    transform: translate(6px, -14px);
  }
}
@keyframes cat-breathe {
  0%,
  100% {
    transform: scaleY(1);
  }
  50% {
    transform: scaleY(0.955);
  }
}

/* ---- Awake: ears perk, tail flicks lazily ---- */
.cat.is-awake .cat__ear--l {
  transform: rotate(-7deg);
}
.cat.is-awake .cat__ear--r {
  transform: rotate(7deg);
}
.cat.is-awake .cat__tail {
  animation: cat-tail 2.6s ease-in-out infinite;
}

/* ---- Happy (hover): tail wags fast, a heart pops, ears flick ---- */
.cat.is-happy .cat__ear--l {
  transform: rotate(-13deg);
}
.cat.is-happy .cat__ear--r {
  transform: rotate(13deg);
}
.cat.is-happy .cat__tail {
  animation: cat-tail 0.5s ease-in-out infinite;
}
.cat.is-happy .cat__heart {
  animation: cat-heart 0.9s ease-out;
}
@keyframes cat-tail {
  0%,
  100% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(-17deg);
  }
}
@keyframes cat-heart {
  0% {
    opacity: 0;
    transform: translateY(2px) scale(0);
  }
  25% {
    opacity: 1;
    transform: translateY(-2px) scale(1.15);
  }
  70% {
    opacity: 1;
    transform: translateY(-6px) scale(1);
  }
  100% {
    opacity: 0;
    transform: translateY(-13px) scale(0.9);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cat__body,
  .cat__tail,
  .cat__z,
  .cat__heart {
    animation: none !important;
  }
  .cat__pupils {
    transition: none;
  }
}

.site-frame {
  position: relative; /* sit above the binary gutters */
  z-index: 1;
  max-width: 56rem; /* == Tailwind max-w-4xl */
  margin-inline: auto;
  border-inline: 1px solid var(--frame-line);
}

/* Soft-media exception to the global zero-radius: faces shouldn't
   feel harsh, so the profile photo (and any .soft-media) keeps a
   small radius while every box/card/button stays hard square. */
.soft-media,
.soft-media img {
  border-radius: var(--r) !important;
}

/* ============================================================
   Box: a full-width section divided from the one above by a
   single rule that spans rail-to-rail. Inner padding gives the
   content breathing room; the rule itself is full-bleed.
   ============================================================ */
.box {
  border-top: 1px solid var(--frame-line);
  padding: 2.5rem 1.5rem;
}
.box:first-child {
  border-top: 0;
}

/* Cell: a plain framed box (e.g. the profile photo, blog masthead). */
.cell {
  border: 1px solid var(--frame-line);
  background: var(--surface);
}

/* ============================================================
   Experience timeline: a rail with nodes down the left, cards on
   the right. The current role gets a pulsing "live" node. Every
   item paints on load — there is no entrance and no JS gating.
   ============================================================ */
.tl {
  position: relative;
  display: grid;
  gap: 1.25rem;
}
/* Rail track. */
.tl::before {
  content: "";
  position: absolute;
  left: 10px;
  top: 12px;
  bottom: 12px;
  width: 2px;
  background: var(--frame-line);
}
/* Rail is "drawn" as you scroll past the section (progressive enhancement). */
@supports (animation-timeline: view()) {
  .tl::after {
    content: "";
    position: absolute;
    left: 10px;
    top: 12px;
    width: 2px;
    height: calc(100% - 24px);
    background: var(--frame-strong);
    transform-origin: top;
    transform: scaleY(0);
    animation: tl-draw linear both;
    animation-timeline: view();
    animation-range: cover 8% cover 68%;
  }
  @keyframes tl-draw {
    to {
      transform: scaleY(1);
    }
  }
}
.tl-item {
  position: relative;
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 0.9rem;
  align-items: start;
}
.tl-marker {
  display: flex;
  justify-content: center;
  padding-top: 10px;
}
.tl-dot {
  position: relative;
  z-index: 1;
  width: 11px;
  height: 11px;
  border-radius: 9999px;
  background: var(--surface);
  border: 2px solid var(--frame-strong);
}
/* Current role: a filled ink node with a slow pulsing ring. */
.tl-dot--live {
  background: currentColor;
  border-color: currentColor;
}
.tl-dot--live::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 9999px;
  border: 2px solid currentColor;
  animation: tl-ping 2.2s ease-out infinite;
}
@keyframes tl-ping {
  0% {
    transform: scale(0.55);
    opacity: 0.55;
  }
  100% {
    transform: scale(1.9);
    opacity: 0;
  }
}

/* No entrance. The timeline used to start at opacity:0 and rise in when an
   observer said so, which was the last thing on the site still gating content
   on JS — the pattern the reveal system was deleted for (see motion.css). A
   renderer that runs JS but never scrolls kept the hero (in view, revealed) and
   so the failsafe stayed quiet, leaving the timeline blank underneath.
   Fade-and-rise-per-section is also the reveal every generated page ships, and
   the hero is already the one entrance this site spends. The rail just paints. */

@media (prefers-reduced-motion: reduce) {
  .tl-dot--live::after {
    animation: none;
    opacity: 0;
  }
  .tl::after {
    animation: none;
    transform: scaleY(1);
  }
}

/* ============================================================
   GitHub activity: monochrome contribution calendar + repo cards.
   ============================================================ */
.gh-panel {
  padding: 1rem 1.1rem;
}
.gh-cal-wrap {
  /* A max-content grid otherwise expands its whole ancestor chain. The
     minmax(0,1fr) track hard-caps the calendar to the available width so it
     scrolls inside instead of widening the page. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  overflow-x: auto;
  padding-bottom: 2px;
}
.gh-cal {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(7, 11px);
  grid-auto-columns: 11px;
  gap: 3px;
  width: max-content;
}
.gh-day {
  display: inline-block;
  vertical-align: middle;
  width: 11px;
  height: 11px;
  border-radius: 2px;
  background: rgba(0, 0, 0, 0.06);
}
.gh-day[data-empty] { background: transparent; }
.gh-day[data-level="1"] { background: rgba(0, 0, 0, 0.24); }
.gh-day[data-level="2"] { background: rgba(0, 0, 0, 0.46); }
.gh-day[data-level="3"] { background: rgba(0, 0, 0, 0.7); }
.gh-day[data-level="4"] { background: rgba(0, 0, 0, 0.92); }
.dark .gh-day { background: rgba(255, 255, 255, 0.08); }
.dark .gh-day[data-empty] { background: transparent; }
.dark .gh-day[data-level="1"] { background: rgba(255, 255, 255, 0.26); }
.dark .gh-day[data-level="2"] { background: rgba(255, 255, 255, 0.48); }
.dark .gh-day[data-level="3"] { background: rgba(255, 255, 255, 0.72); }
.dark .gh-day[data-level="4"] { background: rgba(255, 255, 255, 0.96); }

.gh-cal-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-top: 0.85rem;
}
.gh-legend {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.gh-legend .gh-day {
  width: 10px;
  height: 10px;
}

/* Repo cards */
.gh-repo {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--frame-line);
  background: var(--surface);
  border-radius: var(--r-sm);
  padding: 0.85rem 0.95rem;
  transition:
    background-color var(--dur-2) var(--ease-out-quint),
    border-color var(--dur-2) var(--ease-out-quint);
}
.gh-repo:hover {
  background: var(--surface-2);
  border-color: var(--frame-strong);
}
.gh-repo-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.gh-repo-name {
  font-weight: 600;
  font-size: 0.9rem;
  color: #000;
}
.dark .gh-repo-name { color: #fff; }
.gh-repo-stars {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  flex: none;
  font-family: "Geist Mono", ui-monospace, monospace;
  font-size: 0.72rem;
  color: #6b7280;
}
.gh-star {
  width: 11px;
  height: 11px;
  color: #9ca3af;
}
.gh-repo-desc {
  margin-top: 0.35rem;
  font-size: 0.8rem;
  line-height: 1.45;
  color: #52525b; /* zinc-600 — readable secondary, not washed out */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.dark .gh-repo-desc { color: #a1a1aa; }
.gh-repo-desc--empty { font-style: italic; opacity: 0.7; }
.gh-repo-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: auto;
  padding-top: 0.7rem;
}
.gh-repo-lang {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: "Geist Mono", ui-monospace, monospace;
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  color: #6b7280;
}
.gh-lang-dot {
  width: 8px;
  height: 8px;
  border-radius: 9999px;
  background: currentColor;
  opacity: 0.55;
}
.gh-repo-arrow {
  font-size: 0.85rem;
  color: #9ca3af;
  transform: translate(0, 0);
  transition:
    transform var(--dur-2) var(--ease-out-quint),
    color var(--dur-2) var(--ease-out-quint);
}
.gh-repo:hover .gh-repo-arrow {
  color: #000;
  transform: translate(2px, -2px);
}
.dark .gh-repo:hover .gh-repo-arrow { color: #fff; }

/* ============================================================
   Boxy hover: surfaces shift fill + border instead of floating.
   ============================================================ */
.boxy-hover {
  transition:
    background-color var(--dur-2) var(--ease-out-quint),
    border-color var(--dur-2) var(--ease-out-quint);
}
.boxy-hover:hover {
  background: var(--surface-2);
  border-color: var(--frame-strong);
}

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

