@salifbiaye/create-boutique 0.3.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +5 -2
  2. package/templates/backend/src/scripts/seed.ts +47 -12
  3. package/templates/storefront/locales/en.json +16 -1
  4. package/templates/storefront/locales/fr.json +16 -1
  5. package/templates/storefront/src/app/(main)/page.tsx +123 -0
  6. package/templates/storefront/src/app/layout.tsx +4 -0
  7. package/templates/storefront/src/lib/boutique/schema.ts +1 -1
  8. package/templates/storefront/src/modules/ub/album.tsx +137 -0
  9. package/templates/storefront/src/modules/ub/cinema.tsx +180 -0
  10. package/templates/storefront/src/modules/ub/gala.tsx +178 -0
  11. package/templates/storefront/src/modules/ub/tech.tsx +279 -0
  12. package/templates/storefront/src/styles/ub/album.css +205 -0
  13. package/templates/storefront/src/styles/ub/cinema.css +169 -0
  14. package/templates/storefront/src/styles/ub/gala.css +242 -0
  15. package/templates/storefront/src/styles/ub/tech.css +441 -0
  16. package/templates/storefront/src/styles/ub/variants.css +1 -5
  17. package/templates/storefront/themes/cyber-monday/theme.json +4 -3
  18. package/templates/storefront/themes/cyber-monday-2/campaign.en.json +1 -1
  19. package/templates/storefront/themes/cyber-monday-2/campaign.fr.json +1 -1
  20. package/templates/storefront/themes/cyber-monday-2/theme.json +5 -4
  21. package/templates/storefront/themes/halloween/theme.json +3 -2
  22. package/templates/storefront/themes/halloween-2/theme.json +2 -1
  23. package/templates/storefront/themes/noel/theme.json +3 -2
  24. package/templates/storefront/themes/noel-2/theme.json +3 -2
  25. package/templates/storefront/themes/nouvel-an/campaign.en.json +3 -3
  26. package/templates/storefront/themes/nouvel-an/campaign.fr.json +2 -2
  27. package/templates/storefront/themes/nouvel-an/theme.json +2 -1
  28. package/templates/storefront/themes/nouvel-an-2/campaign.en.json +5 -5
  29. package/templates/storefront/themes/nouvel-an-2/campaign.fr.json +5 -5
  30. package/templates/storefront/themes/nouvel-an-2/theme.json +4 -3
