@celestia-island/hikari 0.55.310 → 0.55.312

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.310",
3
+ "version": "0.55.312",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -2,24 +2,12 @@
2
2
  * Behavioral contract for the DESKTOP modal open/close choreography — run
3
3
  * against the COMPILED stylesheet, not the source text.
4
4
  *
5
- * User direction 2026-11-07 (third revision, supersedes the 2026-11-05
6
- * content-only ride and the 2026-11-06 body scales): ONLY THE HEIGHT MOVES,
7
- * and the CHROME KEEPS ITS NATURAL SIZE. The frame's transform is the plain
8
- * centering translate in EVERY state; the open/close is carried by LAYOUT
9
- * HEIGHT through the family primitive (`useHeightRide` publishes
10
- * `--hk-ride-chrome` / `--hk-ride-content` / `--hk-ride-factor`, and the
11
- * frame maps its surface phases onto `hk-ride--from` / `hk-ride--to`):
12
- *
13
- * frame height : chrome + content × 0.9 ⇄ chrome + content
14
- * content region: scaleY(0.9) ⇄ scaleY(1) (origin top)
15
- *
16
- * The retired grammars, oldest first: the 2026-11-04 whole-frame
17
- * `scale(1, 0.9)`, the 2026-11-05 body-only uniform `scale(0.9)`, the
18
- * 2026-11-06 `scale(1.1, 1)` overshoot, its same-day `scale(0.95, 1)`
19
- * pinch, and the 2026-11-06/07 frame-level `scaleY(0.9)` ride (retired
20
- * because a frame scale squashed the title glyphs and the footer buttons —
21
- * the user's 2026-11-07 report on 0.55.305).
22
- */
5
+ * User direction 2026-11-07 (fifth revision, chosen from a comparison
6
+ * page): the whole window zooms and rises — B3. The frame's transform is
7
+ * the permanent centering translate with a translateY+scale appended, a
8
+ * compositor-only motion; opacity rides the same clock. The body carries
9
+ * NO rule in this choreography (nothing inside is ever scaled on its own).
10
+ */
23
11
  import { describe, expect, it } from "vitest";
24
12
  import { dirname, join } from "node:path";
25
13
  import { fileURLToPath } from "node:url";
@@ -323,8 +311,8 @@ const CENTERING =
323
311
  /** Frame ride states: the centering translate is a PERMANENT prefix and
324
312
  * only the appended scaleY changes (2026-11-06: the window's height rides
325
313
  * the open/close about its center). */
326
- const FRAME_FROM = `${CENTERING} scaleY(0.9)`;
327
- const FRAME_TO = `${CENTERING} scaleY(1)`;
314
+ const FRAME_FROM = `${CENTERING} translateY(10px) scale(0.97)`;
315
+ const FRAME_TO = `${CENTERING} translateY(0) scale(1)`;
328
316
 
329
317
  /** Exact compiled transition values (winner()-normalized) — pinned whole,
330
318
  * not by prefix: an embedded fourth time value (delay) or a smuggled extra
@@ -333,17 +321,10 @@ const FRAME_TO = `${CENTERING} scaleY(1)`;
333
321
  * R2 finding 2026-11-06 (S1): prefix `toContain` let both survive. */
334
322
  const ENTER_TRANSITION =
335
323
  "opacity var(--hk-modal-duration, 0.42s) var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)), " +
336
- "height var(--hk-modal-duration, 0.42s) var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1))";
324
+ "transform var(--hk-modal-duration, 0.42s) var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1))";
337
325
  const LEAVE_TRANSITION =
338
326
  "opacity var(--hk-modal-duration, 0.32s) var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1)), " +
339
- "height var(--hk-modal-duration, 0.32s) var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1))";
340
- /** The ride's own geometry lives in the shared container sheet; the modal
341
- * frame maps its phases onto these two ends. */
342
- const RIDE_SHEET = (() => {
343
- const path = join(here, "HkRideBox.scss");
344
- return compile(path, { loadPaths: [here, join(here, "..", "styles")], style: "expanded" }).css;
345
- })();
346
- const RIDE_RULES = parseRules(stripComments(RIDE_SHEET));
327
+ "transform var(--hk-modal-duration, 0.32s) var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1))";
347
328
 
348
329
  describe("HkModal desktop open/close contract (compiled cascade, user direction 2026-11-07)", () => {
349
330
  it("compiles into a stylesheet with the ride rules present (known-positive)", () => {
@@ -355,17 +336,24 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
355
336
  expect(selectors.some((s) => s.includes("hk-modal-content-enter-from"))).toBe(true);
356
337
  });
357
338
 
358
- it("keeps the frame's transform at the plain centering translate in every state", () => {
359
- // The ride is layout height, not a frame transform — that is the whole
360
- // point (a frame scale squashed the chrome). So the transform must be
361
- // the SAME `translate(...)` in rest and at both ride ends, and adding a
362
- // scale to it in any state is the regression this pins.
339
+ it("rides the whole window on the centering translate plus a zoom pair", () => {
340
+ // B3 (user direction 2026-11-07, fifth revision): the frame's transform
341
+ // is the PERMANENT centering translate with a translateY+scale appended
342
+ // — a compositor-only motion. Rest keeps the bare translate; the two
343
+ // ride ends are the contracted zoom pair, byte-identical across the
344
+ // draggable variant. Anything else (a bare scale, a lost shift token,
345
+ // a different function order) is the regression this pins.
363
346
  for (const state of [...RIDE, { name: "rest", classes: [] as string[] }]) {
347
+ const compressed = state.classes.some((c) =>
348
+ c.includes("enter-from") || c.includes("leave-to"));
349
+ const expected = state.classes.length === 0
350
+ ? CENTERING
351
+ : compressed ? FRAME_FROM : FRAME_TO;
364
352
  for (const [label, frame] of [
365
353
  ["", frameWith(...state.classes)],
366
354
  ["draggable ", draggableFrameWith(...state.classes)],
367
355
  ] as const) {
368
- expect(winner(frame, "transform", DESKTOP), `${label}${state.name}`).toBe(CENTERING);
356
+ expect(winner(frame, "transform", DESKTOP), `${label}${state.name}`).toBe(expected);
369
357
  for (const prop of ["scale", "zoom", "translate", "rotate", "transform-origin"]) {
370
358
  expect(winner(frame, prop, DESKTOP), `${label}${state.name} ${prop}`).toBeUndefined();
371
359
  }
@@ -416,59 +404,16 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
416
404
  "will-change",
417
405
  DESKTOP,
418
406
  );
419
- expect(enterWill).toBe("opacity");
407
+ expect(enterWill).toBe("opacity, transform");
420
408
  // The close direction promotes the same pair of layers.
421
409
  const leaveWill = winner(
422
410
  frameWith("hk-modal-content-leave-to", "hk-modal-content-leave-active"),
423
411
  "will-change",
424
412
  DESKTOP,
425
413
  );
426
- expect(leaveWill).toBe("opacity");
414
+ expect(leaveWill).toBe("opacity, transform");
427
415
  });
428
416
 
429
- it("rides the HEIGHT through the shared container contract, chrome untouched", () => {
430
- // The two ends of the ride are expressed as `height: calc(chrome +
431
- // content × factor)` on the frame (`!important` beats the size morph's
432
- // inline pin for the duration of the flight) and as a top-origin
433
- // `scaleY` on the CONTENT REGION only — so the title bar and the footer
434
- // keep their natural size, which is the 2026-11-07 user requirement.
435
- // A selector may be declared by more than one rule (the ride's geometry
436
- // pairs: the compressed height, the content's natural-height lock and the
437
- // content's scale). Look the declaration up by property, not by "the
438
- // first rule with this selector".
439
- const rideRule = (selector: string, prop = "height"): CssRule | undefined =>
440
- RIDE_RULES.find((r) => r.selector === selector && r.declarations.has(prop));
441
-
442
- const from = rideRule(".hk-ride[data-hk-ride-ready].hk-ride--from");
443
- expect(from, "compressed end rule").toBeTruthy();
444
- expect(from!.declarations.get("height")).toBe(
445
- "calc(var(--hk-ride-chrome, 0px) + var(--hk-ride-content, 0px) * var(--hk-ride-factor, 0.9))",
446
- );
447
- expect(from!.important.has("height"), "the ride outranks the morph's inline pin").toBe(true);
448
-
449
- const to = rideRule(".hk-ride[data-hk-ride-ready].hk-ride--to");
450
- expect(to, "natural end rule").toBeTruthy();
451
- expect(to!.declarations.get("height")).toBe(
452
- "calc(var(--hk-ride-chrome, 0px) + var(--hk-ride-content, 0px))",
453
- );
454
- expect(to!.important.has("height")).toBe(true);
455
-
456
- // Only the content region is ever scaled — top origin, so the compressed
457
- // paint lands exactly in the shorter slot the frame leaves for it.
458
- const scaled = rideRule(".hk-ride[data-hk-ride-ready].hk-ride--from > .hk-ride-content", "transform");
459
- expect(scaled, "content compress rule").toBeTruthy();
460
- expect(scaled!.declarations.get("transform")).toBe(
461
- "scaleY(var(--hk-ride-factor, 0.9))",
462
- );
463
- const origin = rideRule(".hk-ride-content", "transform-origin");
464
- expect(origin!.declarations.get("transform-origin")).toBe("50% 0");
465
-
466
- // …and the CHROME is never named by any ride rule: the modal's header,
467
- // sub-header and footer must not be reachable from the ride sheet.
468
- const CHROME = /hk-modal-(header|subheader|footer|title|close)\b/;
469
- const offenders = RIDE_RULES.filter((r) => CHROME.test(r.selector)).map((r) => r.selector);
470
- expect(offenders, "the ride container must not target modal chrome").toEqual([]);
471
- });
472
417
 
473
418
  it("never transforms the chrome: title bar, sub-header, footer, close button, title glyph", () => {
474
419
  for (const state of RIDE) {
@@ -550,15 +495,6 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
550
495
  ).toBeUndefined();
551
496
  });
552
497
 
553
- it("maps the surface states onto the ride's two ends", () => {
554
- // The modal's TSX adds `hk-ride--from` for openingFrom/closingTo and
555
- // `hk-ride--to` for openingTo/closingFrom; the container sheet keys off
556
- // exactly those class names. A rename on either side has to fail here.
557
- const names = RIDE_RULES.map((r) => r.selector).join("\n");
558
- expect(names).toContain("hk-ride--from");
559
- expect(names).toContain("hk-ride--to");
560
- expect(names).toContain("hk-ride-content");
561
- });
562
498
 
563
499
  it("never lets ANY rule put a ride transform on the content region", () => {
564
500
  // Fail-closed sweep, selector-level and media-independent (R2 finding
@@ -580,8 +516,6 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
580
516
  // width never does). Everything else — scaleX, a 2-arg scale, a
581
517
  // translate, a matrix — is a contract break.
582
518
  const LEGAL_BODY_TRANSFORM = new Set([
583
- "scaleY(var(--hk-ride-factor, 0.9))",
584
- "scaleY(1)",
585
519
  "none",
586
520
  ]);
587
521
  const offenders: string[] = [];
@@ -626,17 +560,6 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
626
560
  "transition",
627
561
  "will-change",
628
562
  "opacity",
629
- // The from-pair carries the ride's height end, and the -active pair
630
- // locks the content region out of the flex shrink for the flight
631
- // (the 0.81-compounding guard, 2026-11-07 R3 finding).
632
- "height",
633
- "flex",
634
- // …and the -active pair pins the footer band to the frame's bottom
635
- // edge for the flight (it would otherwise be pushed out by the
636
- // content region's locked natural box).
637
- "position",
638
- "inset-inline",
639
- "bottom",
640
563
  ]);
641
564
  // Chrome ride states now also carry `transform` (the 2026-11-06 frame
642
565
  // height ride) — but nothing else beyond opacity/transition/will-change.
@@ -660,8 +583,6 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
660
583
  // The body carries no transform contract any more (2026-11-07): any
661
584
  // ride rule targeting it is an offender outright.
662
585
  const LEGAL_BODY = new Set<string>([
663
- "scaleY(var(--hk-ride-factor, 0.9))",
664
- "scaleY(1)",
665
586
  "none",
666
587
  ]);
667
588
  const offenders: string[] = [];
@@ -718,12 +639,7 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
718
639
  const bodyTransform = winner(body, "transform", DESKTOP) ?? "";
719
640
  // The contracted body grammar: nothing but the vertical compression
720
641
  // at the ride's two ends (and no transform at rest).
721
- const expectCompressed = state.classes.some((c) =>
722
- c.includes("enter-from") || c.includes("leave-to"));
723
- const expected = expectCompressed
724
- ? "scaleY(var(--hk-ride-factor, 0.9))"
725
- : "scaleY(1)";
726
- expect(bodyTransform, `body ${state.name}`).toBe(expected);
642
+ expect(bodyTransform, `body ${state.name}`).toBe("");
727
643
  expect(winner(body, "scale", DESKTOP), `body ${state.name}`).toBeUndefined();
728
644
  expect(winner(body, "translate", DESKTOP), `body ${state.name}`).toBeUndefined();
729
645
  expect(winner(body, "zoom", DESKTOP), `body ${state.name}`).toBeUndefined();
@@ -732,7 +648,10 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
732
648
  // the only `scaleY` in the choreography lives on the content region's
733
649
  // shared contract (pinned by the ride test above).
734
650
  const frameTransform = winner(frame, "transform", DESKTOP) ?? "";
735
- expect(frameTransform, `frame ${state.name}`).toBe(CENTERING);
651
+ const compressed = state.classes.some((c) =>
652
+ c.includes("enter-from") || c.includes("leave-to"));
653
+ const frameExpected = compressed ? FRAME_FROM : FRAME_TO;
654
+ expect(frameTransform, `frame ${state.name}`).toBe(frameExpected);
736
655
  expect(frameTransform, `frame ${state.name}`).not.toMatch(FRAME_ESCAPES);
737
656
  expect(winner(frame, "scale", DESKTOP), `frame ${state.name}`).toBeUndefined();
738
657
  expect(winner(frame, "zoom", DESKTOP), `frame ${state.name}`).toBeUndefined();
@@ -57,11 +57,11 @@
57
57
  z-index: 1;
58
58
  top: 50%;
59
59
  left: 50%;
60
- // The centering translate is the frame's WHOLE transform, in every state
61
- // (rest, enter-from/to, leave-from/to). The open/close ride is carried by
62
- // the height instead (see the choreography block below), so the frame can
63
- // never re-seat mid-flight, dragged windows keep their shift, and the
64
- // chrome is never scaled.
60
+ // The centering translate is the frame's PERMANENT transform prefix:
61
+ // every state (rest, enter-from/to, leave-from/to) carries this exact
62
+ // translate, and the open/close choreography only ever APPENDS a
63
+ // translateY + scale to it (see the desktop block) — so the ride can
64
+ // never re-seat the frame and dragged windows keep their shift.
65
65
  transform: translate(
66
66
  calc(-50% + var(--hk-modal-shift-x, 0px)),
67
67
  calc(-50% + var(--hk-modal-shift-y, 0px))
@@ -99,164 +99,76 @@
99
99
  // ------
100
100
  // Content enter/leave transitions (desktop ≥768px)
101
101
  //
102
- // Default window open/close (user direction 2026-11-07, third revision):
103
- // the WINDOW's own height rides, the CHROME keeps its natural size, and
104
- // only the content region is compressed. Concretely:
105
- //
106
- // frame height : chrome + content × 0.9 ⇄ chrome + content
107
- // content region: scaleY(0.9) ⇄ scaleY(1) (origin top)
108
- //
109
- // Why not `transform: scaleY()` on the frame (the 2026-11-05/06 grammar):
110
- // a frame-level scale squashes the title glyphs, the footer buttons and the
111
- // corner radii along with everything else — the user reported exactly that
112
- // ("顶部标题和底部按钮组也跟着压缩") on 0.55.305. The ride is therefore a
113
- // LAYOUT-height animation driven by the family primitive `useHeightRide`,
114
- // which publishes the measured chrome/content split as CSS variables; this
115
- // sheet maps the surface states onto the ride's two ends. Chrome lives
116
- // outside the transform, so its type renders at natural size for the whole
117
- // flight, and the content region keeps its natural LAYOUT box (only its
118
- // paint is compressed about its own top edge), so the compressed paint
119
- // lands exactly in the shorter slot the frame leaves for it.
120
- //
121
- // The centering translate stays the frame's whole transform in every state
122
- // (byte-identical between rest and both ride ends, `--hk-modal-shift-*`
123
- // included), so the ride never re-seats the frame, dragged windows keep
124
- // their shift, and a box that grows by Δ about its center moves its top
125
- // edge up by Δ/2 — the symmetric growth the user asked for.
126
- //
127
- // Scroll containers are untouched by construction: `HkScrollContainer`,
128
- // `HkScrollPin` and the overlay scrollbar measure layout boxes, which the
129
- // content region's transform never changes, and nothing inside the content
130
- // reflows per frame. Surfaces may read `data-hk-riding` to skip work while
131
- // a ride is in flight.
132
- //
133
- // The `!important` on the two height ends is load-bearing: the size morph
134
- // pins the frame's height as an inline px value, and the ride — not the pin
135
- // — owns the box for the duration of the flight. Reduced motion flattens
136
- // both the height transition and the content scale.
102
+ // Default window open/close (user direction 2026-11-07, fifth revision —
103
+ // chosen from a side-by-side comparison page): the WHOLE WINDOW zooms and
104
+ // rises while it fades. A compositor animation end to end — no layout
105
+ // property moves, nothing reflows per frame, the backdrop-filtered fixed
106
+ // layer never re-rasters. The 3% scale is imperceptible as glyph
107
+ // distortion; the 10px rise gives the motion direction. Chrome, content,
108
+ // sticky bars — everything inside keeps its exact shape at every t.
109
+ // (Retired grammars, oldest first: the 2026-11-04 whole-frame scale(1,0.9);
110
+ // the 2026-11-05 body-only uniform scale; the 0.95 pinch; the frame-level
111
+ // scaleY(0.9); and the 2026-11-07 layout-height ride of 0.55.307–310, whose
112
+ // family container — HkRideBox/useHeightRide — was retired with it: it
113
+ // fought the size morph for the same property and animated layout on a
114
+ // backdrop-filtered fixed layer.)
137
115
  // ------
138
116
 
139
117
  @media (min-width: 768px) {
140
- // RESIDENT legs — the ride's continuity never depends on the transition
141
- // classes' lifetime. The surface machine's deadline is advisory: it can
142
- // flip the phase (and drop the classes) before the CSS lands. With the
143
- // legs resident, that class drop only changes the TARGET (calc -> auto,
144
- // scale -> none) and the browser finishes the motion on these legs
145
- // instead of snapping (2026-11-07 user report: the open "replayed" — a
146
- // partial ride, a snap, then a second animation). The height/max-height/
147
- // clip-path legs stay at the base tokens for the size morph (its own
148
- // heightMorph overrides the duration inline when it runs).
149
- .hk-modal-content {
150
- // Let the height leg interpolate to/from `auto` (Chromium 129+; older
151
- // engines just snap that one handoff): when the transition classes come
152
- // off mid-ride the calc end becomes the frame's own `auto` layout, and
153
- // without this the motion would jump instead of finishing.
154
- interpolate-size: allow-keywords;
155
- transition:
156
- opacity var(--hk-modal-duration, 0.42s)
157
- var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
158
- height var(--hk-modal-duration, 0.42s)
159
- var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
160
- max-height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
161
- clip-path var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
162
- }
163
-
164
- // The content's compression leg is resident for the same reason — and it
165
- // is what makes the ENTER start at exactly scaleY(0.9): a freshly
166
- // inserted element's first style never transitions, so the from-pair
167
- // paints at 0.9 and only ever animates from there.
168
- .hk-modal-body {
169
- transition: transform var(--hk-modal-duration, 0.42s)
170
- var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1));
171
- }
172
-
173
- // Open: the window's height rides while the chrome keeps its size.
118
+ // Whole-window zoom + gentle rise + fade (user direction 2026-11-07,
119
+ // fifth revision, chosen from a side-by-side comparison page):
120
+ //
121
+ // transform: translateY(10px) scale(0.97) -> translateY(0) scale(1)
122
+ // opacity : 0 -> 1
123
+ //
124
+ // A COMPOSITOR animation end to end: no layout property moves, so nothing
125
+ // reflows per frame and the backdrop-filtered fixed layer never re-rasters
126
+ // — the previous height-ride choreography (0.55.307–0.55.310) animated the
127
+ // frame's layout height and read as jank on content-heavy windows. The 3%
128
+ // scale is imperceptible as glyph distortion while still giving the window
129
+ // a sense of arriving; the 10px rise adds direction. The centering
130
+ // translate stays the frame's PERMANENT transform prefix (byte-identical
131
+ // in every state, `--hk-modal-shift-*` included) so the ride can never
132
+ // re-seat the frame and dragged windows keep their shift.
174
133
  .hk-modal-content-enter-active {
175
134
  transition:
176
135
  opacity var(--hk-modal-duration, 0.42s)
177
136
  var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
178
- height var(--hk-modal-duration, 0.42s)
137
+ transform var(--hk-modal-duration, 0.42s)
179
138
  var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1));
180
- will-change: opacity;
139
+ will-change: opacity, transform;
181
140
  }
182
141
 
183
- // Close: the reveal's TRUE reverse — same ride, the window settles back
184
- // toward 90% height, width untouched in both directions.
142
+ // Close: the true reverse — the window settles back to 0.97 and dips
143
+ // 10px while it fades. Width, height and chrome never move in either
144
+ // direction.
185
145
  .hk-modal-content-leave-active {
186
146
  transition:
187
147
  opacity var(--hk-modal-duration, 0.32s)
188
148
  var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1)),
189
- height var(--hk-modal-duration, 0.32s)
149
+ transform var(--hk-modal-duration, 0.32s)
190
150
  var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1));
191
- will-change: opacity;
151
+ will-change: opacity, transform;
192
152
  }
