@birdycom/omniaui 0.11.2 → 0.12.0
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/dist/carousel.d.ts +6 -0
- package/dist/carousel.js +46 -0
- package/dist/components-extra.css +39 -0
- package/dist/components.css +146 -15
- package/dist/components.min.css +1 -1
- package/dist/oc-theme.css +1 -1
- package/dist/parts/site-project-card.css +2 -0
- package/dist/showcase.css +10 -3
- package/manifest.json +8 -0
- package/package.json +3 -2
- package/snippets/carousel-loop.html +20 -0
- package/snippets/lightbox.html +13 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// File: packages/ui/src/carousel.d.ts
|
|
2
|
+
// Types for the looping carousel script (.oc-carousel--loop prev/next buttons).
|
|
3
|
+
|
|
4
|
+
export function loopTarget(position: number, max: number, step: number, direction: 1 | -1): number;
|
|
5
|
+
export function bindCarousel(root: HTMLElement): void;
|
|
6
|
+
export function initCarousels(scope?: ParentNode): void;
|
package/dist/carousel.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// File: packages/ui/src/carousel.js
|
|
2
|
+
// Map: next scroll position (wraps at the ends) -> bind one .oc-carousel--loop (prev/next buttons) -> boot
|
|
3
|
+
// Loop for the CSS carousel: the browser's ::scroll-button arrows stop at the first and last slide, so a looping
|
|
4
|
+
// carousel has two real buttons (.oc-carousel-prev / .oc-carousel-next) and this script moves it one slide, from the
|
|
5
|
+
// last slide back to the first and from the first to the last. Swipe, snap, dots and keyboard stay CSS.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where to scroll for one step. `position` and `max` are distances from the start edge (positive, also in RTL),
|
|
9
|
+
* `step` is one slide plus the gap. At an end (within 2px, scroll snapping rounds) it wraps around.
|
|
10
|
+
*/
|
|
11
|
+
export function loopTarget(position, max, step, direction) {
|
|
12
|
+
if (direction > 0) return position >= max - 2 ? 0 : Math.min(max, position + step);
|
|
13
|
+
return position <= 2 ? max : Math.max(0, position - step);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Wire the prev/next buttons of one looping carousel (once). */
|
|
17
|
+
export function bindCarousel(root) {
|
|
18
|
+
if (root.hasAttribute("data-oc-loop")) return;
|
|
19
|
+
const viewport = root.querySelector(".oc-carousel-viewport");
|
|
20
|
+
const items = viewport?.querySelectorAll(".oc-carousel-item");
|
|
21
|
+
if (!viewport || !items?.length) return;
|
|
22
|
+
const rtl = getComputedStyle(viewport).direction === "rtl";
|
|
23
|
+
const move = (direction) => {
|
|
24
|
+
const max = viewport.scrollWidth - viewport.clientWidth;
|
|
25
|
+
// One slide plus the gap: the distance between the first two slides (one slide wide when there is only one).
|
|
26
|
+
const step = items.length > 1 ? Math.abs(items[1].offsetLeft - items[0].offsetLeft) : viewport.clientWidth;
|
|
27
|
+
const position = Math.abs(viewport.scrollLeft);
|
|
28
|
+
const target = loopTarget(position, max, step, direction);
|
|
29
|
+
viewport.scrollTo({ left: rtl ? -target : target });
|
|
30
|
+
};
|
|
31
|
+
root.querySelector(".oc-carousel-prev")?.addEventListener("click", () => move(-1));
|
|
32
|
+
root.querySelector(".oc-carousel-next")?.addEventListener("click", () => move(1));
|
|
33
|
+
root.setAttribute("data-oc-loop", "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wire every looping carousel under scope that is not wired yet. */
|
|
37
|
+
export function initCarousels(scope = document) {
|
|
38
|
+
scope.querySelectorAll(".oc-carousel--loop").forEach((root) => bindCarousel(root));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The docs site loads this file as /js/oc-carousel.js. A wrapper import must not scan the page.
|
|
42
|
+
if (typeof document !== "undefined" && String(import.meta.url).includes("oc-carousel")) {
|
|
43
|
+
const boot = () => initCarousels(document);
|
|
44
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
45
|
+
else boot();
|
|
46
|
+
}
|
|
@@ -503,6 +503,45 @@ a.oc-item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px
|
|
|
503
503
|
.oc-carousel-viewport::scroll-button(inline-end):disabled { opacity: 0.35; cursor: default; }
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/* Loop (.oc-carousel--loop): after the last slide comes the first again. CSS scroll buttons stop at the ends, so this
|
|
507
|
+
variant has two real buttons (.oc-carousel-prev / .oc-carousel-next) driven by carousel.js; swipe, snap, dots and
|
|
508
|
+
keyboard stay CSS. Until the script has run (data-oc-loop), the buttons stay hidden and the CSS arrows remain. */
|
|
509
|
+
.oc-carousel-prev,
|
|
510
|
+
.oc-carousel-next {
|
|
511
|
+
position: absolute;
|
|
512
|
+
inset-block-start: 50%;
|
|
513
|
+
translate: 0 -50%;
|
|
514
|
+
z-index: 1;
|
|
515
|
+
inline-size: 2.25rem;
|
|
516
|
+
block-size: 2.25rem;
|
|
517
|
+
display: grid;
|
|
518
|
+
place-items: center;
|
|
519
|
+
padding: 0;
|
|
520
|
+
font-size: 1rem;
|
|
521
|
+
color: var(--oc-foreground);
|
|
522
|
+
background: var(--oc-background);
|
|
523
|
+
border: 1px solid var(--oc-border);
|
|
524
|
+
border-radius: 50%;
|
|
525
|
+
box-shadow: 0 2px 8px oklch(from var(--color-black-60) l c h / 0.18);
|
|
526
|
+
cursor: pointer;
|
|
527
|
+
}
|
|
528
|
+
.oc-carousel-prev { inset-inline-start: 0.5rem; }
|
|
529
|
+
.oc-carousel-next { inset-inline-end: 0.5rem; }
|
|
530
|
+
.oc-carousel-prev:hover,
|
|
531
|
+
.oc-carousel-next:hover { background: var(--oc-accent); }
|
|
532
|
+
.oc-carousel-prev:focus-visible,
|
|
533
|
+
.oc-carousel-next:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
|
|
534
|
+
.oc-carousel--loop:not([data-oc-loop]) :is(.oc-carousel-prev, .oc-carousel-next) { display: none; }
|
|
535
|
+
/* Centered on the viewport, not on the carousel with its dots underneath. */
|
|
536
|
+
@supports (anchor-name: --a) {
|
|
537
|
+
.oc-carousel-prev,
|
|
538
|
+
.oc-carousel-next { position-anchor: --oc-carousel; inset-block-start: anchor(center); }
|
|
539
|
+
}
|
|
540
|
+
@supports selector(::scroll-button(inline-start)) {
|
|
541
|
+
.oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-start),
|
|
542
|
+
.oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-end) { content: none; }
|
|
543
|
+
}
|
|
544
|
+
|
|
506
545
|
/* ===================================================================
|
|
507
546
|
RESIZABLE — split panes using the native CSS `resize` property (drag the grip in the corner of the resizable panel).
|
|
508
547
|
A full-length drag handle between panes with keyboard support needs script: wrapper responsibility.
|
package/dist/components.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.
|
|
1
|
+
/* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.12.0. Do not edit. */
|
|
2
2
|
/* Semantic theme tokens: every color is light-dark() of an Omnia palette token (oc-colors.css). */
|
|
3
3
|
/* Same source as the desktop export: npx omniacss tokens --format json|dtcg|python */
|
|
4
4
|
:root {
|
|
@@ -1363,37 +1363,120 @@ textarea.oc-input {
|
|
|
1363
1363
|
|
|
1364
1364
|
/* ===================================================================
|
|
1365
1365
|
TOOLTIP
|
|
1366
|
-
Pure CSS
|
|
1366
|
+
Pure CSS. Text: the data-tooltip attribute (drawn by ::after). Richer content, such as a title above lines
|
|
1367
|
+
of values: a child .oc-tooltip__content. With anchor positioning the bubble is anchored to its element and
|
|
1368
|
+
moves to the side that has room; otherwise it sits on the preferred side (top by default).
|
|
1367
1369
|
=================================================================== */
|
|
1368
1370
|
|
|
1369
1371
|
.oc-tooltip {
|
|
1372
|
+
--oc-tooltip-bg: oklch(from var(--oc-popover) l c h / 0.94);
|
|
1373
|
+
--oc-tooltip-gap: 8px;
|
|
1370
1374
|
position: relative;
|
|
1371
1375
|
cursor: help;
|
|
1372
1376
|
}
|
|
1373
1377
|
|
|
1374
|
-
.oc-tooltip::after
|
|
1375
|
-
|
|
1378
|
+
.oc-tooltip::after,
|
|
1379
|
+
.oc-tooltip > .oc-tooltip__content {
|
|
1376
1380
|
position: absolute;
|
|
1377
|
-
bottom: calc(100% +
|
|
1381
|
+
bottom: calc(100% + var(--oc-tooltip-gap));
|
|
1378
1382
|
left: 50%;
|
|
1379
|
-
|
|
1383
|
+
translate: -50% 0;
|
|
1380
1384
|
padding: 6px 10px;
|
|
1381
1385
|
font-size: 0.75rem;
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
+
font-weight: 500;
|
|
1387
|
+
line-height: 1.45;
|
|
1388
|
+
color: var(--oc-popover-foreground);
|
|
1389
|
+
background: var(--oc-tooltip-bg);
|
|
1390
|
+
border: 1px solid var(--oc-border);
|
|
1391
|
+
border-radius: var(--oc-radius);
|
|
1392
|
+
box-shadow: 0 10px 28px -8px oklch(from var(--color-black-60) l c h / 0.3), 0 2px 6px -2px oklch(from var(--color-black-60) l c h / 0.15);
|
|
1393
|
+
backdrop-filter: blur(12px) saturate(1.4);
|
|
1386
1394
|
white-space: nowrap;
|
|
1387
1395
|
pointer-events: none;
|
|
1388
1396
|
opacity: 0;
|
|
1389
|
-
|
|
1397
|
+
scale: 0.96;
|
|
1398
|
+
transition: opacity var(--oc-transition-fast), scale 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
1390
1399
|
z-index: 50;
|
|
1391
1400
|
}
|
|
1392
1401
|
|
|
1402
|
+
.oc-tooltip::after { content: attr(data-tooltip); }
|
|
1403
|
+
/* Rich content only: no empty ::after bubble. */
|
|
1404
|
+
.oc-tooltip:not([data-tooltip])::after { content: none; }
|
|
1405
|
+
|
|
1393
1406
|
/* Also on keyboard focus of the element inside (Tab), not only on mouse hover. */
|
|
1394
1407
|
.oc-tooltip:hover::after,
|
|
1395
|
-
.oc-tooltip:focus-within::after
|
|
1408
|
+
.oc-tooltip:focus-within::after,
|
|
1409
|
+
.oc-tooltip:hover > .oc-tooltip__content,
|
|
1410
|
+
.oc-tooltip:focus-within > .oc-tooltip__content {
|
|
1396
1411
|
opacity: 1;
|
|
1412
|
+
scale: 1;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1416
|
+
.oc-tooltip::after,
|
|
1417
|
+
.oc-tooltip > .oc-tooltip__content { transition: opacity var(--oc-transition-fast); scale: 1; }
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/* Several lines: line breaks in data-tooltip (or in .oc-tooltip__content) start a new line. */
|
|
1421
|
+
.oc-tooltip--multiline::after,
|
|
1422
|
+
.oc-tooltip > .oc-tooltip__content {
|
|
1423
|
+
white-space: pre-line;
|
|
1424
|
+
inline-size: max-content;
|
|
1425
|
+
max-inline-size: 20rem;
|
|
1426
|
+
padding: 8px 12px;
|
|
1427
|
+
text-align: start;
|
|
1428
|
+
font-variant-numeric: tabular-nums;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/* Title line of the rich content: a bit larger, always starting with a capital. */
|
|
1432
|
+
.oc-tooltip__title {
|
|
1433
|
+
display: block;
|
|
1434
|
+
margin-block-end: 2px;
|
|
1435
|
+
font-size: 0.875rem;
|
|
1436
|
+
font-weight: 700;
|
|
1437
|
+
line-height: 1.35;
|
|
1438
|
+
}
|
|
1439
|
+
.oc-tooltip__title::first-letter { text-transform: uppercase; }
|
|
1440
|
+
|
|
1441
|
+
/* Preferred side (default: top). Without anchor positioning these are fixed places next to the element. */
|
|
1442
|
+
.oc-tooltip--bottom::after,
|
|
1443
|
+
.oc-tooltip--bottom > .oc-tooltip__content { bottom: auto; top: calc(100% + var(--oc-tooltip-gap)); }
|
|
1444
|
+
.oc-tooltip--left::after,
|
|
1445
|
+
.oc-tooltip--left > .oc-tooltip__content { bottom: auto; left: auto; top: 50%; right: calc(100% + var(--oc-tooltip-gap)); translate: 0 -50%; }
|
|
1446
|
+
.oc-tooltip--right::after,
|
|
1447
|
+
.oc-tooltip--right > .oc-tooltip__content { bottom: auto; top: 50%; left: calc(100% + var(--oc-tooltip-gap)); translate: 0 -50%; }
|
|
1448
|
+
|
|
1449
|
+
/* Anchor positioning: fixed, so a scroll container (a wide table, a sidebar) does not clip it, and flipped to the
|
|
1450
|
+
opposite side (then the other axis) when the preferred side has no room. ::after uses its element as implicit
|
|
1451
|
+
anchor; .oc-tooltip__content needs a named anchor, scoped to its own .oc-tooltip (anchor-scope). */
|
|
1452
|
+
@supports (position-area: top) {
|
|
1453
|
+
.oc-tooltip::after {
|
|
1454
|
+
position: fixed;
|
|
1455
|
+
inset: auto;
|
|
1456
|
+
translate: none;
|
|
1457
|
+
margin: var(--oc-tooltip-gap);
|
|
1458
|
+
position-area: top;
|
|
1459
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
1460
|
+
}
|
|
1461
|
+
.oc-tooltip--bottom::after { position-area: bottom; }
|
|
1462
|
+
.oc-tooltip--left::after { position-area: left; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
|
|
1463
|
+
.oc-tooltip--right::after { position-area: right; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
@supports (position-area: top) and (anchor-scope: all) {
|
|
1467
|
+
.oc-tooltip { anchor-name: --oc-tooltip; anchor-scope: --oc-tooltip; }
|
|
1468
|
+
.oc-tooltip > .oc-tooltip__content {
|
|
1469
|
+
position: fixed;
|
|
1470
|
+
inset: auto;
|
|
1471
|
+
translate: none;
|
|
1472
|
+
margin: var(--oc-tooltip-gap);
|
|
1473
|
+
position-anchor: --oc-tooltip;
|
|
1474
|
+
position-area: top;
|
|
1475
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
1476
|
+
}
|
|
1477
|
+
.oc-tooltip--bottom > .oc-tooltip__content { position-area: bottom; }
|
|
1478
|
+
.oc-tooltip--left > .oc-tooltip__content { position-area: left; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
|
|
1479
|
+
.oc-tooltip--right > .oc-tooltip__content { position-area: right; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
|
|
1397
1480
|
}
|
|
1398
1481
|
|
|
1399
1482
|
/* ===================================================================
|
|
@@ -3546,7 +3629,9 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
|
|
|
3546
3629
|
.oc-btn--pill { border-radius: 999px; }
|
|
3547
3630
|
.oc-btn--gradient {
|
|
3548
3631
|
border-radius: 999px;
|
|
3549
|
-
|
|
3632
|
+
/* border-box: the gradient also runs under the transparent 1px border of .oc-btn; from the padding box it would
|
|
3633
|
+
repeat in that border and show a thin stripe of the other end's color on both sides. */
|
|
3634
|
+
background: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to)) border-box;
|
|
3550
3635
|
color: var(--color-white-50);
|
|
3551
3636
|
box-shadow: 0 10px 30px -10px var(--oc-accent-to);
|
|
3552
3637
|
transition: transform 0.2s var(--oc-ease-out), box-shadow 0.2s;
|
|
@@ -3736,16 +3821,21 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
|
|
|
3736
3821
|
background: oklch(from var(--oc-accent-from) l c h / 0.14);
|
|
3737
3822
|
color: light-dark(var(--oc-accent-from), oklch(from var(--oc-accent-from) max(l, 0.72) c h));
|
|
3738
3823
|
}
|
|
3824
|
+
/* Pulse: the ring starts at --oc-status-dot-pulse-alpha of the accent and grows to --oc-status-dot-pulse-size.
|
|
3825
|
+
--strong starts at the full accent with a wider ring. */
|
|
3739
3826
|
.oc-status-dot {
|
|
3827
|
+
--oc-status-dot-pulse-alpha: 0.6;
|
|
3828
|
+
--oc-status-dot-pulse-size: 8px;
|
|
3740
3829
|
inline-size: 0.5rem;
|
|
3741
3830
|
aspect-ratio: 1;
|
|
3742
3831
|
border-radius: 50%;
|
|
3743
3832
|
background: var(--oc-accent-from);
|
|
3744
3833
|
animation: oc-pulse 2s infinite;
|
|
3745
3834
|
}
|
|
3835
|
+
.oc-status-dot--strong { --oc-status-dot-pulse-alpha: 1; --oc-status-dot-pulse-size: 10px; }
|
|
3746
3836
|
@keyframes oc-pulse {
|
|
3747
|
-
0% { box-shadow: 0 0 0 0 oklch(from var(--oc-accent-from) l c h /
|
|
3748
|
-
70%, 100% { box-shadow: 0 0 0
|
|
3837
|
+
0% { box-shadow: 0 0 0 0 oklch(from var(--oc-accent-from) l c h / var(--oc-status-dot-pulse-alpha)); }
|
|
3838
|
+
70%, 100% { box-shadow: 0 0 0 var(--oc-status-dot-pulse-size) transparent; }
|
|
3749
3839
|
}
|
|
3750
3840
|
|
|
3751
3841
|
/* Stat strip: big numbers in a bordered row (pairs with .oc-counter for counting up) */
|
|
@@ -4680,6 +4770,45 @@ a.oc-item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px
|
|
|
4680
4770
|
.oc-carousel-viewport::scroll-button(inline-end):disabled { opacity: 0.35; cursor: default; }
|
|
4681
4771
|
}
|
|
4682
4772
|
|
|
4773
|
+
/* Loop (.oc-carousel--loop): after the last slide comes the first again. CSS scroll buttons stop at the ends, so this
|
|
4774
|
+
variant has two real buttons (.oc-carousel-prev / .oc-carousel-next) driven by carousel.js; swipe, snap, dots and
|
|
4775
|
+
keyboard stay CSS. Until the script has run (data-oc-loop), the buttons stay hidden and the CSS arrows remain. */
|
|
4776
|
+
.oc-carousel-prev,
|
|
4777
|
+
.oc-carousel-next {
|
|
4778
|
+
position: absolute;
|
|
4779
|
+
inset-block-start: 50%;
|
|
4780
|
+
translate: 0 -50%;
|
|
4781
|
+
z-index: 1;
|
|
4782
|
+
inline-size: 2.25rem;
|
|
4783
|
+
block-size: 2.25rem;
|
|
4784
|
+
display: grid;
|
|
4785
|
+
place-items: center;
|
|
4786
|
+
padding: 0;
|
|
4787
|
+
font-size: 1rem;
|
|
4788
|
+
color: var(--oc-foreground);
|
|
4789
|
+
background: var(--oc-background);
|
|
4790
|
+
border: 1px solid var(--oc-border);
|
|
4791
|
+
border-radius: 50%;
|
|
4792
|
+
box-shadow: 0 2px 8px oklch(from var(--color-black-60) l c h / 0.18);
|
|
4793
|
+
cursor: pointer;
|
|
4794
|
+
}
|
|
4795
|
+
.oc-carousel-prev { inset-inline-start: 0.5rem; }
|
|
4796
|
+
.oc-carousel-next { inset-inline-end: 0.5rem; }
|
|
4797
|
+
.oc-carousel-prev:hover,
|
|
4798
|
+
.oc-carousel-next:hover { background: var(--oc-accent); }
|
|
4799
|
+
.oc-carousel-prev:focus-visible,
|
|
4800
|
+
.oc-carousel-next:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
|
|
4801
|
+
.oc-carousel--loop:not([data-oc-loop]) :is(.oc-carousel-prev, .oc-carousel-next) { display: none; }
|
|
4802
|
+
/* Centered on the viewport, not on the carousel with its dots underneath. */
|
|
4803
|
+
@supports (anchor-name: --a) {
|
|
4804
|
+
.oc-carousel-prev,
|
|
4805
|
+
.oc-carousel-next { position-anchor: --oc-carousel; inset-block-start: anchor(center); }
|
|
4806
|
+
}
|
|
4807
|
+
@supports selector(::scroll-button(inline-start)) {
|
|
4808
|
+
.oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-start),
|
|
4809
|
+
.oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-end) { content: none; }
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4683
4812
|
/* ===================================================================
|
|
4684
4813
|
RESIZABLE — split panes using the native CSS `resize` property (drag the grip in the corner of the resizable panel).
|
|
4685
4814
|
A full-length drag handle between panes with keyboard support needs script: wrapper responsibility.
|
|
@@ -9720,6 +9849,8 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
|
|
|
9720
9849
|
background:
|
|
9721
9850
|
radial-gradient(circle at 80% 20%, color-mix(in oklch, var(--color-white-50) 30%, transparent), transparent 45%),
|
|
9722
9851
|
linear-gradient(135deg, var(--oc-accent-from), var(--oc-accent-to));
|
|
9852
|
+
/* The hover zoom grows down and sideways (clipped by the frame), never up over the title bar. */
|
|
9853
|
+
transform-origin: 50% 0;
|
|
9723
9854
|
transition: scale 0.5s var(--oc-ease-out);
|
|
9724
9855
|
}
|
|
9725
9856
|
|