added on local at 2026-07-01 21:47:11
| 1 | package MODS::RePricer::Layouts; | |
| 2 | #====================================================================== | |
| 3 | # RePricer — Storefront Layouts registry. | |
| 4 | # | |
| 5 | # A "layout" is the HTML structure of a storefront page (hero shape, | |
| 6 | # product grid style, where about/reviews sit). Layouts are paired | |
| 7 | # independently with color "themes" (in MODS::RePricer::Themes), so any | |
| 8 | # layout can wear any color palette. | |
| 9 | # | |
| 10 | # To add a layout: | |
| 11 | # 1. Drop a template file in TEMPLATES/store_layouts/<slug>.html | |
| 12 | # (it receives the same tvars as any other store template). | |
| 13 | # 2. Add a hash to @LAYOUTS below — copy any existing block. | |
| 14 | # 3. Done. Picker updates, no DB migration, no other code changes. | |
| 15 | # | |
| 16 | # WHAT EACH FIELD DOES: | |
| 17 | # id — number. MUST be unique. Used by storefronts.layout_id. | |
| 18 | # slug — short lowercase name. Doubles as the template filename | |
| 19 | # prefix (TEMPLATES/store_layouts/<slug>.html). | |
| 20 | # name — what creators see in the picker. | |
| 21 | # tagline — one-line description of the vibe / who it's for. | |
| 22 | # best_for — short hint for the creator ("for: visual sellers"). | |
| 23 | # thumbnail — name of a static thumbnail SVG/preview helper. | |
| 24 | # Currently we render it inline via thumbnail_html(). | |
| 25 | #====================================================================== | |
| 26 | use strict; | |
| 27 | use warnings; | |
| 28 | ||
| 29 | #---------------------------------------------------------------------- | |
| 30 | # Layouts — add new ones here. | |
| 31 | #---------------------------------------------------------------------- | |
| 32 | our @LAYOUTS = ( | |
| 33 | { | |
| 34 | id => 1, | |
| 35 | slug => 'catalog', | |
| 36 | name => 'Catalog Classic', | |
| 37 | tagline => 'Hero with floating product cards, big grid, about strip, reviews.', | |
| 38 | best_for => 'Most sellers. The safe, balanced default.', | |
| 39 | }, | |
| 40 | { | |
| 41 | id => 2, | |
| 42 | slug => 'lookbook', | |
| 43 | name => 'Lookbook', | |
| 44 | tagline => 'Magazine-style. Large alternating images with caption text.', | |
| 45 | best_for => 'Visually-driven sellers. Art prints, sculpture, terrain.', | |
| 46 | }, | |
| 47 | { | |
| 48 | id => 3, | |
| 49 | slug => 'spotlight', | |
| 50 | name => 'Single Spotlight', | |
| 51 | tagline => 'One huge featured product up top, smaller catalog below.', | |
| 52 | best_for => 'Sellers with a flagship, pre-orders, or weekly drops.', | |
| 53 | }, | |
| 54 | { | |
| 55 | id => 4, | |
| 56 | slug => 'storyteller', | |
| 57 | name => 'Storyteller', | |
| 58 | tagline => 'About-the-maker section first. Studio photo + bio, then catalog.', | |
| 59 | best_for => 'Premium artisans, eBay-style sellers, brand-driven shops.', | |
| 60 | }, | |
| 61 | { | |
| 62 | id => 5, | |
| 63 | slug => 'workshop', | |
| 64 | name => 'Dense Workshop', | |
| 65 | tagline => 'Compact, technical, lots of products visible at once. Sparse decoration.', | |
| 66 | best_for => 'Functional / utility prints. Prolific sellers with deep catalogs.', | |
| 67 | }, | |
| 68 | { | |
| 69 | id => 6, | |
| 70 | slug => 'gallery', | |
| 71 | name => 'Gallery Wall', | |
| 72 | tagline => 'Masonry image grid. Minimal text per card, hover reveals details.', | |
| 73 | best_for => 'Image-first sellers. Scenery, dioramas, big variety.', | |
| 74 | }, | |
| 75 | { | |
| 76 | id => 7, | |
| 77 | slug => 'marketplace', | |
| 78 | name => 'Marketplace', | |
| 79 | tagline => 'Sidebar filters + dense product grid. Amazon / Etsy shop feel.', | |
| 80 | best_for => 'Big catalogs with many categories. Buyers who want to filter.', | |
| 81 | }, | |
| 82 | { | |
| 83 | id => 8, | |
| 84 | slug => 'boutique', | |
| 85 | name => 'Boutique', | |
| 86 | tagline => 'Premium minimalism. Wide whitespace, big imagery, refined typography.', | |
| 87 | best_for => 'High-end art, sculpture, fine-craft makers. Limited drops.', | |
| 88 | }, | |
| 89 | { | |
| 90 | id => 9, | |
| 91 | slug => 'bento', | |
| 92 | name => 'Bento', | |
| 93 | tagline => 'Mixed-size tiles like a bento box. Modern startup vibe.', | |
| 94 | best_for => 'Variety-driven brands. Tech accessories, varied product lines.', | |
| 95 | }, | |
| 96 | { | |
| 97 | id => 10, | |
| 98 | slug => 'megastore', | |
| 99 | name => 'Megastore', | |
| 100 | tagline => 'Horizontal scrolling rows by category. Netflix-style browsing.', | |
| 101 | best_for => 'Huge catalogs, multiple lines, themed weekly drops.', | |
| 102 | }, | |
| 103 | { | |
| 104 | id => 11, | |
| 105 | slug => 'index', | |
| 106 | name => 'Index', | |
| 107 | tagline => 'Detailed table list. Sortable columns: name, type, price, rating.', | |
| 108 | best_for => 'Buyers who want scannable data. Technical / functional prints.', | |
| 109 | }, | |
| 110 | { | |
| 111 | id => 12, | |
| 112 | slug => 'postcard', | |
| 113 | name => 'Postcard', | |
| 114 | tagline => 'Zine / handmade vibe. Decorative borders, scrappy energy.', | |
| 115 | best_for => 'Indie creators, niche fandoms, kawaii / cute prints.', | |
| 116 | }, | |
| 117 | { | |
| 118 | id => 13, | |
| 119 | slug => 'blueprint', | |
| 120 | name => 'Blueprint', | |
| 121 | tagline => 'Technical drawing aesthetic. Grid background, callout annotations.', | |
| 122 | best_for => 'Engineering parts, mech replicas, technical miniatures.', | |
| 123 | }, | |
| 124 | { | |
| 125 | id => 14, | |
| 126 | slug => 'dropcal', | |
| 127 | name => 'Drop Calendar', | |
| 128 | tagline => 'Timeline of releases. Weekly schedule, dated drops.', | |
| 129 | best_for => 'Subscription / eBay-style drops, episodic releases.', | |
| 130 | }, | |
| 131 | { | |
| 132 | id => 15, | |
| 133 | slug => 'brutalist', | |
| 134 | name => 'Brutalist', | |
| 135 | tagline => 'Raw web. System fonts, no decoration, monochrome, intentional.', | |
| 136 | best_for => 'Retro / Y2K aesthetic, programmer-friendly, anti-design.', | |
| 137 | }, | |
| 138 | { | |
| 139 | id => 16, | |
| 140 | slug => 'console', | |
| 141 | name => 'Console', | |
| 142 | tagline => 'Sci-fi HUD dashboard. Status bars, hex panels, gaming overlays.', | |
| 143 | best_for => 'Cyberpunk / mecha / gaming creators. Tech-forward brands.', | |
| 144 | }, | |
| 145 | { | |
| 146 | id => 17, | |
| 147 | slug => 'reel', | |
| 148 | name => 'Reel', | |
| 149 | tagline => 'Full-page vertical scroll. One product per snap, story-style.', | |
| 150 | best_for => 'Limited drops, cinematic showcase, mobile-first audiences.', | |
| 151 | }, | |
| 152 | { | |
| 153 | id => 18, | |
| 154 | slug => 'magazine', | |
| 155 | name => 'Magazine', | |
| 156 | tagline => 'Multi-column editorial grid. Mixed sizes, pull-quotes, awards-style.', | |
| 157 | best_for => 'Creators with mixed content (designs + write-ups + features).', | |
| 158 | }, | |
| 159 | { | |
| 160 | id => 19, | |
| 161 | slug => 'tabs', | |
| 162 | name => 'Tabs', | |
| 163 | tagline => 'Categorized tabbed sections. Click between collections.', | |
| 164 | best_for => 'Sellers with distinct lines, campaigns, or seasonal collections.', | |
| 165 | }, | |
| 166 | { | |
| 167 | id => 20, | |
| 168 | slug => 'cinema', | |
| 169 | name => 'Cinema', | |
| 170 | tagline => 'Movie poster cards. Featured products as theatrical releases.', | |
| 171 | best_for => 'Dramatic / fantasy / character-driven sellers. Big personalities.', | |
| 172 | }, | |
| 173 | { | |
| 174 | id => 21, | |
| 175 | slug => 'splash', | |
| 176 | name => 'Splash', | |
| 177 | tagline => 'Bold landing page. One huge CTA above the fold, products below.', | |
| 178 | best_for => 'Launches, hype drops, single-purpose campaigns.', | |
| 179 | }, | |
| 180 | { | |
| 181 | id => 22, | |
| 182 | slug => 'sidebar', | |
| 183 | name => 'Sidebar', | |
| 184 | tagline => 'Persistent left nav. Linear / Figma feel with content panels.', | |
| 185 | best_for => 'Detailed catalogs, deep navigation, tech-savvy buyers.', | |
| 186 | }, | |
| 187 | { | |
| 188 | id => 23, | |
| 189 | slug => 'forum', | |
| 190 | name => 'Forum', | |
| 191 | tagline => 'Discussion-style feed. Each design is a thread with votes and replies.', | |
| 192 | best_for => 'Community-driven creators, eBay-style audiences.', | |
| 193 | }, | |
| 194 | { | |
| 195 | id => 24, | |
| 196 | slug => 'holo', | |
| 197 | name => 'Holo', | |
| 198 | tagline => 'Glassmorphism panels. Frosted blur, neon edges, holographic accents.', | |
| 199 | best_for => 'Futurist / sci-fi / sci-fantasy. Glow-heavy creators.', | |
| 200 | }, | |
| 201 | { | |
| 202 | id => 25, | |
| 203 | slug => 'concept', | |
| 204 | name => 'Concept', | |
| 205 | tagline => 'Full-bleed concept posters. Dramatic image takeovers, minimal chrome.', | |
| 206 | best_for => 'Cinematic showcase pieces. Big set-pieces, dioramas, dragons.', | |
| 207 | }, | |
| 208 | { | |
| 209 | id => 26, | |
| 210 | slug => 'pinterest', | |
| 211 | name => 'Pinterest', | |
| 212 | tagline => 'Masonry grid with always-visible captions. Save-and-share feel.', | |
| 213 | best_for => 'Browsable showcases, wishlist culture, idea-board sellers.', | |
| 214 | }, | |
| 215 | { | |
| 216 | id => 27, | |
| 217 | slug => 'newsstand', | |
| 218 | name => 'Newsstand', | |
| 219 | tagline => 'Newspaper columns with headlines, bylines, and pull quotes.', | |
| 220 | best_for => 'Long-form storytellers, behind-the-scenes write-ups, editorial.', | |
| 221 | }, | |
| 222 | { | |
| 223 | id => 28, | |
| 224 | slug => 'manifesto', | |
| 225 | name => 'Manifesto', | |
| 226 | tagline => 'A bold statement fills the page. Products are footnotes to the message.', | |
| 227 | best_for => 'Brand-led creators with strong points of view. Activist makers.', | |
| 228 | }, | |
| 229 | { | |
| 230 | id => 29, | |
| 231 | slug => 'tcg', | |
| 232 | name => 'Trading Cards', | |
| 233 | tagline => 'TCG-style cards with stat blocks, rarity badges, mana costs.', | |
| 234 | best_for => 'Game / character / monster designs. Card-game inspired sellers.', | |
| 235 | }, | |
| 236 | { | |
| 237 | id => 30, | |
| 238 | slug => 'vinyl', | |
| 239 | name => 'Vinyl', | |
| 240 | tagline => 'Square cards like record sleeves. Album-art aesthetic, B-side details.', | |
| 241 | best_for => 'Music-adjacent / sample-pack vibe. Curated drop sellers.', | |
| 242 | }, | |
| 243 | { | |
| 244 | id => 31, | |
| 245 | slug => 'comic', | |
| 246 | name => 'Comic', | |
| 247 | tagline => 'Comic book panels with speech bubbles and action callouts. POW!', | |
| 248 | best_for => 'Action figures, hero designs, kids/family-friendly catalogs.', | |
| 249 | }, | |
| 250 | { | |
| 251 | id => 32, | |
| 252 | slug => 'festival', | |
| 253 | name => 'Festival', | |
| 254 | tagline => 'Music-festival poster aesthetic. Stacked huge type, lineup feel.', | |
| 255 | best_for => 'Drops as events. Limited release campaigns, tour-style launches.', | |
| 256 | }, | |
| 257 | { | |
| 258 | id => 33, | |
| 259 | slug => 'periodic', | |
| 260 | name => 'Periodic', | |
| 261 | tagline => 'Periodic-table grid. Each design is an element with symbol + number.', | |
| 262 | best_for => 'Encyclopedic catalogs. Collectible-set sellers (every-figure-numbered).', | |
| 263 | }, | |
| 264 | { | |
| 265 | id => 34, | |
| 266 | slug => 'diary', | |
| 267 | name => 'Diary', | |
| 268 | tagline => 'Dated journal entries. Each design is a date-stamped log post.', | |
| 269 | best_for => 'Process-driven creators, work-in-progress streams, build journals.', | |
| 270 | }, | |
| 271 | { | |
| 272 | id => 35, | |
| 273 | slug => 'stage', | |
| 274 | name => 'Stage', | |
| 275 | tagline => 'Theatre stage with curtain frames and a spotlight on the featured.', | |
| 276 | best_for => 'Dramatic showcase, character makers, performance/cosplay creators.', | |
| 277 | }, | |
| 278 | { | |
| 279 | id => 36, | |
| 280 | slug => 'receipt', | |
| 281 | name => 'Receipt', | |
| 282 | tagline => 'Long thermal receipt. Each design is a line item with price tag.', | |
| 283 | best_for => 'Indie / handmade / quirky creators. Retro thermal aesthetic.', | |
| 284 | }, | |
| 285 | ); | |
| 286 | ||
| 287 | #---------------------------------------------------------------------- | |
| 288 | # OO interface | |
| 289 | #---------------------------------------------------------------------- | |
| 290 | sub new { return bless({}, shift) } | |
| 291 | ||
| 292 | sub all { | |
| 293 | return [ map { { %$_ } } @LAYOUTS ]; | |
| 294 | } | |
| 295 | ||
| 296 | # Look up a layout by id. Falls back to layout #1 (Catalog Classic) so | |
| 297 | # the storefront never renders without a known template. | |
| 298 | sub by_id { | |
| 299 | my ($self, $id) = @_; | |
| 300 | $id ||= 0; | |
| 301 | $id =~ s/[^0-9]//g; | |
| 302 | foreach my $L (@LAYOUTS) { | |
| 303 | return { %$L } if $L->{id} == $id; | |
| 304 | } | |
| 305 | return { %{ $LAYOUTS[0] } }; | |
| 306 | } | |
| 307 | ||
| 308 | # Resolve a layout id → template filename for store.cgi. | |
| 309 | sub template_for { | |
| 310 | my ($self, $id) = @_; | |
| 311 | my $L = $self->by_id($id); | |
| 312 | return "store_layouts/$L->{slug}.html"; | |
| 313 | } | |
| 314 | ||
| 315 | # Inline mock-up thumbnail for the layout picker. Renders the layout's | |
| 316 | # shape using the supplied theme's actual colors + sample images so the | |
| 317 | # creator can see roughly what their storefront will look like with that | |
| 318 | # layout/theme combo before clicking Preview. | |
| 319 | # | |
| 320 | # Pass the creator's currently-selected theme (a hash from | |
| 321 | # MODS::RePricer::Themes->by_id) so the picker shows themed previews. | |
| 322 | sub thumbnail_html { | |
| 323 | my ($self, $layout, $theme) = @_; | |
| 324 | $layout ||= $LAYOUTS[0]; | |
| 325 | my $slug = $layout->{slug}; | |
| 326 | ||
| 327 | # Build a palette hash once and pass to every sketch. Falls back to a | |
| 328 | # neutral dark palette if no theme was passed (older callers). | |
| 329 | my $p = _theme_palette($theme); | |
| 330 | ||
| 331 | my $sketches = { | |
| 332 | catalog => _sketch_catalog($p), | |
| 333 | lookbook => _sketch_lookbook($p), | |
| 334 | spotlight => _sketch_spotlight($p), | |
| 335 | storyteller => _sketch_storyteller($p), | |
| 336 | workshop => _sketch_workshop($p), | |
| 337 | gallery => _sketch_gallery($p), | |
| 338 | marketplace => _sketch_marketplace($p), | |
| 339 | boutique => _sketch_boutique($p), | |
| 340 | bento => _sketch_bento($p), | |
| 341 | megastore => _sketch_megastore($p), | |
| 342 | index => _sketch_index($p), | |
| 343 | postcard => _sketch_postcard($p), | |
| 344 | blueprint => _sketch_blueprint($p), | |
| 345 | dropcal => _sketch_dropcal($p), | |
| 346 | brutalist => _sketch_brutalist($p), | |
| 347 | console => _sketch_console($p), | |
| 348 | reel => _sketch_reel($p), | |
| 349 | magazine => _sketch_magazine($p), | |
| 350 | tabs => _sketch_tabs($p), | |
| 351 | cinema => _sketch_cinema($p), | |
| 352 | splash => _sketch_splash($p), | |
| 353 | sidebar => _sketch_sidebar($p), | |
| 354 | forum => _sketch_forum($p), | |
| 355 | holo => _sketch_holo($p), | |
| 356 | concept => _sketch_concept($p), | |
| 357 | pinterest => _sketch_pinterest($p), | |
| 358 | newsstand => _sketch_newsstand($p), | |
| 359 | manifesto => _sketch_manifesto($p), | |
| 360 | tcg => _sketch_tcg($p), | |
| 361 | vinyl => _sketch_vinyl($p), | |
| 362 | comic => _sketch_comic($p), | |
| 363 | festival => _sketch_festival($p), | |
| 364 | periodic => _sketch_periodic($p), | |
| 365 | diary => _sketch_diary($p), | |
| 366 | stage => _sketch_stage($p), | |
| 367 | receipt => _sketch_receipt($p), | |
| 368 | }; | |
| 369 | return $sketches->{$slug} || _sketch_catalog($p); | |
| 370 | } | |
| 371 | ||
| 372 | # Convert a theme hash into a palette ready for the sketch helpers. | |
| 373 | # Adds derived values (accent gradient, sample image URLs) so each | |
| 374 | # sketch can stay declarative. | |
| 375 | sub _theme_palette { | |
| 376 | my ($theme) = @_; | |
| 377 | $theme ||= {}; | |
| 378 | my $bg = $theme->{bg} || '#0b1020'; | |
| 379 | my $bg_2 = $theme->{bg_2} || '#141a2e'; | |
| 380 | my $surface = $theme->{surface} || '#1e293b'; | |
| 381 | my $border = $theme->{border} || '#334155'; | |
| 382 | my $text = $theme->{text} || '#cbd5e1'; | |
| 383 | my $text_2 = $theme->{text_2} || '#64748b'; | |
| 384 | my $accent = $theme->{accent} || '#0f766e'; | |
| 385 | my $accent_2 = $theme->{accent_2} || '#059669'; | |
| 386 | return { | |
| 387 | bg => $bg, | |
| 388 | bg_2 => $bg_2, | |
| 389 | surface => $surface, | |
| 390 | border => $border, | |
| 391 | text => $text, | |
| 392 | text_2 => $text_2, | |
| 393 | accent => $accent, | |
| 394 | accent_2 => $accent_2, | |
| 395 | gradient => "linear-gradient(135deg, $accent 0%, $accent_2 100%)", | |
| 396 | # Three reference images cycled through for visual variety. Files | |
| 397 | # ship in /assets/images/ — these particular ones are the strong | |
| 398 | # tabletop minis renders that read well at thumbnail size. | |
| 399 | img_a => "/assets/images/4.webp", | |
| 400 | img_b => "/assets/images/9.webp", | |
| 401 | img_c => "/assets/images/14.webp", | |
| 402 | img_d => "/assets/images/22.webp", | |
| 403 | }; | |
| 404 | } | |
| 405 | ||
| 406 | #---------------------------------------------------------------------- | |
| 407 | # Sketch helpers -- each returns a tiny HTML thumbnail showing the | |
| 408 | # shape of the layout, rendered in the supplied theme's actual colors | |
| 409 | # and including a few real reference images so the picker functions as | |
| 410 | # a real preview. | |
| 411 | #---------------------------------------------------------------------- | |
| 412 | ||
| 413 | sub _bar { | |
| 414 | my ($w, $color, $extra) = @_; | |
| 415 | $extra ||= ''; | |
| 416 | return qq~<span style="display:inline-block;height:5px;width:$w;background:$color;border-radius:2px;$extra"></span>~; | |
| 417 | } | |
| 418 | ||
| 419 | sub _wrap { | |
| 420 | my ($p, $inner) = @_; | |
| 421 | return qq~<div style="background:$p->{bg};height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:6px;font-family:system-ui,sans-serif">$inner</div>~; | |
| 422 | } | |
| 423 | ||
| 424 | sub _header_strip { | |
| 425 | my ($p) = @_; | |
| 426 | return qq~ | |
| 427 | <div style="display:flex;align-items:center;gap:6px;padding:5px 6px;background:$p->{bg_2};border-radius:3px;border-bottom:1px solid $p->{border}"> | |
| 428 | <span style="width:12px;height:12px;background:$p->{gradient};border-radius:3px"></span> | |
| 429 | @{[ _bar('30px', $p->{text}) ]} | |
| 430 | <span style="margin-left:auto;display:flex;gap:4px"> | |
| 431 | @{[ _bar('12px', $p->{text_2}) ]} | |
| 432 | @{[ _bar('12px', $p->{text_2}) ]} | |
| 433 | @{[ _bar('12px', $p->{text_2}) ]} | |
| 434 | </span> | |
| 435 | </div>~; | |
| 436 | } | |
| 437 | ||
| 438 | # Helper: a tiny image tile. Defaults to a square (aspect-ratio:1) so the | |
| 439 | # images don't get crushed into thin strips. Pass a height explicitly only | |
| 440 | # when the layout shape genuinely calls for it (hero strips, list rows). | |
| 441 | sub _tile { | |
| 442 | my ($p, $img, $h) = @_; | |
| 443 | my $sizing = $h ? "height:$h" : "aspect-ratio:1"; | |
| 444 | return qq~<div style="background:url('$img') center/cover;border-radius:3px;border:1px solid $p->{border};$sizing"></div>~; | |
| 445 | } | |
| 446 | ||
| 447 | # 1) Catalog Classic: hero text + 3-col image grid | |
| 448 | sub _sketch_catalog { | |
| 449 | my ($p) = @_; | |
| 450 | my $h = _header_strip($p); | |
| 451 | my $hero = qq~ | |
| 452 | <div style="display:grid;grid-template-columns:1.2fr 1fr;gap:6px;padding:4px 2px"> | |
| 453 | <div style="display:flex;flex-direction:column;gap:4px;justify-content:center"> | |
| 454 | @{[ _bar('80%', $p->{text}) ]} | |
| 455 | @{[ _bar('60%', $p->{accent}) ]} | |
| 456 | @{[ _bar('30px', $p->{gradient}, 'margin-top:6px;height:10px') ]} | |
| 457 | </div> | |
| 458 | <div style="display:flex;gap:3px"> | |
| 459 | @{[ _tile($p, $p->{img_a}) ]} | |
| 460 | @{[ _tile($p, $p->{img_b}) ]} | |
| 461 | </div> | |
| 462 | </div>~; | |
| 463 | my $grid = qq~ | |
| 464 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-top:auto"> | |
| 465 | @{[ _tile($p, $p->{img_a}) ]} | |
| 466 | @{[ _tile($p, $p->{img_b}) ]} | |
| 467 | @{[ _tile($p, $p->{img_c}) ]} | |
| 468 | </div>~; | |
| 469 | return _wrap($p, "$h$hero$grid"); | |
| 470 | } | |
| 471 | ||
| 472 | # 2) Lookbook: editorial cover (text + big image) then alternating rows | |
| 473 | sub _sketch_lookbook { | |
| 474 | my ($p) = @_; | |
| 475 | my $h = _header_strip($p); | |
| 476 | # Editorial cover: little eyebrow + big title bars on left, large image right | |
| 477 | my $cover = qq~ | |
| 478 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:2px 0"> | |
| 479 | <div style="display:flex;flex-direction:column;gap:4px;justify-content:center"> | |
| 480 | @{[ _bar('40%', $p->{accent}, 'height:3px') ]} | |
| 481 | @{[ _bar('95%', $p->{text}, 'height:9px') ]} | |
| 482 | @{[ _bar('70%', $p->{text}, 'height:9px') ]} | |
| 483 | @{[ _bar('30px', $p->{gradient}, 'margin-top:5px;height:9px') ]} | |
| 484 | </div> | |
| 485 | @{[ _tile($p, $p->{img_a}, '78px') ]} | |
| 486 | </div>~; | |
| 487 | # One alternating-row teaser to indicate the magazine flow continues | |
| 488 | my $row = qq~ | |
| 489 | <div style="display:grid;grid-template-columns:1fr 1.4fr;gap:5px;margin-top:auto"> | |
| 490 | <div style="display:flex;flex-direction:column;gap:4px;justify-content:center"> | |
| 491 | @{[ _bar('90%', $p->{text}) ]} | |
| 492 | @{[ _bar('60%', $p->{accent}) ]} | |
| 493 | </div> | |
| 494 | @{[ _tile($p, $p->{img_b}, '40px') ]} | |
| 495 | </div>~; | |
| 496 | return _wrap($p, "$h$cover$row"); | |
| 497 | } | |
| 498 | ||
| 499 | # 3) Spotlight: one huge product, then a strip of small ones | |
| 500 | sub _sketch_spotlight { | |
| 501 | my ($p) = @_; | |
| 502 | my $h = _header_strip($p); | |
| 503 | my $hero = qq~<div style="background:url('$p->{img_a}') center/cover;border-radius:3px;height:80px;display:flex;align-items:flex-end;padding:6px;border:1px solid $p->{border}"><span style="height:8px;width:60%;background:$p->{gradient};border-radius:2px"></span></div>~; | |
| 504 | my $strip = qq~ | |
| 505 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:3px;margin-top:auto"> | |
| 506 | @{[ _tile($p, $p->{img_b}) ]} | |
| 507 | @{[ _tile($p, $p->{img_c}) ]} | |
| 508 | @{[ _tile($p, $p->{img_d}) ]} | |
| 509 | @{[ _tile($p, $p->{img_a}) ]} | |
| 510 | </div>~; | |
| 511 | return _wrap($p, "$h$hero$strip"); | |
| 512 | } | |
| 513 | ||
| 514 | # 4) Storyteller: portrait + bio, then small grid | |
| 515 | sub _sketch_storyteller { | |
| 516 | my ($p) = @_; | |
| 517 | my $h = _header_strip($p); | |
| 518 | my $about = qq~ | |
| 519 | <div style="display:grid;grid-template-columns:1fr 1.4fr;gap:6px;padding:2px 0"> | |
| 520 | <div style="background:url('$p->{img_d}') center/cover;border-radius:50%;height:46px;width:46px;align-self:center;justify-self:center;border:2px solid $p->{accent}"></div> | |
| 521 | <div style="display:flex;flex-direction:column;gap:3px;justify-content:center"> | |
| 522 | @{[ _bar('90%', $p->{text}) ]} | |
| 523 | @{[ _bar('70%', $p->{accent}) ]} | |
| 524 | @{[ _bar('60%', $p->{text_2}) ]} | |
| 525 | </div> | |
| 526 | </div>~; | |
| 527 | my $grid = qq~ | |
| 528 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-top:auto"> | |
| 529 | @{[ _tile($p, $p->{img_a}) ]} | |
| 530 | @{[ _tile($p, $p->{img_b}) ]} | |
| 531 | @{[ _tile($p, $p->{img_c}) ]} | |
| 532 | </div>~; | |
| 533 | return _wrap($p, "$h$about$grid"); | |
| 534 | } | |
| 535 | ||
| 536 | # 5) Dense Workshop: dense 5-col compact grid of small images | |
| 537 | sub _sketch_workshop { | |
| 538 | my ($p) = @_; | |
| 539 | my $h = _header_strip($p); | |
| 540 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 541 | my $row = sub { | |
| 542 | join('', map { _tile($p, $imgs->[$_ % 4]) } (0..4)); | |
| 543 | }; | |
| 544 | my $rows = ''; | |
| 545 | for (1..2) { | |
| 546 | $rows .= qq~<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">@{[ $row->() ]}</div>~; | |
| 547 | } | |
| 548 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:2px;margin-top:2px\">$rows</div>"); | |
| 549 | } | |
| 550 | ||
| 551 | # 6) Gallery Wall: masonry varied heights | |
| 552 | sub _sketch_gallery { | |
| 553 | my ($p) = @_; | |
| 554 | my $h = _header_strip($p); | |
| 555 | my $grid = qq~ | |
| 556 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-top:auto"> | |
| 557 | <div style="display:flex;flex-direction:column;gap:3px"> | |
| 558 | @{[ _tile($p, $p->{img_a}, '30px') ]} | |
| 559 | @{[ _tile($p, $p->{img_b}, '18px') ]} | |
| 560 | </div> | |
| 561 | <div style="display:flex;flex-direction:column;gap:3px"> | |
| 562 | @{[ _tile($p, $p->{img_c}, '18px') ]} | |
| 563 | @{[ _tile($p, $p->{img_d}, '30px') ]} | |
| 564 | </div> | |
| 565 | <div style="display:flex;flex-direction:column;gap:3px"> | |
| 566 | @{[ _tile($p, $p->{img_b}, '24px') ]} | |
| 567 | @{[ _tile($p, $p->{img_a}, '24px') ]} | |
| 568 | </div> | |
| 569 | </div>~; | |
| 570 | return _wrap($p, "$h$grid"); | |
| 571 | } | |
| 572 | ||
| 573 | # 7) Marketplace: sidebar filters + grid of images | |
| 574 | sub _sketch_marketplace { | |
| 575 | my ($p) = @_; | |
| 576 | my $h = _header_strip($p); | |
| 577 | my $body = qq~ | |
| 578 | <div style="display:grid;grid-template-columns:34px 1fr;gap:4px;margin-top:auto;flex:1"> | |
| 579 | <div style="background:$p->{bg_2};border:1px solid $p->{border};border-radius:3px;display:flex;flex-direction:column;gap:4px;padding:4px"> | |
| 580 | @{[ _bar('100%', $p->{accent}) ]} | |
| 581 | @{[ _bar('80%', $p->{text_2}) ]} | |
| 582 | @{[ _bar('70%', $p->{text_2}) ]} | |
| 583 | @{[ _bar('90%', $p->{text_2}) ]} | |
| 584 | </div> | |
| 585 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px"> | |
| 586 | @{[ _tile($p, $p->{img_a}) ]} | |
| 587 | @{[ _tile($p, $p->{img_b}) ]} | |
| 588 | @{[ _tile($p, $p->{img_c}) ]} | |
| 589 | @{[ _tile($p, $p->{img_d}) ]} | |
| 590 | @{[ _tile($p, $p->{img_a}) ]} | |
| 591 | @{[ _tile($p, $p->{img_b}) ]} | |
| 592 | </div> | |
| 593 | </div>~; | |
| 594 | return _wrap($p, "$h$body"); | |
| 595 | } | |
| 596 | ||
| 597 | # 8) Boutique: centered editorial title, then airy 2-col catalog grid | |
| 598 | sub _sketch_boutique { | |
| 599 | my ($p) = @_; | |
| 600 | my $h = _header_strip($p); | |
| 601 | # Centered editorial title with eyebrow | |
| 602 | my $cover = qq~ | |
| 603 | <div style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 0"> | |
| 604 | @{[ _bar('22%', $p->{accent}, 'height:3px') ]} | |
| 605 | @{[ _bar('60%', $p->{text}, 'height:11px') ]} | |
| 606 | @{[ _bar('40%', $p->{text}, 'height:11px') ]} | |
| 607 | </div>~; | |
| 608 | # 2-col airy grid below (real layout's defining trait is the wide gutter) | |
| 609 | my $grid = qq~ | |
| 610 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:auto;padding:0 14px"> | |
| 611 | @{[ _tile($p, $p->{img_a}) ]} | |
| 612 | @{[ _tile($p, $p->{img_b}) ]} | |
| 613 | </div>~; | |
| 614 | return _wrap($p, "$h$cover$grid"); | |
| 615 | } | |
| 616 | ||
| 617 | # 9) Bento: mixed-size image tiles | |
| 618 | sub _sketch_bento { | |
| 619 | my ($p) = @_; | |
| 620 | my $h = _header_strip($p); | |
| 621 | my $body = qq~ | |
| 622 | <div style="display:grid;grid-template-columns:1.4fr 1fr 1fr;grid-template-rows:1fr 1fr;gap:3px;flex:1;margin-top:4px"> | |
| 623 | <div style="background:url('$p->{img_a}') center/cover;border-radius:3px;grid-row:1 / span 2;border:1px solid $p->{border}"></div> | |
| 624 | <div style="background:url('$p->{img_b}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 625 | <div style="background:url('$p->{img_c}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 626 | <div style="background:url('$p->{img_d}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 627 | <div style="background:$p->{gradient};border-radius:3px"></div> | |
| 628 | </div>~; | |
| 629 | return _wrap($p, "$h$body"); | |
| 630 | } | |
| 631 | ||
| 632 | # 10) Megastore: big billboard hero + horizontal rows of small tiles | |
| 633 | sub _sketch_megastore { | |
| 634 | my ($p) = @_; | |
| 635 | my $h = _header_strip($p); | |
| 636 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 637 | my $row = sub { | |
| 638 | join('', map { _tile($p, $imgs->[$_ % 4]) } (0..4)); | |
| 639 | }; | |
| 640 | # Big billboard featuring the first product (matches real layout's hero) | |
| 641 | my $billboard = qq~ | |
| 642 | <div style="background:url('$p->{img_a}') center/cover;height:60px;border-radius:3px;border:1px solid $p->{border};display:flex;align-items:flex-end;padding:5px;margin-bottom:4px"> | |
| 643 | <div style="display:flex;flex-direction:column;gap:3px"> | |
| 644 | @{[ _bar('60px', $p->{text}, 'height:7px') ]} | |
| 645 | @{[ _bar('40px', $p->{gradient}, 'height:7px;border-radius:2px') ]} | |
| 646 | </div> | |
| 647 | </div>~; | |
| 648 | my $body = qq~ | |
| 649 | <div style="display:flex;flex-direction:column;gap:4px;flex:1"> | |
| 650 | @{[ _bar('30%', $p->{text}, 'margin-bottom:1px') ]} | |
| 651 | <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">@{[ $row->() ]}</div> | |
| 652 | </div>~; | |
| 653 | return _wrap($p, "$h$billboard$body"); | |
| 654 | } | |
| 655 | ||
| 656 | # 11) Index: rows of detail with thumbnails | |
| 657 | sub _sketch_index { | |
| 658 | my ($p) = @_; | |
| 659 | my $h = _header_strip($p); | |
| 660 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}, $p->{img_a}]; | |
| 661 | my $rows = ''; | |
| 662 | for my $i (0..4) { | |
| 663 | $rows .= qq~<div style="display:flex;gap:4px;padding:2px 4px;border-bottom:1px solid $p->{border};align-items:center"> | |
| 664 | <div style="width:8px;height:8px;background:url('$imgs->[$i]') center/cover;border-radius:1px;flex-shrink:0"></div> | |
| 665 | <span style="height:3px;width:30%;background:$p->{text};border-radius:1px"></span> | |
| 666 | <span style="height:3px;width:15%;background:$p->{text_2};border-radius:1px"></span> | |
| 667 | <span style="height:3px;width:18%;background:$p->{accent};border-radius:1px;margin-left:auto"></span> | |
| 668 | </div>~; | |
| 669 | } | |
| 670 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1\">$rows</div>"); | |
| 671 | } | |
| 672 | ||
| 673 | # 12) Postcard: dashed border + image grid | |
| 674 | sub _sketch_postcard { | |
| 675 | my ($p) = @_; | |
| 676 | my $h = _header_strip($p); | |
| 677 | my $body = qq~ | |
| 678 | <div style="border:2px dashed $p->{accent};border-radius:4px;padding:5px;flex:1;display:flex;flex-direction:column;gap:4px"> | |
| 679 | <div style="display:flex;justify-content:center;gap:3px;align-items:center"> | |
| 680 | @{[ _bar('25%', $p->{accent}) ]} | |
| 681 | </div> | |
| 682 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;flex:1"> | |
| 683 | @{[ _tile($p, $p->{img_a}) ]} | |
| 684 | @{[ _tile($p, $p->{img_b}) ]} | |
| 685 | @{[ _tile($p, $p->{img_c}) ]} | |
| 686 | @{[ _tile($p, $p->{img_d}) ]} | |
| 687 | </div> | |
| 688 | </div>~; | |
| 689 | return _wrap($p, "$h$body"); | |
| 690 | } | |
| 691 | ||
| 692 | # 13) Blueprint: grid background with image cards | |
| 693 | sub _sketch_blueprint { | |
| 694 | my ($p) = @_; | |
| 695 | my $h = _header_strip($p); | |
| 696 | my $body = qq~ | |
| 697 | <div style="background-image:linear-gradient($p->{border} 1px, transparent 1px), linear-gradient(90deg, $p->{border} 1px, transparent 1px);background-size:8px 8px;background-color:$p->{bg};flex:1;padding:6px;display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-top:2px;border:1px solid $p->{accent}"> | |
| 698 | <div style="background:url('$p->{img_a}') center/cover;border:1px solid $p->{accent};border-radius:0"></div> | |
| 699 | <div style="background:url('$p->{img_b}') center/cover;border:1px solid $p->{accent};border-radius:0"></div> | |
| 700 | <div style="background:url('$p->{img_c}') center/cover;border:1px solid $p->{accent};border-radius:0"></div> | |
| 701 | <div style="background:url('$p->{img_d}') center/cover;border:1px solid $p->{accent};border-radius:0"></div> | |
| 702 | </div>~; | |
| 703 | return _wrap($p, "$h$body"); | |
| 704 | } | |
| 705 | ||
| 706 | # 14) Drop calendar: timeline rows with thumbnails | |
| 707 | sub _sketch_dropcal { | |
| 708 | my ($p) = @_; | |
| 709 | my $h = _header_strip($p); | |
| 710 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}, $p->{img_a}]; | |
| 711 | my $rows = ''; | |
| 712 | for my $i (0..4) { | |
| 713 | $rows .= qq~<div style="display:flex;gap:6px;align-items:center;padding:2px 4px"> | |
| 714 | <span style="width:6px;height:6px;border-radius:50%;background:$p->{accent};flex-shrink:0;box-shadow:0 0 0 2px $p->{bg}"></span> | |
| 715 | <div style="width:10px;height:10px;background:url('$imgs->[$i]') center/cover;border-radius:1px;flex-shrink:0"></div> | |
| 716 | <span style="height:3px;width:30%;background:$p->{text};border-radius:1px"></span> | |
| 717 | <span style="height:3px;width:35%;background:$p->{text_2};border-radius:1px"></span> | |
| 718 | </div>~; | |
| 719 | } | |
| 720 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1;border-left:1px solid $p->{border};margin-left:4px;padding-left:2px\">$rows</div>"); | |
| 721 | } | |
| 722 | ||
| 723 | # 16) Console: HUD dashboard with status bars + image cells | |
| 724 | sub _sketch_console { | |
| 725 | my ($p) = @_; | |
| 726 | my $h = _header_strip($p); | |
| 727 | my $hud = qq~ | |
| 728 | <div style="display:flex;gap:3px;margin-top:2px"> | |
| 729 | <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px"> | |
| 730 | @{[ _bar('80%', $p->{accent}, 'height:3px') ]} | |
| 731 | @{[ _bar('60%', $p->{accent_2}, 'height:3px') ]} | |
| 732 | </div> | |
| 733 | <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px"> | |
| 734 | @{[ _bar('50%', $p->{accent_2}, 'height:3px') ]} | |
| 735 | @{[ _bar('70%', $p->{accent}, 'height:3px') ]} | |
| 736 | </div> | |
| 737 | <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px"> | |
| 738 | @{[ _bar('90%', $p->{accent}, 'height:3px') ]} | |
| 739 | @{[ _bar('40%', $p->{accent_2}, 'height:3px') ]} | |
| 740 | </div> | |
| 741 | </div>~; | |
| 742 | my $grid = qq~ | |
| 743 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto"> | |
| 744 | <div style="background:url('$p->{img_a}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div> | |
| 745 | <div style="background:url('$p->{img_b}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div> | |
| 746 | <div style="background:url('$p->{img_c}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div> | |
| 747 | </div>~; | |
| 748 | return _wrap($p, "$h$hud$grid"); | |
| 749 | } | |
| 750 | ||
| 751 | # 17) Reel: full-bleed sections, scroll-snap feel | |
| 752 | sub _sketch_reel { | |
| 753 | my ($p) = @_; | |
| 754 | my $h = _header_strip($p); | |
| 755 | my $section = sub { | |
| 756 | my ($img) = @_; | |
| 757 | return qq~<div style="background:url('$img') center/cover;flex:1;border-radius:3px;border:1px solid $p->{border};display:flex;align-items:flex-end;padding:5px"> | |
| 758 | <div style="display:flex;flex-direction:column;gap:2px"> | |
| 759 | @{[ _bar('40px', $p->{text}, 'height:5px') ]} | |
| 760 | @{[ _bar('25px', $p->{accent}, 'height:5px') ]} | |
| 761 | </div> | |
| 762 | </div>~; | |
| 763 | }; | |
| 764 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:3px;flex:1\">@{[ $section->($p->{img_a}) ]}@{[ $section->($p->{img_b}) ]}</div>"); | |
| 765 | } | |
| 766 | ||
| 767 | # 18) Magazine: mixed-size editorial grid (2x3 with one tall) | |
| 768 | sub _sketch_magazine { | |
| 769 | my ($p) = @_; | |
| 770 | my $h = _header_strip($p); | |
| 771 | my $body = qq~ | |
| 772 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;grid-template-rows:auto auto;gap:3px;flex:1;margin-top:2px"> | |
| 773 | <div style="grid-row:1 / span 2;background:url('$p->{img_a}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 774 | <div style="background:url('$p->{img_b}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 775 | <div style="background:url('$p->{img_c}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div> | |
| 776 | <div style="grid-column:2 / span 2;background:$p->{surface};border:1px solid $p->{border};border-radius:3px;padding:4px;display:flex;flex-direction:column;gap:3px;justify-content:center"> | |
| 777 | @{[ _bar('80%', $p->{text}) ]} | |
| 778 | @{[ _bar('60%', $p->{accent}) ]} | |
| 779 | </div> | |
| 780 | </div>~; | |
| 781 | return _wrap($p, "$h$body"); | |
| 782 | } | |
| 783 | ||
| 784 | # 19) Tabs: tab strip then grid | |
| 785 | sub _sketch_tabs { | |
| 786 | my ($p) = @_; | |
| 787 | my $h = _header_strip($p); | |
| 788 | my $tabs = qq~ | |
| 789 | <div style="display:flex;gap:0;border-bottom:2px solid $p->{border};margin:2px 0"> | |
| 790 | <span style="padding:3px 8px;background:$p->{accent};color:#fff;font-size:8px;font-weight:700;border-radius:3px 3px 0 0">All</span> | |
| 791 | @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]} | |
| 792 | @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]} | |
| 793 | @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]} | |
| 794 | </div>~; | |
| 795 | my $grid = qq~ | |
| 796 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1"> | |
| 797 | @{[ _tile($p, $p->{img_a}) ]} | |
| 798 | @{[ _tile($p, $p->{img_b}) ]} | |
| 799 | @{[ _tile($p, $p->{img_c}) ]} | |
| 800 | </div>~; | |
| 801 | return _wrap($p, "$h$tabs$grid"); | |
| 802 | } | |
| 803 | ||
| 804 | # 20) Cinema: vertical poster cards with letterbox | |
| 805 | sub _sketch_cinema { | |
| 806 | my ($p) = @_; | |
| 807 | my $h = _header_strip($p); | |
| 808 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 809 | my $row = ''; | |
| 810 | for my $i (0..3) { | |
| 811 | $row .= qq~<div style="aspect-ratio:2/3;background:url('$imgs->[$i]') center/cover;border:2px solid $p->{text};border-radius:0;position:relative"> | |
| 812 | <div style="position:absolute;bottom:0;left:0;right:0;height:30%;background:linear-gradient(0deg,$p->{bg},transparent)"></div> | |
| 813 | </div>~; | |
| 814 | } | |
| 815 | return _wrap($p, "$h<div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:3px;margin-top:2px;flex:1\">$row</div>"); | |
| 816 | } | |
| 817 | ||
| 818 | # 21) Splash: huge CTA at top, tiny product list below | |
| 819 | sub _sketch_splash { | |
| 820 | my ($p) = @_; | |
| 821 | my $h = _header_strip($p); | |
| 822 | my $hero = qq~ | |
| 823 | <div style="background:$p->{gradient};border-radius:3px;padding:8px;flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:4px;min-height:60px"> | |
| 824 | @{[ _bar('70%', '#fff', 'height:8px;opacity:0.9') ]} | |
| 825 | @{[ _bar('40%', '#fff', 'height:8px;opacity:0.9') ]} | |
| 826 | <span style="background:$p->{bg};padding:3px 10px;font-size:8px;color:$p->{text};margin-top:4px;border-radius:2px">CTA</span> | |
| 827 | </div>~; | |
| 828 | my $strip = qq~ | |
| 829 | <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:2px"> | |
| 830 | @{[ _tile($p, $p->{img_a}) ]} | |
| 831 | @{[ _tile($p, $p->{img_b}) ]} | |
| 832 | @{[ _tile($p, $p->{img_c}) ]} | |
| 833 | @{[ _tile($p, $p->{img_d}) ]} | |
| 834 | </div>~; | |
| 835 | return _wrap($p, "$h$hero$strip"); | |
| 836 | } | |
| 837 | ||
| 838 | # 22) Sidebar: left nav rail + content panels | |
| 839 | sub _sketch_sidebar { | |
| 840 | my ($p) = @_; | |
| 841 | my $body = qq~ | |
| 842 | <div style="display:grid;grid-template-columns:50px 1fr;gap:0;height:100%;font-family:system-ui,sans-serif"> | |
| 843 | <div style="background:$p->{bg_2};border-right:1px solid $p->{border};padding:8px 4px;display:flex;flex-direction:column;gap:5px"> | |
| 844 | <span style="width:14px;height:14px;background:$p->{gradient};border-radius:3px;margin:0 auto"></span> | |
| 845 | <span style="height:4px;width:80%;background:$p->{accent};border-radius:1px;margin:6px auto 0"></span> | |
| 846 | <span style="height:4px;width:70%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span> | |
| 847 | <span style="height:4px;width:60%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span> | |
| 848 | <span style="height:4px;width:75%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span> | |
| 849 | <span style="height:4px;width:65%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span> | |
| 850 | </div> | |
| 851 | <div style="padding:8px;display:flex;flex-direction:column;gap:4px;background:$p->{bg}"> | |
| 852 | @{[ _bar('40%', $p->{text}, 'height:7px') ]} | |
| 853 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;flex:1;margin-top:2px"> | |
| 854 | @{[ _tile($p, $p->{img_a}) ]} | |
| 855 | @{[ _tile($p, $p->{img_b}) ]} | |
| 856 | @{[ _tile($p, $p->{img_c}) ]} | |
| 857 | @{[ _tile($p, $p->{img_d}) ]} | |
| 858 | </div> | |
| 859 | </div> | |
| 860 | </div>~; | |
| 861 | return $body; | |
| 862 | } | |
| 863 | ||
| 864 | # 23) Forum: list of "post" rows with vote bars | |
| 865 | sub _sketch_forum { | |
| 866 | my ($p) = @_; | |
| 867 | my $h = _header_strip($p); | |
| 868 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 869 | my $rows = ''; | |
| 870 | for my $i (0..3) { | |
| 871 | $rows .= qq~<div style="display:flex;gap:5px;align-items:center;padding:3px 4px;border-bottom:1px solid $p->{border}"> | |
| 872 | <div style="display:flex;flex-direction:column;align-items:center;gap:1px;width:14px"> | |
| 873 | <span style="font-size:6px;color:$p->{accent}">▲</span> | |
| 874 | <span style="font-size:6px;color:$p->{text};font-weight:700">$i$i</span> | |
| 875 | <span style="font-size:6px;color:$p->{text_3}">▼</span> | |
| 876 | </div> | |
| 877 | <div style="width:18px;height:18px;background:url('$imgs->[$i]') center/cover;border-radius:2px;flex-shrink:0;border:1px solid $p->{border}"></div> | |
| 878 | <div style="flex:1;display:flex;flex-direction:column;gap:2px"> | |
| 879 | @{[ _bar('70%', $p->{text}, 'height:3px') ]} | |
| 880 | @{[ _bar('40%', $p->{text_2}, 'height:3px') ]} | |
| 881 | </div> | |
| 882 | </div>~; | |
| 883 | } | |
| 884 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1\">$rows</div>"); | |
| 885 | } | |
| 886 | ||
| 887 | # 24) Holo: glassmorphism translucent panels with glow | |
| 888 | sub _sketch_holo { | |
| 889 | my ($p) = @_; | |
| 890 | # Background uses an image to suggest the frosted-blur look | |
| 891 | my $body = qq~ | |
| 892 | <div style="background:url('$p->{img_a}') center/cover;height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:5px;position:relative"> | |
| 893 | <div style="position:absolute;inset:0;background:rgba(0,0,0,0.4);backdrop-filter:blur(2px)"></div> | |
| 894 | <div style="position:relative;display:flex;align-items:center;gap:6px;padding:5px 6px;background:rgba(255,255,255,0.10);border:1px solid rgba(255,255,255,0.25);border-radius:6px;box-shadow:0 0 10px $p->{accent_glow}"> | |
| 895 | <span style="width:10px;height:10px;background:$p->{gradient};border-radius:50%"></span> | |
| 896 | @{[ _bar('30px', '#fff', 'opacity:0.9') ]} | |
| 897 | </div> | |
| 898 | <div style="position:relative;background:rgba(255,255,255,0.10);border:1px solid $p->{accent};border-radius:6px;padding:6px;flex:1;display:flex;flex-direction:column;gap:3px;box-shadow:0 0 12px $p->{accent_glow}"> | |
| 899 | @{[ _bar('80%', '#fff', 'opacity:0.85') ]} | |
| 900 | @{[ _bar('55%', $p->{accent}) ]} | |
| 901 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;margin-top:auto;flex:1"> | |
| 902 | <div style="background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:3px"></div> | |
| 903 | <div style="background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:3px"></div> | |
| 904 | </div> | |
| 905 | </div> | |
| 906 | </div>~; | |
| 907 | return $body; | |
| 908 | } | |
| 909 | ||
| 910 | # 26) Pinterest: masonry with caption tiles always visible | |
| 911 | sub _sketch_pinterest { | |
| 912 | my ($p) = @_; | |
| 913 | my $h = _header_strip($p); | |
| 914 | my $col = sub { | |
| 915 | my ($img1, $h1, $img2, $h2) = @_; | |
| 916 | return qq~<div style="display:flex;flex-direction:column;gap:3px"> | |
| 917 | <div style="background:url('$img1') center/cover;border-radius:6px;height:$h1;border:1px solid $p->{border}"></div> | |
| 918 | <div style="background:$p->{surface};border-radius:4px;padding:3px 4px;height:9px"></div> | |
| 919 | <div style="background:url('$img2') center/cover;border-radius:6px;height:$h2;border:1px solid $p->{border}"></div> | |
| 920 | <div style="background:$p->{surface};border-radius:4px;padding:3px 4px;height:9px"></div> | |
| 921 | </div>~; | |
| 922 | }; | |
| 923 | my $body = qq~<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto"> | |
| 924 | @{[ $col->($p->{img_a}, '36px', $p->{img_b}, '24px') ]} | |
| 925 | @{[ $col->($p->{img_c}, '24px', $p->{img_d}, '36px') ]} | |
| 926 | @{[ $col->($p->{img_b}, '30px', $p->{img_a}, '30px') ]} | |
| 927 | </div>~; | |
| 928 | return _wrap($p, "$h$body"); | |
| 929 | } | |
| 930 | ||
| 931 | # 27) Newsstand: newspaper columns | |
| 932 | sub _sketch_newsstand { | |
| 933 | my ($p) = @_; | |
| 934 | my $h = _header_strip($p); | |
| 935 | my $col = qq~<div style="display:flex;flex-direction:column;gap:2px"> | |
| 936 | @{[ _bar('100%', $p->{text}, 'height:4px') ]} | |
| 937 | @{[ _bar('80%', $p->{text_2}, 'height:2px') ]} | |
| 938 | @{[ _bar('100%', $p->{text_2}, 'height:2px') ]} | |
| 939 | @{[ _bar('70%', $p->{text_2}, 'height:2px') ]} | |
| 940 | @{[ _bar('100%', $p->{text_2}, 'height:2px') ]} | |
| 941 | @{[ _bar('60%', $p->{text_2}, 'height:2px') ]} | |
| 942 | </div>~; | |
| 943 | my $body = qq~ | |
| 944 | <div style="text-align:center;padding-bottom:4px;border-bottom:2px solid $p->{text}"> | |
| 945 | @{[ _bar('60%', $p->{text}, 'height:8px;margin:0 auto') ]} | |
| 946 | </div> | |
| 947 | <div style="display:grid;grid-template-columns:1fr 1px 1fr 1px 1fr;gap:5px;flex:1;margin-top:4px"> | |
| 948 | $col<div style="background:$p->{border}"></div>$col<div style="background:$p->{border}"></div>$col | |
| 949 | </div>~; | |
| 950 | return _wrap($p, "$h$body"); | |
| 951 | } | |
| 952 | ||
| 953 | # 28) Manifesto: huge text takes the page, products are tiny tags | |
| 954 | sub _sketch_manifesto { | |
| 955 | my ($p) = @_; | |
| 956 | my $h = _header_strip($p); | |
| 957 | my $body = qq~ | |
| 958 | <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:4px;padding:6px 4px"> | |
| 959 | @{[ _bar('90%', $p->{text}, 'height:12px') ]} | |
| 960 | @{[ _bar('70%', $p->{accent}, 'height:12px') ]} | |
| 961 | @{[ _bar('80%', $p->{text}, 'height:12px') ]} | |
| 962 | </div> | |
| 963 | <div style="display:flex;gap:3px;margin-top:auto"> | |
| 964 | @{[ _tile($p, $p->{img_a}, '14px') ]} | |
| 965 | @{[ _tile($p, $p->{img_b}, '14px') ]} | |
| 966 | @{[ _tile($p, $p->{img_c}, '14px') ]} | |
| 967 | @{[ _tile($p, $p->{img_d}, '14px') ]} | |
| 968 | </div>~; | |
| 969 | return _wrap($p, "$h$body"); | |
| 970 | } | |
| 971 | ||
| 972 | # 29) Trading cards: portrait cards with rarity strip on top | |
| 973 | sub _sketch_tcg { | |
| 974 | my ($p) = @_; | |
| 975 | my $h = _header_strip($p); | |
| 976 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 977 | my $card = sub { | |
| 978 | my ($img) = @_; | |
| 979 | return qq~<div style="display:flex;flex-direction:column;background:$p->{surface};border:1px solid $p->{accent};border-radius:4px;overflow:hidden"> | |
| 980 | <div style="background:$p->{gradient};height:4px"></div> | |
| 981 | <div style="background:url('$img') center/cover;flex:1;min-height:30px"></div> | |
| 982 | <div style="padding:2px 3px;display:flex;justify-content:space-between;align-items:center"> | |
| 983 | <span style="height:2px;width:60%;background:$p->{text};border-radius:1px"></span> | |
| 984 | <span style="font-size:6px;color:$p->{accent};font-weight:700">**</span> | |
| 985 | </div> | |
| 986 | </div>~; | |
| 987 | }; | |
| 988 | my $row = qq~<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto"> | |
| 989 | @{[ $card->($imgs->[0]) ]}@{[ $card->($imgs->[1]) ]}@{[ $card->($imgs->[2]) ]}@{[ $card->($imgs->[3]) ]} | |
| 990 | </div>~; | |
| 991 | return _wrap($p, "$h$row"); | |
| 992 | } | |
| 993 | ||
| 994 | # 30) Vinyl: square album-cover cards | |
| 995 | sub _sketch_vinyl { | |
| 996 | my ($p) = @_; | |
| 997 | my $h = _header_strip($p); | |
| 998 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}]; | |
| 999 | my $sleeve = sub { | |
| 1000 | my ($img) = @_; | |
| 1001 | return qq~<div style="position:relative;background:url('$img') center/cover;aspect-ratio:1;border:1px solid $p->{border};border-radius:1px"> | |
| 1002 | <div style="position:absolute;top:50%;left:50%;width:30%;height:30%;border-radius:50%;background:#000;border:2px solid $p->{accent};transform:translate(-50%,-50%);box-shadow:0 0 0 1px #333 inset"></div> | |
| 1003 | </div>~; | |
| 1004 | }; | |
| 1005 | my $body = qq~<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;flex:1;margin-top:auto"> | |
| 1006 | @{[ $sleeve->($imgs->[0]) ]}@{[ $sleeve->($imgs->[1]) ]} | |
| 1007 | </div>~; | |
| 1008 | return _wrap($p, "$h$body"); | |
| 1009 | } | |
| 1010 | ||
| 1011 | # 31) Comic: panels with thick borders + accent flash | |
| 1012 | sub _sketch_comic { | |
| 1013 | my ($p) = @_; | |
| 1014 | my $h = _header_strip($p); | |
| 1015 | my $body = qq~ | |
| 1016 | <div style="display:grid;grid-template-columns:1.4fr 1fr;grid-template-rows:1fr 1fr;gap:3px;flex:1;margin-top:2px;border:2px solid $p->{text}"> | |
| 1017 | <div style="background:url('$p->{img_a}') center/cover;border-right:2px solid $p->{text};border-bottom:2px solid $p->{text};position:relative"> | |
| 1018 | <span style="position:absolute;top:3px;left:3px;background:$p->{accent};color:#fff;padding:1px 4px;font-size:6px;font-weight:900;border:1px solid $p->{text};transform:rotate(-3deg)">POW!</span> | |
| 1019 | </div> | |
| 1020 | <div style="background:url('$p->{img_b}') center/cover;border-bottom:2px solid $p->{text}"></div> | |
| 1021 | <div style="background:url('$p->{img_c}') center/cover;border-right:2px solid $p->{text}"></div> | |
| 1022 | <div style="background:url('$p->{img_d}') center/cover"></div> | |
| 1023 | </div>~; | |
| 1024 | return _wrap($p, "$h$body"); | |
| 1025 | } | |
| 1026 | ||
| 1027 | # 32) Festival: stacked huge type with date strip | |
| 1028 | sub _sketch_festival { | |
| 1029 | my ($p) = @_; | |
| 1030 | my $h = _header_strip($p); | |
| 1031 | my $body = qq~ | |
| 1032 | <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:3px;padding:4px 0;text-align:center"> | |
| 1033 | @{[ _bar('60%', $p->{text}, 'height:10px;margin:0 auto') ]} | |
| 1034 | @{[ _bar('80%', $p->{accent}, 'height:14px;margin:0 auto') ]} | |
| 1035 | @{[ _bar('50%', $p->{text}, 'height:10px;margin:0 auto') ]} | |
| 1036 | </div> | |
| 1037 | <div style="display:flex;gap:2px;justify-content:center;font-family:ui-monospace,monospace"> | |
| 1038 | @{[ _bar('15%', $p->{text_2}, 'height:4px') ]} | |
| 1039 | @{[ _bar('15%', $p->{text_2}, 'height:4px') ]} | |
| 1040 | @{[ _bar('15%', $p->{text_2}, 'height:4px') ]} | |
| 1041 | </div>~; | |
| 1042 | return _wrap($p, "$h$body"); | |
| 1043 | } | |
| 1044 | ||
| 1045 | # 33) Periodic: element grid | |
| 1046 | sub _sketch_periodic { | |
| 1047 | my ($p) = @_; | |
| 1048 | my $h = _header_strip($p); | |
| 1049 | my $cell = qq~<div style="background:$p->{surface};border:1px solid $p->{border};border-radius:2px;padding:2px;display:flex;flex-direction:column;justify-content:space-between"> | |
| 1050 | <span style="font-size:5px;color:$p->{text_3};line-height:1">01</span> | |
| 1051 | <span style="font-size:9px;color:$p->{accent};font-weight:900;line-height:1;text-align:center;font-family:ui-monospace,monospace">Aa</span> | |
| 1052 | </div>~; | |
| 1053 | my $row = $cell x 5; | |
| 1054 | my $rows = ''; | |
| 1055 | for (1..3) { | |
| 1056 | $rows .= qq~<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">$row</div>~; | |
| 1057 | } | |
| 1058 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:2px;flex:1;margin-top:2px\">$rows</div>"); | |
| 1059 | } | |
| 1060 | ||
| 1061 | # 34) Diary: dated entries with vertical rule | |
| 1062 | sub _sketch_diary { | |
| 1063 | my ($p) = @_; | |
| 1064 | my $h = _header_strip($p); | |
| 1065 | my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}]; | |
| 1066 | my $rows = ''; | |
| 1067 | for my $i (0..2) { | |
| 1068 | $rows .= qq~<div style="display:grid;grid-template-columns:24px 1fr 18px;gap:5px;align-items:center;padding:3px 0;border-bottom:1px dashed $p->{border}"> | |
| 1069 | <div style="font-family:ui-monospace,monospace;text-align:right"> | |
| 1070 | <div style="font-size:8px;color:$p->{accent};font-weight:700;line-height:1">$i$i</div> | |
| 1071 | <div style="font-size:5px;color:$p->{text_3};line-height:1">JAN</div> | |
| 1072 | </div> | |
| 1073 | <div style="display:flex;flex-direction:column;gap:2px"> | |
| 1074 | @{[ _bar('80%', $p->{text}, 'height:3px') ]} | |
| 1075 | @{[ _bar('60%', $p->{text_2}, 'height:2px') ]} | |
| 1076 | </div> | |
| 1077 | <div style="width:18px;height:18px;background:url('$imgs->[$i]') center/cover;border-radius:2px"></div> | |
| 1078 | </div>~; | |
| 1079 | } | |
| 1080 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1;margin-top:2px\">$rows</div>"); | |
| 1081 | } | |
| 1082 | ||
| 1083 | # 35) Stage: spotlight on featured image, side curtains | |
| 1084 | sub _sketch_stage { | |
| 1085 | my ($p) = @_; | |
| 1086 | my $h = _header_strip($p); | |
| 1087 | my $body = qq~ | |
| 1088 | <div style="display:grid;grid-template-columns:8px 1fr 8px;gap:3px;flex:1;margin-top:2px"> | |
| 1089 | <div style="background:linear-gradient(180deg, $p->{accent_deep} 0%, $p->{accent} 100%);border-radius:2px"></div> | |
| 1090 | <div style="background:url('$p->{img_a}') center/cover;border-radius:3px;border:1px solid $p->{accent};box-shadow:0 0 12px $p->{accent_glow};position:relative;display:flex;align-items:flex-end;padding:5px"> | |
| 1091 | <div style="position:absolute;top:-2px;left:50%;transform:translateX(-50%);width:14px;height:6px;background:radial-gradient(ellipse at top, $p->{accent}, transparent);opacity:0.7"></div> | |
| 1092 | @{[ _bar('40px', '#fff', 'height:5px') ]} | |
| 1093 | </div> | |
| 1094 | <div style="background:linear-gradient(180deg, $p->{accent_deep} 0%, $p->{accent} 100%);border-radius:2px"></div> | |
| 1095 | </div>~; | |
| 1096 | return _wrap($p, "$h$body"); | |
| 1097 | } | |
| 1098 | ||
| 1099 | # 36) Receipt: long narrow center column with line items | |
| 1100 | sub _sketch_receipt { | |
| 1101 | my ($p) = @_; | |
| 1102 | my $h = _header_strip($p); | |
| 1103 | my $body = qq~ | |
| 1104 | <div style="background:#f8f8f4;color:#1a1a1a;flex:1;margin:2px auto 0;width:65%;padding:6px;font-family:ui-monospace,monospace;display:flex;flex-direction:column;gap:3px;box-shadow:0 4px 8px rgba(0,0,0,0.3);border-radius:1px"> | |
| 1105 | <div style="text-align:center;border-bottom:1px dashed #1a1a1a;padding-bottom:3px"> | |
| 1106 | <span style="height:5px;width:40%;background:#1a1a1a;display:inline-block;border-radius:1px"></span> | |
| 1107 | </div> | |
| 1108 | @{[ _bar('100%', '#5a5a5a', 'height:2px') ]} | |
| 1109 | @{[ _bar('100%', '#5a5a5a', 'height:2px') ]} | |
| 1110 | @{[ _bar('100%', '#5a5a5a', 'height:2px') ]} | |
| 1111 | @{[ _bar('100%', '#5a5a5a', 'height:2px') ]} | |
| 1112 | @{[ _bar('100%', '#5a5a5a', 'height:2px') ]} | |
| 1113 | <div style="border-top:1px dashed #1a1a1a;padding-top:3px;text-align:right"> | |
| 1114 | <span style="height:4px;width:40%;background:#1a1a1a;display:inline-block;border-radius:1px"></span> | |
| 1115 | </div> | |
| 1116 | </div>~; | |
| 1117 | return _wrap($p, "$h$body"); | |
| 1118 | } | |
| 1119 | ||
| 1120 | # 25) Concept: full-bleed poster rows with overlay | |
| 1121 | sub _sketch_concept { | |
| 1122 | my ($p) = @_; | |
| 1123 | my $h = _header_strip($p); | |
| 1124 | my $row = sub { | |
| 1125 | my ($img) = @_; | |
| 1126 | return qq~<div style="background:url('$img') center/cover;flex:1;border-radius:3px;display:flex;align-items:center;padding:0 6px;position:relative"> | |
| 1127 | <div style="position:absolute;inset:0;background:linear-gradient(90deg,$p->{bg},transparent 60%);border-radius:3px"></div> | |
| 1128 | <div style="position:relative;display:flex;flex-direction:column;gap:2px"> | |
| 1129 | @{[ _bar('40px', $p->{text}, 'height:6px') ]} | |
| 1130 | @{[ _bar('25px', $p->{accent}, 'height:5px') ]} | |
| 1131 | </div> | |
| 1132 | </div>~; | |
| 1133 | }; | |
| 1134 | return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:3px;flex:1\">@{[ $row->($p->{img_a}) ]}@{[ $row->($p->{img_b}) ]}@{[ $row->($p->{img_c}) ]}</div>"); | |
| 1135 | } | |
| 1136 | ||
| 1137 | # 15) Brutalist: massive headline + asymmetric grid + accent blocks | |
| 1138 | sub _sketch_brutalist { | |
| 1139 | my ($p) = @_; | |
| 1140 | return qq~ | |
| 1141 | <div style="background:$p->{bg};height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:5px;border-bottom:5px solid $p->{text}"> | |
| 1142 | <!-- Header strip with massive name + dateline --> | |
| 1143 | <div style="display:flex;justify-content:space-between;align-items:baseline;padding-bottom:5px;border-bottom:5px solid $p->{text}"> | |
| 1144 | <span style="font-family:ui-sans-serif,system-ui;font-weight:900;font-size:18px;line-height:1;color:$p->{text};letter-spacing:-0.04em">Aa.</span> | |
| 1145 | <span style="background:$p->{text};color:$p->{bg};padding:2px 6px;font-family:ui-monospace,monospace;font-size:7px;font-weight:700;letter-spacing:1px">CART</span> | |
| 1146 | </div> | |
| 1147 | <!-- Section label like a newspaper dateline --> | |
| 1148 | <div style="font-family:ui-monospace,monospace;font-size:7px;letter-spacing:2px;text-transform:uppercase;color:$p->{accent};font-weight:700">§ 02 — CATALOG</div> | |
| 1149 | <!-- Asymmetric image grid: one tall + 3 small --> | |
| 1150 | <div style="display:grid;grid-template-columns:1.2fr 1fr;gap:3px;flex:1"> | |
| 1151 | <div style="background:url('$p->{img_a}') center/cover;position:relative"> | |
| 1152 | <span style="position:absolute;top:3px;left:3px;background:$p->{accent};color:#fff;padding:1px 4px;font-family:ui-monospace,monospace;font-size:6px;font-weight:700">01</span> | |
| 1153 | </div> | |
| 1154 | <div style="display:grid;grid-template-rows:1fr 1fr 1fr;gap:3px"> | |
| 1155 | <div style="background:url('$p->{img_b}') center/cover"></div> | |
| 1156 | <div style="background:url('$p->{img_c}') center/cover"></div> | |
| 1157 | <div style="background:url('$p->{img_d}') center/cover"></div> | |
| 1158 | </div> | |
| 1159 | </div> | |
| 1160 | <!-- Footer rule + colophon --> | |
| 1161 | <div style="height:3px;background:$p->{text};margin-top:2px"></div> | |
| 1162 | <div style="display:flex;justify-content:space-between;font-family:ui-monospace,monospace;font-size:7px;letter-spacing:1px;text-transform:uppercase;color:$p->{text_2}"> | |
| 1163 | <span>VOL. I</span> | |
| 1164 | <span>WEBSTLS</span> | |
| 1165 | </div> | |
| 1166 | </div>~; | |
| 1167 | } | |
| 1168 | ||
| 1169 | 1; |