/* ═══════════════════════════════════════════════════════════════════════════
   3D & CODE — the four flip cards, and the sketch frames.

   Loaded after project.css on code-3d.html only.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── The four pills ─────────────────────────────────────────────────────────
   The four cards, restyled after a gradient-menu pattern: a circle at rest
   that opens sideways on hover into a coloured pill carrying its label.

   The source pattern is a React component (react-icons, Tailwind utilities,
   a white circle on a shadow). This site has no build step and no Tailwind, so
   it is rebuilt here in plain CSS on the site's own tokens — but the resting
   circle is the reference's: a filled circle, no stroke, a soft lift under it,
   and a quiet grey icon. The shadow is a fraction of the original's `shadow-lg`
   and it is the only one on the site; see the note in DESIGN.md.

   Opening a pill drops the lift, so the gradient is the only edge it has.

   What each pill carries is the card it replaces. At rest, the question's
   glyph. Open, the answer and the answer's glyph, in that card's gradient.
   All eight glyphs and all four answers survive; the four questions do not
   appear on the page any more.

   A <button>, so the answer opens on focus as well as hover — Tab reaches it,
   and a tap on a touch screen, where there is no hover, focuses it and opens
   it too. */

/* Below 1024 the info column runs the full width of the page and there is no
   second lane to put the pills in, so they sit under the description, flush to
   the left edge, opening rightwards. */
.pills{
  margin: 2.2em 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

/* ─── The run at the top right of the column ──────────────────────────────────
   From 1024 up the info column is a third of the page and the image column
   takes the rest, so the run moves to the column's right edge — a vertical line
   of circles immediately left of the images, its top on the same line as the
   first one.

   The column becomes a flex row to do it: description left, pills right, both
   top-aligned. In flow, not absolutely positioned, so the sticky column still
   measures its own height.

   The lane is fixed at --pill-open and the pills are right-aligned inside it.
   Both matter: without the fixed width, opening a pill would widen the lane and
   reflow the description mid-hover; without the right alignment, a pill would
   grow towards the images instead of away from them. */
@media (min-width: 1024px){
  .proj__info{
    display: flex;
    align-items: flex-start;
    gap: var(--gut);
  }

  .info__text{
    flex: 1 1 auto;
    min-width: 0;               /* or a long word would push the lane off the
                                   column instead of wrapping */
  }
  .info__text > .info__body{ padding-right: 0; }   /* the lane is the inset */

  .pills{
    flex: 0 0 var(--pill-open);
    margin: 0 15px 0 0;         /* the circles stop 15px short of the images
                                   rather than running into their edge */
    align-items: flex-end;      /* right edges pinned, so they open leftwards */
  }

  /* Circle on the right, label to its left — the mirror of the narrow layout,
     so the icon stays on the edge it is anchored to. */
  .pill{ flex-direction: row-reverse; }
  .pill__answer{ padding: 0 0 0 16px; }
}

.pill{
  /* --w is the whole animation: the circle's diameter at rest, the pill's
     width open. One transition, one property. */
  --w: var(--pill-w);           /* the circle at rest — 52px, set on the column
                                   above. Big enough that a hairline symbol
                                   inside it reads at all (at 34 the glyphs were
                                   specks) and a comfortable tap target.
                                   Everything else here derives from it: the
                                   glyph is half, the height is all of it. */
  --open: var(--pill-open);     /* 124px. Measured, not guessed: the longest
                                   answer, "It's Fast", needs 113px including
                                   the circle and its padding, and the 11px over
                                   that leaves the label about a radius clear of
                                   the far end. One width for all four, so they
                                   open the same distance. */

  appearance: none;
  -webkit-appearance: none;
  padding: 0;
  background: none;
  font: inherit;
  cursor: pointer;

  position: relative;
  display: flex;
  align-items: center;
  width: var(--w);
  height: var(--w);
  border: 0;                    /* no stroke — the fill and the lift are the
                                   whole of the circle's edge */
  border-radius: 999px;
  overflow: hidden;             /* the answer is inside the pill before it opens,
                                   so this is what hides it */

  /* --bar-ground, not #fff: it is the token for "a shade brighter than the
     page", which is white on the light theme and #202124 on the dark one. A
     literal white would have burned a hole in the dark page. */
  background: var(--bar-ground);

  /* The one drop shadow on the site, and deliberately a fraction of the
     reference's `shadow-lg` — two very low-alpha layers, a 6px blur and a 2px
     offset, enough to lift a 44px circle off a #F9F9FB ground and no more. On
     the dark theme it all but disappears and the fill carries the separation on
     its own, which is how the top bar does it. */
  box-shadow: 0 2px 6px rgba(0, 0, 0, .055),
              0 1px 2px rgba(0, 0, 0, .04);

  transition: width .42s var(--ease-out-quart),
              box-shadow .42s var(--ease-out-quart);
}

.pill:hover,
.pill:focus{
  width: var(--open);
  box-shadow: none;             /* the gradient takes over from the lift */
}

/* The open state runs off :focus rather than :focus-visible on purpose: a tap
   on a touch screen focuses a button but does not match :focus-visible, so on
   :focus-visible alone the answer would be unreachable on a phone. The ring is
   the one thing still keyed to :focus-visible, so a mouse click opens a pill
   without also ringing it. */
.pill:focus{ outline: none; }
.pill:focus-visible{
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

/* The gradient sits under the content and fades up as the pill opens. Its own
   element rather than a background on the button, so it can cross-fade while
   the width animates — a background-image cannot be transitioned. */
.pill::before{
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--from), var(--to));
  opacity: 0;
  transition: opacity .42s var(--ease-out-quart);
}
.pill:hover::before,
.pill:focus::before{ opacity: 1; }

