/* ============================================================================
   2 · THE OFFERINGS — band-tinted, one mechanism: a grid of cards that turn
   over. Markup: js/render/offers.js. Behaviour: js/behaviour/offers.js.

   §10.9: a card grid, because the reader compares the offerings. Three
   across at desktop, sized as the Game cards on the games page (games_page/
   v2, game-cards.css): three rows of three (a wrapping flex row, so a
   short row would centre). Two across on tablet; one
   across below 768px, capped
   at 300px so a portrait card takes about half a phone's height, its text
   a step smaller (asked for by the page owner, against §5.3's "the cards
   never shrink").

   The card: front, the cover (an image, the radar, or a placeholder tile
   until the cover is designed) with the kind and title over a scrim at its foot (§14.2); back,
   a local dark theme context (.theme-dark) on purple-900 with the summary
   and facts. Each face has a turn button laid over the whole of it. Its
   states: hover and focus-visible lift the card (§10.3: the same treatment
   on both) with the 3px ring on focus, drawn inside the face, which clips;
   active presses it; data-turned on the card turns it over. No disabled
   state: every card can turn. The face turned away takes no clicks.

   The one button (Learn more, or the offering's own action) lies centred on
   the front face above its turn button, and turns away with it: the back
   has none. The cover is dark at its foot, so the button takes the dark
   context (§10.1 secondary: white outline, the offsite arrow). It keeps the
   44px touch floor (§13.2) and loses padding only.

   Recorded exceptions:
   - The turn is a 3D rotation, not transform/opacity alone in the flat
     sense of §8.1, within {motion.slow}.
   - The hop repeats, against §8.1's "nothing loops", asked for by the page
     owner as the hint that the cards turn: one card at a time, now and
     then, only while the cards are in view and none is under the pointer
     or turned over, transform and opacity only,
     and never under reduced motion (behaviour/offers.js).
   - The Online cover's radar sweep loops, at the page owner's request;
     stilled under reduced motion
     (tokens.css).
   ========================================================================= */
.offers {
  padding-block: var(--space-section);
  background: var(--color-canvas-tinted);
}
/* The head spans the container so the title holds one line on desktop,
   asked for by the page owner; the lead keeps its own 60ch measure. */
.offers .section-head { max-width: none; }
.offers__lead {
  margin: var(--space-4) 0 0;
  max-width: 60ch;
  font-size: var(--type-lead);
  line-height: 1.65;
  color: var(--color-body);
}

.offers__grid {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-8) var(--space-6);
}
/* A column's width: the row less its gaps, shared out. */
.offers__grid > .offer { flex: 0 0 calc((100% - 2 * var(--space-6)) / 3); min-width: 0; }

/* The row labels (.kicker, the eyebrow), desktop only: a full-width item
   before each row, close above its cards and a step clear of the row
   before. A short row is centred, so its label moves in to the first
   card's left edge: half a column and half a gap for each card the row
   lacks (--row-cards, from the renderer). */
.offers__label {
  flex: 0 0 100%;
  box-sizing: border-box;
  margin: 0 0 calc(var(--space-4) - var(--space-8));
  padding-left: calc((3 - var(--row-cards, 3)) * (100% + var(--space-6)) / 6);
}
.offers__label:not(:first-child) { margin-top: var(--space-4); }

