/* =========================================================
   DASHBOARD — aesv.io root
   Aggressively-responsive 6-tile grid + big footer below the fold
   ========================================================= */

.dashboard {
  padding: var(--shell-pad-y) var(--shell-pad-x);
  /* The home page lets the header (brand left, greeting right)
     spread to the viewport edges. Tile sizing is handled by the
     grid below, not by clamping the container. */
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  min-height: 100svh;
  box-sizing: border-box;
  /* The intro deals cards from translate(-220, -160) and the outro
     drops them 130vh past their resting spot. Containing both
     directions here keeps the off-canvas travel from creating
     horizontal/vertical scroll on small viewports — cards visibly
     appear from / disappear into "off the table." */
  overflow: clip;
}

/* Header layout + greeting live in foundation.css so every page
   that uses the shared site-header partial renders the same. */

/* ---------- Four-mode responsive grid ----------
   1 PORTRAIT    portrait orientation                → 2 cols × 3 rows
   2 LANDSCAPE   landscape orientation               → 3 cols × 2 rows
   3 TALL BAND   aspect-ratio < 1/2 OR very narrow   → 1 col  × 6 rows
   4 WIDE BAND   aspect-ratio > 5/2 (very wide)      → 6 cols × 1 row

   Modes 3–4 override 1–2 by source-order. Tiles stay 1:1; modes 1, 2
   and 4 cap grid width by viewport height so all six fit without
   scroll. Tall band scrolls vertically — same idiom for phone, watch,
   and tamagotchi-sized viewports (no separate watch mode). */

.dashboard-grid {
  display: grid;
  gap: 1rem; /* 16px → 22px @ billboard */
  width: 100%;
  margin: auto;
}

.dashboard-grid > * {
  aspect-ratio: 1 / 1;
  min-width: 0;
  display: flex;
  position: relative;
}

/* MODE 1 — PORTRAIT (default for portrait orientation). 2 × 3.
   gridW ≈ tileH × 2, where tileH ≈ (avail_h - gaps) / 3. The 180px
   reservation covers shell padding, header, and the dashboard gap. */
@media (orientation: portrait) {
  .dashboard-grid {
    grid-template-columns: repeat(2, 1fr);
    max-width: calc((100svh - 180px) * 2 / 3);
  }
}

/* MODE 2 — LANDSCAPE (default for landscape orientation). 3 × 2.
   gridW ≈ tileH × 3, tileH ≈ (avail_h - gap) / 2. */
@media (orientation: landscape) {
  .dashboard-grid {
    grid-template-columns: repeat(3, 1fr);
    max-width: calc((100svh - 180px) * 3 / 2);
  }
}

/* MODE 2b — WIDE. Above 1200px wide, hold the 3 × 2 landscape grid even
   in portrait orientation, and pin individual tiles to ~420px so they
   don't keep growing past their 1920px-viewport size. The grid still
   fills the dashboard's width; the extra space sits as gutter on either
   side of the centered 3 × 420 tracks. */
@media (min-width: 1200px) {
  .dashboard-grid {
    grid-template-columns: repeat(3, minmax(0, 420px));
    max-width: none;
    justify-content: center;
  }
}

/* MODE 3 — TALL BAND. Only the very smallest viewports (watch,
   tamagotchi) collapse to a single column. Phones in portrait keep
   the 2-column portrait grid so six 1:1 tiles read as a dashboard,
   not a scroll. */
@media (max-width: 299px) {
  .dashboard-grid {
    grid-template-columns: 1fr;
    max-width: 100%;
  }
}

/* MODE 4 — WIDE BAND. Aspect-ratio > 5/2 — very wide short viewports
   (split-screen, ultrawide, kiosk). Single horizontal row. Cap grid
   width so all six 1:1 tiles fit the available height without
   horizontal scroll. */
@media (min-aspect-ratio: 5 / 2) {
  .dashboard-grid {
    grid-template-columns: repeat(6, 1fr);
    max-width: calc((100svh - 140px) * 6);
  }
}

/* MODE 5 — NARROW. Apple Watch Ultra (~410px) and small phones.
   Forces 2 columns regardless of orientation since the landscape
   mode triggers on watches with weird aspect ratios and gives a
   cramped 3-col layout. Drops the height-based max-width cap so
   tiles grow to fill the available width and the date/weather
   reads at a comfortable size. */
@media (max-width: 600px) {
  .dashboard-grid {
    grid-template-columns: repeat(2, 1fr);
    max-width: 100%;
  }
}