193
153
 
194
- // The ride's two ends. The from-pair ships with the FIRST paint (a
195
- // freshly inserted element does not transition into its initial style),
196
- // so the compressed height and the 0.9 content scale are both real at
197
- // frame one — the phase-mapped classes that shipped one tick late are
198
- // exactly what made the open look like clipping (2026-11-07 reports).
199
- // `!important` outranks the size morph's inline pin for the flight.
200
- .hk-modal-content[data-hk-ride-ready].hk-modal-content-enter-from,
201
- .hk-modal-content[data-hk-ride-ready].hk-modal-content-leave-to {
154
+ .hk-modal-content-enter-from,
155
+ .hk-modal-content-leave-to {
202
156
  opacity: 0;
203
- height: calc(
204
- var(--hk-ride-chrome, 0px) + var(--hk-ride-content, 0px) * var(--hk-ride-factor, 0.9)
205
- ) !important;
157
+ transform: translate(
158
+ calc(-50% + var(--hk-modal-shift-x, 0px)),
159
+ calc(-50% + var(--hk-modal-shift-y, 0px))
160
+ )
161
+ translateY(10px) scale(0.97);
206
162
  }
207
163
 
208
- // The natural end is an EXPLICIT calc too: the frame's base height is
209
- // `auto`, and a transition cannot interpolate px -> auto. Without this
210
- // leg the -active body lock made `auto` resolve to (almost) the
211
- // compressed height, so the frame never grew while the content's scale
212
- // unfolded underneath it — the "clipped open" the user reported. When the
213
- // classes come off at rest, the morph's pin (the same px) catches the
214
- // handoff with no visible change.
215
164
  .hk-modal-content-enter-to,
216
165
  .hk-modal-content-leave-from {
217
166
  opacity: 1;
218
- height: calc(var(--hk-ride-chrome, 0px) + var(--hk-ride-content, 0px));
219
- }
220
-
221
- // Only the content region is ever scaled — top origin, so the compressed
222
- // paint lands exactly in the shorter slot the frame leaves for it.
223
- .hk-modal-content-enter-from > .hk-modal-body,
224
- .hk-modal-content-leave-to > .hk-modal-body {
225
- transform: scaleY(var(--hk-ride-factor, 0.9));
226
- transform-origin: 50% 0;
227
- }
228
-
229
- .hk-modal-content-enter-to > .hk-modal-body,
230
- .hk-modal-content-leave-from > .hk-modal-body {
231
- transform: scaleY(1);
232
- transform-origin: 50% 0;
233
- }
234
-
235
- // The content region keeps its NATURAL layout height for the whole
236
- // flight (or the slot shrink and the scale would compound to 0.81 and
237
- // leave a gap above the footer), and the footer pins to the frame's
238
- // bottom edge so it stays visible while the content's natural box
239
- // overflows underneath it. The -active pair spans the flight window in
240
- // both directions.
241
- .hk-modal-content-enter-active > .hk-modal-body,
242
- .hk-modal-content-leave-active > .hk-modal-body {
243
- flex: 0 0 auto;
244
- height: var(--hk-ride-content, auto);
245
- }
246
-
247
- .hk-modal-content-enter-active > .hk-modal-footer,
248
- .hk-modal-content-leave-active > .hk-modal-footer {
249
- position: absolute;
250
- inset-inline: 0;
251
- bottom: 0;
252
- }
253
-
254
- // Close-direction compression timing for the content (the frame's height
255
- // leg above is 0.32s ease-in; a 0.42s content leg would trail it and open
256
- // a gap above the pinned footer).
257
- .hk-modal-content-leave-active > .hk-modal-body {
258
- transition: transform var(--hk-modal-duration, 0.32s)
259
- var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1));
167
+ transform: translate(
168
+ calc(-50% + var(--hk-modal-shift-x, 0px)),
169
+ calc(-50% + var(--hk-modal-shift-y, 0px))
170
+ )
171
+ translateY(0) scale(1);
260
172
  }
261
173
  }
262
174
 
@@ -735,7 +647,7 @@
735
647
  .hk-modal-content {
736
648
  transition: none;
737
649
  }
738
- // The frame's height ride flattens with it: the ride states fall back to
650
+ // The zoom choreography flattens with it: the ride states fall back to
739
651
  // the bare centering translate (same specificity as the desktop state
740
652
  // rules above, later in the sheet, so the flattened state wins by source
741
653
  // order — one painted frame at opacity 0 hides the flip).
@@ -746,11 +658,8 @@
746
658
  calc(-50% + var(--hk-modal-shift-y, 0px))
747
659
  );
748
660
  }
749
- // The content region carries no transform of its own any more (the ride is
750
- // height-only — 2026-11-07). These lines are a belt-and-braces guard only:
751
- // at (0,2,0) they would NOT outrank a future ride rule at (0,4,0), so the
752
- // family sheet's own reduced-motion block (which repeats the full
753
- // selector) is the one that actually flattens the content scale.
661
+ // The content region carries no transform of its own in this choreography
662
+ // (the whole window zooms; the body is never scaled). Belt-and-braces.
754
663
  .hk-modal-content .hk-modal-body {
755
664
  transform: none;
756
665
  transition: none;