.offer {
  /* Tier-3: the inset of everything on the card, the height of its
     buttons (.btn--sm), which the text on both faces keeps clear of, the
     title's size (the floor of {type.h3}), the length of a hop, and the
     height of a partner's mark (smaller where four narrow cards share a
     row) and of the GESDA mark, twice that. */
  --offer-inset: var(--space-6);
  --offer-actions-h: 44px;
  --offer-name-size: 1.25rem;
  --offer-hop: 1100ms;
  --offer-partner-h: var(--space-4);
  --offer-gesda-h: calc(var(--offer-partner-h) * 2);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* --- the hop -------------------------------------------------------------
   behaviour/offers.js sets .offer--hop on one card at a time: it jumps up,
   lands, and bounces once more, a little lower, at its own size. A soft
   shadow shows under it while it is in the air. */
.offer--hop .offer__card { animation: offer-hop var(--offer-hop) ease-in-out; }
.offer::after {
  content: "";
  position: absolute; left: 15%; right: 15%; bottom: calc(-1 * var(--space-3));
  height: var(--space-4);
  border-radius: var(--radius-full);
  background: radial-gradient(closest-side, var(--color-scrim), transparent);
  opacity: 0;
  pointer-events: none;
}
.offer--hop::after { animation: offer-hop-shadow var(--offer-hop) ease-in-out; }
@keyframes offer-hop {
  0%, 60%, 100% { transform: none; }
  30% { transform: translateY(calc(-1 * var(--space-2))); }
  75% { transform: translateY(calc(-0.5 * var(--space-1))); }
  90% { transform: none; }
}
@keyframes offer-hop-shadow {
  0%, 60%, 90%, 100% { opacity: 0; }
  30% { opacity: 0.35; }
  75% { opacity: 0.1; }
}

/* --- the card ------------------------------------------------------------ */
.offer__card {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
  perspective: 1200px;
  transition: transform var(--motion-medium), box-shadow var(--motion-medium);
}
.offer__card:has(.offer__turn:is(:hover, :focus-visible)) {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.offer__card:has(.offer__turn:active) {
  transform: scale(0.99);
  box-shadow: var(--shadow-sm);
}

.offer__inner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform var(--motion-slow);
}
.offer[data-turned] .offer__inner { transform: rotateY(180deg); }

.offer__face {
  position: absolute; inset: 0;
  overflow: hidden;
  border-radius: var(--radius-lg);
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
}
.offer[data-turned] .offer__face--front,
.offer:not([data-turned]) .offer__face--back { pointer-events: none; }
.offer__cover {
  width: 100%; height: 100%;
  object-fit: cover;
}
img.offer__cover { display: block; }
/* The placeholder's label sits high, clear of the status chip above and
   the title below. */
.offer__cover.ph { align-items: start; padding-top: var(--space-16); }
/* The Online cover: the violet radar sweep (markup in lib/render.js) on
   that card's purple-900, centred above the caption and cut by the card's
   sides. */
.offer__cover--radar { position: relative; background: var(--purple-900); }

.radar {
  --scan: 6s;
  position: absolute; top: 40%; left: 50%;
  height: 90%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-full);
  background: radial-gradient(circle, var(--color-photo-glow) 0%, transparent 62%);
}
.radar__grid { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.radar__grid circle,
.radar__grid line {
  fill: none; stroke: var(--color-radar-ring); stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.radar__grid .is-dotted { stroke-dasharray: 2 6; stroke-linecap: round; }

/* The wedge trails the beam anticlockwise. Its leading edge is at 0deg, the
   top, and the whole disc turns clockwise, so the beam crosses a blip at
   bearing --t exactly --t of the way through each turn. */
.radar__sweep {
  position: absolute; inset: 0;
  border-radius: var(--radius-full);
  background: conic-gradient(
    transparent 0deg 300deg,
    var(--color-radar-trail) 300deg,
    var(--color-radar-sweep) 360deg);
  animation: radar-turn var(--scan) linear infinite;
}
.radar__beam {
  position: absolute; left: 50%; top: 0;
  width: 2px; height: 50%; margin-left: -1px;
  background: linear-gradient(to top, transparent, var(--color-radar-beam));
}

.radar__blip {
  position: absolute; inset: 0;
  transform: rotate(calc(var(--t) * 1turn));
}
.radar__blip i {
  position: absolute; left: 50%; top: calc(50% - var(--r));
  width: 10px; height: 10px; margin: -5px 0 0 -5px;
  border-radius: var(--radius-full);
  background: var(--purple-100);
  box-shadow: 0 0 0 5px var(--color-radar-ring), 0 0 18px 4px var(--color-radar-halo);
  opacity: 0.35;
  /* in phase with the sweep: the flash lands when the beam reaches --t */
  animation: radar-ping var(--scan) linear infinite;
  animation-delay: calc(var(--t) * var(--scan) - var(--scan));
}
.radar__core {
  position: absolute; top: 50%; left: 50%;
  width: 8px; height: 8px; margin: -4px 0 0 -4px;
  border-radius: var(--radius-full);
  background: var(--purple-300);
}

@keyframes radar-turn { to { transform: rotate(1turn); } }
@keyframes radar-ping {
  0%   { opacity: 1;    transform: scale(1.35); }
  30%  { opacity: 0.35; transform: scale(1); }
  100% { opacity: 0.35; transform: scale(1); }
}

/* §10.2 chip-status: a dot and the status in words, on the cover */
.offer__status {
  position: absolute; left: var(--offer-inset); top: var(--offer-inset);
  margin: 0;
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--purple-050);
  font-size: var(--type-caption);
  font-weight: 600;
  line-height: 1.4;
  color: var(--purple-900);
}
.offer__status::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: var(--radius-full);
  background: var(--purple-700);
}
/* The marks along the cover's top: GESDA's on the left, a partner's, as
   supplied (§9), on the right, centred on one line. A partner mark
   that reads small at the standard height sets its own multiple of it
   (--offer-partner-scale, from the data). */