/* ---------- Tiles + widget shells ---------- */
.tile-link,
.widget-cell {
  width: 100%;
  height: 100%;
  /* Containers so child labels (and the weather widget's interior
     numerals) can scale with cqi against the cell, not the viewport. */
  container-type: inline-size;
}
.widget-cell > *:not(.cell-hover-label) {
  width: 100%;
  height: 100%;
  min-width: 0;
}
.widget-cell .widget {
  display: flex;
  flex-direction: column;
  height: 100%;
  position: relative;
}
.widget-cell .widget-art {
  flex: 1 1 auto;
  width: 100%;
  height: 100%;
  min-height: 0;
  min-width: 0;
  aspect-ratio: auto;
  position: relative;
  /* The cell wrapper carries the border + cartoon shadow; zero out
     the inner widget-art frame so the music tile doesn't double up
     and read as a thicker border than the other tiles. */
  border: none;
  box-shadow: none;
}
.widget-art img,
.widget-art svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.widget-cell .widget-caption {
  display: none;
}

/* Cell variants share THE TILE chrome from foundation (background and
   border come from ::before; this file only sets sizing). No overflow:
   hidden — that would clip the ::before chrome's shadow (CSS clips
   children's box-shadows under overflow:hidden parents). The image
   inside .widget-art is constrained by object-fit: cover. */
.widget-spotify {
  width: 100%;
  height: 100%;
}
.widget-spotify .widget-art {
  background: var(--ink);
}

.widget-cell-link {
  position: relative;
  text-decoration: none;
  color: inherit;
}
/* Bottom gradient on the cell itself (not the label) — keeps the
   underlying visual visible while the cream label reads on top. The
   gradient is decorative and sized to the full cell; the label is
   positioned independently with the same offset as .tile-label so
   the text lands in the same place across all six tiles. */
.widget-cell-link::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgb(from var(--ink) r g b / 0) 50%,
    rgb(from var(--ink) r g b / 0.6) 100%
  );
  z-index: 4;
  pointer-events: none;
  transition: background var(--dur-press) var(--ease-press);
}
.widget-cell-link:hover::after,
.widget-cell-link:focus-visible::after {
  background: rgb(from var(--ink) r g b / 0.78);
}
.widget-cell-link .cell-hover-label {
  color: var(--cream);
  z-index: 5;
  pointer-events: none;
}
.widget-cell-link .cell-hover-label .tile-label-arrow {
  color: var(--cream);
}

/* ---------- Bare-cell modifier — used by the top-left info cell.
   The bare cell never picks up tile chrome (its parent .widget-cell
   only routes to the tile primitive for the .spotify / .hobbies
   variants), so no resets are needed. The class still tags the cell
   for intro / outro animation overrides further down. */

/* ---------- Info paragraph (the top-left bare cell) ----------
   Plain text broken into four hard lines via <br>. Block layout so
   the inline spans flow as normal text — flex would make every child
   its own line. Sized like a tile-label so it reads as the same
   family of typography. */
/* When-block top-left, weather-block bottom-right. The widget-info
   fills the cell and uses justify-content: space-between to push the
   two paragraphs to opposite ends of the column. */
.widget-info {
  margin: 0;
  padding: 2ch;
  width: 100%;
  box-sizing: border-box;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(12px, 7.3cqi, 44px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}
.widget-info .info-when,
.widget-info .info-weather {
  margin: 0;
}
.widget-info .info-when {
  text-align: left;
}
.widget-info .info-weather {
  text-align: right;
}

/* Hobbies widget shares the unified .widget-cell frame above. Chrome
   (cream paper, ink border, cartoon shadow) comes from the tile
   primitive in foundation.css. */
.widget-hobbies {
  position: relative;
  width: 100%;
  height: 100%;
}
.hobby-pane {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 600ms ease,
    visibility 600ms ease;
  display: flex;
  align-items: stretch;
  justify-content: stretch;
}
.hobby-pane.is-active {
  opacity: 1;
  visibility: visible;
}
.hobby-pane svg {
  width: 100%;
  height: 100%;
  display: block;
}
.hobby-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--ink);
  color: var(--cream);
  font-family: var(--font-display);
  font-size: 13px;
}

/* ---------- Strava pane content — labels gone, hover label on the
   wrapping cell carries the identifier. Track / time / distance / gain
   read directly as Birdie text inside the square. */
/* Strava route polyline — fills the cell ink-on-cream like the chess board.
   The label sits on top via the cell-hover-label gradient. */
.strava-route {
  width: 100%;
  height: 100%;
  display: block;
  background: var(--cream);
}