/* Both glyphs and the answer ride above the gradient. */
.pill > *{ position: relative; }

/* The resting glyph is centred in the circle and stays centred on it as the
   pill opens, which is what makes the opening read as the circle growing a tail
   rather than the content sliding. */
.pill__glyph{
  flex: 0 0 var(--w);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Hanken is subset to latin, so not one of these eight characters comes from
     it. Naming the symbol faces keeps the pen, pencil, die, quad and atom set
     as type; the die, bolt and camera emoji fall through to the emoji face, as
     they did on the old page. */
  font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols 2",
               var(--font-sans);
  /* Half the circle, which is exactly the ratio .themer draws its sun and moon
     at: a 13px icon in a 26px pill. These are hairline outline symbols, so they
     need the size — at the body's 13px they read as flecks. The one place on
     this page something is set above 13px on purpose, and it is an icon rather
     than type. */
  font-size: calc(var(--w) / 2);
  line-height: 1;               /* taller glyphs must not set the pill's height */

  /* Quiet against the white circle, the way the reference sets its icons a step
     down from black. Once a gradient is behind it, it takes the same fixed ink
     the answer does — --ink-quiet on the yellow is 2.6:1. */
  /* Not transitioned on purpose. The resting value comes from a theme token,
     and Chromium does not restart a transition when the custom property behind
     its endpoint changes — with `transition: color` here, switching to the dark
     theme left the glyph pinned at the light grey, 2.6:1 on the dark fill
     instead of 4.8:1. It snaps to the hover ink instead, under a pill that is
     growing beside it, and nothing reads as missing. */
  color: var(--ink-quiet);
}
.pill:hover .pill__glyph,
.pill:focus .pill__glyph{ color: #1C1C1C; }

.pill__answer{
  padding-right: 16px;
  white-space: nowrap;          /* the answer must not wrap mid-open */
  font-weight: 500;
  /* Fixed, not --ink. The four gradients are the same in both themes, so the
     text on them has to be too — --ink would invert to near-white in dark mode
     and drop the label to 1.7:1 on the blue. Ink holds 5.5:1 at worst across
     all four fills. */
  color: #1C1C1C;
  /* Fades in behind the widening edge rather than being dragged out of it. */
  opacity: 0;
  transition: opacity .28s var(--ease-out-quart) .12s;
}
.pill:hover .pill__answer,
.pill:focus .pill__answer{ opacity: 1; }

@media (prefers-reduced-motion: reduce){
  .pill,
  .pill::before,
  .pill__answer{ transition-duration: .01ms; }
}


/* The four fills, the same two stops each card wore. 90deg rather than the
   card's 45deg: a 132x34 pill is nearly all width, and on the diagonal the ramp
   resolved in the first few pixels. */
.pill--design { --from: #6b73ff; --to: #66f792; }
.pill--three-d{ --from: #ff6e3d; --to: #fdd752; }
.pill--code   { --from: #bc77fc; --to: #fe8672; }
.pill--motion { --from: #0ddd6d; --to: #ffe042; }


/* The pill run's two measures live on the column, because the description has
   to know the open width to stay clear of it. */
.proj__info{
  --pill-w:    52px;
  --pill-open: 124px;
}


/* ─── The description, unfolded ───────────────────────────────────────────────
   project.css sets .info__body up as the inside of a <details>: 14px below the
   "Project Info" summary. There is no disclosure here — the description is one
   line, and a control to reveal it would take more room than folding it saves —
   so it sits directly under the title, and the title's own 22px is the whole of
   the gap. */
.info__text > .info__body{ margin-top: 0; }


/* ─── Solved rows ────────────────────────────────────────────────────────────
   The two rows that close the column: the tall render beside the ribbed form,
   and the wide plate beside the portrait.

   project.css pairs on .shot--half and lets the shorter image stretch to the
   row, with `cover` taking the difference off its sides. That is right when two
   ratios are close — every other pair on this page is within 4.5% and loses
   nothing you can see. These four are not: 109 against 64, and 67 against 122.
   Halved, the ribbed form would lose 41% of its width and the plate 45% — and
   in both rows the subject sits off to one side of its frame, so the crop would
   eat it rather than the empty ground.

   So these rows are solved from the ratios instead — each figure takes the
   share that makes both natural heights equal:

       share = (1 / ratio) / Σ(1 / ratio)

   which closes the row flush at zero crop. The widths come out uneven, 37/63
   and 64/36, and that is the point: it is what letting both images keep their
   whole frame costs. Scoped to this page.

   Below 640 the column is single-file and the share is dropped, as project.css
   does with its own pair classes. */
.shot--set{ width: calc((100% - var(--gap-media)) * var(--share)); }

@media (max-width: 639px){
  .shot--set{ width: 100%; }
}


/* ─── The sketch embeds ───────────────────────────────────────────────────────
   The two OpenProcessing frames. What shows before a sketch boots is the
   frame's own ground, and the page it loads is white — so the frame is white
   too. Left on --media-ground it flashes grey on the light theme, and on the
   dark theme the sketch arrives as a white panel inside a dark box.

   Fixed, not themed: it is standing in for the embed's page, which is white
   whatever the site is set to.

   The frame's 107% is cut to the canvas, not to the 760x450 the embed declares.
   Both sketches draw a square-ish canvas, centre it, and put the toolbar above
   it; 107% of a 420px half is what holds that without white flanking it. */
.shot--sketch .shot__frame{ background: #FFF; }
