";
+ inherits: true;
+ initial-value: #2d231b;
+}
+
+:root {
+ --bg: #161310; /* deep warm sumi ink */
+ --bg-tint: #2d231b; /* live: 12% of the theme's background over the ink */
+ --text: #f2ecdd; /* cream, ≈ 15:1 on the ink (AAA) */
+ --dim: #b5a996; /* ≈ 8:1 (AAA) */
+ --faint: #8d8271; /* ≈ 4.6:1, decorative */
+ --tint: #d89868; /* live: the current theme's background hue */
+ --vermilion: #c73e2d; /* the hanko — fixed, never themed */
+ --paper: #f2ecdd; /* the washi counter card */
+ --paper-ink: #241a12; /* ≈ 13:1 on paper (AAA) */
+ --sans: "Montserrat", ui-sans-serif, system-ui, "Segoe UI", sans-serif;
+ --display: var(--sans);
+ --kanji: "Noto Serif CJK JP", "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", serif;
+ --margin: clamp(1.1rem, 3.4vw, 2.75rem);
+ --counter-w: min(23rem, 30vw);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ margin: 0;
+ min-height: 100svh;
+ background: var(--bg-tint);
+ color: var(--text);
+ font-family: var(--sans);
+ line-height: 1.55;
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+}
+
+a {
+ color: var(--tint);
+ text-decoration: none;
+}
+
+:focus-visible {
+ outline: 2px solid var(--tint);
+ outline-offset: 4px;
+}
+
+.counter :focus-visible,
+.downloads :focus-visible {
+ outline-color: var(--paper-ink);
+}
+
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.skip-link {
+ position: fixed;
+ top: 0.5rem;
+ left: 0.5rem;
+ z-index: 100;
+ padding: 0.55rem 0.9rem;
+ background: var(--text);
+ color: var(--bg);
+ font-weight: 700;
+ transform: translateY(-160%);
+ transition: transform 0.18s ease;
+}
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+/* ---- The sheet: wagara ground + film grain, both theme-tinted ---- */
+.ground,
+.grain {
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+}
+
+/* Seigaiha fans as an alpha mask, painted with the live tint. Same geometry as
+ the engine's pattern: overlapping upper semicircles in three concentric radii,
+ rows offset by half a period. */
+.ground {
+ background: var(--tint);
+ opacity: 0.05;
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cpath fill='none' stroke='white' stroke-width='2' d='M-120,0%20A60,60%200%200%200%200,0%20M-100,0%20A40,40%200%200%200%20-20,0%20M-80,0%20A20,20%200%200%200%20-40,0%20M0,0%20A60,60%200%200%200%20120,0%20M20,0%20A40,40%200%200%200%20100,0%20M40,0%20A20,20%200%200%200%2080,0%20M120,0%20A60,60%200%200%200%20240,0%20M140,0%20A40,40%200%200%200%20220,0%20M160,0%20A20,20%200%200%200%20200,0%20M-60,60%20A60,60%200%200%200%2060,60%20M-40,60%20A40,40%200%200%200%2040,60%20M-20,60%20A20,20%200%200%200%2020,60%20M60,60%20A60,60%200%200%200%20180,60%20M80,60%20A40,40%200%200%200%20160,60%20M100,60%20A20,20%200%200%200%20140,60%20M-120,120%20A60,60%200%200%200%200,120%20M-100,120%20A40,40%200%200%200%20-20,120%20M-80,120%20A20,20%200%200%200%20-40,120%20M0,120%20A60,60%200%200%200%20120,120%20M20,120%20A40,40%200%200%200%20100,120%20M40,120%20A20,20%200%200%200%2080,120%20M120,120%20A60,60%200%200%200%20240,120%20M140,120%20A40,40%200%200%200%20220,120%20M160,120%20A20,20%200%200%200%20200,120'/%3E%3C/svg%3E");
+ mask-repeat: repeat;
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cpath fill='none' stroke='white' stroke-width='2' d='M-120,0%20A60,60%200%200%200%200,0%20M-100,0%20A40,40%200%200%200%20-20,0%20M-80,0%20A20,20%200%200%200%20-40,0%20M0,0%20A60,60%200%200%200%20120,0%20M20,0%20A40,40%200%200%200%20100,0%20M40,0%20A20,20%200%200%200%2080,0%20M120,0%20A60,60%200%200%200%20240,0%20M140,0%20A40,40%200%200%200%20220,0%20M160,0%20A20,20%200%200%200%20200,0%20M-60,60%20A60,60%200%200%200%2060,60%20M-40,60%20A40,40%200%200%200%2040,60%20M-20,60%20A20,20%200%200%200%2020,60%20M60,60%20A60,60%200%200%200%20180,60%20M80,60%20A40,40%200%200%200%20160,60%20M100,60%20A20,20%200%200%200%20140,60%20M-120,120%20A60,60%200%200%200%200,120%20M-100,120%20A40,40%200%200%200%20-20,120%20M-80,120%20A20,20%200%200%200%20-40,120%20M0,120%20A60,60%200%200%200%20120,120%20M20,120%20A40,40%200%200%200%20100,120%20M40,120%20A20,20%200%200%200%2080,120%20M120,120%20A60,60%200%200%200%20240,120%20M140,120%20A40,40%200%200%200%20220,120%20M160,120%20A20,20%200%200%200%20200,120'/%3E%3C/svg%3E");
+ -webkit-mask-repeat: repeat;
+}
+
+.grain {
+ z-index: 1;
+ opacity: 0.09;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
+ mix-blend-mode: soft-light;
+}
+
+/* ---- Crop marks (the sheet is a print proof) ---- */
+.marks {
+ position: fixed;
+ inset: 0;
+ z-index: 2;
+ pointer-events: none;
+}
+.mark {
+ position: absolute;
+ width: 13px;
+ height: 13px;
+ opacity: 0.5;
+}
+.mark::before,
+.mark::after {
+ content: "";
+ position: absolute;
+ background: var(--dim);
+}
+.mark::before {
+ inset: 6px 0 auto 0;
+ height: 1px;
+}
+.mark::after {
+ inset: 0 auto 0 6px;
+ width: 1px;
+}
+.mark--tl {
+ top: var(--margin);
+ left: var(--margin);
+}
+.mark--tr {
+ top: var(--margin);
+ right: var(--margin);
+}
+.mark--bl {
+ bottom: var(--margin);
+ left: var(--margin);
+}
+.mark--br {
+ bottom: var(--margin);
+ right: var(--margin);
+}
+
+/* ---- The stage: the plate and its proof slug ---- */
+.stage {
+ position: fixed;
+ z-index: 2;
+ top: calc(var(--margin) + 2.4rem);
+ bottom: calc(var(--margin) + 2rem);
+ left: var(--margin);
+ right: calc(var(--counter-w) + var(--margin) * 2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.plate {
+ margin: 0;
+ max-width: 100%;
+ max-height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.95rem;
+}
+
+/* The cover itself. JS drives the exact width/height per aspect ratio;
+ without JS the square poster stands in. */
+.plate__card {
+ position: relative;
+ display: block;
+ width: min(100%, 60vh, 34rem);
+ aspect-ratio: 1;
+ padding: 0;
+ border: 0;
+ background: var(--tint);
+ cursor: pointer;
+ box-shadow:
+ 0 26px 70px rgba(8, 6, 4, 0.55),
+ 0 4px 18px rgba(8, 6, 4, 0.35);
+}
+
+.plate__poster,
+.plate__cover {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+.plate__poster {
+ display: block;
+ object-fit: cover;
+}
+.plate__poster[hidden] {
+ display: none;
+}
+.plate__cover svg {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* The proof slug: theme 赤土 · pattern · layout · ratio. */
+.plate__slug {
+ margin: 0;
+ font-size: 0.68rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--dim);
+ white-space: nowrap;
+}
+.plate__slug > span:not(:first-child)::before {
+ content: "·";
+ margin: 0 0.6em;
+ color: var(--faint);
+}
+.plate__slug [lang="ja"] {
+ font-family: var(--kanji);
+ letter-spacing: 0.1em;
+}
+
+/* ---- The counter card: a washi menu card, signed with a hanko ---- */
+.counter {
+ position: fixed;
+ z-index: 3;
+ top: 50%;
+ right: var(--margin);
+ width: var(--counter-w);
+ transform: translateY(-50%) rotate(-0.4deg);
+ padding: 2rem 1.7rem 2.2rem;
+ background: var(--paper);
+ color: var(--paper-ink);
+ box-shadow:
+ 0 22px 55px rgba(8, 6, 4, 0.5),
+ 0 3px 12px rgba(8, 6, 4, 0.3);
+}
+/* The kicker tick, echoing the one the engine draws on every cover. */
+.counter::before {
+ content: "";
+ position: absolute;
+ top: 1.1rem;
+ left: 1.7rem;
+ width: 56px;
+ height: 7px;
+ background: var(--vermilion);
+}
+/* The house seal, stamped slightly askew. */
+.counter::after {
+ content: "a";
+ position: absolute;
+ right: 0.95rem;
+ bottom: 0.9rem;
+ width: 1.9rem;
+ height: 1.9rem;
+ display: grid;
+ place-items: center;
+ background: var(--vermilion);
+ color: var(--paper);
+ font-family: var(--display);
+ font-weight: 900;
+ font-size: 1.05rem;
+ line-height: 1;
+ border-radius: 3px;
+ transform: rotate(6deg);
+}
+
+.counter__statement {
+ margin: 1.1rem 0 0;
+ font-family: var(--display);
+ font-weight: 900;
+ font-size: clamp(0.95rem, 1.3vw, 1.15rem);
+ line-height: 1.22;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+}
+.counter__statement > span {
+ white-space: nowrap;
+}
+
+.counter__detail {
+ margin: 0.55rem 0 0;
+ font-size: 0.72rem;
+ line-height: 1.6;
+ opacity: 0.78;
+ text-wrap: pretty;
+}
+
+.field {
+ display: block;
+ margin-top: 1.15rem;
+}
+.field__label {
+ display: block;
+ font-size: 0.6rem;
+ text-transform: uppercase;
+ letter-spacing: 0.16em;
+ opacity: 0.65;
+}
+.field__input {
+ width: 100%;
+ padding: 0.35rem 0;
+ border: 0;
+ border-bottom: 2px solid rgba(36, 26, 18, 0.35);
+ background: transparent;
+ color: var(--paper-ink);
+ font-family: var(--display);
+ font-weight: 700;
+ font-size: 0.95rem;
+ caret-color: var(--vermilion);
+}
+.field__input::placeholder {
+ color: rgba(36, 26, 18, 0.42);
+}
+.field__input:focus {
+ outline: none;
+ border-bottom-color: var(--vermilion);
+}
+
+/* Theme stamps: one mini palette per theme, built from the engine's catalog. */
+.themes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: 1.15rem;
+}
+.theme {
+ width: 2.4rem;
+ height: 2.4rem;
+ display: grid;
+ place-items: center;
+ padding: 0;
+ border: 2px solid rgba(36, 26, 18, 0.18);
+ border-radius: 50%;
+ font-family: var(--kanji);
+ font-size: 0.92rem;
+ line-height: 1;
+ cursor: pointer;
+ transition: transform 0.15s ease;
+}
+.theme:hover,
+.theme:focus-visible {
+ transform: scale(1.1);
+}
+.theme[aria-pressed="true"] {
+ border-color: var(--vermilion);
+ box-shadow: 0 0 0 2px var(--paper), 0 0 0 3.5px var(--vermilion);
+}
+
+/* Primary interaction: the hanko. Press it, the house decides. */
+.omakase {
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+ margin-top: 1.35rem;
+ padding: 0;
+ border: 0;
+ background: none;
+ color: var(--paper-ink);
+ font: inherit;
+ cursor: pointer;
+}
+.omakase__seal {
+ position: relative;
+ width: 3.2rem;
+ height: 3.2rem;
+ display: grid;
+ place-items: center;
+ background: var(--vermilion);
+ color: var(--paper);
+ border-radius: 50%;
+ font-family: var(--kanji);
+ font-size: 1.5rem;
+ line-height: 1;
+ box-shadow:
+ inset 0 0 0 2px rgba(242, 236, 221, 0.22),
+ 0 3px 10px rgba(8, 6, 4, 0.3);
+ transition: transform 0.12s ease;
+}
+.omakase:hover .omakase__seal,
+.omakase:focus-visible .omakase__seal {
+ transform: scale(1.06);
+}
+.omakase:active .omakase__seal {
+ transform: scale(0.92);
+}
+.omakase__seal::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border: 1px solid var(--vermilion);
+ border-radius: 50%;
+ opacity: 0;
+ pointer-events: none;
+}
+.omakase:hover .omakase__seal::after,
+.omakase:active .omakase__seal::after {
+ animation: seal-radiate 1.1s ease-out infinite;
+}
+@keyframes seal-radiate {
+ from {
+ transform: scale(1);
+ opacity: 0.7;
+ }
+ to {
+ transform: scale(1.9);
+ opacity: 0;
+ }
+}
+.omakase__label {
+ font-family: var(--display);
+ font-weight: 900;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ border-bottom: 2px solid transparent;
+}
+.omakase:hover .omakase__label {
+ border-bottom-color: var(--vermilion);
+}
+
+.counter__hint {
+ margin: 1.05rem 0 0;
+ padding-right: 2.4rem; /* clears the house seal in the corner */
+ font-size: 0.62rem;
+ line-height: 1.5;
+ opacity: 0.68;
+ text-wrap: pretty;
+}
+.counter__hint [lang="ja"] {
+ font-family: var(--kanji);
+}
+
+/* ---- The stamp drawer: downloads ---- */
+.downloads-wrap {
+ position: fixed;
+ z-index: 4;
+ right: var(--margin);
+ bottom: calc(var(--margin) + 1.6rem);
+}
+
+.download-toggle {
+ padding: 0.6rem 1rem;
+ border: 1px solid rgba(242, 236, 221, 0.3);
+ background: rgba(22, 19, 16, 0.55);
+ color: var(--text);
+ font: inherit;
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition: border-color 0.15s ease, color 0.15s ease;
+}
+.download-toggle:hover {
+ border-color: var(--tint);
+ color: var(--tint);
+}
+
+.downloads {
+ position: absolute;
+ bottom: calc(100% + 0.6rem);
+ right: 0;
+ width: 19.5rem;
+ display: grid;
+ gap: 0.15rem;
+ padding: 1.35rem 1.15rem 1.15rem;
+ background: var(--paper);
+ color: var(--paper-ink);
+ box-shadow: 0 22px 55px rgba(8, 6, 4, 0.5);
+}
+.downloads[hidden] {
+ display: none;
+}
+.downloads::before {
+ content: "";
+ position: absolute;
+ top: 0.65rem;
+ left: 1.15rem;
+ width: 40px;
+ height: 6px;
+ background: var(--vermilion);
+}
+
+/* While the drawer is open, one scrim veils everything behind it uniformly. */
+.downloads-open::after {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 5;
+ background: rgba(12, 10, 8, 0.64);
+ animation: scrim-in 0.25s ease;
+}
+@keyframes scrim-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+.downloads-open .downloads-wrap {
+ z-index: 6;
+}
+.download-toggle {
+ transition: opacity 0.25s ease, border-color 0.15s ease, color 0.15s ease;
+}
+.downloads-open .download-toggle {
+ opacity: 0.25;
+}
+
+.downloads__close {
+ justify-self: end;
+ padding: 0.15rem 0.35rem;
+ border: 0;
+ background: none;
+ color: var(--paper-ink);
+ font: inherit;
+ font-size: 0.72rem;
+ cursor: pointer;
+ opacity: 0.75;
+}
+.download__note {
+ display: none; /* desktop-only builds — only worth saying on touch devices */
+ margin: 0 0 0.4rem;
+ padding: 0.4rem 0.5rem;
+ font-size: 0.74rem;
+ line-height: 1.5;
+ border-bottom: 1px dotted currentColor;
+}
+@media (pointer: coarse) {
+ .download__note {
+ display: block;
+ }
+}
+.download {
+ display: flex;
+ align-items: center;
+ gap: 0.7rem;
+ padding: 0.45rem 0.8rem;
+ border-radius: 999px;
+ color: var(--paper-ink);
+ transition: background 0.12s ease;
+}
+.download:hover,
+.download:focus-visible {
+ background: rgba(36, 26, 18, 0.1);
+}
+.download__icon {
+ width: 1.5rem;
+ height: 1.5rem;
+ flex: none;
+ color: var(--paper-ink);
+}
+.download__os {
+ font-weight: 700;
+}
+.download__format {
+ margin-left: auto;
+ font-size: 0.72rem;
+ opacity: 0.7;
+}
+.download--minor {
+ font-size: 0.82rem;
+}
+.download--minor .download__os {
+ font-weight: 400;
+}
+.download--minor .download__icon {
+ width: 1.15rem;
+ height: 1.15rem;
+ opacity: 0.7;
+}
+.download--all {
+ justify-content: center;
+ padding-left: 0;
+ font-weight: 700;
+ opacity: 1;
+}
+
+/* ---- Loose margin annotations ---- */
+.source {
+ position: fixed;
+ z-index: 3;
+ left: var(--margin);
+ top: var(--margin);
+ color: var(--text);
+ font-size: 0.78rem;
+ border-bottom: 1px solid var(--tint);
+ padding-bottom: 0.15rem;
+ transition: color 0.15s ease;
+}
+.source:hover {
+ color: var(--tint);
+}
+.source::after {
+ content: "→";
+ display: inline-block;
+ margin-left: 0.35rem;
+ transition: transform 0.15s ease;
+}
+.source:hover::after {
+ transform: translateX(4px);
+}
+
+.edge {
+ position: fixed;
+ z-index: 3;
+ left: 50%;
+ transform: translateX(-50%);
+ margin: 0;
+ padding: 0.2rem 0.7rem;
+ background: var(--bg-tint); /* opaque backing over the ground so the label stays legible */
+ color: var(--faint);
+ font-size: 0.68rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+.edge--bottom {
+ bottom: calc(var(--margin) - 0.15rem);
+}
+
+/* ---- Cursor follower: a hanko dot trailing the pointer ---- */
+.follower {
+ position: fixed;
+ z-index: 90;
+ top: 0;
+ left: 0;
+ width: 26px;
+ height: 26px;
+ margin: -13px 0 0 -13px;
+ border: 1.5px solid var(--vermilion);
+ border-radius: 50%;
+ opacity: 0;
+ pointer-events: none;
+ will-change: transform;
+}
+.follower::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 4px;
+ height: 4px;
+ margin: -2px 0 0 -2px;
+ border-radius: 50%;
+ background: var(--vermilion);
+}
+@media not (pointer: fine) {
+ .follower {
+ display: none;
+ }
+}
+
+/* ---- The live engine is unavailable: the poster stands in, controls bow out ---- */
+.no-live .field,
+.no-live .themes,
+.no-live .omakase {
+ display: none;
+}
+.no-live .plate__card {
+ cursor: default;
+}
+
+/* ---- Small viewports: plate on top, counter card below ---- */
+@media (max-width: 52rem) {
+ .stage {
+ top: calc(var(--margin) + 2.2rem);
+ bottom: auto;
+ left: var(--margin);
+ right: var(--margin);
+ height: min(46vh, 24rem);
+ }
+ .plate__card {
+ width: min(100%, 40vh, 34rem);
+ }
+ .counter {
+ top: auto;
+ bottom: calc(var(--margin) + 3.6rem);
+ left: var(--margin);
+ right: var(--margin);
+ width: auto;
+ transform: rotate(-0.4deg);
+ padding: 1.2rem 1.15rem 1.5rem;
+ }
+ .counter::before {
+ top: 0.7rem;
+ left: 1.15rem;
+ }
+ .counter__statement {
+ margin-top: 0.7rem;
+ }
+ .counter__detail {
+ display: none;
+ }
+ .field {
+ margin-top: 0.75rem;
+ }
+ .themes,
+ .omakase {
+ margin-top: 0.85rem;
+ }
+ .counter__hint {
+ margin-top: 0.7rem;
+ }
+ .plate__slug {
+ font-size: 0.58rem;
+ letter-spacing: 0.1em;
+ white-space: normal;
+ text-align: center;
+ max-width: 100%;
+ }
+ .downloads-wrap {
+ right: var(--margin);
+ bottom: var(--margin);
+ }
+ /* The drawer becomes a bottom sheet. */
+ .downloads {
+ position: fixed;
+ top: auto;
+ left: calc(var(--margin) * 0.5);
+ right: calc(var(--margin) * 0.5);
+ bottom: calc(var(--margin) * 0.5);
+ width: auto;
+ max-height: 48vh;
+ overflow-y: auto;
+ z-index: 6;
+ }
+ .source {
+ top: auto;
+ bottom: calc(var(--margin) + 0.05rem);
+ left: var(--margin);
+ max-width: calc(100vw - 2 * var(--margin) - 9rem);
+ font-size: 0.7rem;
+ text-wrap: balance;
+ background: var(--bg-tint);
+ padding: 0.2rem 0.4rem 0.3rem;
+ }
+ .edge--bottom {
+ display: none;
+ }
+}
+
+/* Reveal baseline: JS fades the counter in. Without JS it stays visible. */
+.js [data-reveal] {
+ opacity: 0;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .js [data-reveal] {
+ opacity: 1;
+ }
+ .skip-link {
+ transition: none;
+ }
+ .theme,
+ .omakase__seal,
+ .download-toggle {
+ transition: none;
+ }
+ .omakase:hover .omakase__seal::after,
+ .omakase:active .omakase__seal::after {
+ animation: none;
+ }
+ .follower {
+ display: none;
+ }
+}
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..e6fd6f2
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+ acag · article cover art generator — omakase covers, plated live
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to source
+
+
+
+
+
+
+
+ acag — article cover art generator: omakase covers in six sumi-ê themes, five wagara
+ patterns, three layouts, five formats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ terracotta 赤土
+ seigaiha
+ editorial
+ 1:1
+
+
+
+
+
+
+
+
+
+
+
+ Read the source, build it yourself
+
+
+
+
+ Rendered live · Rust → WebAssembly
+
+
+
+
+
diff --git a/web/js/main.js b/web/js/main.js
new file mode 100644
index 0000000..4138354
--- /dev/null
+++ b/web/js/main.js
@@ -0,0 +1,531 @@
+// The omakase counter. The real acag engine (Rust → WebAssembly) plates every
+// cover on the page: the same render_cover_svg the desktop app rasterizes,
+// injected as inline SVG. Each round the house re-rolls the style, the plate
+// morphs to the new aspect ratio, and the engine typesets the title live,
+// character by character — auto-wrap, auto-size, WCAG AAA, all of it running in
+// the browser. Reduced motion / no WASM / no GSAP fall back to the pre-rendered
+// poster (the same config, rendered by the same code at build time).
+
+const BASE_INK = { r: 0x16, g: 0x13, b: 0x10 };
+const BG_MIX = 0.12; // how much of the theme's background tints the page ink
+const TINT_MIN_LIGHTNESS = 0.58; // dark theme hues are lifted to stay legible
+const DRIFT_SECONDS = 11; // idle time before the house plates the next cover
+const FIRST_DRIFT_SECONDS = 6.5;
+const TYPE_MIN_MS = 30;
+const TYPE_JITTER_MS = 40;
+const PARALLAX_PX = 7;
+const PARALLAX_DEG = 1.3;
+
+// Mirrors `poster_config()` in examples/site.rs, so swapping the poster for the
+// first live render is invisible.
+const FIRST_PLATE = { title: 'Design systems that scale', category: 'engineering', number: '014' };
+const FIRST_STYLE = { theme: 0, pattern: 0, layout: 0, format: 0, strength: 1, grain: 0.25 };
+const BRAND = 'skvggor.dev';
+
+// The house menu: titles the engine typesets while the visitor watches.
+const PLATES = [
+ FIRST_PLATE,
+ { title: 'Performance without the magic', category: 'engineering', number: '021' },
+ { title: 'The quiet art of refactoring legacy code', category: 'essays', number: '007' },
+ { title: 'Ship less, design more', category: 'product', number: '032' },
+ { title: 'Naming things is design work', category: 'design', number: '009' },
+ { title: 'A field guide to code review', category: 'process', number: '017' },
+ { title: 'Typography for terminal people', category: 'design', number: '026' },
+ { title: 'What the compiler taught me about patience', category: 'essays', number: '041' },
+];
+
+// Page-chrome kanji only — the engine never draws Japanese glyphs on artwork.
+const THEME_KANJI = {
+ terracotta: '赤土',
+ sumi: '墨',
+ matcha: '抹茶',
+ washi: '和紙',
+ ai: '藍',
+ sakura: '桜',
+};
+
+const root = document.documentElement;
+const stage = document.getElementById('stage');
+const card = document.getElementById('card');
+const cover = document.getElementById('cover');
+const poster = document.getElementById('poster');
+const slug = document.getElementById('slug');
+const slugTheme = document.getElementById('slug-theme');
+const slugPattern = document.getElementById('slug-pattern');
+const slugLayout = document.getElementById('slug-layout');
+const slugFormat = document.getElementById('slug-format');
+const themesGroup = document.getElementById('themes');
+const omakaseButton = document.getElementById('omakase');
+const titleInput = document.getElementById('title-input');
+const hint = document.getElementById('hint');
+const downloadToggle = document.getElementById('download-toggle');
+const downloadsPanel = document.getElementById('downloads');
+const downloadsClose = document.getElementById('downloads-close');
+const followerElement = document.getElementById('follower');
+
+const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+const hasGsap = typeof window.gsap !== 'undefined';
+const animated = hasGsap && !reducedMotion;
+
+root.classList.add('js');
+
+let renderCover = null;
+let catalog = null;
+let live = false;
+let style = { ...FIRST_STYLE };
+let plateIndex = 0;
+let plate = PLATES[plateIndex];
+let customTitle = null;
+let typeCall = null;
+let driftCall = null;
+let renderQueued = false;
+
+function clamp(value, low, high) {
+ return value < low ? low : value > high ? high : value;
+}
+
+/* ---- Color math: tint the chrome from the engine's own palette ---- */
+
+function hexToRgb(hex) {
+ return {
+ r: parseInt(hex.slice(1, 3), 16),
+ g: parseInt(hex.slice(3, 5), 16),
+ b: parseInt(hex.slice(5, 7), 16),
+ };
+}
+
+function rgbToHex({ r, g, b }) {
+ const part = (v) => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, '0');
+ return `#${part(r)}${part(g)}${part(b)}`;
+}
+
+function mix(a, b, amount) {
+ return {
+ r: a.r + (b.r - a.r) * amount,
+ g: a.g + (b.g - a.g) * amount,
+ b: a.b + (b.b - a.b) * amount,
+ };
+}
+
+function rgbToHsl({ r, g, b }) {
+ const rn = r / 255;
+ const gn = g / 255;
+ const bn = b / 255;
+ const max = Math.max(rn, gn, bn);
+ const min = Math.min(rn, gn, bn);
+ const l = (max + min) / 2;
+ if (max === min) return { h: 0, s: 0, l };
+ const d = max - min;
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ let h;
+ if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
+ else if (max === gn) h = ((bn - rn) / d + 2) / 6;
+ else h = ((rn - gn) / d + 4) / 6;
+ return { h, s, l };
+}
+
+function hslToRgb({ h, s, l }) {
+ if (s === 0) return { r: l * 255, g: l * 255, b: l * 255 };
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ const channel = (t) => {
+ let tc = t;
+ if (tc < 0) tc += 1;
+ if (tc > 1) tc -= 1;
+ if (tc < 1 / 6) return p + (q - p) * 6 * tc;
+ if (tc < 1 / 2) return q;
+ if (tc < 2 / 3) return p + (q - p) * (2 / 3 - tc) * 6;
+ return p;
+ };
+ return { r: channel(h + 1 / 3) * 255, g: channel(h) * 255, b: channel(h - 1 / 3) * 255 };
+}
+
+/** The theme's background hue, lifted enough to read on the dark chrome. */
+function chromeTint(themeBackground) {
+ const hsl = rgbToHsl(hexToRgb(themeBackground));
+ if (hsl.l < TINT_MIN_LIGHTNESS) hsl.l = TINT_MIN_LIGHTNESS;
+ return rgbToHex(hslToRgb(hsl));
+}
+
+function pageInk(themeBackground) {
+ return rgbToHex(mix(BASE_INK, hexToRgb(themeBackground), BG_MIX));
+}
+
+/* ---- The engine ---- */
+
+function currentTitle() {
+ return customTitle !== null ? customTitle : plate.title;
+}
+
+function renderSvg(title, grain) {
+ return renderCover(
+ title,
+ plate.category,
+ '',
+ plate.number,
+ BRAND,
+ style.theme,
+ style.pattern,
+ style.layout,
+ style.format,
+ style.strength,
+ grain,
+ );
+}
+
+function inject(title, grain = style.grain) {
+ cover.innerHTML = renderSvg(title, grain);
+}
+
+/* ---- Plate geometry ---- */
+
+function cardSize() {
+ const box = stage.getBoundingClientRect();
+ const slugSpace = slug.getBoundingClientRect().height + 18;
+ const availableWidth = box.width * 0.97;
+ const availableHeight = Math.max(120, box.height - slugSpace);
+ const { width, height } = catalog.formats[style.format];
+ const scale = Math.min(availableWidth / width, availableHeight / height);
+ return { width: width * scale, height: height * scale };
+}
+
+function applySize(immediate) {
+ const size = cardSize();
+ if (animated && !immediate) {
+ return window.gsap.to(card, {
+ width: size.width,
+ height: size.height,
+ duration: 0.75,
+ ease: 'power3.inOut',
+ });
+ }
+ card.style.width = `${size.width}px`;
+ card.style.height = `${size.height}px`;
+ card.style.aspectRatio = 'auto';
+ return null;
+}
+
+/* ---- Chrome tinting ---- */
+
+function applyTint(immediate) {
+ const theme = catalog.themes[style.theme];
+ const values = { '--tint': chromeTint(theme.bg), '--bg-tint': pageInk(theme.bg) };
+ if (animated && !immediate) {
+ window.gsap.to(root, { ...values, duration: 0.75, ease: 'power2.inOut' });
+ window.gsap.to(card, { backgroundColor: theme.bg, duration: 0.75, ease: 'power2.inOut' });
+ return;
+ }
+ root.style.setProperty('--tint', values['--tint']);
+ root.style.setProperty('--bg-tint', values['--bg-tint']);
+ card.style.backgroundColor = theme.bg;
+}
+
+function updateSlug() {
+ const theme = catalog.themes[style.theme];
+ const kanji = THEME_KANJI[theme.label];
+ slugTheme.innerHTML = kanji ? `${theme.label} ${kanji}` : theme.label;
+ slugPattern.textContent = catalog.patterns[style.pattern];
+ slugLayout.textContent = catalog.layouts[style.layout];
+ slugFormat.textContent = catalog.formats[style.format].label.replace(' · ', ' ');
+ for (const button of themesGroup.children) {
+ button.setAttribute('aria-pressed', String(Number(button.dataset.theme) === style.theme));
+ }
+}
+
+/* ---- The typewriter: the engine typesets while you watch ---- */
+
+function cancelType() {
+ if (typeCall) {
+ typeCall.kill();
+ typeCall = null;
+ }
+}
+
+function typeTitle(title, onDone) {
+ cancelType();
+ let position = 0;
+ const step = () => {
+ position += 1;
+ const done = position >= title.length;
+ // Grain only lands with the final stroke; feTurbulence per keystroke is waste.
+ inject(title.slice(0, position), done ? style.grain : 0);
+ if (done) {
+ typeCall = null;
+ onDone();
+ return;
+ }
+ typeCall = window.gsap.delayedCall((TYPE_MIN_MS + Math.random() * TYPE_JITTER_MS) / 1000, step);
+ };
+ step();
+}
+
+/* ---- Drift: left idle, the house keeps plating ---- */
+
+function killDrift() {
+ if (driftCall) {
+ driftCall.kill();
+ driftCall = null;
+ }
+}
+
+function scheduleDrift(seconds = DRIFT_SECONDS) {
+ killDrift();
+ if (!animated || customTitle !== null) return;
+ driftCall = window.gsap.delayedCall(seconds, () => omakase());
+}
+
+/* ---- Omakase: re-roll the style, morph the plate, retype the title ---- */
+
+function nextStyle() {
+ const random = (length) => Math.floor(Math.random() * length);
+ let theme = random(catalog.themes.length);
+ while (theme === style.theme) theme = random(catalog.themes.length);
+ return {
+ theme,
+ pattern: random(catalog.patterns.length),
+ layout: random(catalog.layouts.length),
+ format: random(catalog.formats.length),
+ strength: 0.7 + Math.random() * 0.3,
+ grain: Math.random() < 0.5 ? 0.18 + Math.random() * 0.22 : 0,
+ };
+}
+
+function omakase() {
+ if (!live) return;
+ cancelType();
+ killDrift();
+ style = nextStyle();
+ if (customTitle === null) {
+ plateIndex = (plateIndex + 1) % PLATES.length;
+ plate = PLATES[plateIndex];
+ }
+
+ if (!animated) {
+ applyTint(true);
+ applySize(true);
+ inject(currentTitle());
+ updateSlug();
+ return;
+ }
+
+ const retype = customTitle === null;
+ const timeline = window.gsap.timeline();
+ timeline.to(cover, { opacity: 0, y: 12, duration: 0.38, ease: 'power2.in' }, 0);
+ timeline.to(slug, { opacity: 0, duration: 0.3, ease: 'power2.in' }, 0);
+ timeline.add(() => {
+ applyTint();
+ applySize();
+ }, 0.18);
+ timeline.add(() => {
+ window.gsap.set(cover, { y: 0 });
+ updateSlug();
+ window.gsap.to(slug, { opacity: 1, duration: 0.5, ease: 'power2.out' });
+ window.gsap.to(cover, { opacity: 1, duration: 0.4, ease: 'power2.out' });
+ if (retype) {
+ typeTitle(currentTitle(), () => scheduleDrift());
+ } else {
+ inject(currentTitle());
+ scheduleDrift();
+ }
+ }, 1.0);
+}
+
+/** A lighter round for direct theme choices: same plate, new ink. */
+function restyleTheme(themeIndex) {
+ if (!live || themeIndex === style.theme) return;
+ cancelType();
+ style.theme = themeIndex;
+ if (!animated) {
+ applyTint(true);
+ inject(currentTitle());
+ updateSlug();
+ return;
+ }
+ applyTint();
+ window.gsap.to(cover, {
+ opacity: 0,
+ duration: 0.2,
+ ease: 'power2.in',
+ onComplete: () => {
+ inject(currentTitle());
+ updateSlug();
+ window.gsap.to(cover, { opacity: 1, duration: 0.3, ease: 'power2.out' });
+ },
+ });
+ scheduleDrift();
+}
+
+/* ---- Controls ---- */
+
+function buildThemeStamps() {
+ catalog.themes.forEach((theme, index) => {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'theme';
+ button.dataset.theme = String(index);
+ button.style.backgroundColor = theme.bg;
+ button.style.color = theme.text;
+ button.setAttribute('aria-pressed', String(index === style.theme));
+ button.setAttribute('aria-label', `${theme.label} theme`);
+ button.title = theme.label;
+ const kanji = document.createElement('span');
+ kanji.lang = 'ja';
+ kanji.textContent = THEME_KANJI[theme.label] ?? theme.label.slice(0, 1);
+ button.append(kanji);
+ button.addEventListener('click', () => restyleTheme(index));
+ themesGroup.append(button);
+ });
+}
+
+function queueCustomRender() {
+ if (renderQueued) return;
+ renderQueued = true;
+ requestAnimationFrame(() => {
+ renderQueued = false;
+ inject(currentTitle());
+ });
+}
+
+function wireControls() {
+ card.addEventListener('click', () => omakase());
+ omakaseButton.addEventListener('click', () => omakase());
+
+ titleInput.addEventListener('input', () => {
+ const value = titleInput.value.trim();
+ if (value) {
+ customTitle = value;
+ killDrift();
+ cancelType();
+ queueCustomRender();
+ } else {
+ customTitle = null;
+ inject(currentTitle());
+ scheduleDrift();
+ }
+ });
+}
+
+/* ---- Downloads drawer ---- */
+
+function wireDownloads() {
+ const close = () => {
+ downloadsPanel.hidden = true;
+ downloadToggle.setAttribute('aria-expanded', 'false');
+ document.body.classList.remove('downloads-open');
+ };
+ downloadToggle.addEventListener('click', () => {
+ const open = downloadsPanel.hidden;
+ downloadsPanel.hidden = !open;
+ downloadToggle.setAttribute('aria-expanded', String(open));
+ document.body.classList.toggle('downloads-open', open);
+ if (open) downloadsClose.focus();
+ });
+ downloadsClose.addEventListener('click', () => {
+ close();
+ downloadToggle.focus();
+ });
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape' && !downloadsPanel.hidden) {
+ close();
+ downloadToggle.focus();
+ }
+ });
+}
+
+/* ---- Pointer life: parallax plate, hanko follower ---- */
+
+function wirePointer() {
+ if (!animated || !window.matchMedia('(pointer: fine)').matches) return;
+
+ const moveX = window.gsap.quickTo(card, 'x', { duration: 0.7, ease: 'power2.out' });
+ const moveY = window.gsap.quickTo(card, 'y', { duration: 0.7, ease: 'power2.out' });
+ const tiltX = window.gsap.quickTo(card, 'rotationX', { duration: 0.7, ease: 'power2.out' });
+ const tiltY = window.gsap.quickTo(card, 'rotationY', { duration: 0.7, ease: 'power2.out' });
+ window.gsap.set(card, { transformPerspective: 900 });
+
+ const followX = window.gsap.quickTo(followerElement, 'x', { duration: 0.35, ease: 'power3.out' });
+ const followY = window.gsap.quickTo(followerElement, 'y', { duration: 0.35, ease: 'power3.out' });
+ let followerShown = false;
+
+ window.addEventListener('pointermove', (event) => {
+ const nx = (event.clientX / window.innerWidth) * 2 - 1;
+ const ny = (event.clientY / window.innerHeight) * 2 - 1;
+ moveX(nx * PARALLAX_PX);
+ moveY(ny * PARALLAX_PX * 0.7);
+ tiltY(nx * PARALLAX_DEG);
+ tiltX(-ny * PARALLAX_DEG);
+ followX(event.clientX);
+ followY(event.clientY);
+ if (!followerShown) {
+ followerShown = true;
+ window.gsap.set(followerElement, { x: event.clientX, y: event.clientY });
+ window.gsap.to(followerElement, { opacity: 0.85, duration: 0.4 });
+ }
+ });
+ document.addEventListener('pointerdown', () => {
+ window.gsap.fromTo(
+ followerElement,
+ { scale: 1 },
+ { scale: 1.55, duration: 0.35, ease: 'power2.out', yoyo: true, repeat: 1 },
+ );
+ });
+ document.addEventListener('mouseleave', () => {
+ followerShown = false;
+ window.gsap.to(followerElement, { opacity: 0, duration: 0.3 });
+ });
+}
+
+/* ---- Boot ---- */
+
+function reveal() {
+ const items = document.querySelectorAll('[data-reveal]');
+ if (!animated) {
+ for (const item of items) item.style.opacity = 1;
+ return;
+ }
+ window.gsap.set(items, { y: 10, opacity: 0 });
+ window.gsap.to(items, { y: 0, opacity: 1, duration: 0.8, ease: 'power2.out', stagger: 0.12, delay: 0.15 });
+}
+
+function withoutLiveEngine() {
+ root.classList.add('no-live');
+ hint.textContent = 'The live engine needs WebAssembly — this plate was pre-rendered by the same code.';
+ reveal();
+}
+
+async function boot() {
+ if (hasGsap) window.gsap.ticker.lagSmoothing(0);
+
+ let module;
+ try {
+ module = await import('../wasm/acag.js');
+ await module.default();
+ } catch (error) {
+ console.warn('acag wasm unavailable:', error);
+ withoutLiveEngine();
+ return;
+ }
+
+ renderCover = module.render_cover;
+ catalog = JSON.parse(module.catalog());
+ live = true;
+
+ buildThemeStamps();
+ applySize(true);
+ applyTint(true);
+ inject(currentTitle());
+ updateSlug();
+ poster.hidden = true;
+
+ wireControls();
+ wirePointer();
+ reveal();
+ scheduleDrift(FIRST_DRIFT_SECONDS);
+
+ window.addEventListener('resize', () => {
+ if (live) applySize(true);
+ });
+}
+
+wireDownloads();
+boot();
diff --git a/web/js/vendor/gsap.min.js b/web/js/vendor/gsap.min.js
new file mode 100644
index 0000000..3bb5803
--- /dev/null
+++ b/web/js/vendor/gsap.min.js
@@ -0,0 +1,11 @@
+/*!
+ * GSAP 3.13.0
+ * https://gsap.com
+ *
+ * @license Copyright 2025, GreenSock. All rights reserved.
+ * Subject to the terms at https://gsap.com/standard-license.
+ * @author: Jack Doyle, jack@greensock.com
+ */
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&ze}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Zt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Ot(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function za(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Aa(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Ba(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Da(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Fa(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ha(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ia(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||U)||0))}function Ja(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0U)&&e.render(r,!0)),Ba(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function ub(t){return Aa(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&Pt(t,"onInterrupt"),t}function xb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:ue,add:Vt,kill:de,modifier:he,rawVars:0},a={targetTest:0,get:0,getSetter:ie,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;ra(i,ra(va(t,n),a)),yt(i.prototype,yt(n,va(t,a))),pt[i.prop=e]=i,t.targetTest&&(gt.push(i),ft[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}S(e,i),t.register&&t.register(ze,i,ge)}else Ct.push(t)}function Ab(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:Dt.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Dt[e])p=Dt[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&St,p&St,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&St,e&St]}else if("hsl"===e.substr(0,3))if(p=c=e.match(tt),r){if(~e.indexOf("="))return p=e.match(et),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=N?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof Jt?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&za(this,t),t===this._recent&&(this._recent=this._last),Ba(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ja(Rt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=xt(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=Jt.to(i,ra({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||U,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Sa(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ra({startAt:{time:xt(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),sb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),sb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+U)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Ba(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Ba(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=N;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,La(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Sa(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(oa(I,Ha(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=X.autoSleep||120;var e=I._first;if((!e||!e._ts)&&X.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Nt);ra(Qt.prototype,{_lock:0,_hasPause:0,_forcing:0});function bc(t,e,i,n,a,o){var u,h,l,f;if(pt[t]&&!1!==(u=new pt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Gt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||J(t))return r(t)?Gt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Gt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new ge(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function hc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Ut,qt,Vt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?re:te:$t;if(r(n)&&(~n.indexOf("random(")&&(n=pb(n)),"="===n.charAt(1)&&(!(d=ka(p,n)+(Za(p)||0))&&0!==d||(n=d))),!f||p!==n||qt)return isNaN(p*n)||""===n?(c||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new ge(this._pt,t,e,0,1,oe,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=pb(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ka(d,l)-d:parseFloat(l)-d,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},x)"ease"===l||"easeEach"===l||hc(l,x[l],u,x.easeEach);for(l in u)for(C=u[l].sort(function(t,e){return t.t-e.t}),o=z=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&Aa(t,1),r||L||(Pt(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),E.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||Rt.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Wt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return qt=1,t.vars[e]="+=0",Wt(t,s),qt=0,o?R(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ia(r)+Za(h.e)),h.b&&(h.b=u.s+Za(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(Ja(this,0),this.parent||ya(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?ub(this):this.scrollTrigger&&this.scrollTrigger.kill(!!L),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Ut&&!0!==Ut.vars.overwrite)._first||ub(this),this.parent&&i!==this.timeline.totalDuration()&&Sa(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Ot(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),ub(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ha(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?fa(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=yt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in a=c[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||za(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&ub(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Wa(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Wa(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Nt);ra(Jt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){Jt[r]=function(){var t=new Qt,e=kt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function pc(t,e,r){return t.setAttribute(e,r)}function xc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var $t=function _setterPlain(t,e,r){return t[e]=r},te=function _setterFunc(t,e,r){return t[e](r)},re=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ie=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?pc:$t},ne=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},se=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},oe=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},ue=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},he=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},de=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?za(this,i,"_pt"):i.dep||(e=1),i=r;return!e},_e=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},ge=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=xc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ne,this.d=s||this,this.set=o||$t,this.pr=u||0,(this._next=t)&&(t._prev=this)}ha(vt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ft[t]=1}),ot.TweenMax=ot.TweenLite=Jt,ot.TimelineLite=ot.TimelineMax=Qt,I=new Qt({sortChildren:!1,defaults:Z,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),X.stringFilter=Gb;function Fc(t){return(be[t]||Me).map(function(t){return t()})}function Gc(){var t=Date.now(),o=[];2
+
+
+ https://skvggor.github.io/acag/
+ 2026-07-15
+ monthly
+ 1.0
+
+