/* Fallback when no polyline (indoor activities, missing scope) — show stats. */
.hobby-strava-stats {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  padding: clamp(16px, 2.4cqi, 28px);
  background: var(--cream);
  container-type: inline-size;
  gap: clamp(6px, 1cqi, 12px);
}
.hobby-strava-stats .strava-name {
  font-family: var(--font-display);
  font-size: clamp(18px, 3.4cqi, 32px);
  line-height: 1.05;
  margin: 0 0 clamp(8px, 1.4cqi, 14px);
  color: var(--ink);
  overflow-wrap: anywhere;
}
.hobby-strava-stats .strava-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(4px, 1cqi, 10px);
  margin: 0;
  margin-top: auto;
}
.hobby-strava-stats .strava-stats > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.hobby-strava-stats dt {
  font-family: var(--font-display);
  font-size: clamp(11px, 1.2cqi, 13px);
  color: var(--ink-mute);
}
.hobby-strava-stats dd {
  font-family: var(--font-display);
  font-size: clamp(15px, 2.4cqi, 22px);
  line-height: 1;
  color: var(--ink);
  margin: 0;
  overflow-wrap: anywhere;
}

/* Tile — single-word identifier bottom-left. Lavender on .tile-primary.
   Chrome + press behavior come from THE TILE primitive in foundation.css. */
.tile {
  display: block;
  text-decoration: none;
  color: var(--ink);
  width: 100%;
  height: 100%;
}
.tile:hover {
  color: var(--ink);
}

/* Single label rule — applies to both .tile-label (work / about / files)
   and .cell-hover-label (music / activities). Both are positioned the
   same way so labels line up across all six tiles regardless of which
   variant the tile uses. The sizing reads cqi against the cell, which
   has no padding affecting container-inline-size — tile-label and
   cell-hover-label therefore measure identically. */
.tile-label,
.widget-cell-link .cell-hover-label {
  position: absolute;
  bottom: clamp(8px, 5cqi, 24px);
  left: clamp(8px, 5cqi, 24px);
  right: clamp(8px, 5cqi, 24px);
  font-family: var(--font-display);
  font-size: clamp(10px, 7cqi, 36px);
  line-height: 1;
  letter-spacing: -0.02em;
  display: inline-flex;
  align-items: baseline;
  gap: clamp(3px, 1.5cqi, 12px);
  color: inherit;
}
.tile-label-arrow {
  font-family: var(--font-display);
  font-size: clamp(7px, 4.5cqi, 22px);
  line-height: 1;
}
.tile-primary {
  --tile-bg: var(--lavender);
  color: var(--offwhite);
}
.tile-primary .tile-label,
.tile-primary .tile-label-arrow {
  color: var(--offwhite);
}

/* Music + activities labels read cream against the gradient overlay so
   they stay legible over album art / chess board / strava polyline. */
.widget-cell-link .cell-hover-label,
.widget-cell-link .cell-hover-label .tile-label-arrow {
  color: var(--cream);
}

/* =========================================================
   DASHBOARD — INTRO
   Header text fades in, then a tiny pause, then the six grid
   cells are dealt in source order. Source-order stagger lands
   correctly across all four grid modes:
     · landscape (3×2): rows fill L→R, top row first
     · portrait (2×3):  reads L→R per row, top to bottom
     · tall band (1×6): top → bottom
     · wide band (6×1): left → right
   Scoped under .dashboard so the shared site-header partial
   only animates on the home route.
   ========================================================= */

@keyframes dashboard-intro-text {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
/* Card-deal feel: every card flies in from a single source above the
   middle of the grid (the dealer's hand) to its own resting spot.
   dashboard.js measures the grid + each cell on load and sets per-cell
   --deal-x / --deal-y / --deal-rot so the cards converge on one point
   regardless of which responsive grid mode is active. The fallbacks
   (-220, -160, -15deg) cover the case where JS hasn't run yet — same
   off-axis upper-left for every card.
   Two things make it read as flight rather than evaporate-on-arrival:
   · opacity ramps to 1 inside the first 8%, so the card is solid for
     the bulk of its visible travel
   · the start is far enough to give the eye a real arc to track
   The cartoon shadow lives on the tile's ::before chrome layer and
   stays at --shadow-md throughout — animating it on the wrapper here
   would render a second shadow on top of ::before's during flight. */
@keyframes dashboard-intro-deal {
  0% {
    opacity: 0;
    transform: translate(var(--deal-x, -220px), var(--deal-y, -160px))
      rotate(var(--deal-rot, -15deg));
  }
  8% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: translate(0, 0) rotate(0);
  }
}

.dashboard .site-header {
  animation: dashboard-intro-text 280ms ease-out backwards;
}

/* The bare info cell (slot 1) is just text — it has no card chrome,
   so dealing it from off-axis would look weird. It fades in like the
   header, slightly delayed so the eye reads brand → greeting → info
   as a single text wave before the cards drop. */
.dashboard-grid > .widget-cell-bare {
  animation: dashboard-intro-text 280ms ease-out 120ms backwards;
}

/* Sequential deal (cards 2–6):
   · 320ms flight × back-out easing → 280ms of visible travel after
     the opacity ramp, then ~9% overshoot past 0 and settle
   · 110ms stagger → next card starts while current is still mid-air,
     so the dealer reads as fast-handed without losing the per-card
     rhythm
   · 480ms slot-2 delay → header + info text have settled + a beat */
