added on WebSTLs (webstls.com) at 2026-07-01 22:27:00
| 1 | <!-- ===================================================================== | |
| 2 | Public model detail page (buyer-facing). Two-column layout: | |
| 3 | Left — hero image + thumbnail carousel + tabbed body content | |
| 4 | (Description, Files, Print Settings, Reviews) | |
| 5 | Right — sticky purchase card (designer, rating, price, actions, | |
| 6 | meta rows) | |
| 7 | ||
| 8 | dp- prefix on all classes follows the design reference at | |
| 9 | html/Website/details.html. Markup is digital-only for now; physical | |
| 10 | bits (Buy a Print action, Shipping tab) can be added later without | |
| 11 | touching the surrounding shell. | |
| 12 | ===================================================================== --> | |
| 13 | ||
| 14 | <!-- Inject the storefront's theme CSS variables before our own styles | |
| 15 | so this listing page picks up the same --col-* values that the | |
| 16 | storefront homepage / catalog pages use. Without this the page | |
| 17 | falls back to site.css defaults and reads as a different site. --> | |
| 18 | <style>:root { $theme_css_vars }</style> | |
| 19 | ||
| 20 | <style> | |
| 21 | /* | |
| 22 | THEME-AWARE — every color here comes from the storefront's active | |
| 23 | theme via the --col-* CSS variables that MODS::WebSTLs::Themes.pm | |
| 24 | injects into every store.cgi page. This means the same listing | |
| 25 | details template renders correctly across all 18+ store layouts | |
| 26 | AND all color themes (dark, light, neon, paper, etc.) without | |
| 27 | duplicating the markup per theme. Only semantic colors that | |
| 28 | transcend theme (green for "Free", red for danger, gold for | |
| 29 | stars) stay hardcoded -- those aren't supposed to change between | |
| 30 | themes anyway. | |
| 31 | */ | |
| 32 | ||
| 33 | /* Breadcrumb */ | |
| 34 | .dp-breadcrumb { background:var(--col-surface-1);border-bottom:1px solid var(--col-border);padding:14px 32px; } | |
| 35 | .dp-breadcrumb-inner { max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--col-text-3); } | |
| 36 | .dp-breadcrumb a { color:var(--col-text-2);text-decoration:none;transition:color 0.15s; } | |
| 37 | .dp-breadcrumb a:hover { color:var(--col-accent-bright); } | |
| 38 | .dp-breadcrumb-sep { font-size:11px;color:var(--col-text-3);opacity:0.6; } | |
| 39 | .dp-breadcrumb-current { color:var(--col-text);font-weight:600; } | |
| 40 | ||
| 41 | /* Layout */ | |
| 42 | .dp-layout { max-width:1400px;margin:0 auto;padding:28px 32px 80px;display:grid;grid-template-columns:1fr 400px;gap:36px;align-items:start; } | |
| 43 | .dp-left { display:flex;flex-direction:column;gap:18px;min-width:0; } | |
| 44 | .dp-right { display:flex;flex-direction:column;gap:16px; } | |
| 45 | ||
| 46 | /* Hero */ | |
| 47 | .dp-hero-img { position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--col-border);aspect-ratio:16/10;background:var(--col-surface-2); } | |
| 48 | .dp-hero-img img { width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.4s ease; } | |
| 49 | .dp-hero-img:hover img { transform:scale(1.02); } | |
| 50 | .dp-hero-overlay { position:absolute;inset:0;background:linear-gradient(to top, var(--col-bg) 0%, rgba(0,0,0,0.05) 45%, transparent 100%);opacity:0.85;pointer-events:none; } | |
| 51 | .dp-hero-title-wrap { position:absolute;bottom:0;left:0;right:0;padding:24px 28px; } | |
| 52 | .dp-hero-eyebrow { font-size:11px;font-weight:700;color:var(--col-accent-bright);letter-spacing:2px;text-transform:uppercase;margin-bottom:6px; } | |
| 53 | .dp-hero-title { font-family:var(--font-display,sans-serif);font-size:clamp(28px,4vw,46px);color:var(--col-text);line-height:1.0;letter-spacing:0.5px; } | |
| 54 | .dp-hero-badges { display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap; } | |
| 55 | .dp-hero-badge { padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700; } | |
| 56 | .dp-badge-cat { background:rgba(0,0,0,0.35);border:1px solid var(--col-border);color:#fff;font-weight:700;border-radius:6px; } | |
| 57 | .dp-badge-src { background:rgba(0,0,0,0.35);border:1px solid var(--col-border);color:#fff;font-weight:700;border-radius:6px; } | |
| 58 | .dp-badge-free { background:rgba(34,197,94,0.18);border:1px solid rgba(34,197,94,0.4);color:#4ade80; } | |
| 59 | ||
| 60 | /* Thumbs */ | |
| 61 | .dp-thumbs { display:flex;gap:8px;overflow-x:auto;padding-bottom:4px; } | |
| 62 | .dp-thumb { flex-shrink:0;width:120px;height:84px;border-radius:8px;overflow:hidden;border:2px solid var(--col-border);cursor:pointer;transition:border-color 0.2s,box-shadow 0.2s;background:var(--col-surface-2);position:relative; } | |
| 63 | .dp-thumb:hover { border-color:var(--col-accent); } | |
| 64 | .dp-thumb.active { border-color:var(--col-accent-bright);box-shadow:0 0 0 1px var(--col-accent-bright); } | |
| 65 | .dp-thumb img { width:100%;height:100%;object-fit:cover;display:block; } | |
| 66 | .dp-thumb-num { position:absolute;top:3px;left:3px;background:var(--col-bg);color:var(--col-text);font-size:10px;font-weight:800;padding:2px 7px;border-radius:5px;letter-spacing:0.5px;border:1px solid var(--col-border-2);opacity:0.92; } | |
| 67 | ||
| 68 | /* Tabs */ | |
| 69 | .dp-tabs { background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:14px;overflow:hidden; } | |
| 70 | .dp-tab-nav { display:flex;border-bottom:1px solid var(--col-border);background:var(--col-surface-2); } | |
| 71 | .dp-tab-btn { flex:1;padding:13px 14px;background:none;border:none;border-bottom:2px solid transparent;font-size:13px;font-weight:700;color:var(--col-text-2);cursor:pointer;transition:all 0.2s;letter-spacing:0.3px;font-family:inherit; } | |
| 72 | .dp-tab-btn:hover { color:var(--col-text);background:var(--col-surface-1); } | |
| 73 | .dp-tab-btn.active { color:#fff;border-bottom-color:var(--col-accent);background:var(--col-accent-glow);font-weight:800; } | |
| 74 | .dp-tab-pane { padding:24px 28px;display:none; } | |
| 75 | .dp-tab-pane.active { display:block; } | |
| 76 | .dp-tab-empty { color:var(--col-text-3);font-size:13px;font-style:italic;text-align:center;padding:30px 0; } | |
| 77 | ||
| 78 | /* Description tab */ | |
| 79 | .dp-desc { font-size:14.5px;color:var(--col-text-2);line-height:1.75; } | |
| 80 | .dp-desc p { margin:0 0 14px; } | |
| 81 | .dp-desc p:last-child { margin-bottom:0; } | |
| 82 | .dp-desc-h { font-size:12px;font-weight:700;color:var(--col-accent-bright);letter-spacing:1.5px;text-transform:uppercase;margin:24px 0 12px; } | |
| 83 | .dp-highlights { display:grid;grid-template-columns:1fr 1fr;gap:10px; } | |
| 84 | .dp-h-item { display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px; } | |
| 85 | .dp-h-dot { width:6px;height:6px;border-radius:50%;background:var(--col-accent);margin-top:6px;flex-shrink:0;box-shadow:0 0 6px var(--col-accent-glow); } | |
| 86 | .dp-h-text { font-size:13px;color:var(--col-text-2);line-height:1.5; } | |
| 87 | .dp-included { display:flex;flex-direction:column;gap:8px; } | |
| 88 | .dp-inc-item { display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:9px;font-size:13px;color:var(--col-text-2); } | |
| 89 | .dp-inc-dot { width:7px;height:7px;border-radius:50%;background:var(--col-accent-bright);box-shadow:0 0 6px var(--col-accent-glow);flex-shrink:0; } | |
| 90 | ||
| 91 | /* Files table */ | |
| 92 | .dp-files-table { width:100%;border-collapse:collapse; } | |
| 93 | .dp-files-table th { text-align:left;padding:10px 14px;font-size:11px;font-weight:700;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase;border-bottom:1px solid var(--col-border); } | |
| 94 | .dp-files-table td { padding:12px 14px;font-size:13px;color:var(--col-text-2);border-bottom:1px solid var(--col-border);opacity:0.95; } | |
| 95 | .dp-files-table tr:last-child td { border-bottom:none; } | |
| 96 | .dp-files-table tr:hover td { background:var(--col-surface-2);color:var(--col-text); } | |
| 97 | .dp-file-name { color:var(--col-text);font-weight:600; } | |
| 98 | /* File-format chips keep semantic colors (each ext gets its own | |
| 99 | hue) -- these read as filetypes regardless of theme. */ | |
| 100 | .dp-file-fmt { display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;font-family:var(--font-mono,monospace); } | |
| 101 | .dp-fmt-stl { background:rgba(34,197,94,0.10);border:1px solid rgba(34,197,94,0.28);color:#4ade80; } | |
| 102 | .dp-fmt-3mf { background:rgba(99,102,241,0.10);border:1px solid rgba(99,102,241,0.28);color:#a5b4fc; } | |
| 103 | .dp-fmt-step { background:rgba(6,182,212,0.10);border:1px solid rgba(6,182,212,0.28);color:#22d3ee; } | |
| 104 | .dp-fmt-obj { background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.28);color:#fbbf24; } | |
| 105 | .dp-fmt-zip { background:rgba(124,58,237,0.10);border:1px solid rgba(124,58,237,0.28);color:#c4b5fd; } | |
| 106 | .dp-fmt-other { background:rgba(148,163,184,0.10);border:1px solid rgba(148,163,184,0.28);color:#cbd5e1; } | |
| 107 | ||
| 108 | /* Print settings */ | |
| 109 | .dp-settings-grid { display:grid;grid-template-columns:1fr 1fr;gap:14px; } | |
| 110 | .dp-setting-card { padding:16px 18px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px; } | |
| 111 | .dp-setting-label { font-size:11px;font-weight:700;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px; } | |
| 112 | .dp-setting-value { font-family:var(--font-mono,monospace);font-size:15px;font-weight:600;color:var(--col-accent-bright); } | |
| 113 | ||
| 114 | /* Reviews */ | |
| 115 | .dp-rv-summary { display:flex;align-items:center;gap:24px;padding:18px 20px;margin-bottom:16px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px; } | |
| 116 | .dp-rv-big { font-family:var(--font-display,sans-serif);font-size:48px;color:var(--col-text);line-height:1; } | |
| 117 | /* Gold stars stay semantic — read as rating regardless of theme. */ | |
| 118 | .dp-rv-stars-big { color:#f59e0b;font-size:16px;letter-spacing:2px; } | |
| 119 | .dp-rv-total { font-size:12px;color:var(--col-text-3);margin-top:4px; } | |
| 120 | .dp-rv-empty { padding:34px 18px;text-align:center;color:var(--col-text-3);font-size:13px; } | |
| 121 | ||
| 122 | /* Purchase card */ | |
| 123 | .dp-pc { position:sticky;top:24px;background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:16px;overflow:hidden; } | |
| 124 | .dp-pc-inner { padding:24px 26px; } | |
| 125 | .dp-pc-title { font-family:var(--font-display,sans-serif);font-size:26px;color:var(--col-text);letter-spacing:0.3px;line-height:1.1;margin-bottom:14px; } | |
| 126 | ||
| 127 | .dp-designer-row { display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:18px;border-bottom:1px solid var(--col-border); } | |
| 128 | .dp-designer-avatar { width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--col-accent),var(--col-accent-deep));display:flex;align-items:center;justify-content:center;font-family:var(--font-display,sans-serif);font-size:17px;color:#fff;flex-shrink:0;border:2px solid var(--col-accent); } | |
| 129 | .dp-designer-info { flex:1;min-width:0; } | |
| 130 | .dp-designer-by { font-size:10px;color:var(--col-text-3);text-transform:uppercase;letter-spacing:0.8px; } | |
| 131 | .dp-designer-name { font-size:14px;font-weight:700;color:var(--col-accent-bright);text-decoration:none; } | |
| 132 | .dp-designer-name:hover { color:var(--col-text); } | |
| 133 | .dp-store-link { color:var(--col-text-2);text-decoration:none;font-size:12px; } | |
| 134 | .dp-store-link:hover { color:var(--col-text); } | |
| 135 | ||
| 136 | .dp-rating-block { display:flex;align-items:center;gap:10px;margin-bottom:18px; } | |
| 137 | .dp-rating-stars { color:#f59e0b;font-size:15px;letter-spacing:1px; } | |
| 138 | .dp-rating-num { font-size:15px;font-weight:700;color:var(--col-text); } | |
| 139 | .dp-rating-count { font-size:12px;color:var(--col-text-3); } | |
| 140 | ||
| 141 | .dp-price-block { display:flex;align-items:flex-end;gap:12px;margin-bottom:18px; } | |
| 142 | .dp-price-main { font-family:var(--font-display,sans-serif);font-size:42px;color:var(--col-text);line-height:1; } | |
| 143 | .dp-price-free { padding:4px 10px;border-radius:6px;background:rgba(34,197,94,0.16);border:1px solid rgba(34,197,94,0.4);color:#4ade80;font-size:12px;font-weight:700;margin-bottom:6px; } | |
| 144 | ||
| 145 | .dp-actions { display:flex;flex-direction:column;gap:10px;margin-bottom:18px; } | |
| 146 | .dp-btn-primary { width:100%;padding:14px 0;background:linear-gradient(135deg,var(--col-accent),var(--col-accent-deep));color:#fff;border:none;border-radius:11px;font-size:14.5px;font-weight:700;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;box-shadow:0 8px 24px var(--col-accent-glow);letter-spacing:0.3px;font-family:inherit; } | |
| 147 | .dp-btn-primary:hover { transform:translateY(-2px);filter:brightness(1.08); } | |
| 148 | .dp-btn-secondary { width:100%;padding:12px 0;background:var(--col-surface-2);border:1px solid var(--col-border-2);border-radius:11px;color:var(--col-text);font-size:13.5px;font-weight:600;cursor:pointer;transition:all 0.15s;font-family:inherit; } | |
| 149 | .dp-btn-secondary:hover { background:var(--col-accent-glow);border-color:var(--col-accent); } | |
| 150 | .dp-btn-wishlist { width:100%;padding:10px 0;background:none;border:1px solid var(--col-border);border-radius:11px;color:var(--col-text-3);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;font-family:inherit; } | |
| 151 | .dp-btn-wishlist:hover { border-color:rgba(239,68,68,0.4);color:#f87171; } | |
| 152 | ||
| 153 | /* Meta rows */ | |
| 154 | .dp-meta { display:flex;flex-direction:column;gap:1px;background:var(--col-border);border-radius:10px;overflow:hidden;margin-top:14px; } | |
| 155 | .dp-meta-row { display:flex;justify-content:space-between;align-items:center;padding:11px 14px;background:var(--col-surface-2);font-size:12.5px; } | |
| 156 | .dp-meta-label { color:var(--col-text-3);font-weight:500; } | |
| 157 | .dp-meta-value { color:var(--col-text);font-weight:600; } | |
| 158 | .dp-meta-value.blue { color:var(--col-accent-bright); } | |
| 159 | .dp-meta-value.green { color:#4ade80; } | |
| 160 | ||
| 161 | /* Tag chips */ | |
| 162 | .dp-tags { display:flex;flex-wrap:wrap;gap:6px;margin-top:14px; } | |
| 163 | .dp-tag { display:inline-block;padding:5px 11px;background:rgba(0,0,0,0.35);border:1px solid var(--col-border);border-radius:6px;font-size:11.5px;font-weight:600;color:#fff;text-decoration:none;letter-spacing:0.3px; } | |
| 164 | .dp-tag:hover { background:var(--col-accent);color:#fff;border-color:var(--col-accent); } | |
| 165 | ||
| 166 | /* Source-platform note */ | |
| 167 | .dp-source-note { margin-top:14px;padding:11px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;font-size:12px;color:var(--col-text-2); } | |
| 168 | .dp-source-note a { color:var(--col-accent-bright);text-decoration:underline; } | |
| 169 | ||
| 170 | /* License footer */ | |
| 171 | .dp-license { margin:0 20px 20px;padding:14px 16px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;font-size:11.5px;color:var(--col-text-3);line-height:1.55; } | |
| 172 | ||
| 173 | /* Related strip */ | |
| 174 | .dp-section { padding:48px 32px;background:var(--col-surface-1);border-top:1px solid var(--col-border); } | |
| 175 | .dp-section-inner { max-width:1400px;margin:0 auto; } | |
| 176 | .dp-section-hd { display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:22px; } | |
| 177 | .dp-section-eyebrow { font-size:11px;font-weight:700;color:var(--col-accent-bright);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px; } | |
| 178 | .dp-section-title { font-family:var(--font-display,sans-serif);font-size:26px;color:var(--col-text);letter-spacing:0.3px;line-height:1; } | |
| 179 | .dp-rel-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px; } | |
| 180 | .dp-rel-card { background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px;overflow:hidden;text-decoration:none;display:block;transition:transform 0.2s,border-color 0.2s; } | |
| 181 | .dp-rel-card:hover { transform:translateY(-4px);border-color:var(--col-accent); } | |
| 182 | .dp-rel-thumb { position:relative;aspect-ratio:4/3;overflow:hidden;background:var(--col-surface-1); } | |
| 183 | .dp-rel-thumb img { width:100%;height:100%;object-fit:cover; } | |
| 184 | .dp-rel-price { position:absolute;top:8px;right:8px;padding:2px 9px;border-radius:14px;font-size:11px;font-weight:700;background:var(--col-surface-1);border:1px solid var(--col-border-2);color:var(--col-text); } | |
| 185 | .dp-rel-price.free { background:rgba(34,197,94,0.2);border-color:rgba(34,197,94,0.4);color:#4ade80; } | |
| 186 | .dp-rel-body { padding:10px 12px 12px; } | |
| 187 | .dp-rel-title { font-size:13px;font-weight:700;color:var(--col-text);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } | |
| 188 | .dp-rel-foot { display:flex;align-items:center;gap:7px;font-size:11px;color:var(--col-text-3); } | |
| 189 | .dp-rel-rating { color:#f59e0b;font-weight:700; } | |
| 190 | ||
| 191 | /* Responsive */ | |
| 192 | @media (max-width:1024px) { | |
| 193 | .dp-layout { grid-template-columns:1fr;padding:20px 22px 60px; } | |
| 194 | .dp-pc { position:static; } | |
| 195 | .dp-highlights { grid-template-columns:1fr; } | |
| 196 | .dp-settings-grid { grid-template-columns:1fr; } | |
| 197 | } | |
| 198 | </style> | |
| 199 | ||
| 200 | ||
| 201 | <!-- The storefront header (brand + search + sign in + cart + nav) | |
| 202 | is now injected by listing_details.cgi via the shared | |
| 203 | MODS::WebSTLs::StoreChrome helper, so every buyer-facing page | |
| 204 | (cart, checkout, listing_details, etc.) shows the same chrome | |
| 205 | and there is no duplicate header here. --> | |
| 206 | ||
| 207 | ||
| 208 | <!-- ===== Breadcrumb ============================================== --> | |
| 209 | <nav class="dp-breadcrumb" aria-label="Breadcrumb"> | |
| 210 | <div class="dp-breadcrumb-inner"> | |
| 211 | <a href="/store.cgi?subdomain=$store_subdomain">$store_name</a> | |
| 212 | <span class="dp-breadcrumb-sep">›</span> | |
| 213 | <a href="/store.cgi?subdomain=$store_subdomain&page=shop">$breadcrumb_cat</a> | |
| 214 | <span class="dp-breadcrumb-sep">›</span> | |
| 215 | <span class="dp-breadcrumb-current">$title</span> | |
| 216 | </div> | |
| 217 | </nav> | |
| 218 | ||
| 219 | ||
| 220 | <!-- ===== Main two-column ======================================== --> | |
| 221 | <div class="dp-layout"> | |
| 222 | ||
| 223 | <!-- ─── LEFT ─────────────────────────────────────────────────── --> | |
| 224 | <div class="dp-left"> | |
| 225 | ||
| 226 | <!-- Hero image --> | |
| 227 | <div class="dp-hero-img"> | |
| 228 | <img src="$hero_url" alt="$title" id="dpHero"> | |
| 229 | <div class="dp-hero-overlay"></div> | |
| 230 | <div class="dp-hero-title-wrap"> | |
| 231 | [if:$has_category]<div class="dp-hero-eyebrow">$category</div>[/if] | |
| 232 | <div class="dp-hero-title">$title</div> | |
| 233 | <div class="dp-hero-badges"> | |
| 234 | [if:$has_category]<span class="dp-hero-badge dp-badge-cat">$category</span>[/if] | |
| 235 | [if:$is_free]<span class="dp-hero-badge dp-badge-free">Free download</span>[/if] | |
| 236 | [if:$has_source]<span class="dp-hero-badge dp-badge-src">From $source_label</span>[/if] | |
| 237 | </div> | |
| 238 | </div> | |
| 239 | </div> | |
| 240 | ||
| 241 | <!-- Thumbnail strip --> | |
| 242 | [if:$has_gallery] | |
| 243 | <div class="dp-thumbs"> | |
| 244 | [loop:@gallery] | |
| 245 | <div class="dp-thumb[if:$loop1.is_active] active[/if]" data-dp-thumb="$loop1.url"> | |
| 246 | <span class="dp-thumb-num">$loop1.idx</span> | |
| 247 | <img src="$loop1.url" alt="$loop1.alt" loading="lazy"> | |
| 248 | </div> | |
| 249 | [/loop] | |
| 250 | </div> | |
| 251 | [/if] | |
| 252 | ||
| 253 | <!-- Tabs --> | |
| 254 | <div class="dp-tabs"> | |
| 255 | <div class="dp-tab-nav" role="tablist"> | |
| 256 | <button class="dp-tab-btn active" data-dp-tab="dp-desc">Description</button> | |
| 257 | [if:$has_files]<button class="dp-tab-btn" data-dp-tab="dp-files">Files Included</button>[/if] | |
| 258 | [if:$has_print_settings]<button class="dp-tab-btn" data-dp-tab="dp-print">Print Settings</button>[/if] | |
| 259 | <button class="dp-tab-btn" data-dp-tab="dp-reviews">Reviews</button> | |
| 260 | </div> | |
| 261 | ||
| 262 | <!-- DESCRIPTION --> | |
| 263 | <div class="dp-tab-pane active" id="dp-desc"> | |
| 264 | [if:$has_tagline]<div style="font-size:16px;color:#e2e8f0;font-weight:600;margin-bottom:18px;line-height:1.5">$tagline</div>[/if] | |
| 265 | [if:$has_description]<div class="dp-desc">$description</div>[/if] | |
| 266 | [if:!$has_description][if:!$has_tagline]<div class="dp-tab-empty">The seller hasn't added a description yet.</div>[/if][/if] | |
| 267 | ||
| 268 | [if:$has_highlights] | |
| 269 | <div class="dp-desc-h">Highlights</div> | |
| 270 | <div class="dp-highlights"> | |
| 271 | [loop:@highlights] | |
| 272 | <div class="dp-h-item"> | |
| 273 | <span class="dp-h-dot"></span> | |
| 274 | <span class="dp-h-text">$loop1.text</span> | |
| 275 | </div> | |
| 276 | [/loop] | |
| 277 | </div> | |
| 278 | [/if] | |
| 279 | ||
| 280 | [if:$has_included] | |
| 281 | <div class="dp-desc-h">What's in the Pack</div> | |
| 282 | <div class="dp-included"> | |
| 283 | [loop:@included] | |
| 284 | <div class="dp-inc-item"> | |
| 285 | <span class="dp-inc-dot"></span> | |
| 286 | <span>$loop1.text</span> | |
| 287 | </div> | |
| 288 | [/loop] | |
| 289 | </div> | |
| 290 | [/if] | |
| 291 | </div> | |
| 292 | ||
| 293 | <!-- FILES --> | |
| 294 | [if:$has_files] | |
| 295 | <div class="dp-tab-pane" id="dp-files"> | |
| 296 | <table class="dp-files-table"> | |
| 297 | <thead> | |
| 298 | <tr><th>File Name</th><th>Format</th><th>Size</th></tr> | |
| 299 | </thead> | |
| 300 | <tbody> | |
| 301 | [loop:@files] | |
| 302 | <tr> | |
| 303 | <td class="dp-file-name">$loop1.name</td> | |
| 304 | <td><span class="dp-file-fmt $loop1.fmt_class">$loop1.ext</span></td> | |
| 305 | <td>$loop1.size_human</td> | |
| 306 | </tr> | |
| 307 | [/loop] | |
| 308 | </tbody> | |
| 309 | </table> | |
| 310 | </div> | |
| 311 | [/if] | |
| 312 | ||
| 313 | <!-- PRINT SETTINGS --> | |
| 314 | [if:$has_print_settings] | |
| 315 | <div class="dp-tab-pane" id="dp-print"> | |
| 316 | <div class="dp-settings-grid"> | |
| 317 | [loop:@print_settings] | |
| 318 | <div class="dp-setting-card"> | |
| 319 | <div class="dp-setting-label">$loop1.label</div> | |
| 320 | <div class="dp-setting-value">$loop1.value</div> | |
| 321 | </div> | |
| 322 | [/loop] | |
| 323 | </div> | |
| 324 | </div> | |
| 325 | [/if] | |
| 326 | ||
| 327 | <!-- REVIEWS --> | |
| 328 | <div class="dp-tab-pane" id="dp-reviews"> | |
| 329 | [if:$has_rating] | |
| 330 | <div class="dp-rv-summary"> | |
| 331 | <div style="text-align:center"> | |
| 332 | <div class="dp-rv-big">$rating_num</div> | |
| 333 | <div class="dp-rv-stars-big">$rating_stars</div> | |
| 334 | <div class="dp-rv-total">$rating_n review(s)</div> | |
| 335 | </div> | |
| 336 | <div style="flex:1;color:#64748b;font-size:13px">Reviews from verified buyers appear here as soon as a sale is rated.</div> | |
| 337 | </div> | |
| 338 | [/if] | |
| 339 | [if:!$has_rating]<div class="dp-rv-empty">No reviews yet. Be the first to review this design after your purchase.</div>[/if] | |
| 340 | </div> | |
| 341 | ||
| 342 | </div><!-- /.dp-tabs --> | |
| 343 | </div><!-- /.dp-left --> | |
| 344 | ||
| 345 | ||
| 346 | <!-- ─── RIGHT ─────────────────────────────────────────────────── --> | |
| 347 | <div class="dp-right"> | |
| 348 | <div class="dp-pc"> | |
| 349 | <div class="dp-pc-inner"> | |
| 350 | ||
| 351 | <div class="dp-pc-title">$title</div> | |
| 352 | ||
| 353 | <!-- Designer --> | |
| 354 | <div class="dp-designer-row"> | |
| 355 | <div class="dp-designer-avatar">$designer_initial</div> | |
| 356 | <div class="dp-designer-info"> | |
| 357 | <div class="dp-designer-by">Designed by</div> | |
| 358 | <a href="/store.cgi?subdomain=$store_subdomain" class="dp-designer-name">$designer_name</a> | |
| 359 | <div><a href="/store.cgi?subdomain=$store_subdomain" class="dp-store-link">$store_name</a></div> | |
| 360 | </div> | |
| 361 | </div> | |
| 362 | ||
| 363 | <!-- Rating --> | |
| 364 | [if:$has_rating] | |
| 365 | <div class="dp-rating-block"> | |
| 366 | <span class="dp-rating-stars">$rating_stars</span> | |
| 367 | <span class="dp-rating-num">$rating_num</span> | |
| 368 | <span class="dp-rating-count">/ 5.0 · $rating_n review(s)</span> | |
| 369 | </div> | |
| 370 | [/if] | |
| 371 | ||
| 372 | <!-- Price + (when applicable) printed copy + variant chooser. | |
| 373 | Three flavors: | |
| 374 | digital -> single price + Add to Cart (legacy path) | |
| 375 | physical -> printed price + color/material picker + stock | |
| 376 | both -> kind toggle, swaps price + reveals variant | |
| 377 | picker only when buyer chooses Printed copy --> | |
| 378 | [if:$is_kind_digital] | |
| 379 | <div class="dp-price-block"> | |
| 380 | <div class="dp-price-main">$price_label</div> | |
| 381 | [if:$is_free]<div class="dp-price-free">FREE</div>[/if] | |
| 382 | </div> | |
| 383 | [/if] | |
| 384 | ||
| 385 | [if:$is_kind_physical] | |
| 386 | <div class="dp-price-block"> | |
| 387 | <div class="dp-price-main">$phys_price_label</div> | |
| 388 | <div class="text-xs text-dim" style="margin-top:4px;line-height:1.5"> | |
| 389 | $prod_label[if:$has_ship_from] · $ship_from_label[/if]<br> | |
| 390 | <strong>$stock_label</strong> | |
| 391 | </div> | |
| 392 | </div> | |
| 393 | [/if] | |
| 394 | ||
| 395 | [if:$is_kind_both] | |
| 396 | <div class="dp-price-block" id="dpKindBlock"> | |
| 397 | <div role="tablist" style="display:flex;gap:0;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:4px;margin-bottom:14px"> | |
| 398 | <button type="button" id="dpKindDigital" data-kind="digital" role="tab" style="flex:1;padding:10px 8px;background:var(--col-accent);color:#fff;border:0;border-radius:7px;font-family:inherit;font-size:12px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;cursor:pointer">Digital file</button> | |
| 399 | <button type="button" id="dpKindPhysical" data-kind="physical" role="tab" style="flex:1;padding:10px 8px;background:transparent;color:var(--col-text-2);border:0;border-radius:7px;font-family:inherit;font-size:12px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;cursor:pointer">Printed copy</button> | |
| 400 | </div> | |
| 401 | <div class="dp-price-main" id="dpPriceLabel">$price_label</div> | |
| 402 | <div class="text-xs text-dim" id="dpPriceMeta" style="margin-top:4px;line-height:1.5;display:none"> | |
| 403 | $prod_label[if:$has_ship_from] · $ship_from_label[/if]<br> | |
| 404 | <strong>$stock_label</strong> | |
| 405 | </div> | |
| 406 | </div> | |
| 407 | [/if] | |
| 408 | ||
| 409 | <!-- Color + Material picker. Shown when buyer is buying the | |
| 410 | printed copy. JS toggles visibility based on chosen kind. --> | |
| 411 | [if:$offers_physical] | |
| 412 | <div id="dpVariantPicker" style="display:[if:$is_kind_physical]block[/if][if:!$is_kind_physical]none[/if];margin-bottom:14px"> | |
| 413 | [if:$has_color_choices] | |
| 414 | <label class="form-label" style="font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-3);display:block;margin-bottom:4px">Color</label> | |
| 415 | <select id="dpColor" class="select" style="width:100%;margin-bottom:10px"> | |
| 416 | [loop:@color_choices] | |
| 417 | <option value="$loop1.value">$loop1.label</option> | |
| 418 | [/loop] | |
| 419 | </select> | |
| 420 | [/if] | |
| 421 | [if:$has_material_choices] | |
| 422 | <label class="form-label" style="font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-3);display:block;margin-bottom:4px">Material</label> | |
| 423 | <select id="dpMaterial" class="select" style="width:100%"> | |
| 424 | [loop:@material_choices] | |
| 425 | <option value="$loop1.value">$loop1.label</option> | |
| 426 | [/loop] | |
| 427 | </select> | |
| 428 | [/if] | |
| 429 | </div> | |
| 430 | [/if] | |
| 431 | ||
| 432 | <!-- Actions --> | |
| 433 | <div class="dp-actions"> | |
| 434 | <button type="button" class="dp-btn-primary we-add-to-cart" id="dpAddToCart" data-mid="$model_id" data-sid="$store_id" data-kind="$product_kind"[if:$is_sold_out] disabled[/if]> | |
| 435 | [if:$is_sold_out]Sold out[/if][if:!$is_sold_out][if:$is_free]↓ Download for Free[/if][if:!$is_free]↓ Add to Cart[/if][/if] | |
| 436 | </button> | |
| 437 | [if:$has_files][if:!$is_kind_physical]<button type="button" class="dp-btn-secondary">▶ Preview Files</button>[/if][/if] | |
| 438 | <button type="button" class="dp-btn-wishlist">♡ Add to Wishlist</button> | |
| 439 | </div> | |
| 440 | ||
| 441 | <!-- Meta rows --> | |
| 442 | <div class="dp-meta"> | |
| 443 | [if:$has_files] | |
| 444 | <div class="dp-meta-row"> | |
| 445 | <span class="dp-meta-label">Files included</span> | |
| 446 | <span class="dp-meta-value">$file_count file(s)</span> | |
| 447 | </div> | |
| 448 | <div class="dp-meta-row"> | |
| 449 | <span class="dp-meta-label">Total size</span> | |
| 450 | <span class="dp-meta-value">$total_size</span> | |
| 451 | </div> | |
| 452 | [/if] | |
| 453 | <div class="dp-meta-row"> | |
| 454 | <span class="dp-meta-label">Total downloads</span> | |
| 455 | <span class="dp-meta-value blue">↓ $total_downloads</span> | |
| 456 | </div> | |
| 457 | [if:$has_category] | |
| 458 | <div class="dp-meta-row"> | |
| 459 | <span class="dp-meta-label">Category</span> | |
| 460 | <span class="dp-meta-value">$category</span> | |
| 461 | </div> | |
| 462 | [/if] | |
| 463 | [if:$has_updated] | |
| 464 | <div class="dp-meta-row"> | |
| 465 | <span class="dp-meta-label">Updated</span> | |
| 466 | <span class="dp-meta-value">$updated_pretty</span> | |
| 467 | </div> | |
| 468 | [/if] | |
| 469 | </div> | |
| 470 | ||
| 471 | <!-- Tags --> | |
| 472 | [if:$has_tags] | |
| 473 | <div class="dp-tags"> | |
| 474 | [loop:@tags] | |
| 475 | <span class="dp-tag">$loop1.name</span> | |
| 476 | [/loop] | |
| 477 | </div> | |
| 478 | [/if] | |
| 479 | ||
| 480 | <!-- Source platform note --> | |
| 481 | [if:$has_source] | |
| 482 | <div class="dp-source-note"> | |
| 483 | This design was originally posted on <strong>$source_label</strong>. <a href="$source_url" target="_blank" rel="noopener">View the original listing →</a> | |
| 484 | </div> | |
| 485 | [/if] | |
| 486 | ||
| 487 | </div><!-- /.dp-pc-inner --> | |
| 488 | ||
| 489 | <div class="dp-license"> | |
| 490 | Licensed as <strong style="color:#94a3b8">$license</strong>. Digital files are delivered after purchase; redistribution is not permitted. | |
| 491 | </div> | |
| 492 | </div><!-- /.dp-pc --> | |
| 493 | </div><!-- /.dp-right --> | |
| 494 | ||
| 495 | </div><!-- /.dp-layout --> | |
| 496 | ||
| 497 | ||
| 498 | <!-- ===== More from this designer ================================ --> | |
| 499 | [if:$has_related] | |
| 500 | <section class="dp-section"> | |
| 501 | <div class="dp-section-inner"> | |
| 502 | <div class="dp-section-hd"> | |
| 503 | <div> | |
| 504 | <div class="dp-section-eyebrow">More from $designer_name</div> | |
| 505 | <div class="dp-section-title">Other designs from this seller</div> | |
| 506 | </div> | |
| 507 | <a href="/store.cgi?subdomain=$store_subdomain" style="font-size:13px;font-weight:600;color:#60a5fa;text-decoration:none;padding:8px 16px;border:1px solid rgba(59,130,246,0.3);border-radius:8px">View all →</a> | |
| 508 | </div> | |
| 509 | <div class="dp-rel-grid"> | |
| 510 | [loop:@related] | |
| 511 | <a href="/listing_details.cgi?id=$loop1.id" class="dp-rel-card"> | |
| 512 | <div class="dp-rel-thumb"> | |
| 513 | <img src="$loop1.hero" alt="$loop1.title" loading="lazy"> | |
| 514 | <span class="dp-rel-price[if:$loop1.is_free] free[/if]">$loop1.price_label</span> | |
| 515 | </div> | |
| 516 | <div class="dp-rel-body"> | |
| 517 | <div class="dp-rel-title">$loop1.title</div> | |
| 518 | [if:$loop1.rating] | |
| 519 | <div class="dp-rel-foot"> | |
| 520 | <span class="dp-rel-rating">★ $loop1.rating</span> | |
| 521 | <span>· $loop1.rating_n review(s)</span> | |
| 522 | </div> | |
| 523 | [/if] | |
| 524 | </div> | |
| 525 | </a> | |
| 526 | [/loop] | |
| 527 | </div> | |
| 528 | </div> | |
| 529 | </section> | |
| 530 | [/if] | |
| 531 | ||
| 532 | ||
| 533 | <!-- ===== JS: thumbnail click + tab switch ======================== --> | |
| 534 | <script> | |
| 535 | (function(){ | |
| 536 | // Thumbnail click swaps the hero image. Active class moves with it. | |
| 537 | var hero = document.getElementById('dpHero'); | |
| 538 | var thumbs = document.querySelectorAll('[data-dp-thumb]'); | |
| 539 | thumbs.forEach(function(t){ | |
| 540 | t.addEventListener('click', function(){ | |
| 541 | if (!hero) return; | |
| 542 | var url = this.getAttribute('data-dp-thumb'); | |
| 543 | if (!url) return; | |
| 544 | hero.src = url; | |
| 545 | thumbs.forEach(function(o){ o.classList.remove('active'); }); | |
| 546 | this.classList.add('active'); | |
| 547 | }); | |
| 548 | }); | |
| 549 | ||
| 550 | // Tab switcher. Pure JS, no framework. Single .active on the | |
| 551 | // currently visible tab pane / button pair. | |
| 552 | var tabBtns = document.querySelectorAll('[data-dp-tab]'); | |
| 553 | tabBtns.forEach(function(btn){ | |
| 554 | btn.addEventListener('click', function(){ | |
| 555 | var target = this.getAttribute('data-dp-tab'); | |
| 556 | if (!target) return; | |
| 557 | tabBtns.forEach(function(o){ o.classList.remove('active'); }); | |
| 558 | this.classList.add('active'); | |
| 559 | document.querySelectorAll('.dp-tab-pane').forEach(function(p){ p.classList.remove('active'); }); | |
| 560 | var pane = document.getElementById(target); | |
| 561 | if (pane) pane.classList.add('active'); | |
| 562 | }); | |
| 563 | }); | |
| 564 | ||
| 565 | // Kind toggle for 'both' listings. Swaps the headline price, the | |
| 566 | // ships-in / stock meta strip, the data-kind attribute on the Add | |
| 567 | // to Cart button (which the POST below reads), and the visibility | |
| 568 | // of the color/material picker. | |
| 569 | var kDigital = document.getElementById('dpKindDigital'); | |
| 570 | var kPhysical = document.getElementById('dpKindPhysical'); | |
| 571 | var priceLabel = document.getElementById('dpPriceLabel'); | |
| 572 | var priceMeta = document.getElementById('dpPriceMeta'); | |
| 573 | var variantBox = document.getElementById('dpVariantPicker'); | |
| 574 | var addBtn = document.getElementById('dpAddToCart'); | |
| 575 | var digitalPriceText = "$price_label"; | |
| 576 | var physicalPriceText = "$phys_price_label"; | |
| 577 | if (kDigital && kPhysical && addBtn) { | |
| 578 | function setKind(kind) { | |
| 579 | var picked = (kind === 'physical'); | |
| 580 | kDigital.style.background = picked ? 'transparent' : 'var(--col-accent)'; | |
| 581 | kDigital.style.color = picked ? 'var(--col-text-2)' : '#fff'; | |
| 582 | kPhysical.style.background = picked ? 'var(--col-accent)' : 'transparent'; | |
| 583 | kPhysical.style.color = picked ? '#fff' : 'var(--col-text-2)'; | |
| 584 | if (priceLabel) priceLabel.textContent = picked ? physicalPriceText : digitalPriceText; | |
| 585 | if (priceMeta) priceMeta.style.display = picked ? 'block' : 'none'; | |
| 586 | if (variantBox) variantBox.style.display = picked ? 'block' : 'none'; | |
| 587 | addBtn.setAttribute('data-kind', kind); | |
| 588 | } | |
| 589 | kDigital.addEventListener('click', function(){ setKind('digital'); }); | |
| 590 | kPhysical.addEventListener('click', function(){ setKind('physical'); }); | |
| 591 | } | |
| 592 | ||
| 593 | // Add to cart. Reads model+storefront from the button, kind+color | |
| 594 | // +material from the variant picker. POSTs to /cart_add.cgi and | |
| 595 | // updates the header cart badge if present. | |
| 596 | function getVal(id) { var el = document.getElementById(id); return el ? el.value : ''; } | |
| 597 | if (addBtn) { | |
| 598 | addBtn.addEventListener('click', function() { | |
| 599 | if (addBtn.disabled) return; | |
| 600 | var mid = addBtn.getAttribute('data-mid'); | |
| 601 | var sid = addBtn.getAttribute('data-sid'); | |
| 602 | var kind = addBtn.getAttribute('data-kind') || 'digital'; | |
| 603 | var color = (kind === 'physical') ? getVal('dpColor') : ''; | |
| 604 | var material = (kind === 'physical') ? getVal('dpMaterial') : ''; | |
| 605 | var orig = addBtn.textContent; | |
| 606 | addBtn.disabled = true; | |
| 607 | addBtn.textContent = 'Adding...'; | |
| 608 | var fd = new FormData(); | |
| 609 | fd.append('model_id', mid); | |
| 610 | fd.append('storefront_id', sid); | |
| 611 | fd.append('kind', kind); | |
| 612 | fd.append('color', color); | |
| 613 | fd.append('material', material); | |
| 614 | fetch('/cart_add.cgi', { method: 'POST', body: fd, credentials: 'same-origin' }) | |
| 615 | .then(function(r) { return r.json(); }) | |
| 616 | .then(function(data) { | |
| 617 | if (!data || !data.success) throw new Error(data && data.error || 'add failed'); | |
| 618 | addBtn.textContent = 'Added ✓'; | |
| 619 | var link = document.getElementById('we-cart-link'); | |
| 620 | if (link) { | |
| 621 | var badge = link.querySelector('.we-cart-badge'); | |
| 622 | if (badge) { badge.textContent = data.count; } | |
| 623 | } | |
| 624 | setTimeout(function() { addBtn.disabled = false; addBtn.textContent = orig; }, 1500); | |
| 625 | }) | |
| 626 | .catch(function(err) { | |
| 627 | addBtn.disabled = false; | |
| 628 | addBtn.textContent = orig; | |
| 629 | if (typeof appToast === 'function') { | |
| 630 | appToast({ message: 'Add to cart failed: ' + err.message, type: 'error' }); | |
| 631 | } else { | |
| 632 | alert('Add to cart failed: ' + err.message); | |
| 633 | } | |
| 634 | }); | |
| 635 | }); | |
| 636 | } | |
| 637 | })(); | |
| 638 | </script> |