.offer__marks {
  position: absolute; left: var(--offer-inset); right: var(--offer-inset); top: var(--offer-inset);
  display: flex; align-items: center; gap: var(--space-4);
  min-height: var(--offer-gesda-h);
}
.offer__gesda { width: auto; height: var(--offer-gesda-h); }
.offer__partner {
  margin-left: auto;
  width: auto; height: calc(var(--offer-partner-h) * var(--offer-partner-scale, 1));
}

/* --- the front: kind and title over a scrim at the cover's foot ---------- */
.offer__caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: var(--space-12) var(--offer-inset) calc(var(--offer-actions-h) + 2 * var(--offer-inset));
  background: linear-gradient(to top, var(--color-scrim-strong) 55%, transparent);
}
.offer__kind {
  margin: 0;
  font-family: var(--font-accent);
  font-size: var(--type-eyebrow);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.offer__name {
  margin: var(--space-2) 0 0;
  font-size: var(--offer-name-size);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--color-ink);
}

/* --- the back: text only ------------------------------------------------- */
.offer__face--back {
  transform: rotateY(180deg);
  display: flex;
  flex-direction: column;
  padding: var(--offer-inset);
  box-sizing: border-box;
  background: var(--color-canvas);
}
.offer__summary {
  margin: 0;
  font-size: var(--type-body);
  line-height: 1.55;
  color: var(--color-ink);
}
/* Figures in data: Space Grotesk's home (§4.2 data-label). */
.offer__facts {
  margin: auto 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-hairline);
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4);
  font-family: var(--font-accent);
  font-size: var(--type-data-label);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.02em;
  color: var(--color-ink);
}

/* --- the turn buttons, one over each face -------------------------------- */
.offer__turn {
  position: absolute; inset: 0; z-index: 1;
  margin: 0; padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  background: none;
  cursor: pointer;
}
.offer__turn:focus-visible { outline: 3px solid var(--color-focus-ring); outline-offset: -3px; }
/* Off for now: the cards do not turn (behaviour/offers.js is not loaded,
   app.js), so the turn buttons, and the lift on hovering them, go. */
.offer__turn { display: none; }

/* --- the button, centred on the front above its turn button ------------
   Clicks either side of it fall through to the turn. The outline button
   of the dark context, as the Alumni portal button (white border and
   arrow; the wash on hover and press), at least a little over half the
   card wide so the labels line up across a row. */
.offer__actions {
  position: absolute; left: var(--offer-inset); right: var(--offer-inset); bottom: var(--offer-inset);
  z-index: 2;
  display: flex; justify-content: center;
  pointer-events: none;
}
.offer__actions .btn {
  min-height: var(--offer-actions-h);
  min-width: 55%;
  padding: 0 var(--space-6);
  white-space: nowrap;
  pointer-events: auto;
}
/* Coming soon: no link yet, so no arrow, no hover; a click goes to the turn. */
.offer__actions .offer__soon { pointer-events: none; }

/* ============================================================================
   RESPONSIVE (§13.1) — review at 1440, 1180, 980, 720, 480, 390.
   ========================================================================= */
@media (max-width: 1199px) {
  .offer { --offer-inset: var(--space-4); --offer-name-size: var(--type-h4); --offer-partner-h: var(--space-3); }
  .offer__summary { font-size: var(--type-body-sm); }
  .offer__actions .btn { padding: 0 var(--space-4); }
}
@media (max-width: 1023px) {
  .offers__grid > .offer { flex-basis: calc((100% - var(--space-6)) / 2); }
  /* Two across, the rows no longer follow the groups: no labels. */
  .offers__label { display: none; }
  .offer { --offer-inset: var(--space-6); --offer-name-size: 1.25rem; --offer-partner-h: var(--space-4); }
  .offer__summary { font-size: var(--type-body); }
  .offer__actions .btn { padding: 0 var(--space-6); }
}
@media (max-width: 767px) {
  .offers__grid { max-width: 300px; margin-inline: auto; gap: var(--space-6); }
  .offers__grid > .offer { flex-basis: 100%; }
  /* A smaller card, its text a step down to match. */
  .offer { --offer-inset: var(--space-4); --offer-name-size: var(--type-h4); }
  .offer__caption { padding-top: var(--space-8); }
  .offer__summary { font-size: var(--type-body-sm); }
  .offer__facts { font-size: var(--type-caption); }
  .offer__actions .btn { padding: 0 var(--space-4); font-size: var(--type-body-sm); }
}