.dashboard-grid > * {
  animation: dashboard-intro-deal 320ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}
.dashboard-grid > *:nth-child(2) {
  animation-delay: 480ms;
}
.dashboard-grid > *:nth-child(3) {
  animation-delay: 590ms;
}
.dashboard-grid > *:nth-child(4) {
  animation-delay: 700ms;
}
.dashboard-grid > *:nth-child(5) {
  animation-delay: 810ms;
}
.dashboard-grid > *:nth-child(6) {
  animation-delay: 920ms;
}

@media (prefers-reduced-motion: reduce) {
  .dashboard .site-header,
  .dashboard-grid > * {
    animation: none;
  }
}

/* ---------- Dashboard outro — fires when a same-origin link inside
   the dashboard is clicked. dashboard.js adds .is-leaving, runs the
   animation, then navigates. Cards fall in source order (mirroring
   the deal); the header fades quickly so the outro stays under
   ~560ms total — long enough to read as a deliberate exit, short
   enough that nav still feels snappy. fill-mode: forwards holds the
   off-screen state until the next document loads. */
@keyframes dashboard-outro-text {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-4px);
  }
}
/* Anticipation lift then gravity fall all the way off-screen. The
   tiny upward lift at 18% adds the "plucked up before dropping"
   character the cards earn from being placed by hand. 130vh
   guarantees the card exits the viewport regardless of viewport
   size + a little rotational tumble. */
@keyframes dashboard-outro-fall {
  0% {
    opacity: 1;
    transform: translate(0, 0) rotate(0);
  }
  18% {
    opacity: 1;
    transform: translate(0, -14px) rotate(-2deg);
  }
  100% {
    opacity: 0;
    transform: translate(14px, 130vh) rotate(14deg);
  }
}

.dashboard.is-leaving .site-header {
  animation: dashboard-outro-text 140ms ease-in forwards;
}
/* The bare info cell is just text — fade out like the header rather
   than tumble. Same selector pattern as the intro override. */
.dashboard.is-leaving .dashboard-grid > .widget-cell-bare {
  animation: dashboard-outro-text 140ms ease-in forwards;
}
/* Snappier sequential fall (cards 2–6):
   · 220ms drop with strong gravity easing
   · 55ms stagger → quick visible cascade
   · Last card lands off-screen at 4×55 + 220 = 440ms
   OUTRO_CAP_MS in dashboard.js covers the failsafe nav. */
.dashboard.is-leaving .dashboard-grid > * {
  animation: dashboard-outro-fall 220ms cubic-bezier(0.55, 0, 0.95, 0.3) forwards;
}
.dashboard.is-leaving .dashboard-grid > *:nth-child(2) {
  animation-delay: 0ms;
}
.dashboard.is-leaving .dashboard-grid > *:nth-child(3) {
  animation-delay: 55ms;
}
.dashboard.is-leaving .dashboard-grid > *:nth-child(4) {
  animation-delay: 110ms;
}
.dashboard.is-leaving .dashboard-grid > *:nth-child(5) {
  animation-delay: 165ms;
}
.dashboard.is-leaving .dashboard-grid > *:nth-child(6) {
  animation-delay: 220ms;
}

@media (prefers-reduced-motion: reduce) {
  .dashboard.is-leaving .site-header,
  .dashboard.is-leaving .dashboard-grid > * {
    animation: none;
  }
}

/* =========================================================
   DASHBOARD — Per-mode chrome adjustments
   Layout (column count) is set by the four-mode block at the top
   of this file. Rules below tune chrome only at extreme viewport
   sizes; they don't override the grid layout itself.

   Type sizing rides the rem cascade through the html font-size
   knob in foundation.css — there are no per-element font-size
   overrides at billboard or wall.
   ========================================================= */

/* ---------- Tall band & narrow phones — tighter shell + single
   footer column so columns don't blow out. */
@media (max-width: 699px) {
  .dashboard {
    padding: 0.75rem;
    gap: 0.75rem;
  }
  .footer-cols {
    grid-template-columns: 1fr;
    gap: var(--s-3);
  }
  .footer-bottom {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ---------- Watch / tamagotchi chrome — keep brand + greeting on
   one line at narrow viewports, just smaller. The previous version
   stacked them vertically, which the user noted ate scarce vertical
   real estate on the Apple Watch when there was plenty of width to
   fit both on a row. */
@media (max-width: 480px) and (max-height: 600px) {
  .dashboard .brand {
    font-size: 0.9375rem;
    gap: 0.375rem;
  }
  .dashboard .brand img {
    width: 1.25rem;
  }
  .dashboard-greeting {
    font-size: 0.9375rem;
    line-height: 1.2;
    text-align: right;
  }
  .dashboard-greeting .meta {
    display: none;
  }
}