@@ -0,0 +1,178 @@
1
+ import Link from "next/link"
2
+ import { preload } from "react-dom"
3
+ import type { Campaign } from "@lib/boutique/campaign"
4
+ import { t } from "@lib/boutique/i18n"
5
+ import { getProductPrice } from "@lib/util/get-product-price"
6
+ import { HttpTypes } from "@medusajs/types"
7
+ import { CampaignCountdown } from "./campaign-blocks"
8
+ import FadeImg from "./fade-img"
9
+ import { Icon } from "./icons"
10
+
11
+ /**
12
+ * Accueil « Gala » (thèmes Nouvel An, theme.json → "home": "gala") : noir, or et argent.
13
+ * Variante 1 : texte à gauche. Variante 2 (data-variant) : tout centré, l'année en grand chiffre argenté.
14
+ * Sections : hero plein cadre, bande « minuit » (compte à rebours), sélection encadrée d'or,
15
+ * « les 12 coups de minuit » (chiffres romains), bannière feuille d'or. CSS : styles/ub/gala.css.
16
+ */
17
+
18
+ type P = HttpTypes.StoreProduct
19
+ const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
20
+ const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]
21
+
22
+ export function GalaHero({ campaign }: { campaign: Campaign }) {
23
+ const { hero } = campaign
24
+ const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
25
+ if (image) preload(image, { as: "image", fetchPriority: "high" })
26
+ const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
27
+ const before = hl ? hero.title.slice(0, hero.title.indexOf(hl)).trim() : hero.title
28
+ const after = hl ? hero.title.slice(hero.title.indexOf(hl) + hl.length).trim() : ""
29
+ const year = new Date(campaign.endsAt).getFullYear()
30
+
31
+ return (
32
+ <section className="ub-gl-hero" style={image ? { ["--gl-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
33
+ <div className="ub-gl-hero__sparkle" aria-hidden />
34
+ <div className="ub-wrap ub-gl-hero__in">
35
+ <div className="ub-gl-hero__text">
36
+ {hero.eyebrow && <p className="ub-gl-eyebrow">{hero.eyebrow}</p>}
37
+ {/* Variante 2 : l'année en très grand (masquée en variante 1 par le CSS) */}
38
+ <p className="ub-gl-year" aria-hidden>
39
+ {year}
40
+ </p>
41
+ <h1 className="ub-gl-title">
42
+ <span className="ub-gl-title__a">{before}</span>
43
+ {hl && before && " "}
44
+ {hl &&<span className="ub-gl-title__b">{hl}</span>}
45
+ {after && <span className="ub-gl-title__a"> {after}</span>}
46
+ </h1>
47
+ {hero.sub && <p className="ub-gl-sub">{hero.sub}</p>}
48
+ <div className="ub-gl-actions">
49
+ <Link href="/store" className="ub-gl-btn ub-gl-btn--solid">
50
+ {hero.cta}
51
+ </Link>
52
+ {hero.cta2 && (
53
+ <Link href="/store" className="ub-gl-btn ub-gl-btn--ghost">
54
+ {hero.cta2}
55
+ </Link>
56
+ )}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </section>
61
+ )
62
+ }
63
+
64
+ /** Bande « minuit » : grand compte à rebours doré + texte promo + bouton. */
65
+ export function GalaMidnight({ campaign }: { campaign: Campaign }) {
66
+ if (campaign.showCountdown === false) return null
67
+ return (
68
+ <section className="ub-gl-midnight">
69
+ <div className="ub-wrap ub-gl-midnight__in">
70
+ <div>
71
+ <p className="ub-gl-eyebrow">{t("gala.midnight")}</p>
72
+ <div className="ub-gl-midnight__cd">
73
+ <CampaignCountdown endsAt={campaign.endsAt} />
74
+ </div>
75
+ </div>
76
+ <div className="ub-gl-midnight__text">
77
+ {campaign.promo?.title && <h2 className="ub-gl-h2">{campaign.promo.title}</h2>}
78
+ {campaign.promo?.text && <p>{campaign.promo.text}</p>}
79
+ <Link href="/store" className="ub-gl-link">
80
+ {campaign.promo?.cta ?? t("nav.cta")} <Icon name="arrowRight" />
81
+ </Link>
82
+ </div>
83
+ </div>
84
+ </section>
85
+ )
86
+ }
87
+
88
+ function price(p: P) {
89
+ const { cheapestPrice } = getProductPrice({ product: p })
90
+ return { price: cheapestPrice, onSale: cheapestPrice?.price_type === "sale" }
91
+ }
92
+
93
+ /** Sélection : cartes sombres avec cadre intérieur doré, prix en or. */
94
+ export function GalaSelection({ products, title }: { products: P[]; title: string }) {
95
+ const items = products.slice(0, 6)
96
+ if (!items.length) return null
97
+ return (
98
+ <section className="ub-wrap ub-gl-section">
99
+ <div className="ub-gl-head">
100
+ <span className="ub-gl-rule" aria-hidden />
101
+ <h2 className="ub-gl-h2">{title}</h2>
102
+ <span className="ub-gl-rule" aria-hidden />
103
+ </div>
104
+ <ul className="ub-gl-grid">
105
+ {items.map((p) => {
106
+ const { price: pr, onSale } = price(p)
107
+ const image = imageOf(p)
108
+ return (
109
+ <li key={p.id}>
110
+ <Link href={`/products/${p.handle}`} className="ub-gl-card">
111
+ <span className="ub-gl-card__media">
112
+ {image ? <FadeImg src={image} alt="" /> : <span className="ub-ph" />}
113
+ {onSale && pr?.percentage_diff && <span className="ub-gl-card__off">-{pr.percentage_diff}%</span>}
114
+ </span>
115
+ <span className="ub-gl-card__name">{p.title}</span>
116
+ {pr && (
117
+ <span className="ub-gl-card__price">
118
+ <b>{pr.calculated_price}</b>
119
+ {onSale && <s>{pr.original_price}</s>}
120
+ </span>
121
+ )}
122
+ </Link>
123
+ </li>
124
+ )
125
+ })}
126
+ </ul>
127
+ </section>
128
+ )
129
+ }
130
+
131
+ /** « Les 12 coups de minuit » : jusqu'à 12 offres numérotées en chiffres romains, défilement horizontal. */
132
+ export function GalaStrokes({ products }: { products: P[] }) {
133
+ const items = products.slice(0, 12)
134
+ if (items.length < 4) return null
135
+ return (
136
+ <section className="ub-gl-section ub-gl-strokes">
137
+ <div className="ub-wrap ub-gl-head ub-gl-head--left">
138
+ <h2 className="ub-gl-h2">{t("gala.strokes")}</h2>
139
+ <p>{t("gala.strokesSub")}</p>
140
+ </div>
141
+ <ol className="ub-gl-strokes__row">
142
+ {items.map((p, i) => {
143
+ const { price: pr } = price(p)
144
+ const image = imageOf(p)
145
+ return (
146
+ <li key={p.id}>
147
+ <Link href={`/products/${p.handle}`} className="ub-gl-stroke">
148
+ <span className="ub-gl-stroke__num" aria-hidden>
149
+ {ROMAN[i]}
150
+ </span>
151
+ <span className="ub-gl-stroke__img">{image ? <FadeImg src={image} alt="" /> : <span className="ub-ph" />}</span>
152
+ <span className="ub-gl-stroke__name">{p.title}</span>
153
+ {pr && <b className="ub-gl-stroke__price">{pr.calculated_price}</b>}
154
+ </Link>
155
+ </li>
156
+ )
157
+ })}
158
+ </ol>
159
+ </section>
160
+ )
161
+ }
162
+
163
+ /** Bannière feuille d'or : titre du hero, bouton noir. */
164
+ export function GalaFoil({ campaign }: { campaign: Campaign }) {
165
+ return (
166
+ <section className="ub-wrap ub-gl-section">
167
+ <div className="ub-gl-foil">
168
+ <span className="ub-gl-foil__confetti" aria-hidden />
169
+ {campaign.hero.eyebrow && <p className="ub-gl-eyebrow">{campaign.hero.eyebrow}</p>}
170
+ <h2 className="ub-gl-h2">{campaign.hero.title}</h2>
171
+ {campaign.hero.sub && <p>{campaign.hero.sub}</p>}
172
+ <Link href="/store" className="ub-gl-btn ub-gl-btn--dark">
173
+ {campaign.hero.cta}
174
+ </Link>
175
+ </div>
176
+ </section>
177
+ )
178
+ }
@@ -0,0 +1,279 @@
1
+ import Link from "next/link"
2
+ import { preload } from "react-dom"
3
+ import type { Campaign } from "@lib/boutique/campaign"
4
+ import { t } from "@lib/boutique/i18n"
5
+ import { getProductPrice } from "@lib/util/get-product-price"
6
+ import { HttpTypes } from "@medusajs/types"
7
+ import { CampaignCountdown } from "./campaign-blocks"
8
+ import FadeImg from "./fade-img"
9
+ import { Icon, type IconName } from "./icons"
10
+
11
+ /**
12
+ * Accueil « Tech » (thèmes Cyber Monday, theme.json → "home": "tech") : la boutique comme un lancement produit.
13
+ * Cyber Monday 1 : nuit violette, texte à gauche, visuel néon sur le côté.
14
+ * Cyber Monday 2 (data-variant) : blanc, le visuel couvre tout le hero, mot en lettres « tuiles ».
15
+ * Sections : hero, bandeau défilant, « Flash deals » (compte à rebours + cartes), bento de l'offre phare,
16
+ * grille « Deals du moment », bannière finale. CSS : styles/ub/tech.css.
17
+ */
18
+
19
+ type P = HttpTypes.StoreProduct
20
+ const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
21
+ const priceOf = (p: P) => {
22
+ const { cheapestPrice } = getProductPrice({ product: p })
23
+ return { price: cheapestPrice, onSale: cheapestPrice?.price_type === "sale" }
24
+ }
25
+
26
+ export function TechHero({ campaign }: { campaign: Campaign }) {
27
+ const { hero } = campaign
28
+ const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
29
+ if (image) preload(image, { as: "image", fetchPriority: "high" })
30
+ const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
31
+ const before = hl ? hero.title.slice(0, hero.title.indexOf(hl)).trim() : hero.title
32
+ const after = hl ? hero.title.slice(hero.title.indexOf(hl) + hl.length).trim() : ""
33
+ const [b1, b2, b3] = hero.bubble ?? []
34
+
35
+ return (
36
+ <section className="ub-tc-hero" data-has-image={image ? "" : undefined}>
37
+ <div className="ub-tc-hero__grid" aria-hidden />
38
+ <div className="ub-tc-hero__beam" aria-hidden />
39
+ {/* Cyber Monday 2 : le visuel couvre tout le hero (masqué par le CSS en variante 1) */}
40
+ {image && (
41
+ <div className="ub-tc-hero__cover" aria-hidden>
42
+ <img src={image} alt="" fetchPriority="high" decoding="async" />
43
+ </div>
44
+ )}
45
+ <div className="ub-wrap ub-tc-hero__in">
46
+ <div className="ub-tc-hero__text">
47
+ {hero.eyebrow && (
48
+ <p className="ub-tc-chip">
49
+ <span className="ub-tc-chip__dot" aria-hidden />
50
+ {hero.eyebrow}
51
+ </p>
52
+ )}
53
+ <h1 className="ub-tc-title">
54
+ {before && <span className="ub-tc-title__a">{before} </span>}
55
+ {hl && (
56
+ <span className="ub-tc-title__hl">
57
+ {/* Variante 2 : une tuile par lettre (comme les lettres du visuel) */}
58
+ {[...hl].map((c, i) => (
59
+ <span key={i} className={c === " " ? "ub-tc-tile ub-tc-tile--sp" : "ub-tc-tile"}>
60
+ {c}
61
+ </span>
62
+ ))}
63
+ </span>
64
+ )}
65
+ {after && <span className="ub-tc-title__a"> {after}</span>}
66
+ </h1>
67
+ {hero.sub && <p className="ub-tc-sub">{hero.sub}</p>}
68
+ <div className="ub-tc-actions">
69
+ <Link href="/store" className="ub-tc-btn ub-tc-btn--solid">
70
+ {hero.cta} <Icon name="arrowRight" />
71
+ </Link>
72
+ {hero.cta2 && (
73
+ <Link href="/store" className="ub-tc-btn ub-tc-btn--ghost">
74
+ {hero.cta2}
75
+ </Link>
76
+ )}
77
+ </div>
78
+ {campaign.showCountdown !== false && (
79
+ <div className="ub-tc-hero__cd">
80
+ <CampaignCountdown endsAt={campaign.endsAt} />
81
+ </div>
82
+ )}
83
+ </div>
84
+ {/* Cyber Monday 1 : visuel sur le côté, cadre lumineux + pastille de remise */}
85
+ <div className="ub-tc-hero__side">
86
+ {image ? <img src={image} alt={t("hero.photo")} fetchPriority="high" decoding="async" /> : <span className="ub-ph" />}
87
+ {b2 && (
88
+ <p className="ub-tc-badge">
89
+ <small>{b1}</small>
90
+ <b>{b2}</b>
91
+ <small>{b3}</small>
92
+ </p>
93
+ )}
94
+ </div>
95
+ </div>
96
+ </section>
97
+ )
98
+ }
99
+
100
+ /** Bandeau défilant (textes de la barre du haut + remise), dupliqué pour une boucle continue. */
101
+ export function TechTicker({ campaign }: { campaign: Campaign }) {
102
+ const items = [campaign.hero.eyebrow, ...(campaign.topbar ?? [])].filter(Boolean) as string[]
103
+ if (!items.length) return null
104
+ const row = (hidden?: boolean) => (
105
+ <ul className="ub-tc-ticker__row" aria-hidden={hidden || undefined}>
106
+ {items.map((s, i) => (
107
+ <li key={i}>
108
+ <Icon name="sparkle" /> {s}
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ )
113
+ return (
114
+ <div className="ub-tc-ticker">
115
+ <div className="ub-tc-ticker__track">
116
+ {row()}
117
+ {row(true)}
118
+ </div>
119
+ </div>
120
+ )
121
+ }
122
+
123
+ function Card({ p, rank }: { p: P; rank?: number }) {
124
+ const { price, onSale } = priceOf(p)
125
+ const image = imageOf(p)
126
+ return (
127
+ <Link href={`/products/${p.handle}`} className="ub-tc-card">
128
+ <span className="ub-tc-card__media">
129
+ {image ? <FadeImg src={image} alt="" /> : <span className="ub-ph" />}
130
+ {onSale && price?.percentage_diff && <span className="ub-tc-off">-{price.percentage_diff}%</span>}
131
+ {rank != null && <span className="ub-tc-card__rank">#{String(rank).padStart(2, "0")}</span>}
132
+ </span>
133
+ <span className="ub-tc-card__body">
134
+ <span className="ub-tc-card__name">{p.title}</span>
135
+ {price && (
136
+ <span className="ub-tc-card__price">
137
+ <b>{price.calculated_price}</b>
138
+ {onSale && <s>{price.original_price}</s>}
139
+ </span>
140
+ )}
141
+ </span>
142
+ <span className="ub-tc-card__go" aria-hidden>
143
+ <Icon name="arrowUpRight" />
144
+ </span>
145
+ </Link>
146
+ )
147
+ }
148
+
149
+ /** « Flash deals » : compte à rebours façon écran + les offres les plus remisées en défilement horizontal. */
150
+ export function TechFlash({ campaign, products }: { campaign: Campaign; products: P[] }) {
151
+ const items = products.slice(0, 8)
152
+ if (!items.length) return null
153
+ return (
154
+ <section className="ub-tc-section ub-tc-flash">
155
+ <div className="ub-wrap">
156
+ <div className="ub-tc-flash__head">
157
+ <div>
158
+ <p className="ub-tc-kicker">
159
+ <Icon name="sparkle" /> {t("tech.flash")}
160
+ </p>
161
+ <h2 className="ub-tc-h2">{campaign.promo?.title ?? t("tech.flashTitle")}</h2>
162
+ </div>
163
+ {campaign.showCountdown !== false && (
164
+ <div className="ub-tc-flash__cd">
165
+ <CampaignCountdown endsAt={campaign.endsAt} />
166
+ </div>
167
+ )}
168
+ </div>
169
+ <ol className="ub-tc-flash__row">
170
+ {items.map((p, i) => (
171
+ <li key={p.id}>
172
+ <Card p={p} rank={i + 1} />
173
+ </li>
174
+ ))}
175
+ </ol>
176
+ </div>
177
+ </section>
178
+ )
179
+ }
180
+
181
+ /** Bento : l'offre phare en grande carte + 4 cartes. */
182
+ export function TechBento({ campaign, products }: { campaign: Campaign; products: P[] }) {
183
+ const [hero, ...rest] = products.filter((p) => imageOf(p))
184
+ if (!hero) return null
185
+ const { price, onSale } = priceOf(hero)
186
+ const image = imageOf(hero)
187
+ return (
188
+ <section className="ub-tc-section">
189
+ <div className="ub-wrap ub-tc-bento">
190
+ <Link href={`/products/${hero.handle}`} className="ub-tc-bento__main">
191
+ <span className="ub-tc-bento__img">{image && <FadeImg src={image} alt="" />}</span>
192
+ <span className="ub-tc-bento__txt">
193
+ <span className="ub-tc-kicker">{campaign.promo?.eyebrow ?? t("tech.deal")}</span>
194
+ <span className="ub-tc-bento__name">{hero.title}</span>
195
+ {price && (
196
+ <span className="ub-tc-card__price ub-tc-card__price--lg">
197
+ <b>{price.calculated_price}</b>
198
+ {onSale && <s>{price.original_price}</s>}
199
+ {onSale && price.percentage_diff && <span className="ub-tc-off">-{price.percentage_diff}%</span>}
200
+ </span>
201
+ )}
202
+ <span className="ub-tc-btn ub-tc-btn--solid">
203
+ {campaign.promo?.cta ?? t("nav.cta")} <Icon name="arrowRight" />
204
+ </span>
205
+ </span>
206
+ </Link>
207
+ {rest.slice(0, 4).map((p) => (
208
+ <Card key={p.id} p={p} />
209
+ ))}
210
+ </div>
211
+ </section>
212
+ )
213
+ }
214
+
215
+ /** Grille de tous les produits. */
216
+ export function TechGrid({ products, title }: { products: P[]; title: string }) {
217
+ if (!products.length) return null
218
+ return (
219
+ <section className="ub-tc-section">
220
+ <div className="ub-wrap">
221
+ <div className="ub-tc-head">
222
+ <h2 className="ub-tc-h2">{title}</h2>
223
+ <Link href="/store" className="ub-tc-link">
224
+ {t("tech.all")} <Icon name="arrowRight" />
225
+ </Link>
226
+ </div>
227
+ <ul className="ub-tc-grid">
228
+ {products.map((p) => (
229
+ <li key={p.id}>
230
+ <Card p={p} />
231
+ </li>
232
+ ))}
233
+ </ul>
234
+ </div>
235
+ </section>
236
+ )
237
+ }
238
+
239
+ /** Avantages (livraison, paiement, retours) en ligne, style fiche technique. */
240
+ export function TechSpecs({ freeShipping }: { freeShipping?: string }) {
241
+ const specs: [IconName, string, string][] = [
242
+ ["truck", t("trust.shipping"), freeShipping ? t("trust.shippingSub", { freeShipping }) : t("trust.shippingSubAll")],
243
+ ["lock", t("trust.secure"), t("trust.secureSub")],
244
+ ["returns", t("trust.returns"), t("trust.returnsSub")],
245
+ ["shield", t("tech.warranty"), t("tech.warrantySub")],
246
+ ]
247
+ return (
248
+ <section className="ub-wrap ub-tc-specs">
249
+ {specs.map(([ic, a, b]) => (
250
+ <div key={a} className="ub-tc-spec">
251
+ <Icon name={ic} />
252
+ <span>
253
+ <b>{a}</b>
254
+ <small>{b}</small>
255
+ </span>
256
+ </div>
257
+ ))}
258
+ </section>
259
+ )
260
+ }
261
+
262
+ /** Bannière finale : texte promo + bouton, sur fond lumineux (variante 1) ou noir (variante 2). */
263
+ export function TechFinal({ campaign }: { campaign: Campaign }) {
264
+ const promo = campaign.promo
265
+ if (!promo?.title) return null
266
+ return (
267
+ <section className="ub-wrap ub-tc-section">
268
+ <div className="ub-tc-final">
269
+ <div className="ub-tc-final__glow" aria-hidden />
270
+ {promo.eyebrow && <p className="ub-tc-kicker">{promo.eyebrow}</p>}
271
+ <h2 className="ub-tc-final__title">{promo.title}</h2>
272
+ {promo.text && <p className="ub-tc-final__text">{promo.text}</p>}
273
+ <Link href="/store" className="ub-tc-btn ub-tc-btn--solid">
274
+ {promo.cta ?? t("nav.cta")} <Icon name="arrowRight" />
275
+ </Link>
276
+ </div>
277
+ </section>
278
+ )
279
+ }
@@ -0,0 +1,205 @@
1
+ /* Accueil « Album » (thèmes Noël) — modules/ub/album.tsx. Velours, guirlande, bois, polaroïds, papier déchiré, ticket.
2
+ Noël 1 : rouge dominant. Noël 2 : vert sapin dominant, or et bordeaux. */
3
+
4
+ [data-theme="noel"]:not([data-variant]),
5
+ [data-variant="noel-2"] {
6
+ --al-red: #a8111b; --al-red-dark: #6e0a12; --al-green: #1d4d33; --al-green-dark: #123522;
7
+ --al-cream: #f4eadb; --al-gold: #dcb866; --al-wood: #6b4226;
8
+ --bg: #f4eadb; --surface: #ede0cc; --tint-1: #e8d7bd; --tint-2: #e2cfb2; --tint-3: #f0e2cc; --tint-4: #e9dcc6;
9
+ --ink: #2b1b14; --ink-muted: #6d5646; --border: #dcc8ab;
10
+ --primary: #a8111b; --on-primary: #fff6e8; --accent: #1d4d33; --on-accent: #fff6e8;
11
+ --sale: #a8111b; --hero-bg: #6e0a12; --hero-ink: #fff6e8; --hero-muted: #f0d9c0;
12
+ --band-bg: #1d4d33; --band-ink: #fff6e8; --bubble-bg: #dcb866; --bubble-ink: #2b1b14;
13
+ --topbar-bg: #6e0a12; --topbar-ink: #f7dfa6; --focus: #dcb866;
14
+ --ub-font-head: var(--font-poppins); --ub-r-card: 6px; --ub-r-btn: 6px;
15
+ --al-script: "Segoe Script", "Brush Script MT", "Snell Roundhand", "Apple Chancery", cursive;
16
+ --al-serif: "DM Serif Display", Georgia, "Times New Roman", serif;
17
+ }
18
+ [data-variant="noel-2"] {
19
+ --al-red: #1d4d33; --al-red-dark: #0f2f1f; --al-green: #6e0a12; --al-green-dark: #4a060c;
20
+ --primary: #1d4d33; --accent: #6e0a12; --hero-bg: #0f2f1f; --topbar-bg: #0f2f1f; --band-bg: #6e0a12;
21
+ }
22
+
23
+ /* ---------- Hero velours ---------- */
24
+ .ub-al-hero {
25
+ position: relative;
26
+ isolation: isolate;
27
+ overflow: hidden;
28
+ display: flex;
29
+ align-items: center;
30
+ min-height: clamp(520px, 46vw, 720px);
31
+ text-align: center;
32
+ color: var(--hero-ink);
33
+ background:
34
+ radial-gradient(60% 70% at 50% 40%, color-mix(in srgb, var(--al-red) 90%, #ff3b3b) 0%, var(--al-red) 45%, var(--al-red-dark) 100%);
35
+ }
36
+ .ub-al-hero[data-has-image] { background: var(--al-red-dark) var(--al-img) center / cover no-repeat; }
37
+ .ub-al-hero[data-has-image]::after { content: ""; position: absolute; inset: 0; z-index: -1; background: radial-gradient(50% 60% at 50% 45%, rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.55)); }
38
+ /* Rideau (plis) à gauche quand il n'y a pas d'image */
39
+ .ub-al-hero__curtain {
40
+ position: absolute;
41
+ inset: 0 auto 0 0;
42
+ z-index: -1;
43
+ width: 18%;
44
+ background: repeating-linear-gradient(90deg, rgb(0 0 0 / 0.28) 0 14px, rgb(255 255 255 / 0.06) 14px 26px, rgb(0 0 0 / 0.18) 26px 40px);
45
+ mask-image: linear-gradient(90deg, #000 60%, transparent);
46
+ }
47
+ .ub-al-hero[data-has-image] .ub-al-hero__curtain { display: none; }
48
+ .ub-al-hero__in { display: grid; justify-items: center; gap: var(--space-4); padding-top: var(--space-16); padding-bottom: var(--space-16); }
49
+ .ub-al-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: 0.3em; text-transform: uppercase; color: var(--hero-muted); }
50
+ .ub-al-title {
51
+ max-width: 16ch;
52
+ font-family: var(--al-script);
53
+ font-weight: 400;
54
+ font-size: clamp(48px, 6.6vw, 100px);
55
+ line-height: 1.1;
56
+ background: linear-gradient(180deg, #fff3cf 0%, var(--al-gold) 55%, #b88a36 100%);
57
+ -webkit-background-clip: text;
58
+ background-clip: text;
59
+ color: transparent;
60
+ filter: drop-shadow(0 4px 18px rgb(0 0 0 / 0.35));
61
+ animation: ub-rise 0.8s cubic-bezier(0.2, 0.7, 0.2, 1) both;
62
+ }
63
+ .ub-al-sub { max-width: 48ch; font-size: 16px; line-height: 25px; color: var(--hero-muted); }
64
+ .ub-al-btn {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ height: 48px;
69
+ padding: 0 28px;
70
+ border: 1.5px solid var(--al-gold);
71
+ border-radius: var(--ub-r-btn);
72
+ color: #fff3cf;
73
+ font-size: 14px;
74
+ font-weight: 600;
75
+ letter-spacing: 0.12em;
76
+ text-transform: uppercase;
77
+ transition: background 0.2s, color 0.2s;
78
+ }
79
+ .ub-al-btn:hover { background: var(--al-gold); color: var(--ink); }
80
+ .ub-al-btn--dark { border-color: var(--al-red); color: var(--al-red); }
81
+ .ub-al-btn--dark:hover { background: var(--al-red); color: #fff6e8; }
82
+
83
+ /* ---------- Guirlande ---------- */
84
+ .ub-al-garland {
85
+ position: relative;
86
+ z-index: 2;
87
+ height: 46px;
88
+ margin: -23px 0;
89
+ background:
90
+ radial-gradient(circle at 10px 30px, #e8c060 5px, transparent 6px) 0 0 / 86px 46px repeat-x,
91
+ radial-gradient(circle at 50px 34px, #d1202a 5px, transparent 6px) 0 0 / 86px 46px repeat-x,
92
+ radial-gradient(ellipse 22px 14px at 50% 50%, #2e6b45 60%, transparent 62%) 0 6px / 30px 34px repeat-x,
93
+ radial-gradient(ellipse 22px 14px at 50% 50%, #1e4f33 60%, transparent 62%) 15px 12px / 30px 30px repeat-x,
94
+ linear-gradient(180deg, transparent 12px, #173d28 12px, #173d28 34px, transparent 34px);
95
+ filter: drop-shadow(0 6px 8px rgb(0 0 0 / 0.25));
96
+ }
97
+
98
+ /* ---------- Section verte : bois + photos suspendues ---------- */
99
+ .ub-al-story { padding: var(--space-16) 0; background: var(--al-green); background-image: radial-gradient(rgb(255 255 255 / 0.05) 1px, transparent 1px); background-size: 18px 18px; color: #fff6e8; }
100
+ .ub-al-story__in { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: var(--space-12); align-items: center; }
101
+ .ub-al-hang { display: flex; justify-content: center; gap: var(--space-8); }
102
+ .ub-al-hang__item { position: relative; display: grid; justify-items: center; padding-top: 70px; transform: rotate(calc((var(--i) * 2 - 1) * 4deg)); transition: transform 0.3s; }
103
+ .ub-al-hang__item:hover { transform: rotate(0deg) translateY(-4px); }
104
+ .ub-al-hang__ribbon { position: absolute; top: 0; left: 50%; width: 4px; height: 80px; margin-left: -2px; background: var(--al-red); box-shadow: 0 0 0 1px rgb(0 0 0 / 0.1); }
105
+ .ub-al-hang__ribbon::after { content: ""; position: absolute; top: -8px; left: -9px; width: 22px; height: 12px; border-radius: 50%; background: var(--al-red); }
106
+ .ub-al-frame { display: block; width: clamp(120px, 12vw, 170px); aspect-ratio: 4 / 5; padding: 8px 8px 28px; background: #fbf6ec; border-radius: 3px; box-shadow: 0 16px 30px rgb(0 0 0 / 0.35); }
107
+ .ub-al-frame img { width: 100%; height: 100%; object-fit: cover; }
108
+ .ub-al-sign {
109
+ position: relative;
110
+ display: grid;
111
+ gap: var(--space-3);
112
+ padding: clamp(28px, 4vw, 48px);
113
+ border-radius: 10px;
114
+ color: #fff6e8;
115
+ background:
116
+ repeating-linear-gradient(0deg, rgb(0 0 0 / 0.08) 0 2px, transparent 2px 22px),
117
+ repeating-linear-gradient(90deg, rgb(255 255 255 / 0.04) 0 1px, transparent 1px 7px),
118
+ linear-gradient(180deg, #7c4b2a, var(--al-wood) 55%, #54321b);
119
+ box-shadow: inset 0 0 0 6px rgb(0 0 0 / 0.12), 0 20px 40px rgb(0 0 0 / 0.35);
120
+ }
121
+ .ub-al-sign__eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: var(--al-gold); }
122
+ .ub-al-sign p { color: rgb(255 246 232 / 0.85); line-height: 25px; }
123
+ /* Neige posée sur le haut du panneau */
124
+ .ub-al-sign__snow { position: absolute; left: -6px; right: -6px; top: -10px; height: 22px; border-radius: 12px 12px 30px 30px / 12px 12px 20px 20px; background: #ffffff; box-shadow: 0 3px 6px rgb(0 0 0 / 0.15); }
125
+ .ub-al-h2 { font-family: var(--al-serif); font-weight: 400; font-size: clamp(30px, 3.6vw, 48px); line-height: 1.1; }
126
+ .ub-al-h2--script { font-family: var(--al-script); text-align: center; color: var(--al-red); margin-bottom: var(--space-12) !important; }
127
+ .ub-al-h2--light { color: #fff3cf; }
128
+
129
+ /* ---------- Section crème : polaroïds + étiquettes ---------- */
130
+ .ub-al-cream { padding: var(--space-16) 0; background: var(--al-cream); background-image: radial-gradient(rgb(0 0 0 / 0.035) 1px, transparent 1px); background-size: 14px 14px; }
131
+ .ub-al-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-12) var(--space-8); max-width: 980px; margin: 0 auto; padding: 0; list-style: none; }
132
+ .ub-al-item { display: grid; grid-template-columns: 170px minmax(0, 1fr); align-items: center; gap: var(--space-4); }
133
+ .ub-al-item:nth-child(even) { grid-template-columns: minmax(0, 1fr) 170px; }
134
+ .ub-al-item:nth-child(even) .ub-al-polaroid { order: 2; }
135
+ .ub-al-polaroid { position: relative; display: block; padding: 10px 10px 34px; background: #ffffff; box-shadow: 0 12px 26px rgb(43 27 20 / 0.22); transform: rotate(calc((var(--i) % 3 - 1) * 4deg)); transition: transform 0.3s; }
136
+ .ub-al-polaroid:hover { transform: rotate(0) scale(1.04); }
137
+ .ub-al-polaroid__img { position: relative; display: block; aspect-ratio: 1; overflow: hidden; background: var(--surface); }
138
+ .ub-al-polaroid__img img { width: 100%; height: 100%; object-fit: cover; }
139
+ .ub-al-polaroid__img .ub-ph { position: absolute; inset: 0; }
140
+ .ub-al-tape { position: absolute; top: -12px; left: 50%; z-index: 1; width: 70px; height: 24px; margin-left: -35px; background: rgb(236 214 170 / 0.8); transform: rotate(-4deg); box-shadow: 0 1px 2px rgb(0 0 0 / 0.1); }
141
+ .ub-al-label {
142
+ position: relative;
143
+ display: grid;
144
+ gap: 4px;
145
+ padding: 16px 20px;
146
+ color: #fff6e8;
147
+ background: var(--al-red);
148
+ /* Bords en papier déchiré */
149
+ clip-path: polygon(0% 8%, 4% 0%, 9% 6%, 15% 1%, 22% 7%, 30% 0%, 38% 6%, 46% 1%, 54% 7%, 62% 0%, 70% 6%, 78% 1%, 86% 7%, 93% 0%, 100% 6%, 99% 50%, 100% 94%, 94% 100%, 87% 94%, 80% 100%, 72% 93%, 64% 100%, 56% 94%, 48% 100%, 40% 93%, 32% 100%, 24% 94%, 16% 100%, 8% 93%, 0% 100%, 1% 50%);
150
+ transform: rotate(calc((var(--i) % 2) * 2deg - 1deg));
151
+ }
152
+ .ub-al-item:nth-child(4n + 2) .ub-al-label,
153
+ .ub-al-item:nth-child(4n + 3) .ub-al-label { background: var(--al-green); }
154
+ .ub-al-label b { font-size: 16px; line-height: 21px; }
155
+ .ub-al-label span { display: flex; align-items: baseline; gap: 8px; font-weight: 700; color: #f7dfa6; }
156
+ .ub-al-label s { font-size: 12px; font-weight: 500; color: rgb(255 246 232 / 0.6); }
157
+ .ub-al-label em { position: absolute; top: 10px; right: 14px; font-style: normal; font-size: 12px; font-weight: 800; color: #f7dfa6; }
158
+ .ub-al-more { display: flex; justify-content: center; margin-top: var(--space-12); }
159
+
160
+ /* ---------- Section rouge : ticket perforé ---------- */
161
+ .ub-al-red { padding: var(--space-16) 0; background: var(--al-red); background-image: radial-gradient(rgb(255 255 255 / 0.06) 1.5px, transparent 1.5px); background-size: 22px 22px; }
162
+ .ub-al-ticket {
163
+ display: grid;
164
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
165
+ max-width: 860px;
166
+ margin: 0 auto;
167
+ border-radius: 14px;
168
+ background: #fbf2df;
169
+ color: var(--ink);
170
+ box-shadow: 0 24px 50px rgb(0 0 0 / 0.3);
171
+ /* Encoches rondes de chaque côté de la perforation */
172
+ mask: radial-gradient(circle 14px at calc(58.3%) 0, transparent 98%, #000) top / 100% 51% no-repeat,
173
+ radial-gradient(circle 14px at calc(58.3%) 100%, transparent 98%, #000) bottom / 100% 51% no-repeat;
174
+ }
175
+ .ub-al-ticket__main { display: grid; gap: 6px; padding: clamp(24px, 4vw, 40px); border-right: 2px dashed rgb(43 27 20 / 0.25); }
176
+ .ub-al-ticket__main .ub-al-sign__eyebrow { color: var(--al-red); }
177
+ .ub-al-ticket__big { font-family: var(--al-serif); font-size: clamp(56px, 7vw, 92px); line-height: 1; color: var(--al-red); }
178
+ .ub-al-ticket__main p:last-child { color: var(--ink-muted); }
179
+ .ub-al-ticket__stub { display: grid; align-content: center; justify-items: center; gap: var(--space-4); padding: var(--space-6); }
180
+ .ub-al-ticket__stub .ub-countdown__c { background: var(--al-cream); border: 1px solid var(--border); color: var(--ink); box-shadow: none; }
181
+ .ub-al-ticket__stub .ub-al-btn { border-color: var(--al-red); background: var(--al-red); color: #fff6e8; }
182
+
183
+ body:has(.ub-album) .ub-footcta { display: none; }
184
+
185
+ @media (max-width: 900px) {
186
+ .ub-al-hero { min-height: 0; }
187
+ .ub-al-story__in { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
188
+ .ub-al-list { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
189
+ .ub-al-item, .ub-al-item:nth-child(even) { grid-template-columns: 130px minmax(0, 1fr); }
190
+ .ub-al-item:nth-child(even) .ub-al-polaroid { order: 0; }
191
+ .ub-al-ticket { grid-template-columns: minmax(0, 1fr); mask: none; }
192
+ .ub-al-ticket__main { border-right: 0; border-bottom: 2px dashed rgb(43 27 20 / 0.25); }
193
+ }
194
+ @media (prefers-reduced-motion: reduce) {
195
+ .ub-al-title { animation: none; }
196
+ }
197
+
198
+ /* Pied de page des thèmes Noël : fond foncé du thème, texte crème (l'ancienne règle « plus sombre que la page »
199
+ donnait un gris illisible sur le fond crème). */
200
+ [data-theme="noel"]:not([data-variant]) .ub-footwrap,
201
+ [data-variant="noel-2"] .ub-footwrap { --foot-bg: var(--al-red-dark); --foot-ink: #fff6e8; --cta-bg: var(--al-red); --cta-ink: #fff6e8; }
202
+ [data-theme="noel"]:not([data-variant]) .ub-footwrap .ub-footer,
203
+ [data-variant="noel-2"] .ub-footwrap .ub-footer { border: 0; }
204
+ [data-theme="noel"]:not([data-variant]) .ub-footwrap .ub-footer .ub-logo__mark,
205
+ [data-variant="noel-2"] .ub-footwrap .ub-footer .ub-logo__mark { background: var(--al-gold); }