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"
+
+
+
+ 
