diff --git a/public/holofoils/ancient.png b/public/holofoils/ancient.png new file mode 100644 index 0000000..a060580 Binary files /dev/null and b/public/holofoils/ancient.png differ diff --git a/public/holofoils/angular.png b/public/holofoils/angular.png new file mode 100644 index 0000000..e015630 Binary files /dev/null and b/public/holofoils/angular.png differ diff --git a/public/holofoils/cosmos-bottom-trans.png b/public/holofoils/cosmos-bottom-trans.png new file mode 100644 index 0000000..cf7b9c5 Binary files /dev/null and b/public/holofoils/cosmos-bottom-trans.png differ diff --git a/public/holofoils/cosmos-bottom.png b/public/holofoils/cosmos-bottom.png new file mode 100644 index 0000000..43c2cef Binary files /dev/null and b/public/holofoils/cosmos-bottom.png differ diff --git a/public/holofoils/cosmos-middle-trans.png b/public/holofoils/cosmos-middle-trans.png new file mode 100644 index 0000000..e2f3822 Binary files /dev/null and b/public/holofoils/cosmos-middle-trans.png differ diff --git a/public/holofoils/cosmos-middle.gif b/public/holofoils/cosmos-middle.gif new file mode 100644 index 0000000..f10f703 Binary files /dev/null and b/public/holofoils/cosmos-middle.gif differ diff --git a/public/holofoils/cosmos-middle.png b/public/holofoils/cosmos-middle.png new file mode 100644 index 0000000..af0ce39 Binary files /dev/null and b/public/holofoils/cosmos-middle.png differ diff --git a/public/holofoils/cosmos-top-trans.png b/public/holofoils/cosmos-top-trans.png new file mode 100644 index 0000000..2bd668c Binary files /dev/null and b/public/holofoils/cosmos-top-trans.png differ diff --git a/public/holofoils/cosmos-top.png b/public/holofoils/cosmos-top.png new file mode 100644 index 0000000..d69e104 Binary files /dev/null and b/public/holofoils/cosmos-top.png differ diff --git a/public/holofoils/cosmos.png b/public/holofoils/cosmos.png new file mode 100644 index 0000000..74b6493 Binary files /dev/null and b/public/holofoils/cosmos.png differ diff --git a/public/holofoils/crossover.png b/public/holofoils/crossover.png new file mode 100644 index 0000000..d495389 Binary files /dev/null and b/public/holofoils/crossover.png differ diff --git a/public/holofoils/galaxy-source.png b/public/holofoils/galaxy-source.png new file mode 100644 index 0000000..c6e6ec0 Binary files /dev/null and b/public/holofoils/galaxy-source.png differ diff --git a/public/holofoils/galaxy.jpg b/public/holofoils/galaxy.jpg new file mode 100644 index 0000000..f8a0615 Binary files /dev/null and b/public/holofoils/galaxy.jpg differ diff --git a/public/holofoils/geometric.png b/public/holofoils/geometric.png new file mode 100644 index 0000000..d495389 Binary files /dev/null and b/public/holofoils/geometric.png differ diff --git a/public/holofoils/glitter.png b/public/holofoils/glitter.png new file mode 100644 index 0000000..2bc0e38 Binary files /dev/null and b/public/holofoils/glitter.png differ diff --git a/public/holofoils/grain.webp b/public/holofoils/grain.webp new file mode 100644 index 0000000..adbae00 Binary files /dev/null and b/public/holofoils/grain.webp differ diff --git a/public/holofoils/illusion-mask.png b/public/holofoils/illusion-mask.png new file mode 100644 index 0000000..7d3fc94 Binary files /dev/null and b/public/holofoils/illusion-mask.png differ diff --git a/public/holofoils/illusion.png b/public/holofoils/illusion.png new file mode 100644 index 0000000..4ba2e13 Binary files /dev/null and b/public/holofoils/illusion.png differ diff --git a/public/holofoils/illusion2.png b/public/holofoils/illusion2.png new file mode 100644 index 0000000..e744f9b Binary files /dev/null and b/public/holofoils/illusion2.png differ diff --git a/public/holofoils/metal.png b/public/holofoils/metal.png new file mode 100644 index 0000000..a0faaad Binary files /dev/null and b/public/holofoils/metal.png differ diff --git a/public/holofoils/rainbow.jpg b/public/holofoils/rainbow.jpg new file mode 100644 index 0000000..fe7efca Binary files /dev/null and b/public/holofoils/rainbow.jpg differ diff --git a/public/holofoils/stylish.png b/public/holofoils/stylish.png new file mode 100644 index 0000000..b33664c Binary files /dev/null and b/public/holofoils/stylish.png differ diff --git a/public/holofoils/stylish2.png b/public/holofoils/stylish2.png new file mode 100644 index 0000000..0eeb6f1 Binary files /dev/null and b/public/holofoils/stylish2.png differ diff --git a/public/holofoils/trainerbg.jpg b/public/holofoils/trainerbg.jpg new file mode 100644 index 0000000..b52473c Binary files /dev/null and b/public/holofoils/trainerbg.jpg differ diff --git a/public/holofoils/trainerbg.png b/public/holofoils/trainerbg.png new file mode 100644 index 0000000..2e923b7 Binary files /dev/null and b/public/holofoils/trainerbg.png differ diff --git a/public/holofoils/vmaxbg.jpg b/public/holofoils/vmaxbg.jpg new file mode 100644 index 0000000..bdd105e Binary files /dev/null and b/public/holofoils/vmaxbg.jpg differ diff --git a/public/holofoils/wave.png b/public/holofoils/wave.png new file mode 100644 index 0000000..4af8c29 Binary files /dev/null and b/public/holofoils/wave.png differ diff --git a/src/assets/css/_card.scss b/src/assets/css/_card.scss new file mode 100644 index 0000000..f06f465 --- /dev/null +++ b/src/assets/css/_card.scss @@ -0,0 +1,2115 @@ +// ============================================================================= +// POKEMON CARD HOLOGRAPHIC EFFECTS +// Refactored to SCSS with logical groupings, mixins, and reduced repetition +// ============================================================================= + + +// ============================================================================= +// VARIABLES & DESIGN TOKENS +// ============================================================================= + +:root { + // Card geometry + --card-aspect: aspect-ratio: 23 / 32; + + // Card colors + --card-edge: hsl(47, 100%, 78%); + --card-back: hsl(205, 100%, 25%); + --card-glow: hsl(175, 100%, 90%); + + // Sunpillar palette (base) + --sunpillar-1: hsl(2, 100%, 73%); + --sunpillar-2: hsl(53, 100%, 69%); + --sunpillar-3: hsl(93, 100%, 69%); + --sunpillar-4: hsl(176, 100%, 76%); + --sunpillar-5: hsl(228, 100%, 74%); + --sunpillar-6: hsl(283, 100%, 73%); + + // Sunpillar active colors (remapped per context via mixins/overrides) + --sunpillar-clr-1: var(--sunpillar-1); + --sunpillar-clr-2: var(--sunpillar-2); + --sunpillar-clr-3: var(--sunpillar-3); + --sunpillar-clr-4: var(--sunpillar-4); + --sunpillar-clr-5: var(--sunpillar-5); + --sunpillar-clr-6: var(--sunpillar-6); +} + + +// ============================================================================= +// SCSS VARIABLES +// ============================================================================= + +// Shared texture/asset references +$grain: url('/public/holofoils/grain.webp'); +$glitter: url('/public/holofoils/glitter.png'); +$glittersize: 25%; + + +// ============================================================================= +// MIXINS +// ============================================================================= + +/// Emits CSS custom properties for the 7-stop rainbow palette. +@mixin rainbow-colors { + --r-clr-1: #{hsl(0, 57%, 37%)}; + --r-clr-2: #{hsl(40, 53%, 39%)}; + --r-clr-3: #{hsl(90, 60%, 35%)}; + --r-clr-4: #{hsl(180, 60%, 35%)}; + --r-clr-5: #{hsl(180, 60%, 35%)}; + --r-clr-6: #{hsl(210, 57%, 39%)}; + --r-clr-7: #{hsl(280, 55%, 31%)}; +} + +/// Emits the 7-stop rainbow gradient used across multiple card types. +/// @param {String} $angle - gradient angle (e.g. '-30deg', '-60deg') +@mixin rainbow-gradient($angle: '-30deg') { + background-image: linear-gradient( + #{$angle}, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1) + ); +} + +/// Emits the 6-stop repeating sunpillar gradient. +@mixin sunpillar-gradient { + background-image: repeating-linear-gradient( + calc(var(--angle)), + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ); +} + +// Returns the radiant-rare hatch gradient as a value so it can be +// embedded inside a multi-layer background-image declaration. +@function radiant-hatch($deg) { + @return repeating-linear-gradient(#{$deg}, + hsl(0, 0%, 10%) 0%, + hsl(0, 0%, 10%) var(--barwidth), + hsl(0, 0%, 20%) calc(var(--barwidth) + 0.01%), + hsl(0, 0%, 20%) calc(var(--barwidth) * 2), + hsl(0, 0%, 35%) calc(var(--barwidth) * 2 + 0.01%), + hsl(0, 0%, 35%) calc(var(--barwidth) * 3), + hsl(0, 0%, 42.5%) calc(var(--barwidth) * 3 + 0.01%), + hsl(0, 0%, 42.5%) calc(var(--barwidth) * 4), + hsl(0, 0%, 50%) calc(var(--barwidth) * 4 + 0.01%), + hsl(0, 0%, 50%) calc(var(--barwidth) * 5), + hsl(0, 0%, 42.5%) calc(var(--barwidth) * 5 + 0.01%), + hsl(0, 0%, 42.5%) calc(var(--barwidth) * 6), + hsl(0, 0%, 35%) calc(var(--barwidth) * 6 + 0.01%), + hsl(0, 0%, 35%) calc(var(--barwidth) * 7), + hsl(0, 0%, 20%) calc(var(--barwidth) * 7 + 0.01%), + hsl(0, 0%, 20%) calc(var(--barwidth) * 8), + hsl(0, 0%, 10%) calc(var(--barwidth) * 8 + 0.01%), + hsl(0, 0%, 10%) calc(var(--barwidth) * 9), + hsl(0, 0%, 0%) calc(var(--barwidth) * 9 + 0.01%), + hsl(0, 0%, 0%) calc(var(--barwidth) * 10) + ); +} + +/// The shared shine + stripe pattern used by rare-ultra (pokémon & supporter), +/// rare-shiny, rare-shiny-v, rare-holo-vstar, and trainer-gallery-v. +@mixin v-shine-background { + --space: 5%; + --angle: 133deg; + --imgsize: cover; + + background-image: + var(--foil), + repeating-linear-gradient(0deg, + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ), + repeating-linear-gradient(var(--angle), + #0e152e 0%, + hsl(180, 10%, 60%) 3.8%, + hsl(180, 29%, 66%) 4.5%, + hsl(180, 10%, 60%) 5.2%, + #0e152e 10%, + #0e152e 12% + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 0%, 0.10) 12%, + hsla(0, 0%, 0%, 0.15) 20%, + hsla(0, 0%, 0%, 0.25) 120% + ); + background-blend-mode: soft-light, hue, hard-light; + background-size: var(--imgsize), 200% 700%, 300% 100%, 200% 100%; + background-position: + center center, + 0% var(--background-y), + calc(var(--background-x) + (var(--background-y) * 0.2)) var(--background-y), + var(--background-x) var(--background-y); +} + +/// Shared :after layer for v-shine (flipped/exclusion pass). +@mixin v-shine-after { + content: ''; + background-position: + center center, + 0% var(--background-y), + calc((var(--background-x) + (var(--background-y) * 0.2)) * -1) calc(var(--background-y) * -1), + var(--background-x) var(--background-y); + background-size: var(--imgsize), 200% 400%, 195% 100%, 200% 100%; + filter: brightness(calc((var(--pointer-from-center) * .4) + .8)) contrast(1.5) saturate(1.25); + mix-blend-mode: exclusion; +} + +/// Shared :before overlay spotlight. +@mixin spotlight-before { + content: ''; + -webkit-mask-image: none; + mask-image: none; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 0%, + hsla(0, 0%, 0%, 0) 40% + ); + background-position: center; + background-size: cover; + mix-blend-mode: overlay; + opacity: 0.75; + z-index: 1; +} + +/// No-mask fallback for cards using the illusion foil pattern. +@mixin no-mask-illusion { + --mask: none; + --foil: url('/public/holofoils/illusion.png'); + --imgsize: 33%; + -webkit-mask-image: var(--mask); + mask-image: var(--mask); + -webkit-mask-size: var(--imgsize); + mask-size: var(--imgsize); +} + +/// Hardware-accelerated translate layer. +@mixin gpu-layer { + -webkit-transform: translate3d(0px, 0px, 0.01px); + transform: translate3d(0px, 0px, 0.01px); + will-change: transform; +} + + +// ============================================================================= +// PLACEHOLDER EXTENSIONS +// ============================================================================= + +// Reusable active-glow box-shadow +%rotator-active-shadow { + box-shadow: + 0 0 3px -1px white, + 0 0 3px 1px var(--card-edge), + 0 0 12px 2px var(--card-glow), + 0px 10px 20px -5px black, + 0 0 40px -30px var(--card-glow), + 0 0 50px -20px var(--card-glow); +} + +// Shared masked-shine mask application +%masked-shine { + -webkit-mask-image: var(--mask); + mask-image: var(--mask); + -webkit-mask-size: cover; + mask-size: cover; + -webkit-mask-position: center center; + mask-position: center center; +} + +// Secret rare shine layer +%secret-rare-shine { + --shift: 1px; + --imgsize: cover; + background-image: + var(--glitter), + var(--glitter), + conic-gradient( + var(--sunpillar-clr-4), + var(--sunpillar-clr-5), + var(--sunpillar-clr-6), + var(--sunpillar-clr-1), + var(--sunpillar-clr-4) + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(150, 0%, 0%, .98) 10%, + hsla(0, 0%, 95%, .15) 90% + ); + background-size: + var(--glittersize) var(--glittersize), + var(--glittersize) var(--glittersize), + cover, cover; + background-position: 45% 45%, 55% 55%, center center, center center; + background-blend-mode: soft-light, hard-light, overlay; + mix-blend-mode: color-dodge; + filter: brightness(calc(0.4 + (var(--pointer-from-center) * 0.2))) contrast(1) saturate(2.7); +} + +%secret-rare-shine-before { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--foil), + linear-gradient(45deg, hsl(46, 95%, 50%), hsl(52, 100%, 69%)), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(10, 20%, 90%, 0.95) 10%, + hsl(0, 0%, 0%) 70% + ); + background-size: var(--imgsize), cover, cover; + background-position: center center, center center, center center; + background-blend-mode: hard-light, multiply; + mix-blend-mode: lighten; + filter: brightness(1.25) contrast(1.25) saturate(0.35); + opacity: .8; +} + +%secret-rare-shine-after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: var(--glitter); + background-size: var(--glittersize) var(--glittersize); + background-position: + calc(50% - ((var(--shift) * 2) * var(--pointer-from-left)) + var(--shift)) + calc(50% - ((var(--shift) * 2) * var(--pointer-from-top)) + var(--shift)); + filter: brightness(calc((var(--pointer-from-center) * 0.6) + 0.6)) contrast(1.5); + mix-blend-mode: overlay; +} + +%secret-rare-glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(45, 8%, 80%, 0.3) 0%, + hsl(22, 15%, 12%) 180% + ); + filter: brightness(1.3) contrast(1.5); + mix-blend-mode: hard-light; +} + +// Shiny rare / shiny V shared shine layers +%shiny-shine { + --space: 5%; + --angle: 133deg; + --imgsize: cover; + background-image: + var(--foil), + repeating-linear-gradient(0deg, + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ), + repeating-linear-gradient(var(--angle), + #0e152e 0%, + hsl(180, 10%, 60%) 3.8%, + hsl(180, 29%, 66%) 4.5%, + hsl(180, 10%, 60%) 5.2%, + #0e152e 10%, + #0e152e 12% + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 0%, 0.10) 12%, + hsla(0, 0%, 0%, 0.15) 20%, + hsla(0, 0%, 0%, 0.25) 120% + ); + background-position: + center center, + 0% var(--background-y), + calc(var(--background-x) + (var(--background-y) * 0.2)) var(--background-y), + var(--background-x) var(--background-y); + background-blend-mode: soft-light, hue, hard-light; + background-size: var(--imgsize), 200% 700%, 300% 100%, 200% 100%; + filter: brightness(calc((var(--pointer-from-center) * 0.4) + .4)) contrast(1.4) saturate(2.25); +} + +%shiny-shine-after { + content: ''; + background-position: + center center, + 0% var(--background-y), + calc((var(--background-x) + (var(--background-y) * 0.2)) * -1) calc(var(--background-y) * -1), + var(--background-x) var(--background-y); + background-size: var(--imgsize), 200% 400%, 195% 100%, 200% 100%; + filter: brightness(calc((var(--pointer-from-center) * .4) + .8)) contrast(1.5) saturate(1.25); + mix-blend-mode: exclusion; +} + +%shiny-shine-before { + content: ''; + -webkit-mask-image: none; + mask-image: none; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 0%, + hsla(0, 0%, 0%, 0) 40% + ); + background-position: center; + background-size: cover; + mix-blend-mode: overlay; + opacity: 0.75; + z-index: 1; +} + +// Trainer Gallery Holo shared layers +%tg-holo-shine { + --space: 5%; + --angle: -22deg; + --imgsize: 300% 400%; + clip-path: var(--clip-borders); + background-image: + repeating-linear-gradient(var(--angle), + hsla(283, 49%, 60%, 0.75) calc(var(--space) * 1), + hsla(2, 74%, 59%, 0.75) calc(var(--space) * 2), + hsla(53, 67%, 53%, 0.75) calc(var(--space) * 3), + hsla(93, 56%, 52%, 0.75) calc(var(--space) * 4), + hsla(176, 38%, 50%, 0.75) calc(var(--space) * 5), + hsla(228, 100%, 77%, 0.75) calc(var(--space) * 6), + hsla(283, 49%, 61%, 0.75) calc(var(--space) * 7) + ); + background-blend-mode: color-dodge; + background-size: var(--imgsize); + background-position: + 0% calc(var(--background-y) * 1), + var(--background-x) var(--background-y); + filter: brightness(calc((var(--pointer-from-center) * 0.3) + 0.5)) contrast(2.3) saturate(1); +} + +%tg-holo-shine-after { + content: ''; + background-image: radial-gradient( + farthest-corner ellipse + at calc((var(--pointer-x) * 0.5) + 25%) calc((var(--pointer-y) * 0.5) + 25%), + hsl(0, 0%, 100%) 5%, + hsla(300, 100%, 11%, 0.6) 40%, + hsl(0, 0%, 22%) 120% + ); + background-position: center center; + background-size: 400% 500%; + filter: brightness(calc((var(--pointer-from-center) * 0.2) + 0.4)) contrast(.85) saturate(1.1); + mix-blend-mode: hard-light; +} + +%tg-holo-glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 1) 10%, + hsla(0, 0%, 100%, 0.6) 35%, + hsla(180, 11%, 35%, 1) 60% + ); + mix-blend-mode: soft-light; +} + +// Rainbow alt shine layers (shared by rare rainbow alt + trainer gallery vmax) +%rainbow-alt-shine { + --imgsize: cover; + @include rainbow-colors; + background-image: + repeating-linear-gradient( + var(--angle), + hsla(283, 49%, 60%, 0.75) calc(var(--space) * 1), + hsla(2, 70%, 58%, 0.75) calc(var(--space) * 2), + hsla(53, 67%, 53%, 0.75) calc(var(--space) * 3), + hsla(93, 56%, 52%, 0.75) calc(var(--space) * 4), + hsla(176, 38%, 50%, 0.75) calc(var(--space) * 5), + hsla(228, 100%, 77%, 0.75) calc(var(--space) * 6), + hsla(283, 49%, 61%, 0.75) calc(var(--space) * 7) + ), + var(--glitter), + linear-gradient(-30deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-size: 200% 400%, var(--glittersize) var(--glittersize), 400% 400%; + background-position: + 0% calc(var(--background-y) * 1), + center center, + calc(var(--background-x) * 1.5) calc(var(--background-y) * 1.5); + background-blend-mode: luminosity, overlay; + filter: brightness(calc((var(--pointer-from-center) * 0.3) + 0.3)) contrast(3) saturate(1.8); +} + +%rainbow-alt-shine-after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--glitter), + linear-gradient(-60deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-blend-mode: overlay; + background-size: var(--glittersize) var(--glittersize), 400% 400%; + background-position: + center center, + calc(var(--background-x) * -1.5) calc(var(--background-y) * -1.5); + filter: brightness(calc((var(--pointer-from-center) * 0.5) + 0.6)) contrast(3) saturate(1); + mix-blend-mode: color-dodge; + opacity: calc(1.2 + (var(--pointer-from-center) / 2) * -1); +} + +%rainbow-alt-shine-before { + content: ''; + background-image: var(--foil); + background-size: var(--imgsize); + background-position: center center; + filter: brightness(1.5) contrast(1.5); + opacity: calc((var(--pointer-from-center) + 0.6) * 0.4); + background-blend-mode: difference; + mix-blend-mode: color-dodge; +} + + +// ============================================================================= +// BASE CARD STRUCTURE +// ============================================================================= + +.card { + @include gpu-layer; + + pointer-events: none; + z-index: calc(var(--card-scale) * 2); + will-change: transform, visibility, z-index; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + + // Anti-alias trick + &, + * { outline: 1px solid transparent; } + + // Aspect ratio & radius (shared with rotator) + &, + &__rotator { + aspect-ratio: var(--card-aspect); + border-radius: var(--card-radius); + } + + // ── Static hover (non-interactive) ────────────────────────────────────── + &:not(.interactive) { + .card__translater, + .card__rotator, + .card__shine, + .card__glare { transition: all 0.3s ease; } + + &:hover { + --pointer-x: 25% !important; + --pointer-y: 10% !important; + --card-scale: 1.1 !important; + --card-opacity: 1 !important; + --translate-x: 0px !important; + --translate-y: -10px !important; + --rotate-x: 7deg !important; + --rotate-y: -19deg !important; + --background-x: 44% !important; + --background-y: 36% !important; + --pointer-from-center: .9 !important; + --pointer-from-top: .11 !important; + --pointer-from-left: .25 !important; + } + } + + // ── Type-based glow overrides ──────────────────────────────────────────── + &.water { --card-glow: hsl(192, 97%, 60%); } + &.fire { --card-glow: hsl(9, 81%, 59%); } + &.grass { --card-glow: hsl(96, 81%, 65%); } + &.lightning { --card-glow: hsl(54, 87%, 63%); } + &.psychic { --card-glow: hsl(281, 62%, 58%); } + &.fighting { --card-glow: rgb(145, 90, 39); } + &.darkness { --card-glow: hsl(189, 77%, 27%); } + &.metal { --card-glow: hsl(184, 20%, 70%); } + &.dragon { --card-glow: hsl(51, 60%, 35%); } + &.fairy { --card-glow: hsl(323, 100%, 89%); } + + // ── State ──────────────────────────────────────────────────────────────── + &.interacting { z-index: calc(var(--card-scale) * 120); } + + &.active { + .card__translater, + .card__rotator { touch-action: none; } + } + + // ── Shared clip-path / asset tokens ───────────────────────────────────── + --grain: #{$grain}; + --glitter: #{$glitter}; + --glittersize: #{$glittersize}; + + --space: 5%; + --angle: 133deg; + --imgsize: cover; + + --red: #f80e35; + --yellow: #eedf10; + --green: #21e985; + --blue: #0dbde9; + --violet: #c929f1; + + --clip: inset(9.85% 8% 52.85% 8%); + --clip-invert: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 47.15%, 91.5% 47.15%, 91.5% 9.85%, 8% 9.85%, 8% 47.15%, 0 50%); + --clip-stage: polygon(91.5% 9.85%, 57% 9.85%, 54% 12%, 17% 12%, 16% 14%, 12% 16%, 8% 16%, 8% 47.15%, 92% 47.15%); + --clip-stage-invert: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 47.15%, 91.5% 47.15%, 91.5% 9.85%, 57% 9.85%, 54% 12%, 17% 12%, 16% 14%, 12% 16%, 8% 16%, 8% 47.15%, 0 50%); + --clip-trainer: inset(14.5% 8.5% 48.2% 8.5%); + --clip-borders: inset(2.8% 4% round 2.55% / 1.5%); + + // ── Foil brightness (reverse holo type overrides) ──────────────────────── + --foil-brightness: 0.55; + &.lightning { --foil-brightness: 0.7; } + &.darkness { --foil-brightness: 0.8; } + &.metal { --foil-brightness: 0.6; } +} + + +// ============================================================================= +// LAYOUT LAYERS: TRANSLATER / ROTATOR +// ============================================================================= + +.card__translater, +.card__rotator { + display: grid; + perspective: 600px; + will-change: transform, box-shadow; + transform-origin: center; + -webkit-transform-origin: center; + transform-style: preserve-3d; +} + +.card__translater { + width: auto; + position: relative; + --translate-z: calc(var(--card-scale) * 150px + 0.01px); + -webkit-transform: translate3d(var(--translate-x), var(--translate-y), var(--translate-z)) scale(var(--card-scale)); + transform: translate3d(var(--translate-x), var(--translate-y), var(--translate-z)) scale(var(--card-scale)); +} + +.card__rotator { + -webkit-transform: rotateY(var(--rotate-x)) rotateX(var(--rotate-y)); + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: rotateY(var(--rotate-x)) rotateX(var(--rotate-y)); + pointer-events: auto; + + // Button reset + &:is(button) { + border: none; + background: transparent; + padding: 0; + -webkit-appearance: none; + appearance: none; + } + + // ── Box shadow states ──────────────────────────────────────────────────── + &, + .card.active &:focus { + transition: box-shadow 0.4s ease, opacity 0.33s ease-out; + box-shadow: + 0 0 3px -1px transparent, + 0 0 2px 1px transparent, + 0 0 5px 0px transparent, + 0px 10px 20px -5px black, + 0 2px 15px -5px black, + 0 0 20px 0px transparent; + } + + .card.active &, + &:focus { @extend %rotator-active-shadow; } + + // All children share card geometry and 3D context + * { + width: 100%; + display: grid; + grid-area: 1/1; + aspect-ratio: var(--card-aspect); + border-radius: var(--card-radius); + image-rendering: optimizeQuality; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + pointer-events: none; + overflow: hidden; + } + + img { + height: auto; + &:not(.card__back) { @include gpu-layer; } + } +} + + +// ============================================================================= +// CARD FACES +// ============================================================================= + +.card__back { + background-color: var(--card-back); + -webkit-transform: rotateY(180deg) translateZ(1px); + transform: rotateY(180deg) translateZ(1px); + backface-visibility: visible; +} + +.card__front { + opacity: 1; + transition: opacity 0.33s ease-out; + @include gpu-layer; + + &, + * { backface-visibility: hidden; } +} + +// ── Loading state ──────────────────────────────────────────────────────────── +.loading { + .card__front { opacity: 0; } + .card__back { + -webkit-transform: rotateY(0deg); + transform: rotateY(0deg); + } +} + + +// ============================================================================= +// SHINE & GLARE BASE LAYERS +// ============================================================================= + +.card__shine { + display: grid; + transform: translateZ(1px); + overflow: hidden; + z-index: 3; + background: transparent; + background-size: cover; + background-position: center; + filter: brightness(.85) contrast(2.75) saturate(.65); + mix-blend-mode: color-dodge; + opacity: var(--card-opacity); + + &::before, + &::after { + // Shifted sunpillar mapping for depth layering + --sunpillar-clr-1: var(--sunpillar-5); + --sunpillar-clr-2: var(--sunpillar-6); + --sunpillar-clr-3: var(--sunpillar-1); + --sunpillar-clr-4: var(--sunpillar-2); + --sunpillar-clr-5: var(--sunpillar-3); + --sunpillar-clr-6: var(--sunpillar-4); + grid-area: 1/1; + transform: translateZ(1px); + border-radius: var(--card-radius); + } + + &::after { + // Shifted again for uppermost layer + --sunpillar-clr-1: var(--sunpillar-6); + --sunpillar-clr-2: var(--sunpillar-1); + --sunpillar-clr-3: var(--sunpillar-2); + --sunpillar-clr-4: var(--sunpillar-3); + --sunpillar-clr-5: var(--sunpillar-4); + --sunpillar-clr-6: var(--sunpillar-5); + transform: translateZ(1.2px); + } +} + +.card__glare { + transform: translateZ(1.41px); + overflow: hidden; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.8) 10%, + hsla(0, 0%, 100%, 0.65) 20%, + hsla(0, 0%, 0%, 0.5) 90% + ); + opacity: var(--card-opacity); + mix-blend-mode: overlay; +} + +.card__shine, +.card__glare { + will-change: transform, opacity, background-image, background-size, + background-position, background-blend-mode, filter; +} + + +// ============================================================================= +// MASKING +// ============================================================================= + +.card.masked { + .card__shine, + .card__shine::before, + .card__shine::after { @extend %masked-shine; } +} + +// Rare holo glare clips — applied to holo, cosmos, and reverse holo +.card[data-rarity='rare holo'], +.card[data-rarity='rare holo cosmos'], +.card[data-rarity$='reverse holo'] { + .card__glare::after { clip-path: var(--clip); } + &[data-subtypes^='stage'] .card__glare::after { clip-path: var(--clip-stage); } + &[data-supertype='trainer'] .card__glare::after { clip-path: var(--clip-trainer); } +} + + +// ============================================================================= +// AMAZING RARE +// ============================================================================= + +.card[data-rarity='amazing rare'] { + .card__shine { + background-image: + var(--glitter), + var(--glitter), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(150, 20%, 10%, 1) 10%, + hsla(177, 22%, 80%, 0.1) 50%, + hsla(0, 0%, 95%, .98) 90% + ); + background-size: + var(--glittersize) var(--glittersize), + var(--glittersize) var(--glittersize), + cover; + background-position: 40% 45%, 55% 55%, center center; + background-blend-mode: soft-light, color-burn; + filter: brightness(1) contrast(1) saturate(.9); + + &::before { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--foil), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(50, 20%, 90%, 0.95) 10%, + rgba(181, 139, 164, 0.5) 50%, + hsl(0, 0%, 0%) 60% + ); + background-size: cover, cover; + background-position: center center, center center; + background-blend-mode: color-burn; + mix-blend-mode: lighten; + filter: brightness(1) contrast(1) saturate(1); + opacity: 0.5; + } + + &::after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: repeating-linear-gradient( + calc(var(--angle)), + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ); + background-size: 400% 800%; + background-position: + calc(50% + (50% - var(--background-x)) * 3) + calc(50% + (50% - var(--background-y)) * 3); + filter: brightness(calc(0.75 - (var(--pointer-from-center) * 0.5))) contrast(1) saturate(1); + mix-blend-mode: saturation; + } + } + + // ── Masked glare ───────────────────────────────────────────────────────── + &.masked .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(50, 20%, 90%, 0.45) 0%, + hsla(150, 20%, 30%, 0.45) 45%, + hsla(0, 0%, 0%, .9) 120% + ); + filter: brightness(.9) contrast(2); + + &::after { + content: ''; + -webkit-mask-image: var(--mask); + mask-image: var(--mask); + -webkit-mask-size: cover; + mask-size: cover; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(50, 20%, 90%, 0.75) 0%, + hsla(150, 20%, 30%, 0.65) 45%, + hsla(0, 0%, 0%, 1) 90% + ); + filter: brightness(1) contrast(1.5); + mix-blend-mode: overlay; + opacity: 1; + } + } + + // ── No-mask fallback ───────────────────────────────────────────────────── + &:not(.masked) { + .card__shine { clip-path: var(--clip); } + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 1) 10%, + hsla(0, 0%, 100%, 0.85) 20%, + hsla(0, 0%, 0%, 0.35) 90% + ); + mix-blend-mode: multiply; + } + } +} + + +// ============================================================================= +// COSMOS HOLO (rare holo cosmos) +// ============================================================================= + +.card[data-rarity='rare holo cosmos'] { + &[data-subtypes^='stage'] { + .card__shine, + .card__glare::after { clip-path: var(--clip-stage); } + } + &[data-subtypes^='supporter'] { + .card__shine, + .card__glare::after { clip-path: var(--clip-trainer); } + } + + // This stripe is identical across all three shine layers; + // only blend mode + position differ per layer, so we declare it once as a variable. + $cosmos-stripe: repeating-linear-gradient(82deg, + hsl(53, 65%, 60%) calc(var(--space) * 1), + hsl(93, 56%, 50%) calc(var(--space) * 2), + hsl(176, 54%, 49%) calc(var(--space) * 3), + hsl(228, 59%, 55%) calc(var(--space) * 4), + hsl(283, 60%, 55%) calc(var(--space) * 5), + hsl(326, 59%, 51%) calc(var(--space) * 6), + hsl(326, 59%, 51%) calc(var(--space) * 7), + hsl(283, 60%, 55%) calc(var(--space) * 8), + hsl(228, 59%, 55%) calc(var(--space) * 9), + hsl(176, 54%, 49%) calc(var(--space) * 10), + hsl(93, 56%, 50%) calc(var(--space) * 11), + hsl(53, 65%, 60%) calc(var(--space) * 12) + ); + + .card__shine { + --space: 4%; + clip-path: var(--clip); + background-image: + url('/public/holofoils/cosmos-bottom.png'), + $cosmos-stripe, + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(180, 100%, 89%, 0.5) 5%, + hsla(180, 14%, 57%, 0.3) 40%, + hsl(0, 0%, 0%) 130% + ); + background-blend-mode: color-burn, multiply; + background-position: + var(--cosmosbg, center center), + calc(10% + (var(--pointer-from-left) * 80%)) calc(10% + (var(--pointer-from-top) * 80%)), + center center; + background-size: cover, 400% 900%, cover; + filter: brightness(1) contrast(1) saturate(.8); + mix-blend-mode: color-dodge; + + &::before { + content: ''; + z-index: 2; + background-image: url('/public/holofoils/cosmos-middle-trans.png'), $cosmos-stripe; + background-blend-mode: lighten, multiply; + background-position: + var(--cosmosbg, center center), + calc(15% + (var(--pointer-from-left) * 70%)) calc(15% + (var(--pointer-from-top) * 70%)), + center center; + background-size: cover, 400% 900%, cover; + filter: brightness(1.25) contrast(1.75) saturate(.8); + mix-blend-mode: overlay; + } + + &::after { + content: ''; + z-index: 3; + background-image: url('/public/holofoils/cosmos-top-trans.png'), $cosmos-stripe; + background-blend-mode: multiply, multiply; + background-position: + var(--cosmosbg, center center), + calc(20% + (var(--pointer-from-left) * 60%)) calc(20% + (var(--pointer-from-top) * 60%)), + center center; + background-size: cover, 400% 900%, cover; + filter: brightness(1.25) contrast(1.75) saturate(.8); + mix-blend-mode: multiply; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(204, 100%, 95%, 0.8) 5%, + hsla(250, 15%, 20%, 1) 150% + ); + filter: brightness(.75) contrast(2) saturate(2); + mix-blend-mode: overlay; + opacity: calc(var(--card-opacity) * (0.25 + var(--pointer-from-center))); + + &::after { + content: ''; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(280, 100%, 96%) 5%, + hsl(0, 0%, 10%) 60% + ); + filter: brightness(.75) contrast(2.5) saturate(2); + mix-blend-mode: soft-light; + opacity: calc(1 - var(--pointer-from-top) * .75); + } + } +} + + +// ============================================================================= +// RADIANT RARE +// ============================================================================= + +.card[data-rarity='radiant rare'] { + .card__shine { + clip-path: var(--clip-borders); + &::after { clip-path: var(--clip); } + } + + .card__shine { + --barwidth: 1.2%; + --space: 200px; + --imgsize: cover; + + background-image: + radial-gradient( + farthest-corner ellipse + at calc((var(--pointer-x) * 0.5) + 25%) calc((var(--pointer-y) * 0.5) + 25%), + hsl(0, 0%, 95%) 20%, + var(--card-glow) 130% + ), + radiant-hatch(45deg), + radiant-hatch(-45deg); + + background-size: cover, 210% 210%, 210% 210%; + background-position: + center, + calc(((var(--background-x) - 50%) * 1.5) + 50%) calc(((var(--background-y) - 50%) * 1.5) + 50%), + calc(((var(--background-x) - 50%) * 1.5) + 50%) calc(((var(--background-y) - 50%) * 1.5) + 50%); + background-blend-mode: exclusion, darken, color-dodge; + filter: brightness(.5) contrast(2) saturate(1.75); + mix-blend-mode: color-dodge; + + &::after { + content: ''; + background-image: + var(--foil), + repeating-linear-gradient(55deg, + hsl(3, 95%, 85%) calc(var(--space) * 1), + hsl(207, 100%, 84%) calc(var(--space) * 2), + hsl(29, 100%, 85%) calc(var(--space) * 3), + hsl(160, 100%, 86%) calc(var(--space) * 4), + hsl(309, 94%, 87%) calc(var(--space) * 5), + hsl(188, 95%, 85%) calc(var(--space) * 6), + hsl(3, 95%, 85%) calc(var(--space) * 7) + ); + background-size: var(--imgsize), 400% 100%; + background-position: + center, + calc(((var(--background-x) - 50%) * -2.5) + 50%) calc(((var(--background-y) - 50%) * -2.5) + 50%); + filter: brightness(.6) contrast(3) saturate(2); + mix-blend-mode: color-dodge; + background-blend-mode: hard-light; + } + + &::before { + content: ''; + z-index: 2; + grid-area: 1/1; + background-image: + var(--glitter), + radial-gradient( + farthest-corner ellipse + at calc((var(--pointer-x) * 0.5) + 25%) calc((var(--pointer-y) * 0.5) + 25%), + hsla(0, 0%, 58%, 0.8) 10%, + hsla(0, 0%, 20%, 0.9) 20%, + hsla(0, 0%, 20%, 0.5) 50% + ); + background-position: center; + background-size: 15% 15%, 350% 350%; + background-blend-mode: color-dodge; + mix-blend-mode: overlay; + filter: brightness(.66) contrast(2) saturate(.5); + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.33) 0%, + hsl(0, 0%, 25%) 110% + ); + filter: brightness(1) contrast(1.5); + mix-blend-mode: hard-light; + } + + &:not(.masked) { + .card__shine, + .card__shine::after { + --mask: none; + --foil: url('/public/holofoils/trainerbg.png'); + --imgsize: 25% auto; + } + .card__shine::after { background-blend-mode: difference; } + } +} + + +// ============================================================================= +// RAINBOW ALT & TRAINER GALLERY VMAX (shared shine) +// ============================================================================= + +.card[data-rarity='rare rainbow alt'], +.card[data-rarity='rare holo vmax'][data-trainer-gallery='true'] { + .card__shine { @extend %rainbow-alt-shine; } + .card__shine::after { @extend %rainbow-alt-shine-after; } + .card__shine::before { @extend %rainbow-alt-shine-before; } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(50, 20%, 90%, 0.75) 0%, + hsla(150, 20%, 30%, 0.65) 45%, + hsla(0, 0%, 0%, 1) 100% + ); + filter: brightness(.9) contrast(2); + opacity: calc(var(--card-opacity) * 0.75); + } + + &:not(.masked) .card__shine { + --mask: none; + --foil: none; + --imgsize: 25% auto; + } +} + +// Vmax-only glare override +.card[data-rarity='rare holo vmax'][data-trainer-gallery='true'] { + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(50, 30%, 90%) 0%, + hsl(162, 5%, 40%), + hsl(0, 0%, 0%) 120% + ); + filter: brightness(1) contrast(1); + opacity: calc(var(--card-opacity) * var(--pointer-from-center) * 0.85); + } +} + + +// ============================================================================= +// RAINBOW SECRET HOLO (rare rainbow) +// ============================================================================= + +.card[data-rarity='rare rainbow'] { + .card__shine { + @include rainbow-colors; + + background-image: + linear-gradient(-45deg, var(--r-clr-1), var(--r-clr-5)), + var(--glitter), + linear-gradient(-30deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-blend-mode: luminosity, soft-light; + background-size: 200% 200%, var(--glittersize) var(--glittersize), 400% 400%; + background-position: + calc(25% + (50% * var(--pointer-from-left))) calc(25% + (50% * var(--pointer-from-top))), + center center, + calc(25% + (var(--pointer-x) / 2)) calc(25% + (var(--pointer-y) / 2)); + filter: brightness(calc((var(--pointer-from-center) * 0.25) + 0.6)) contrast(2.2) saturate(0.75); + + &::after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--glitter), + linear-gradient(-60deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-blend-mode: soft-light; + background-size: var(--glittersize) var(--glittersize), 400% 400%; + background-position: center center, var(--pointer-x) var(--pointer-y); + filter: brightness(calc((var(--pointer-from-center) * 0.3) + 0.55)) contrast(2) saturate(1); + mix-blend-mode: color-dodge; + } + + &::before { + content: ''; + background-image: var(--foil); + background-size: var(--imgsize); + background-position: center center; + filter: brightness(2.5) contrast(1); + opacity: calc((var(--pointer-from-center) + 0.4) * 0.6); + background-blend-mode: difference; + mix-blend-mode: darken; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 80%), + hsla(187, 10%, 85%, 0.25) 30%, + hsl(197, 6%, 25%) 120% + ); + filter: brightness(.9) contrast(1.75); + opacity: calc(var(--pointer-from-center) * 0.9); + mix-blend-mode: hard-light; + } + + &:not(.masked) .card__shine { + --foil: url('/public/holofoils/illusion-mask.png'); + --imgsize: 33%; + } +} + + +// ============================================================================= +// REGULAR HOLO (rare holo) +// ============================================================================= + +.card[data-rarity='rare holo'] { + &[data-subtypes^='stage'] { + .card__shine, + .card__glare::after { clip-path: var(--clip-stage); } + } + &[data-subtypes^='supporter'], + &[data-subtypes^='item'] { + .card__shine, + .card__glare::after { clip-path: var(--clip-trainer); } + } + + .card__shine { + clip-path: var(--clip); + + // Prismatic diagonal bands — no scanlines + background-image: + repeating-linear-gradient(110deg, + var(--violet), var(--blue), var(--green), var(--yellow), var(--red), + var(--violet), var(--blue), var(--green), var(--yellow), var(--red), + var(--violet), var(--blue), var(--green), var(--yellow), var(--red) + ); + background-position: + calc(((50% - var(--background-x)) * 2.6) + 50%) + calc(((50% - var(--background-y)) * 3.5) + 50%); + background-size: 400% 400%; + filter: brightness(1.1) contrast(1.1) saturate(1.2); + mix-blend-mode: color-dodge; + opacity: var(--card-opacity); + + // Second diagonal pass at a different angle adds depth without grids + &::before { + content: ''; + background-image: + repeating-linear-gradient(65deg, + var(--violet), var(--blue), var(--green), var(--yellow), var(--red), + var(--violet), var(--blue), var(--green), var(--yellow), var(--red) + ); + background-position: + calc(((50% - var(--background-x)) * -1.8) + 50%) + calc(((50% - var(--background-y)) * -2.4) + 50%); + background-size: 400% 400%; + mix-blend-mode: screen; + filter: brightness(0.9) contrast(1.2) saturate(1.4); + opacity: 0.5; + } + + // Specular hot-spot + &::after { + content: ''; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 90%, 0.8) 0%, + hsla(0, 0%, 78%, 0.1) 25%, + hsl(0, 0%, 0%) 90% + ); + background-position: center center; + background-size: cover; + mix-blend-mode: luminosity; + filter: brightness(0.6) contrast(4); + } + } + + .card__glare { + opacity: calc(var(--card-opacity) * .8); + filter: brightness(0.8) contrast(1.5); + mix-blend-mode: overlay; + + &::after { + content: ''; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(180, 100%, 95%) 5%, + hsla(0, 0%, 39%, 0.25) 55%, + hsla(0, 0%, 0%, 0.36) 110% + ); + mix-blend-mode: overlay; + filter: brightness(.6) contrast(3); + } + } +} + + +// ============================================================================= +// REVERSE HOLO + HOLOFOIL VARIANTS +// Targets both data-rarity (TCG sets) and data-variant (TCGPlayer variants) +// ============================================================================= + +.card[data-rarity$='reverse holo'], +.card[data-variant='Reverse Holofoil'], +.card[data-variant='Holofoil'], +.card[data-variant='1st Edition Holofoil'], +.card[data-variant='Unlimited Holofoil'] { + .card__shine { + background-image: + radial-gradient(circle at var(--pointer-x) var(--pointer-y), #fff 5%, #000 50%, #fff 80%), + linear-gradient(-45deg, #000 15%, #fff, #000 85%), + var(--foil); + background-blend-mode: soft-light, difference; + background-size: 120% 120%, 200% 200%, cover; + background-position: + center center, + calc(100% * var(--pointer-from-left)) calc(100% * var(--pointer-from-top)), + center center; + filter: brightness(var(--foil-brightness)) contrast(1.5) saturate(1); + mix-blend-mode: color-dodge; + opacity: calc((1.5 * var(--card-opacity)) - var(--pointer-from-center)); + } + + .card__glare { + opacity: var(--card-opacity); + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.8) 10%, + hsla(0, 0%, 100%, 0.5) 20%, + hsla(0, 0%, 0%, 0.75) 90% + ); + filter: brightness(.7) contrast(1.5); + + &::after { + content: ''; + opacity: var(--card-opacity); + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 10%, + hsla(0, 0%, 100%, 0.5) 20%, + hsla(0, 0%, 0%, 0.5) 120% + ); + filter: brightness(1) contrast(1.5); + } + } + + &:not(.masked) { + // Reverse holo by rarity: effect lives on borders, not artwork + &[data-rarity$='reverse holo'] { + .card__shine { --foil: none; clip-path: var(--clip-invert); } + &[data-subtypes^='stage'] .card__glare::after { clip-path: var(--clip-stage-invert); } + &[data-supertype='trainer'] .card__glare::after { clip-path: var(--clip-trainer-invert); } + } + + // Holofoil variants by variant field: full-bleed, no clip restriction + &[data-variant='Holofoil'], + &[data-variant='Reverse Holofoil'], + &[data-variant='1st Edition Holofoil'], + &[data-variant='Unlimited Holofoil'] { + .card__shine { --foil: none; clip-path: none; } + } + } +} + + +// ============================================================================= +// SECRET RARE (GOLD) — generic + specific promo (swsh145) +// ============================================================================= + +.card[data-rarity='rare secret'], +.card[data-set='swshp'][data-number='swsh145'] { + .card__shine { @extend %secret-rare-shine; } + .card__shine::before { @extend %secret-rare-shine-before; } + .card__shine::after { @extend %secret-rare-shine-after; } + .card__glare { @extend %secret-rare-glare; } + + &:not(.masked) .card__shine { + --foil: url('/public/holofoils/geometric.png'); + --imgsize: 33%; + filter: brightness(calc((var(--pointer-from-center) * 0.3) + 0.2)) contrast(2) saturate(0.75); + } +} + + +// ============================================================================= +// SHINY RARE (HOLO) +// ============================================================================= + +.card[data-rarity='rare shiny'] { + .card__shine { + @extend %shiny-shine; + clip-path: var(--clip); + &[data-subtypes^='stage'] { clip-path: var(--clip-stage); } + &::after { @extend %shiny-shine-after; } + &::before { @extend %shiny-shine-before; } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 0%, + hsl(320, 5%, 15%) 150% + ); + background-size: cover; + background-position: center center; + mix-blend-mode: multiply; + filter: brightness(1.2) contrast(1) saturate(.7); + opacity: calc(var(--card-opacity) * var(--pointer-from-center)); + } + + &:not(.masked) { + .card__shine, + .card__shine::after { + @include no-mask-illusion; + background-blend-mode: exclusion, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * .3) + .35)) contrast(2) saturate(1.5); + } + .card__shine::after { + filter: brightness(calc((var(--pointer-from-center) * .4) + .5)) contrast(1.4) saturate(1.2); + mix-blend-mode: difference; + } + } +} + + +// ============================================================================= +// SHINY V (ULTRA RARE) +// ============================================================================= + +.card[data-rarity='rare shiny v'] { + .card__shine { + @extend %shiny-shine; + &::after { @extend %shiny-shine-after; } + &::before { @extend %shiny-shine-before; } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 90%) 5%, + hsl(200, 5%, 45%) 80%, + hsl(320, 40%, 10%) 150% + ); + background-size: 120% 140%; + background-position: center center; + mix-blend-mode: darken; + filter: brightness(.88) contrast(2.25) saturate(.7); + opacity: calc(var(--card-opacity) * var(--pointer-from-center) * 0.75); + } + + &:not(.masked) { + .card__shine, + .card__shine::after { + @include no-mask-illusion; + background-blend-mode: exclusion, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * .3) + .35)) contrast(2) saturate(1.5); + } + .card__shine::before { content: none; display: none; } + .card__shine::after { filter: brightness(calc((var(--pointer-from-center) * .5) + .8)) contrast(1.6) saturate(1.4); } + } +} + + +// ============================================================================= +// SHINY VMAX +// ============================================================================= + +.card[data-rarity='rare shiny vmax'] { + .card__shine { + --imgsize: cover; + --angle: -30deg; + @include rainbow-colors; + + background-image: + var(--glitter), + var(--glitter), + linear-gradient(-30deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(248, 5%, 10%, 1) 10%, + hsla(206, 5%, 80%, 0.1) 50%, + hsla(0, 0%, 95%, .98) 90% + ); + background-size: + var(--glittersize) var(--glittersize), + var(--glittersize) var(--glittersize), + 400% 400%, cover; + background-position: + 40% 45%, 55% 55%, + calc(var(--background-x) * 1.5) calc(var(--background-y) * 1.5), + center center; + background-blend-mode: soft-light, overlay, color-burn; + filter: brightness(1) contrast(1) saturate(.85); + + &::before { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--foil), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(248, 5%, 91%, 0.95) 10%, + hsla(206, 5%, 68%, 0.5) 50%, + hsl(0, 0%, 0%) 120% + ); + background-size: var(--imgsize), cover; + background-position: center center, center center; + background-blend-mode: color-burn; + mix-blend-mode: lighten; + filter: brightness(1) contrast(1) saturate(.4); + opacity: 0.35; + } + + &::after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: repeating-linear-gradient( + calc(var(--angle)), + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ); + background-size: 400% 800%; + background-position: + calc(50% + (50% - var(--background-x)) * 3) + calc(50% + (50% - var(--background-y)) * 3); + filter: brightness(calc(0.75 - (var(--pointer-from-center) * 0.5))) contrast(1) saturate(1); + mix-blend-mode: hue; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(248, 5%, 90%, 0.45) 0%, + hsla(206, 5%, 30%, 0.45) 45%, + hsla(0, 0%, 0%, .33) 120% + ); + filter: brightness(1) contrast(1.25); + + &::after { + content: ''; + -webkit-mask-image: var(--mask); + mask-image: var(--mask); + -webkit-mask-size: cover; + mask-size: cover; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(248, 5%, 90%, 0.75) 0%, + hsla(206, 5%, 30%, 0.65) 45%, + hsla(0, 0%, 0%, .75) 100% + ); + filter: brightness(1) contrast(1.25); + mix-blend-mode: overlay; + opacity: 1; + } + } +} + + +// ============================================================================= +// SPECIAL: swsh12pt5 #160 (Rainbow Secret variant) +// ============================================================================= + +.card[data-rarity='rare secret'][data-set='swsh12pt5'][data-number='160'] { + --shift: 1px; + @include rainbow-colors; + + .card__shine { + background-image: + linear-gradient(-45deg, var(--r-clr-1), var(--r-clr-5)), + var(--glitter), + linear-gradient(-30deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-blend-mode: luminosity, soft-light; + background-size: 200% 200%, var(--glittersize) var(--glittersize), 400% 400%; + background-position: + calc(25% + (50% * var(--pointer-from-left))) calc(25% + (50% * var(--pointer-from-top))), + calc(50% - ((var(--shift) * 2) * var(--pointer-from-left)) + var(--shift)) calc(50% - ((var(--shift) * 2) * var(--pointer-from-top)) + var(--shift)), + calc(25% + (var(--pointer-x) / 2)) calc(25% + (var(--pointer-y) / 2)); + filter: brightness(calc((var(--pointer-from-center) * 0.5) + .75)) contrast(2) saturate(1); + + &::after { + content: ''; + -webkit-mask-image: none !important; + mask-image: none !important; + background-image: + var(--glitter), + linear-gradient(-60deg, + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), + var(--r-clr-1), var(--r-clr-2), var(--r-clr-3), var(--r-clr-4), + var(--r-clr-5), var(--r-clr-6), var(--r-clr-7), var(--r-clr-1) + ); + background-blend-mode: soft-light; + background-size: var(--glittersize) var(--glittersize), 400% 400%; + background-position: + calc(50% - ((var(--shift) * 2) * var(--pointer-from-left)) - var(--shift)) calc(50% - ((var(--shift) * 2) * var(--pointer-from-top)) - var(--shift)), + var(--pointer-x) var(--pointer-y); + filter: brightness(calc((var(--pointer-from-center) * 0.35) + 0.35)) contrast(2) saturate(1); + mix-blend-mode: exclusion; + } + + &::before { + content: ''; + background-image: var(--foil); + background-size: var(--imgsize); + background-position: center center; + filter: brightness(2.5) contrast(1); + opacity: calc((var(--pointer-from-center) + 0.4) * 0.6); + mix-blend-mode: multiply; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 80%), + hsla(0, 0%, 74.9%, 0.25) 30%, + hsl(0, 0%, 21.6%) 130% + ); + filter: brightness(.9) contrast(2); + opacity: calc(var(--pointer-from-center) * .9); + mix-blend-mode: hard-light; + } + + &:not(.masked) .card__shine { + --foil: url('/public/holofoils/illusion-mask.png'); + --imgsize: 33%; + } +} + + +// ============================================================================= +// ULTRA RARE — FULL ART (pokémon + supporter share v-shine) +// ============================================================================= + +.card[data-rarity='rare ultra'][data-supertype='pokémon'], +.card[data-rarity='rare ultra'][data-subtypes*='supporter'] { + .card__shine { + @include v-shine-background; + filter: brightness(calc((var(--pointer-from-center) * 0.4) + .4)) contrast(1.4) saturate(2.25); + &::after { @include v-shine-after; } + &::before { @include spotlight-before; } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 75%) 5%, + hsl(200, 5%, 35%) 60%, + hsl(320, 40%, 10%) 150% + ); + background-size: 120% 150%; + background-position: center center; + mix-blend-mode: hard-light; + filter: brightness(1) contrast(1.2) saturate(1); + opacity: calc(var(--card-opacity) * .75); + } + + &:not(.masked) { + .card__shine, + .card__shine::after { + @include no-mask-illusion; + background-blend-mode: exclusion, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * .3) + .35)) contrast(2) saturate(1.5); + } + .card__shine::before { content: none; display: none; } + .card__shine::after { filter: brightness(calc((var(--pointer-from-center) * .5) + .8)) contrast(1.6) saturate(1.4); } + } +} + +// Supporter-specific glare tweak +.card[data-rarity='rare ultra'][data-subtypes*='supporter'] { + .card__shine { + filter: brightness(calc((var(--pointer-from-center) * 0.05) + .8)) contrast(1.75) saturate(1.2); + &::after { + filter: brightness(calc((var(--pointer-from-center) * .4) + .85)) contrast(2) saturate(.5); + } + &::before { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 0%, + hsla(0, 0%, 0%, 0) 80% + ); + mix-blend-mode: screen; + opacity: 0.5; + } + } + .card__glare { + opacity: calc(var(--card-opacity) * .75); + mix-blend-mode: multiply; + filter: brightness(1.5) contrast(1.4) saturate(1); + background-size: 170% 170%; + } + &:not(.masked) { + .card__shine, + .card__shine::after { + --foil: url('/public/holofoils/trainerbg.png'); + --imgsize: 20%; + background-blend-mode: color-burn, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * 0.05) + .6)) contrast(1.5) saturate(1.2); + } + } +} + + +// ============================================================================= +// TRAINER GALLERY RARE HOLO +// ============================================================================= + +.card[data-rarity='trainer gallery rare holo'], +.card[data-rarity='rare holo'][data-trainer-gallery='true'], +.card[data-set='swshp'][data-number='swsh020'] { + .card__shine { @extend %tg-holo-shine; } + .card__shine::after { @extend %tg-holo-shine-after; } + .card__shine::before { content: none; display: none; } + + .card__glare { @extend %tg-holo-glare; } + .card__glare::before, + .card__glare::after { content: none; display: none; } +} + + +// ============================================================================= +// TRAINER GALLERY SECRET RARE (GOLD & BLACK) +// ============================================================================= + +.card[data-rarity='rare secret'][data-trainer-gallery='true'] { + // Unmask all shine layers + .card__shine, + .card__shine::before, + .card__shine::after { + -webkit-mask-image: none !important; + mask-image: none !important; + } + + .card__shine { + background-image: + var(--glitter), + var(--glitter), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(152.7, 21.6%, 10%) 10%, + hsla(177, 22%, 80%, 0.1) 50%, + hsla(0, 0%, 95%, .98) 90% + ), + linear-gradient(45deg, hsl(46, 95%, 50%), hsl(52, 100%, 69%)); + background-size: + var(--glittersize) var(--glittersize), + var(--glittersize) var(--glittersize), + cover, cover; + background-position: 40% 45%, 55% 55%, center center, center center; + background-blend-mode: soft-light, darken, color; + filter: brightness(1) contrast(1) saturate(1); + mix-blend-mode: color-dodge; + + &::before { + content: ''; + background-image: + var(--foil), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(50, 20%, 90%, 0.95) 10%, + hsla(324, 22%, 63%, 0.5) 50%, + hsl(0, 0%, 0%) 90% + ); + background-size: var(--imgsize), cover; + background-position: center center, center center; + background-blend-mode: color-burn; + mix-blend-mode: exclusion; + filter: brightness(1) contrast(1) saturate(1); + opacity: 1; + } + + &::after { + content: ''; + background-image: + var(--glitter), + conic-gradient( + var(--sunpillar-clr-4), + var(--sunpillar-clr-5), + var(--sunpillar-clr-6), + var(--sunpillar-clr-1), + var(--sunpillar-clr-2), + var(--sunpillar-clr-3), + var(--sunpillar-clr-4) + ); + background-size: var(--glittersize) var(--glittersize), cover; + background-blend-mode: luminosity; + filter: brightness(calc((var(--pointer-from-center) * 0.5) + 0.6)) contrast(2) saturate(3); + mix-blend-mode: soft-light; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(40, 100%, 95%, 0.2) 10%, + hsla(40, 20%, 5%, 1) 180% + ); + filter: brightness(1) contrast(1); + mix-blend-mode: hard-light; + } + + &:not(.masked) { + .card__shine { + --foil: url('/public/holofoils/geometric.png'); + --imgsize: 33%; + filter: brightness(calc((var(--pointer-from-center) * 0.3) + 0.2)) contrast(2) saturate(0.75); + } + .card__glare { filter: brightness(.5) contrast(1); } + } +} + + +// ============================================================================= +// TRAINER GALLERY V (rare holo v + trainer-gallery) +// ============================================================================= + +.card[data-rarity='rare holo v'][data-trainer-gallery='true'] { + .card__shine { + @include v-shine-background; + filter: brightness(calc((var(--pointer-from-center) * 0.4) + .4)) contrast(1.4) saturate(2.25); + &::after { @include v-shine-after; } + } + + .card__glare { opacity: calc(var(--card-opacity) * .4); } + + &:not(.masked) { + .card__shine, + .card__shine::after { + @include no-mask-illusion; + background-blend-mode: exclusion, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * .3) + .35)) contrast(2) saturate(1.5); + } + .card__shine::after { filter: brightness(calc((var(--pointer-from-center) * .5) + .8)) contrast(1.6) saturate(1.4); } + } +} + + +// ============================================================================= +// V REGULAR (rare holo v) & V-UNION +// ============================================================================= + +.card[data-rarity='rare holo v'], +.card[data-subtypes='v-union'] { + .card__shine { + --space: 5%; + --angle: 133deg; + --imgsize: 500px; + + background-image: + var(--grain), + repeating-linear-gradient(0deg, + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ), + repeating-linear-gradient(var(--angle), + #0e152e 0%, + hsl(180, 10%, 60%) 3.8%, + hsl(180, 29%, 66%) 4.5%, + hsl(180, 10%, 60%) 5.2%, + #0e152e 10%, + #0e152e 12% + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 0%, 0.10) 12%, + hsla(0, 0%, 0%, 0.15) 20%, + hsla(0, 0%, 0%, 0.25) 120% + ); + background-blend-mode: screen, hue, hard-light; + background-size: var(--imgsize) 100%, 200% 700%, 300% 100%, 200% 100%; + background-position: + center, + 0% var(--background-y), + var(--background-x) var(--background-y), + var(--background-x) var(--background-y); + filter: brightness(.8) contrast(2.95) saturate(.65); + + &::after { + content: ''; + background-position: + center, + 0% var(--background-y), + calc(var(--background-x) * -1) calc(var(--background-y) * -1), + var(--background-x) var(--background-y); + background-size: var(--imgsize) 100%, 200% 400%, 195% 100%, 200% 100%; + filter: brightness(1) contrast(2.5) saturate(1.75); + mix-blend-mode: soft-light; + } + } + + .card__glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(0, 0%, 100%) 0%, + hsla(210, 3%, 54%, 0.33) 45%, + hsla(0, 0%, 20%, 0.9) 130% + ); + opacity: calc(var(--card-opacity) * .5); + mix-blend-mode: hard-light; + filter: brightness(.9) contrast(1.75); + } + + &:not(.masked) .card__shine { filter: brightness(.7) contrast(2) saturate(.5); } +} + + +// ============================================================================= +// VMAX (rare holo vmax) +// ============================================================================= + +.card[data-rarity='rare holo vmax'] { + .card__shine { + --space: 6%; + --angle: 133deg; + --imgsize: cover; + + background-image: + var(--foil), + repeating-linear-gradient(-33deg, + hsl(2, 70%, 47%) calc(var(--space) * 1), + hsl(228, 60%, 64%) calc(var(--space) * 2), + hsl(176, 55%, 39%) calc(var(--space) * 3), + hsl(123, 68%, 35%) calc(var(--space) * 4), + hsl(283, 75%, 57%) calc(var(--space) * 5), + hsl(2, 70%, 47%) calc(var(--space) * 6) + ), + repeating-linear-gradient(var(--angle), + hsla(227, 53%, 12%, 0.5) 0%, + hsl(180, 10%, 50%) 2.5%, + hsl(83, 50%, 35%) 5%, + hsl(180, 10%, 50%) 7.5%, + hsla(227, 53%, 12%, 0.5) 10%, + hsla(227, 53%, 12%, 0.5) 15% + ), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(189, 76%, 77%, 0.6) 0%, + hsla(147, 59%, 77%, 0.6) 25%, + hsla(271, 55%, 69%, 0.6) 50%, + hsla(355, 56%, 72%, 0.6) 75% + ); + background-blend-mode: difference, luminosity, soft-light; + background-size: var(--imgsize), 1100% 1100%, 600% 600%, 200% 200%; + background-position: + center, + var(--background-x) var(--background-y), + var(--background-x) var(--background-y), + var(--background-x) var(--background-y); + filter: brightness(calc((var(--pointer-from-center) * .4) + .4)) contrast(2) saturate(1); + + &::after { + content: ''; + background-image: + repeating-linear-gradient(0deg, + var(--sunpillar-clr-1) calc(var(--space) * 1), + var(--sunpillar-clr-2) calc(var(--space) * 2), + var(--sunpillar-clr-3) calc(var(--space) * 3), + var(--sunpillar-clr-4) calc(var(--space) * 4), + var(--sunpillar-clr-5) calc(var(--space) * 5), + var(--sunpillar-clr-6) calc(var(--space) * 6), + var(--sunpillar-clr-1) calc(var(--space) * 7) + ), + repeating-linear-gradient(var(--angle), + #0e152e 0%, + hsl(180, 10%, 60%) 3.8%, + hsl(180, 29%, 66%) 4.5%, + hsl(180, 10%, 60%) 5.2%, + #0e152e 10%, + #0e152e 12% + ); + background-blend-mode: hue, hard-light; + background-size: 200% 700%, 300% 100%, 200% 100%; + background-position: + 0% var(--background-y), + var(--background-x) var(--background-y), + var(--background-x) var(--background-y); + mix-blend-mode: lighten; + opacity: calc((0.3 * var(--card-opacity)) + var(--card-opacity) * var(--pointer-from-center) * 0.5); + filter: saturate(1.5); + } + } + + .card__glare { + mix-blend-mode: hard-light; + filter: brightness(1) contrast(1); + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.75) 0%, + hsl(0, 0%, 0%) 120% + ); + opacity: calc((0.2 * var(--card-opacity)) + var(--card-opacity) * var(--pointer-from-center) * 0.8); + } + + &[data-supertype='pokémon']:not(.masked) { + .card__shine, + .card__shine::after { + --mask: none; + --foil: url('/public/holofoils/vmaxbg.jpg'); + --imgsize: 60% 30%; + } + } +} + + +// ============================================================================= +// V STAR (rare holo vstar) +// ============================================================================= + +.card.masked[data-rarity='rare holo vstar'] { + $vstar-mask: + var(--mask), + radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0) 0%, + hsla(0, 0%, 100%, .5) 120% + ); + + .card__shine, + .card__shine::before, + .card__shine::after { + -webkit-mask-image: $vstar-mask; + mask-image: $vstar-mask; + } +} + +.card[data-rarity='rare holo vstar'] { + .card__shine { + @include v-shine-background; + filter: brightness(calc((var(--pointer-from-center) * .75) + .25)) contrast(2) saturate(1.25); + + &::after { + content: ''; + background-size: var(--imgsize), 200% 400%, 195% 100%, 200% 100%; + background-position: + center center, + 0% var(--background-y), + calc(var(--background-x) * -1) calc(var(--background-y) * -1), + var(--background-x) var(--background-y); + filter: brightness(calc((var(--pointer-from-center) * .75) + .5)) contrast(1.5) saturate(1.5); + mix-blend-mode: exclusion; + } + + &::before { + content: ''; + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(190, 7%, 80%, 0.75) 0%, + hsla(260, 7%, 50%, 0.25) 45%, + hsl(310, 7%, 50%) 120% + ); + mix-blend-mode: hard-light; + z-index: 2; + opacity: 0.8; + } + } + + .card__glare { + filter: brightness(.7) contrast(2); + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsl(195, 90%, 90%) 5%, + hsl(300, 3%, 60%) 60%, + hsl(350, 0%, 15%) 150% + ); + mix-blend-mode: hard-light; + opacity: calc(var(--card-opacity) * (var(--pointer-from-center) * .75)); + } + + &:not(.masked) { + .card__shine, + .card__shine::after { + --mask: none; + --foil: url('/public/holofoils/ancient.png'); + --imgsize: 18% 15%; + background-blend-mode: exclusion, hue, hard-light; + filter: brightness(calc((var(--pointer-from-center) * .25) + .35)) contrast(1.8) saturate(1.75); + } + .card__shine::after { + filter: brightness(calc((var(--pointer-from-center) * .75) + .5)) contrast(1.5) saturate(1.5); + } + .card__glare { filter: brightness(.55) contrast(2); } + } +} \ No newline at end of file diff --git a/src/assets/css/_holofoil-integration.scss b/src/assets/css/_holofoil-integration.scss new file mode 100644 index 0000000..02aa74a --- /dev/null +++ b/src/assets/css/_holofoil-integration.scss @@ -0,0 +1,349 @@ +// ============================================================================= +// HOLOFOIL INTEGRATION +// _holofoil-integration.scss +// ============================================================================= + +@import "card"; + + +// ----------------------------------------------------------------------------- +// 1. WRAPPER NORMALISATION +// ----------------------------------------------------------------------------- + +%holofoil-wrapper-base { + --card-aspect: 0.718; + --card-radius: 4.55% / 3.5%; + + --pointer-x: 50%; + --pointer-y: 50%; + --background-x: 50%; + --background-y: 50%; + --pointer-from-center: 0; + --pointer-from-top: 0.5; + --pointer-from-left: 0.5; + --card-scale: 1; + --card-opacity: 0; + + --grain: url('/public/holofoils/grain.webp'); + --glitter: url('/public/holofoils/glitter.png'); + --glittersize: 25%; + --space: 5%; + --angle: 133deg; + --imgsize: cover; + + --red: #f80e35; + --yellow: #eedf10; + --green: #21e985; + --blue: #0dbde9; + --violet: #c929f1; + + --clip: inset(9.85% 8% 52.85% 8%); + --clip-invert: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 47.15%, 91.5% 47.15%, 91.5% 9.85%, 8% 9.85%, 8% 47.15%, 0 50%); + --clip-stage: polygon(91.5% 9.85%, 57% 9.85%, 54% 12%, 17% 12%, 16% 14%, 12% 16%, 8% 16%, 8% 47.15%, 92% 47.15%); + --clip-stage-invert: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 47.15%, 91.5% 47.15%, 91.5% 9.85%, 57% 9.85%, 54% 12%, 17% 12%, 16% 14%, 12% 16%, 8% 16%, 8% 47.15%, 0 50%); + --clip-trainer: inset(14.5% 8.5% 48.2% 8.5%); + --clip-borders: inset(2.8% 4% round 2.55% / 1.5%); + + --sunpillar-clr-1: var(--sunpillar-1); + --sunpillar-clr-2: var(--sunpillar-2); + --sunpillar-clr-3: var(--sunpillar-3); + --sunpillar-clr-4: var(--sunpillar-4); + --sunpillar-clr-5: var(--sunpillar-5); + --sunpillar-clr-6: var(--sunpillar-6); + + // NOTE: no overflow:hidden here -- that would clip the lift/scale transform + // on .image-grow. Overflow is handled by the child .holo-shine/.holo-glare. + position: relative; + isolation: isolate; + border-radius: var(--card-radius); +} + +%holofoil-energy-glows { + &[data-energy="Water"] { --card-glow: hsl(192, 97%, 60%); } + &[data-energy="Fire"] { --card-glow: hsl(9, 81%, 59%); } + &[data-energy="Grass"] { --card-glow: hsl(96, 81%, 65%); } + &[data-energy="Lightning"] { --card-glow: hsl(54, 87%, 63%); } + &[data-energy="Psychic"] { --card-glow: hsl(281, 62%, 58%); } + &[data-energy="Fighting"] { --card-glow: rgb(145, 90, 39); } + &[data-energy="Darkness"] { --card-glow: hsl(189, 77%, 27%); } + &[data-energy="Metal"] { --card-glow: hsl(184, 20%, 70%); } + &[data-energy="Dragon"] { --card-glow: hsl(51, 60%, 35%); } + &[data-energy="Fairy"] { --card-glow: hsl(323, 100%, 89%); } +} + + +// ----------------------------------------------------------------------------- +// 2. SHINE + GLARE CHILD DIVS +// ----------------------------------------------------------------------------- + +%shine-base { + pointer-events: none; + position: absolute; + inset: 0; + border-radius: var(--card-radius); + overflow: hidden; // clipping lives here, not on the parent + z-index: 3; + will-change: transform, opacity, background-image, background-size, + background-position, background-blend-mode, filter; + + &::before, + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--card-radius); + } +} + +%glare-base { + pointer-events: none; + position: absolute; + inset: 0; + border-radius: var(--card-radius); + z-index: 4; + transform: translateZ(0); + overflow: hidden; + will-change: transform, opacity, background-image, background-size, + background-position, background-blend-mode, filter; +} + + +// ----------------------------------------------------------------------------- +// 3. MODES +// ----------------------------------------------------------------------------- + +// -- 3a. GRID ----------------------------------------------------------------- +// No idle animation. Effect is invisible until hover. + +.image-grow, +.card-image-wrap { + @extend %holofoil-wrapper-base; + @extend %holofoil-energy-glows; + + // No effect if the image fell back to default.jpg + &[data-default="true"] { + .holo-shine, + .holo-glare { display: none !important; } + } + + .holo-shine { @extend %shine-base; } + .holo-glare { @extend %glare-base; } +} + + +// -- 3b. GRID HOVER ----------------------------------------------------------- +// The existing main.scss .image-grow:hover handles lift + scale. +// We layer the holo effect on top without overriding transform or transition. + +.image-grow:hover, +.image-grow[data-holo-active] { + --card-opacity: 0.45; +} + + +// -- 3c. MODAL ---------------------------------------------------------------- +// Sweeps once per minute. Peaks at 0.35. +// Pointer tracking bumps opacity to 0.45 while hovering. + +@keyframes holo-modal-pulse { + 0% { + --card-opacity: 0; + --pointer-x: 50%; --pointer-y: 50%; + --background-x: 50%; --background-y: 50%; + --pointer-from-center: 0; --pointer-from-left: 0.5; --pointer-from-top: 0.5; + } + 4% { --card-opacity: 0; } + 8% { + --card-opacity: 0.35; + --pointer-x: 25%; --pointer-y: 15%; + --background-x: 38%; --background-y: 28%; + --pointer-from-center: 0.85; --pointer-from-left: 0.25; --pointer-from-top: 0.15; + } + 25% { + --pointer-x: 70%; --pointer-y: 30%; + --background-x: 64%; --background-y: 34%; + --pointer-from-center: 0.9; --pointer-from-left: 0.70; --pointer-from-top: 0.30; + } + 45% { + --pointer-x: 80%; --pointer-y: 70%; + --background-x: 74%; --background-y: 68%; + --pointer-from-center: 0.88; --pointer-from-left: 0.80; --pointer-from-top: 0.70; + } + 65% { + --pointer-x: 35%; --pointer-y: 80%; + --background-x: 38%; --background-y: 76%; + --pointer-from-center: 0.8; --pointer-from-left: 0.35; --pointer-from-top: 0.80; + } + 85% { + --card-opacity: 0.35; + --pointer-x: 25%; --pointer-y: 15%; + --background-x: 38%; --background-y: 28%; + --pointer-from-center: 0.85; + } + 90% { --card-opacity: 0; } + 100% { + --card-opacity: 0; + --pointer-x: 50%; --pointer-y: 50%; + --background-x: 50%; --background-y: 50%; + --pointer-from-center: 0; + } +} + +.card-image-wrap.holo-modal-mode { + --card-opacity: 0; + + .holo-shine, + .holo-glare { + animation: holo-modal-pulse 60s ease-in-out infinite; + animation-delay: var(--shimmer-delay, -2s); + } + + &[data-holo-active] { + --card-opacity: 0.45; + .holo-shine, + .holo-glare { animation-play-state: paused; } + } +} + + +// ----------------------------------------------------------------------------- +// 4. RARITY -> CLIP-PATH BRIDGE +// ----------------------------------------------------------------------------- + +.image-grow, +.card-image-wrap { + + // No effect on common/uncommon or unrecognised wrapper + &[data-rarity="common"], + &[data-rarity="uncommon"], + &:not([data-rarity]) { + .holo-shine, + .holo-glare { display: none; } + } + + // Standard holo — artwork area only + &[data-rarity="rare holo"] { + .holo-shine { clip-path: var(--clip); } + &[data-subtypes^="stage"] .holo-shine { clip-path: var(--clip-stage); } + &[data-subtypes^="supporter"] .holo-shine, + &[data-subtypes^="item"] .holo-shine { clip-path: var(--clip-trainer); } + } + + // Cosmos holo + &[data-rarity="rare holo cosmos"] { + .holo-shine { clip-path: var(--clip); } + &[data-subtypes^="stage"] .holo-shine { clip-path: var(--clip-stage); } + &[data-subtypes^="supporter"] .holo-shine { clip-path: var(--clip-trainer); } + } + + &[data-rarity="radiant rare"] { .holo-shine { clip-path: var(--clip-borders); } } + &[data-rarity="amazing rare"] { .holo-shine { clip-path: var(--clip); } } + + &[data-rarity="trainer gallery rare holo"], + &[data-rarity="rare holo"][data-trainer-gallery="true"] { + .holo-shine { clip-path: var(--clip-borders); } + } + + &[data-rarity="rare shiny"] { + .holo-shine { clip-path: var(--clip); } + &[data-subtypes^="stage"] .holo-shine { clip-path: var(--clip-stage); } + } + + // Reverse holo by rarity — borders only + &[data-rarity$="reverse holo"] { .holo-shine { clip-path: var(--clip-invert); } } + // Reverse Holofoil variant — borders only + &[data-variant="Reverse Holofoil"] { .holo-shine { clip-path: var(--clip-invert); } } + + // True holofoil variants + full-bleed rarities — no clip + &[data-variant="Holofoil"], + &[data-variant="1st Edition Holofoil"], + &[data-variant="Unlimited Holofoil"], + &[data-rarity="rare ultra"], + &[data-rarity="rare holo v"], + &[data-rarity="rare holo vmax"], + &[data-rarity="rare holo vstar"], + &[data-rarity="rare shiny v"], + &[data-rarity="rare shiny vmax"], + &[data-rarity="rare rainbow"], + &[data-rarity="rare rainbow alt"], + &[data-rarity="rare secret"] { + .holo-shine { clip-path: none; } + } + + // Foil variant shine/glare — clip handled above per variant type + &[data-variant="Holofoil"], + &[data-variant="Reverse Holofoil"], + &[data-variant="1st Edition Holofoil"], + &[data-variant="Unlimited Holofoil"] { + .holo-shine { + background-image: + radial-gradient( + circle at var(--pointer-x) var(--pointer-y), + #fff 5%, #000 50%, #fff 80% + ), + linear-gradient( + var(--foil-angle, -45deg), + #000 15%, #fff, #000 85% + ); + background-blend-mode: soft-light, difference; + background-size: 120% 120%, 200% 200%; + background-position: + center center, + calc(100% * var(--pointer-from-left)) calc(100% * var(--pointer-from-top)); + filter: brightness(var(--foil-brightness, 0.4)) contrast(1.3) saturate(var(--foil-saturation, 0.5)); + mix-blend-mode: color-dodge; + opacity: calc((var(--card-opacity) * 0.9) - (var(--pointer-from-center) * 0.1)); + } + + .holo-glare { + opacity: calc(var(--card-opacity) * 0.5); + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.5) 10%, + hsla(0, 0%, 100%, 0.25) 30%, + hsla(0, 0%, 0%, 0.4) 90% + ); + filter: brightness(0.7) contrast(1.2); + mix-blend-mode: overlay; + } + } +} + + +// ----------------------------------------------------------------------------- +// 5. DEFAULT HOLO SHINE / GLARE +// Fallback for rarities not explicitly handled above. +// ----------------------------------------------------------------------------- + +.image-grow, +.card-image-wrap { + &[data-rarity]:not([data-rarity="common"]):not([data-rarity="uncommon"]) { + + .holo-shine { + background-image: + repeating-linear-gradient(110deg, + var(--violet), var(--blue), var(--green), var(--yellow), var(--red), + var(--violet), var(--blue), var(--green), var(--yellow), var(--red) + ); + background-position: + calc(((50% - var(--background-x)) * 2.6) + 50%) + calc(((50% - var(--background-y)) * 3.5) + 50%); + background-size: 400% 400%; + filter: brightness(0.7) contrast(0.9) saturate(0.8); + mix-blend-mode: color-dodge; + opacity: calc(var(--card-opacity) * 0.6); + } + + .holo-glare { + background-image: radial-gradient( + farthest-corner circle at var(--pointer-x) var(--pointer-y), + hsla(0, 0%, 100%, 0.35) 10%, + hsla(0, 0%, 100%, 0.15) 30%, + hsla(0, 0%, 0%, 0.35) 90% + ); + opacity: calc(var(--card-opacity) * 0.4); + mix-blend-mode: overlay; + filter: brightness(0.7) contrast(1.1); + } + } +} \ No newline at end of file diff --git a/src/assets/css/main.scss b/src/assets/css/main.scss index ccc83b3..57ffa40 100644 --- a/src/assets/css/main.scss +++ b/src/assets/css/main.scss @@ -23,6 +23,9 @@ $container-max-widths: ( @import "_bootstrap"; +// ── Holofoil ────────────────────────────────────────────────────────────── +//@import "_holofoil-integration"; // also pulls in _card.scss + /* -------------------------------------------------- Root Variables -------------------------------------------------- */ @@ -160,12 +163,12 @@ html { .image-grow { transition: box-shadow 350ms ease, transform 350ms ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.24); + //box-shadow: 0 2px 4px rgba(0, 0, 0, 0.24); &:hover, &:focus { box-shadow: 0 8px 10px rgba(0, 0, 0, 0.2); - transform: translateY(-0.9rem) scale(1.02); + //transform: translateY(-0.9rem) scale(1.02); } } @@ -292,7 +295,7 @@ $tiers: ( .card-image { aspect-ratio: 23 / 32; object-fit: cover; - z-index: 998; + z-index: 1; cursor: pointer; } @@ -400,6 +403,7 @@ $tiers: ( .price-row { position: relative; + z-index: 2; margin-top: -1.25rem; border-radius: 0.33rem; background: linear-gradient( diff --git a/src/assets/js/holofoil-init.js b/src/assets/js/holofoil-init.js new file mode 100644 index 0000000..b95a44b --- /dev/null +++ b/src/assets/js/holofoil-init.js @@ -0,0 +1,280 @@ +/** + * holofoil-init.js + * ----------------------------------------------------------------------------- + * Instruments .image-grow and .card-image-wrap with the holofoil effect system. + * + * GRID (.image-grow) + * Effect is invisible at rest. On hover, pointer tracking drives the shine + * and glare layers. The card lift/scale comes from main.scss as before. + * + * MODAL (.card-image-wrap) + * Effect sweeps autonomously once per minute via CSS animation. + * Pointer tracking takes over while the user hovers the image. + * + * DEFAULT FALLBACK + * If data-default="true" is set (onerror in the Astro markup), no effect + * is applied -- even if the attribute appears after stamp() has run. + * ----------------------------------------------------------------------------- + */ + +(function HolofoilSystem() { + + 'use strict'; + + // -- Constants -------------------------------------------------------------- + + const SHIMMER_SEL = [ + '.image-grow[data-rarity]', + '.image-grow[data-variant="Holofoil"]', + '.image-grow[data-variant="1st Edition Holofoil"]', + '.image-grow[data-variant="Unlimited Holofoil"]', + '.image-grow[data-variant="Reverse Holofoil"]', + '.card-image-wrap[data-rarity]', + '.card-image-wrap[data-variant="Holofoil"]', + '.card-image-wrap[data-variant="1st Edition Holofoil"]', + '.card-image-wrap[data-variant="Unlimited Holofoil"]', + '.card-image-wrap[data-variant="Reverse Holofoil"]', + ].join(','); + + const ALL_WRAPPERS_SEL = '.image-grow, .card-image-wrap'; + + // Foil variant visual randomisation + const FOIL_ANGLE_MIN = -65, FOIL_ANGLE_MAX = -25; + const FOIL_BRITE_MIN = 0.18, FOIL_BRITE_MAX = 0.32; + const FOIL_SAT_MIN = 0.40, FOIL_SAT_MAX = 0.75; + + const SKIP_RARITIES = new Set(['common', 'uncommon', '']); + + + // -- Helpers ---------------------------------------------------------------- + + const rand = (min, max) => parseFloat((Math.random() * (max - min) + min).toFixed(2)); + const clamp01 = n => Math.max(0, Math.min(1, n)); + + function pointerVars(x, y, rect) { + const fromLeft = clamp01((x - rect.left) / rect.width); + const fromTop = clamp01((y - rect.top) / rect.height); + const fromCenter = clamp01(Math.sqrt((fromLeft - 0.5) ** 2 + (fromTop - 0.5) ** 2) * 2); + return { + px: fromLeft * 100, + py: fromTop * 100, + fromLeft, + fromTop, + fromCenter, + bgX: 50 + (fromLeft - 0.5) * 30, + bgY: 50 + (fromTop - 0.5) * 30, + }; + } + + function applyPointerVars(el, v) { + el.style.setProperty('--pointer-x', v.px.toFixed(1) + '%'); + el.style.setProperty('--pointer-y', v.py.toFixed(1) + '%'); + el.style.setProperty('--pointer-from-left', v.fromLeft.toFixed(3)); + el.style.setProperty('--pointer-from-top', v.fromTop.toFixed(3)); + el.style.setProperty('--pointer-from-center', v.fromCenter.toFixed(3)); + el.style.setProperty('--background-x', v.bgX.toFixed(1) + '%'); + el.style.setProperty('--background-y', v.bgY.toFixed(1) + '%'); + } + + const isHoloVariant = v => ['Holofoil', 'Reverse Holofoil', '1st Edition Holofoil', 'Unlimited Holofoil'].includes(v); + const isModalWrapper = el => el.classList.contains('card-image-wrap'); + const isDefault = el => el.dataset.default === 'true'; + + + // -- Child injection -------------------------------------------------------- + + function injectChildren(el) { + if (el.querySelector('.holo-shine')) return; + const shine = document.createElement('div'); + shine.className = 'holo-shine'; + const glare = document.createElement('div'); + glare.className = 'holo-glare'; + el.appendChild(shine); + el.appendChild(glare); + } + + + // -- Default image guard ---------------------------------------------------- + + /** + * Watch for the onerror handler in the Astro markup setting data-default="true" + * after stamp() has already run. Hide the effect children immediately when seen. + */ + function watchForDefault(el) { + if (isDefault(el)) return; + + var observer = new MutationObserver(function() { + if (isDefault(el)) { + var shine = el.querySelector('.holo-shine'); + var glare = el.querySelector('.holo-glare'); + if (shine) shine.style.display = 'none'; + if (glare) glare.style.display = 'none'; + observer.disconnect(); + } + }); + + observer.observe(el, { attributes: true, attributeFilter: ['data-default'] }); + } + + + // -- Stamp ------------------------------------------------------------------ + + function stamp(el) { + if (el.dataset.holoInit) return; + + // Skip if already a default fallback image + if (isDefault(el)) { + el.dataset.holoInit = 'skip'; + return; + } + + const rarity = (el.dataset.rarity || '').toLowerCase(); + const variant = el.dataset.variant || ''; + + const hasHoloRarity = rarity && !SKIP_RARITIES.has(rarity); + const hasHoloVariant = isHoloVariant(variant); + + if (!hasHoloRarity && !hasHoloVariant) { + el.dataset.holoInit = 'skip'; + return; + } + + injectChildren(el); + + // Per-card foil visual randomisation (angle/brightness/saturation) + if (hasHoloVariant) { + el.style.setProperty('--foil-angle', Math.round(rand(FOIL_ANGLE_MIN, FOIL_ANGLE_MAX)) + 'deg'); + el.style.setProperty('--foil-brightness', rand(FOIL_BRITE_MIN, FOIL_BRITE_MAX).toFixed(2)); + el.style.setProperty('--foil-saturation', rand(FOIL_SAT_MIN, FOIL_SAT_MAX ).toFixed(2)); + } + + // Modal-only: set a stable delay offset for the autonomous CSS animation + if (isModalWrapper(el)) { + el.classList.add('holo-modal-mode'); + el.style.setProperty('--shimmer-delay', rand(-8, 0) + 's'); + } + + watchForDefault(el); + + el.dataset.holoInit = '1'; + } + + function stampAll(root) { + (root || document).querySelectorAll(ALL_WRAPPERS_SEL).forEach(stamp); + } + + + // -- Pointer tracking ------------------------------------------------------- + + const pointerState = new WeakMap(); + + function onPointerEnter(e) { + const el = e.currentTarget; + if (el.dataset.holoInit !== '1' || isDefault(el)) return; + + el.dataset.holoActive = '1'; + if (!pointerState.has(el)) pointerState.set(el, { rafId: null }); + } + + function onPointerMove(e) { + const el = e.currentTarget; + if (el.dataset.holoInit !== '1') return; + + const state = pointerState.get(el); + if (!state) return; + + if (state.rafId) cancelAnimationFrame(state.rafId); + state.rafId = requestAnimationFrame(function() { + const rect = el.getBoundingClientRect(); + applyPointerVars(el, pointerVars(e.clientX, e.clientY, rect)); + state.rafId = null; + }); + } + + function onPointerLeave(e) { + const el = e.currentTarget; + if (el.dataset.holoInit !== '1') return; + + const state = pointerState.get(el); + if (state && state.rafId) { cancelAnimationFrame(state.rafId); state.rafId = null; } + + delete el.dataset.holoActive; + + if (isModalWrapper(el)) { + // Let the CSS animation resume driving --card-opacity + el.style.removeProperty('--card-opacity'); + } + } + + function attachListeners(el) { + if (el.dataset.holoListeners) return; + el.addEventListener('pointerenter', onPointerEnter, { passive: true }); + el.addEventListener('pointermove', onPointerMove, { passive: true }); + el.addEventListener('pointerleave', onPointerLeave, { passive: true }); + el.dataset.holoListeners = '1'; + } + + function attachAllListeners(root) { + (root || document).querySelectorAll(SHIMMER_SEL).forEach(function(el) { + stamp(el); + if (el.dataset.holoInit === '1') attachListeners(el); + }); + } + + + // -- MutationObserver: react to HTMX / infinite scroll ---------------------- + + function observeGrid() { + var grid = document.getElementById('cardGrid'); + if (!grid) return; + + new MutationObserver(function(mutations) { + for (var i = 0; i < mutations.length; i++) { + var nodes = mutations[i].addedNodes; + for (var j = 0; j < nodes.length; j++) { + var node = nodes[j]; + if (node.nodeType !== 1) continue; + if (node.matches && node.matches(ALL_WRAPPERS_SEL)) { + stamp(node); + if (node.dataset.holoInit === '1') attachListeners(node); + } + if (node.querySelectorAll) { + node.querySelectorAll(ALL_WRAPPERS_SEL).forEach(function(el) { + stamp(el); + if (el.dataset.holoInit === '1') attachListeners(el); + }); + } + } + } + }).observe(grid, { childList: true, subtree: true }); + } + + function observeModal() { + var modal = document.getElementById('cardModal'); + if (!modal) return; + + new MutationObserver(function() { + modal.querySelectorAll(ALL_WRAPPERS_SEL).forEach(function(el) { + stamp(el); + if (el.dataset.holoInit === '1') attachListeners(el); + }); + }).observe(modal, { childList: true, subtree: true }); + } + + + // -- Bootstrap -------------------------------------------------------------- + + function init() { + stampAll(); + attachAllListeners(); + observeGrid(); + observeModal(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + +})(); \ No newline at end of file diff --git a/src/assets/js/priceChart.js b/src/assets/js/priceChart.js index 48e7543..dfa3f34 100644 --- a/src/assets/js/priceChart.js +++ b/src/assets/js/priceChart.js @@ -39,10 +39,27 @@ function buildChartData(history, rangeKey) { const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff); - const allDates = [...new Set(filtered.map(r => r.calculatedAt))] - .sort((a, b) => new Date(a) - new Date(b)); + // Always build the full date axis for the selected window, even if sparse. + // Generate one label per day in the range so the x-axis reflects the + // chosen period rather than collapsing to only the days that have data. + const dataDateSet = new Set(filtered.map(r => r.calculatedAt)); + const allDates = [...dataDateSet].sort((a, b) => new Date(a) - new Date(b)); - const labels = allDates.map(formatDate); + // If we have real data, expand the axis to span from cutoff → today so + // empty stretches at the start/end of a range are visible. + let axisLabels = allDates; + if (allDates.length > 0 && RANGE_DAYS[rangeKey] !== Infinity) { + const start = new Date(cutoff); + const end = new Date(); + const expanded = []; + // Step through every day in the window + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + expanded.push(d.toISOString().split('T')[0]); + } + axisLabels = expanded; + } + + const labels = axisLabels.map(formatDate); const lookup = {}; for (const row of filtered) { @@ -50,16 +67,14 @@ function buildChartData(history, rangeKey) { lookup[row.condition][row.calculatedAt] = Number(row.marketPrice); } - // Check specifically whether the active condition has any data points - const activeConditionDates = allDates.filter( + const activeConditionHasData = allDates.some( date => lookup[activeCondition]?.[date] != null ); - const activeConditionHasData = activeConditionDates.length > 0; const datasets = CONDITIONS.map(condition => { const isActive = condition === activeCondition; const colors = CONDITION_COLORS[condition]; - const data = allDates.map(date => lookup[condition]?.[date] ?? null); + const data = axisLabels.map(date => lookup[condition]?.[date] ?? null); return { label: condition, data, @@ -75,23 +90,29 @@ function buildChartData(history, rangeKey) { }; }); - return { labels, datasets, hasData: allDates.length > 0, activeConditionHasData }; + return { + labels, + datasets, + hasData: allDates.length > 0, + activeConditionHasData, + }; } function updateChart() { if (!chartInstance) return; const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange); - // Show empty state if no data at all, or if the active condition specifically has no data - if (!hasData || !activeConditionHasData) { - setEmptyState(true); - return; - } - - setEmptyState(false); - chartInstance.data.labels = labels; + // Always push the new labels/datasets to the chart so the x-axis + // reflects the selected time window — even when there's no data for + // the active condition. Then toggle the empty state overlay on top. + chartInstance.data.labels = labels; chartInstance.data.datasets = datasets; chartInstance.update('none'); + + // Show the empty state overlay if the active condition has no points + // in this window, but leave the (empty) chart visible underneath so + // the axis communicates the selected period. + setEmptyState(!hasData || !activeConditionHasData); } function initPriceChart(canvas) { @@ -114,12 +135,8 @@ function initPriceChart(canvas) { const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange); - if (!hasData || !activeConditionHasData) { - setEmptyState(true); - return; - } - - setEmptyState(false); + // Render the chart regardless — show empty state overlay if needed + setEmptyState(!hasData || !activeConditionHasData); chartInstance = new Chart(canvas.getContext('2d'), { type: 'line', diff --git a/src/components/CardGrid.astro b/src/components/CardGrid.astro index 4bb78c4..ac8f0f0 100644 --- a/src/components/CardGrid.astro +++ b/src/components/CardGrid.astro @@ -44,6 +44,8 @@ import BackToTop from "./BackToTop.astro" + + \ No newline at end of file diff --git a/src/components/SetIcon.astro b/src/components/SetIcon.astro index 080d721..306b699 100644 --- a/src/components/SetIcon.astro +++ b/src/components/SetIcon.astro @@ -124,6 +124,7 @@ import phantasmal_flames from "/src/svg/set/phantasmal_flames.svg?raw"; import destined_rivals from "/src/svg/set/destined_rivals.svg?raw"; import surging_sparks from "/src/svg/set/surging_sparks.svg?raw"; import team_rocket from "/src/svg/set/team_rocket.svg?raw"; +import perfect_order from "/src/svg/set/perfect_order.svg?raw"; const { set } = Astro.props; @@ -252,6 +253,7 @@ const setMap = { "ASC": ascended_heroes, "DRI": destined_rivals, "SSP": surging_sparks, +"ME03": perfect_order, }; const svg = setMap[set as keyof typeof setMap] ?? ""; diff --git a/src/pages/partials/card-modal.astro b/src/pages/partials/card-modal.astro index 28d25ab..41cede8 100644 --- a/src/pages/partials/card-modal.astro +++ b/src/pages/partials/card-modal.astro @@ -190,13 +190,26 @@ const altSearchUrl = (card: any) => {
- {card?.productName} + + +
+ {card?.productName} +
+ @@ -266,9 +279,9 @@ const altSearchUrl = (card: any) => {
- Volatility + Volatility {
+/-
-
{card.productName}
+
{card.productName}
{conditionOrder.map((condition) => ( diff --git a/src/svg/set/perfect_order.svg b/src/svg/set/perfect_order.svg new file mode 100644 index 0000000..2183bb1 --- /dev/null +++ b/src/svg/set/perfect_order.svg @@ -0,0 +1 @@ + \ No newline at end of file